使用CGContext缩放10位NSImage时遇主线程警告及色彩空间问题
问题
我用NSImage加载10位HEIC、AVIF格式图片,用CGContext缩放时反复收到以下警告:
This method should not be called on the main thread as it may lead to UI unresponsiveness.
但改用NSBitmapImageRep作为当前上下文时不会出现该警告,不过NSBitmapImageRep没法保留色彩空间(比如不支持广色域)。
我想知道:
- NSBitmapImageRep有什么特殊配置能避免这个警告?
- 能不能在使用NSBitmapImageRep缩放时保留色彩空间?
示例文件:Prana-hdr.avif(10位广色域AVIF图)
原CGContext缩放代码:
var bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue //10-bit or 12-bit need to be drawn using 16 bpc context e.g. converted var bitsPerComponent = cgImage.bitsPerComponent if cgImage.bitsPerComponent > 8 && cgImage.bitsPerComponent <= 16 { bitsPerComponent = 16 } var cgContext = CGContext(data: nil, width: Int(newSize.width), height: Int(newSize.height), bitsPerComponent: bitsPerComponent, bytesPerRow: 0, space: cgImage.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: bitmapInfo) if cgContext == nil { // context cannot be created with provided values. Continue with 8bps 32bpp RGBA context cgContext = CGContext(data: nil, width: Int(newSize.width), height: Int(newSize.height), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) } guard cgContext != nil else { return self } cgContext!.interpolationQuality = .high cgContext!.draw(cgImage, in: CGRect(origin: .zero, size: newSize), byTiling: false) guard let resizedCGImage = cgContext!.makeImage() else { return self } let resizedImage = NSImage(cgImage: resizedCGImage, size: NSMakeSize(Double(resizedCGImage.width), Double(resizedCGImage.height)))
尝试关联NSGraphicsContext仍有警告的代码:
NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(cgContext: cgContext!, flipped: false) draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), from: .zero, operation: .copy, fraction: 1.0) NSGraphicsContext.restoreGraphicsState()
回答
一、NSBitmapImageRep无警告的原因
NSBitmapImageRep内部默认会将图像解码、绘制这类耗时操作放到后台线程执行,不会占用主线程资源。而直接用CGContext时,所有计算都在当前线程(主线程)运行,处理10位高比特深的HEIC/AVIF时计算量极大,自然触发主线程阻塞的警告。
二、让NSBitmapImageRep保留色彩空间的方法
关键是手动初始化NSBitmapImageRep时匹配原始图像的色彩空间、比特深度参数,而不是用默认设置。具体实现如下:
func resizedImage(with newSize: NSSize) -> NSImage? { guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } // 提取原始图像的色彩空间与比特深度信息 let colorSpace = cgImage.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)! var bitsPerComponent = cgImage.bitsPerComponent // 10/12位图像转用16位上下文处理,避免色彩丢失 if bitsPerComponent > 8 && bitsPerComponent <= 16 { bitsPerComponent = 16 } // 初始化匹配参数的NSBitmapImageRep guard let bitmapRep = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: Int(newSize.width), pixelsHigh: Int(newSize.height), bitsPerSample: bitsPerComponent, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSColorSpaceName(rawValue: colorSpace.name!), bytesPerRow: 0, bitsPerPixel: bitsPerComponent * 4 ) else { // 初始化失败时降级到8位sRGB方案 return fallbackResizedImage(with: newSize) } // 关联上下文并执行高质量缩放 NSGraphicsContext.saveGraphicsState() let context = NSGraphicsContext(bitmapImageRep: bitmapRep) NSGraphicsContext.current = context context?.cgContext.interpolationQuality = .high let drawRect = NSRect(origin: .zero, size: newSize) self.draw(in: drawRect, from: .zero, operation: .copy, fraction: 1.0) NSGraphicsContext.restoreGraphicsState() // 从bitmapRep生成最终NSImage let resizedImage = NSImage(size: newSize) resizedImage.addRepresentation(bitmapRep) return resizedImage } // 降级方案:自定义bitmapRep初始化失败时使用 func fallbackResizedImage(with newSize: NSSize) -> NSImage? { guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! guard let cgContext = CGContext( data: nil, width: Int(newSize.width), height: Int(newSize.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { return nil } cgContext.interpolationQuality = .high cgContext.draw(cgImage, in: CGRect(origin: .zero, size: newSize)) guard let resizedCGImage = cgContext.makeImage() else { return nil } return NSImage(cgImage: resizedCGImage, size: newSize) }
额外注意事项
- 初始化NSBitmapImageRep时,
colorSpaceName必须和原始图像一致,比如广色域的Display P3或Rec.2020,才能保留色彩空间; - 10/12位图像转用16位上下文处理,是因为系统对非8位的直接支持有限,16位可以完整保留原始色彩信息;
- 即使NSBitmapImageRep会后台处理,若缩放操作频繁,建议主动把整个缩放逻辑放到
DispatchQueue.global().async里执行,彻底杜绝主线程阻塞风险。
内容的提问来源于stack exchange,提问作者Marek H
相关产品推荐
相关产品推荐

