Using CoreImage to filter photos, I have found that saving to JPG file will result in an image that has a subtle but visible blue hue. In this example using a B&W image, the histogram reveals how the colors have been shifted in the saved file.
Output [] Histogram shows the color layers are offset
-- Issue demonstrated with MacOS 'Preview' App
I can show a similar result using only the Preview App.
-- Issue demonstrated in custom MacOS App
The code here is as bare bones as possible, creating a CIImage from the photo and immediately saving it without performing any filters. In this example I chose 0.61 for compression as it resulted in a similar file size as the original. The distortion seems to be broader if using a higher compression ratio, but I could not find any value that would eliminate it.
if let img = CIImage(contentsOf: url) {
let dest = procFolder.url(named: "InOut.jpg")
img.jpgWrite(url: dest)
}
extension CIImage {
func jpgWrite(url: URL) {
let prop: [NSBitmapImageRep.PropertyKey: Any] = [
.compressionFactor: 0.61
]
let bitmap = NSBitmapImageRep(ciImage: self)
let data = bitmap.representation(using: NSBitmapImageRep.FileType.jpeg, properties: prop)
do {
try data?.write(to: url, options: .atomic)
} catch {
log.error(error)
}
}
}
Update 1: Using @Frank Schlegel's answer for saving JPG file
The JPG now carries a Color Sync Profile, and I can (unscientifically) track a ~10% performance boost for portrait images (less for landscape), which are nice improvements. But, unfortunately the resulting file is still skewing the colors in the same way demonstrated in the histograms above.
extension CIImage {
static let writeContext = CIContext(mtlDevice: MTLCreateSystemDefaultDevice()!, options: [
// using an extended working color space allows you to retain wide gamut information, e.g., if the input is in DisplayP3
.workingColorSpace: CGColorSpace(name: CGColorSpace.extendedSRGB)!,
.workingFormat: CIFormat.RGBAh // 16 bit color depth, needed in extended space
])
func jpgWrite(url: URL) {
// write the output in the same color space as the input; fallback to sRGB if it can't be determined
let outputColorSpace = colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
do {
try CIImage.writeContext.writeJPEGRepresentation(of: self, to: url, colorSpace: outputColorSpace, options: [:])
} catch {
}
}
}
Question:
How can I open a B&W JPG as a CIImage, and re-save a JPG file avoiding any color shifting?
I never found the underlying cause of this issue and therefore no 'true' solution as I was seeking. In discussion with @Frank Schlegel, it led to a belief that it is an artifact of Apple's jpeg converter. And the issue was certainly more apparent when using test files that appear monochrome but actually had small amount of color info in them.
The simplest fix for my app was to ensure there was no color in the source image, so I drop the saturation to 0 prior to saving the file.
let params = [
"inputBrightness": brightness, // -1...1, This filter calculates brightness by adding a bias value: color.rgb + vec3(brightness)
"inputContrast": contrast, // 0...2, this filter uses the following formula: (color.rgb - vec3(0.5)) * contrast + vec3(0.5)
"inputSaturation": saturation // 0...2
]
image.applyingFilter("CIColorControls", parameters: params)