Skip to content

Commit

Permalink
Fixes CGImage extension method for colour space converting copying (#23)
Browse files Browse the repository at this point in the history
* Fixes colour space conversion with an Accelerate (vImage) backed implementation

* Only convert color space if needed

* Some cleanup
  • Loading branch information
mz2 authored Dec 10, 2021
1 parent 075eb9a commit 5acefb1
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 10 deletions.
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ let package = Package(
.process("iphone5.jpg"),
.process("Pixls/DNG/hdrmerge-bayer-fp16-w-pred-deflate_.dng"),
.process("Pixls/X3F/DP2M1726_.X3F"),
.process("outline-invert_2x.png")
]
),
]
Expand Down
40 changes: 35 additions & 5 deletions Sources/Carpaccio/CGImage+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import CoreGraphics
import ImageIO
import Accelerate

#if os(iOS)
import MobileCoreServices
Expand Down Expand Up @@ -106,7 +107,7 @@ public extension CGImage {
throw CGImageExtensionError.failedToLoadCGImage
}

if let colorSpace = colorSpace {
if let colorSpace = colorSpace, colorSpace != cgImage.colorSpace {
return try cgImage.convertedToColorSpace(colorSpace)
}
return cgImage
Expand Down Expand Up @@ -157,10 +158,39 @@ public extension CGImage {
return pngData
}

func convertedToColorSpace(_ colorSpace: CGColorSpace) throws -> CGImage {
guard let convertedImage = self.copy(colorSpace: colorSpace) else {
throw CGImageExtensionError.failedToConvertColorSpace
/// Return a copy of the image converted to the target color space.
///
/// Callee is responsible for checking that the colour space of the source image is not already the target color space.
func convertedToColorSpace(_ colorSpace: CGColorSpace, renderingIntent: CGColorRenderingIntent = .defaultIntent)
throws -> CGImage {
precondition(self.colorSpace != colorSpace)
guard
let sourceImageFormat = vImage_CGImageFormat(cgImage: self),
let destinationImageFormat = vImage_CGImageFormat(
// TODO: Generalise this to work with arbitrary color space bit depths
bitsPerComponent: 8,
// + 1 for alpha (TODO: generalise over alpha / non-alpha channeled)
bitsPerPixel: 8 * (colorSpace.numberOfComponents + 1),
colorSpace: colorSpace,
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue),
renderingIntent: renderingIntent) else {
throw CGImageExtensionError.failedToConvertColorSpace
}

let sourceBuffer = try vImage_Buffer(cgImage: self)
var destinationBuffer = try vImage_Buffer(width: Int(sourceBuffer.width),
height: Int(sourceBuffer.height),
bitsPerPixel: destinationImageFormat.bitsPerPixel)

let converter = try vImageConverter.make(sourceFormat: sourceImageFormat,
destinationFormat: destinationImageFormat)
try converter.convert(source: sourceBuffer, destination: &destinationBuffer)

defer {
sourceBuffer.free()
destinationBuffer.free()
}
return convertedImage

return try destinationBuffer.createCGImage(format: destinationImageFormat)
}
}
7 changes: 5 additions & 2 deletions Sources/Carpaccio/ImageLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,11 @@ public class ImageLoader: ImageLoaderProtocol, URLBackedImageLoaderProtocol {

try stopIfCancelled(cancelChecker, "Before converting color space of thumbnail image")

let image = try thumbnail.convertedToColorSpace(colorSpace)
return image
if thumbnail.colorSpace != colorSpace {
let image = try thumbnail.convertedToColorSpace(colorSpace)
return image
}
return thumbnail
}()

// Crop letterboxing out, if needed
Expand Down
29 changes: 26 additions & 3 deletions Tests/CarpaccioTests/CarpaccioTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,38 @@ class CarpaccioTests: XCTestCase {
XCTAssertEqual(map[image3], "walrus")

// Simulate moving images to a Keepers subdirectory
let newURL1 = originalURL1.deletingLastPathComponent().appendingPathComponent("Keepers").appendingPathComponent(originalURL1.lastPathComponent)
let newURL1 = originalURL1
.deletingLastPathComponent()
.appendingPathComponent("Keepers")
.appendingPathComponent(originalURL1.lastPathComponent)
image1.updateURL(newURL1)
let newURL2 = originalURL1.deletingLastPathComponent().appendingPathComponent("Keepers").appendingPathComponent(originalURL2.lastPathComponent)

let newURL2 = originalURL1
.deletingLastPathComponent()
.appendingPathComponent("Keepers")
.appendingPathComponent(originalURL2.lastPathComponent)
image2.updateURL(newURL2)
let newURL3 = originalURL1.deletingLastPathComponent().appendingPathComponent("Keepers").appendingPathComponent(originalURL3.lastPathComponent)

let newURL3 = originalURL1.deletingLastPathComponent()
.appendingPathComponent("Keepers")
.appendingPathComponent(originalURL3.lastPathComponent)
image3.updateURL(newURL3)

XCTAssertEqual(map[image1], "cat")
XCTAssertEqual(map[image2], "dog")
XCTAssertEqual(map[image3], "walrus")
}

func testColorSpaceConversion() throws {
guard let url = Bundle.module.url(forResource: "outline-invert_2x", withExtension: "png") else {
XCTAssert(false)
return
}

let dataProvider = CGDataProvider(filename: url.path)!
let image = CGImage(pngDataProviderSource: dataProvider, decode: nil, shouldInterpolate: true, intent: .defaultIntent)!
XCTAssertEqual(image.colorSpace!.name, CGColorSpace.genericGrayGamma2_2)
let convertedImage = try image.convertedToColorSpace(CGColorSpace(name: CGColorSpace.sRGB)!, renderingIntent: .perceptual)
XCTAssertEqual(convertedImage.colorSpace!.name, CGColorSpace.sRGB)
}
}
Binary file added Tests/CarpaccioTests/outline-invert_2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 5acefb1

Please sign in to comment.