Kaynağa Gözat

Improve macOS graphics context for high bit depth

onevcat 2 ay önce
ebeveyn
işleme
6bd4a13761
1 değiştirilmiş dosya ile 78 ekleme ve 20 silme
  1. 78 20
      Sources/Image/GraphicsContext.swift

+ 78 - 20
Sources/Image/GraphicsContext.swift

@@ -46,29 +46,15 @@ enum GraphicsContext {
     
     static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> CGContext? {
         #if os(macOS)
-        guard let rep = NSBitmapImageRep(
-            bitmapDataPlanes: nil,
-            pixelsWide: Int(size.width),
-            pixelsHigh: Int(size.height),
-            bitsPerSample: cgImage?.bitsPerComponent ?? 8,
-            samplesPerPixel: 4,
-            hasAlpha: true,
-            isPlanar: false,
-            colorSpaceName: .calibratedRGB,
-            bytesPerRow: 0,
-            bitsPerPixel: 0) else
-        {
-            assertionFailure("[Kingfisher] Image representation cannot be created.")
-            return nil
-        }
-        rep.size = size
-        guard let context = NSGraphicsContext(bitmapImageRep: rep) else {
+        let descriptor = BitmapContextDescriptor(size: size, cgImage: cgImage)
+        guard let context = descriptor.makeContext() else {
             assertionFailure("[Kingfisher] Image context cannot be created.")
             return nil
         }
-        
-        NSGraphicsContext.current = context
-        return context.cgContext
+        let graphicsContext = NSGraphicsContext(cgContext: context, flipped: false)
+        graphicsContext.imageInterpolation = .high
+        NSGraphicsContext.current = graphicsContext
+        return graphicsContext.cgContext
         #elseif os(watchOS)
         guard let context = UIGraphicsGetCurrentContext() else {
             return nil
@@ -96,3 +82,75 @@ enum GraphicsContext {
 }
 
 #endif
+
+#if os(macOS)
+private struct BitmapContextDescriptor {
+    let width: Int
+    let height: Int
+    let bitsPerComponent: Int
+    let bytesPerRow: Int
+    let colorSpace: CGColorSpace
+    let bitmapInfo: CGBitmapInfo
+    
+    init(size: CGSize, cgImage: CGImage?) {
+        width = max(Int(size.width.rounded(.down)), 1)
+        height = max(Int(size.height.rounded(.down)), 1)
+        colorSpace = BitmapContextDescriptor.resolveColorSpace(from: cgImage)
+        bitsPerComponent = BitmapContextDescriptor.supportedBitsPerComponent(from: cgImage)
+        let hasAlpha = BitmapContextDescriptor.containsAlpha(from: cgImage)
+        bitmapInfo = BitmapContextDescriptor.bitmapInfo(hasAlpha: hasAlpha)
+        let componentsPerPixel = colorSpace.numberOfComponents + 1 // Reserve an alpha (or skip) slot.
+        let bitsPerPixel = componentsPerPixel * bitsPerComponent
+        bytesPerRow = BitmapContextDescriptor.alignedBytesPerRow(bitsPerPixel: bitsPerPixel, width: width)
+    }
+    
+    func makeContext() -> CGContext? {
+        CGContext(
+            data: nil,
+            width: width,
+            height: height,
+            bitsPerComponent: bitsPerComponent,
+            bytesPerRow: bytesPerRow,
+            space: colorSpace,
+            bitmapInfo: bitmapInfo.rawValue
+        )
+    }
+    
+    private static func supportedBitsPerComponent(from cgImage: CGImage?) -> Int {
+        guard let bits = cgImage?.bitsPerComponent, bits > 0 else { return 8 }
+        if bits <= 8 { return 8 }
+        return 16
+    }
+    
+    private static func resolveColorSpace(from cgImage: CGImage?) -> CGColorSpace {
+        guard let cgColorSpace = cgImage?.colorSpace else {
+            return CGColorSpaceCreateDeviceRGB()
+        }
+        let components = cgColorSpace.numberOfComponents
+        if components == 1 || components == 3 {
+            return cgColorSpace
+        }
+        return CGColorSpaceCreateDeviceRGB()
+    }
+    
+    private static func containsAlpha(from cgImage: CGImage?) -> Bool {
+        guard let alphaInfo = cgImage?.alphaInfo else { return true }
+        switch alphaInfo {
+        case .none, .noneSkipFirst, .noneSkipLast:
+            return false
+        default:
+            return true
+        }
+    }
+    
+    private static func bitmapInfo(hasAlpha: Bool) -> CGBitmapInfo {
+        let alphaInfo: CGImageAlphaInfo = hasAlpha ? .premultipliedLast : .noneSkipLast
+        return CGBitmapInfo(rawValue: alphaInfo.rawValue)
+    }
+    
+    private static func alignedBytesPerRow(bitsPerPixel: Int, width: Int) -> Int {
+        let rawBytes = (bitsPerPixel * width + 7) / 8
+        return (rawBytes + 0x3F) & ~0x3F
+    }
+}
+#endif