Files
to-live-photo/Sources/LivePhotoCore/AIEnhancer/ImageFormatConverter.swift
empty a49fee4b52 fix: 安全审查 P3 问题修复(10项)
无障碍与用户体验:
- 补全 OnboardingView/ProcessingView/ResultView/WallpaperGuideView 无障碍标注
- ContentView 添加后台切换截屏保护(模糊覆盖层)
- EditorView iPad 右侧面板改为响应式宽度(minWidth:320, maxWidth:420)
- ResultView CelebrationParticles 改用 Task 替代 DispatchQueue 延迟
- DesignSystem accessibility key 从中文改为英文
- EditorView/WallpaperGuideView 生成按钮 accentColor 统一为设计系统
- OnboardingView 第四页添加相册权限预告提示

核心库优化:
- LivePhotoLogger 添加日志安全注释
- CacheManager.makeWorkPaths 添加磁盘空间预检查(<500MB 报错)
- ImageFormatConverter BGRA/ARGB 转换改用 vImagePermuteChannels 加速

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 20:11:50 +08:00

270 lines
8.8 KiB
Swift

//
// ImageFormatConverter.swift
// LivePhotoCore
//
// Utilities for converting between CGImage and CVPixelBuffer formats.
//
import Accelerate
import CoreGraphics
import CoreVideo
import Foundation
import VideoToolbox
/// Utilities for image format conversion
enum ImageFormatConverter {
/// Convert CGImage to CVPixelBuffer for Core ML input
/// - Parameters:
/// - image: Input CGImage
/// - pixelFormat: Output pixel format (default BGRA)
/// - Returns: CVPixelBuffer ready for Core ML
static func cgImageToPixelBuffer(
_ image: CGImage,
pixelFormat: OSType = kCVPixelFormatType_32BGRA
) throws -> CVPixelBuffer {
let width = image.width
let height = image.height
// Create pixel buffer
var pixelBuffer: CVPixelBuffer?
let attrs: [CFString: Any] = [
kCVPixelBufferCGImageCompatibilityKey: true,
kCVPixelBufferCGBitmapContextCompatibilityKey: true,
kCVPixelBufferMetalCompatibilityKey: true,
]
let status = CVPixelBufferCreate(
kCFAllocatorDefault,
width,
height,
pixelFormat,
attrs as CFDictionary,
&pixelBuffer
)
guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
throw AIEnhanceError.inputImageInvalid
}
// Lock buffer for writing
CVPixelBufferLockBaseAddress(buffer, [])
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else {
throw AIEnhanceError.inputImageInvalid
}
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
let colorSpace = CGColorSpaceCreateDeviceRGB()
// Create bitmap context to draw into pixel buffer
guard
let context = CGContext(
data: baseAddress,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
| CGBitmapInfo.byteOrder32Little.rawValue
)
else {
throw AIEnhanceError.inputImageInvalid
}
// Draw image into context (this converts the format)
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
return buffer
}
/// Convert CVPixelBuffer to CGImage
/// - Parameter pixelBuffer: Input pixel buffer
/// - Returns: CGImage representation
static func pixelBufferToCGImage(_ pixelBuffer: CVPixelBuffer) throws -> CGImage {
var cgImage: CGImage?
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage)
guard let image = cgImage else {
throw AIEnhanceError.inferenceError("Failed to create CGImage from pixel buffer")
}
return image
}
/// Extract raw RGBA pixel data from CVPixelBuffer
/// - Parameter pixelBuffer: Input pixel buffer
/// - Returns: Array of RGBA bytes
static func pixelBufferToRGBAData(_ pixelBuffer: CVPixelBuffer) throws -> [UInt8] {
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else {
throw AIEnhanceError.inferenceError("Cannot access pixel buffer data")
}
let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
// Handle BGRA format (most common from Core ML)
if pixelFormat == kCVPixelFormatType_32BGRA {
return convertBGRAToRGBA(
baseAddress: baseAddress,
width: width,
height: height,
bytesPerRow: bytesPerRow
)
}
// Handle RGBA format
if pixelFormat == kCVPixelFormatType_32RGBA {
var result = [UInt8](repeating: 0, count: width * height * 4)
for y in 0..<height {
let srcRow = baseAddress.advanced(by: y * bytesPerRow)
let dstOffset = y * width * 4
memcpy(&result[dstOffset], srcRow, width * 4)
}
return result
}
// Handle ARGB format
if pixelFormat == kCVPixelFormatType_32ARGB {
return convertARGBToRGBA(
baseAddress: baseAddress,
width: width,
height: height,
bytesPerRow: bytesPerRow
)
}
throw AIEnhanceError.inferenceError("Unsupported pixel format: \(pixelFormat)")
}
/// Create CVPixelBuffer from raw RGBA data
/// - Parameters:
/// - rgbaData: RGBA pixel data
/// - width: Image width
/// - height: Image height
/// - Returns: CVPixelBuffer
static func rgbaDataToPixelBuffer(
_ rgbaData: [UInt8],
width: Int,
height: Int
) throws -> CVPixelBuffer {
var pixelBuffer: CVPixelBuffer?
let attrs: [CFString: Any] = [
kCVPixelBufferCGImageCompatibilityKey: true,
kCVPixelBufferCGBitmapContextCompatibilityKey: true,
]
let status = CVPixelBufferCreate(
kCFAllocatorDefault,
width,
height,
kCVPixelFormatType_32BGRA,
attrs as CFDictionary,
&pixelBuffer
)
guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
throw AIEnhanceError.inputImageInvalid
}
CVPixelBufferLockBaseAddress(buffer, [])
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else {
throw AIEnhanceError.inputImageInvalid
}
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
// Convert RGBA to BGRA while copying
for y in 0..<height {
let dstRow = baseAddress.advanced(by: y * bytesPerRow).assumingMemoryBound(to: UInt8.self)
let srcOffset = y * width * 4
for x in 0..<width {
let srcIdx = srcOffset + x * 4
let dstIdx = x * 4
// RGBA -> BGRA swap
dstRow[dstIdx + 0] = rgbaData[srcIdx + 2] // B
dstRow[dstIdx + 1] = rgbaData[srcIdx + 1] // G
dstRow[dstIdx + 2] = rgbaData[srcIdx + 0] // R
dstRow[dstIdx + 3] = rgbaData[srcIdx + 3] // A
}
}
return buffer
}
// MARK: - Private Helpers
private static func convertBGRAToRGBA(
baseAddress: UnsafeMutableRawPointer,
width: Int,
height: Int,
bytesPerRow: Int
) -> [UInt8] {
var result = [UInt8](repeating: 0, count: width * height * 4)
let dstBytesPerRow = width * 4
var srcBuffer = vImage_Buffer(
data: baseAddress,
height: vImagePixelCount(height),
width: vImagePixelCount(width),
rowBytes: bytesPerRow
)
result.withUnsafeMutableBufferPointer { dstPtr in
var dstBuffer = vImage_Buffer(
data: dstPtr.baseAddress!,
height: vImagePixelCount(height),
width: vImagePixelCount(width),
rowBytes: dstBytesPerRow
)
// BGRA RGBA: [2,1,0,3]
let permuteMap: [UInt8] = [2, 1, 0, 3]
vImagePermuteChannels_ARGB8888(&srcBuffer, &dstBuffer, permuteMap, vImage_Flags(kvImageNoFlags))
}
return result
}
private static func convertARGBToRGBA(
baseAddress: UnsafeMutableRawPointer,
width: Int,
height: Int,
bytesPerRow: Int
) -> [UInt8] {
var result = [UInt8](repeating: 0, count: width * height * 4)
let dstBytesPerRow = width * 4
var srcBuffer = vImage_Buffer(
data: baseAddress,
height: vImagePixelCount(height),
width: vImagePixelCount(width),
rowBytes: bytesPerRow
)
result.withUnsafeMutableBufferPointer { dstPtr in
var dstBuffer = vImage_Buffer(
data: dstPtr.baseAddress!,
height: vImagePixelCount(height),
width: vImagePixelCount(width),
rowBytes: dstBytesPerRow
)
// ARGB RGBA: [1,2,3,0]
let permuteMap: [UInt8] = [1, 2, 3, 0]
vImagePermuteChannels_ARGB8888(&srcBuffer, &dstBuffer, permuteMap, vImage_Flags(kvImageNoFlags))
}
return result
}
}