You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 10:52:29