Swift: Convert RGB (UIColor) to CMYK with ICC Profile

2.4k views Asked by At

I've successfully made the following function to convert a UIColor into CMYK values using Swift 2 code in Xcode 7.2. However, the returned values are similar to a formula based conversion:

func RGBtoCMYK(rgbColor: UIColor) -> (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) {
    /*
    let iccFileName = "CoatedGRACoL2006"
    let iccProfile: CFDataRef = CGDataProviderCreateWithFilename(iccFileName) as! CFDataRef
    let colorSpace: CGColorSpaceRef = CGColorSpaceCreateWithICCProfile(iccProfile)!
    */
    let colorSpace: CGColorSpaceRef = CGColorSpaceCreateDeviceCMYK()!
    let intent = CGColorRenderingIntent.RenderingIntentPerceptual
    let cmykColor = CGColorCreateCopyByMatchingToColorSpace(colorSpace, intent, rgbColor.CGColor, nil)
    let c: CGFloat = round(CGColorGetComponents(cmykColor)[0] * 100)
    let m: CGFloat = round(CGColorGetComponents(cmykColor)[1] * 100)
    let y: CGFloat = round(CGColorGetComponents(cmykColor)[2] * 100)
    let k: CGFloat = round(CGColorGetComponents(cmykColor)[3] * 100)
    return (c, m, y, k)
}

Instead, I'd like to use an ICC Profile. In the remarked-out area I tried to change the ColorSpace from the CGCreateDeviceCMYK() to CGCreateWithICCProfile() which is looking for NSData input. I downloaded the standard CMYK ICC Profiles from Adobe and dragged "CoatedGRACoL2006.icc" into my Asset.xcassets project folder. The code compiled but generated a long list of errors while running the app. Regarding how to change the ColorSpace to an ICC Profile based color space, any input, comments, or suggestions would be appreciated.

3

There are 3 answers

0
Thomas Zoechling On BEST ANSWER

You can use NSData to load the profile's data from your bundle. NSData and CFData are toll-free bridged so can use a NSData instance whenever an API requires aCFData one.

I moved your color conversion method into an extension:

extension UIColor {
    func colorComponentsByMatchingToColorSpace(colorSpace: CGColorSpace) -> (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) {
        let intent = CGColorRenderingIntent.RenderingIntentPerceptual
        let cmykColor = CGColorCreateCopyByMatchingToColorSpace(colorSpace, intent, self.CGColor, nil)
        let c: CGFloat = round(CGColorGetComponents(cmykColor)[0] * 100)
        let m: CGFloat = round(CGColorGetComponents(cmykColor)[1] * 100)
        let y: CGFloat = round(CGColorGetComponents(cmykColor)[2] * 100)
        let k: CGFloat = round(CGColorGetComponents(cmykColor)[3] * 100)
        return (c, m, y, k)
    }
}

To load the .icc file from your bundle and call the extension method you can use the following:

guard let iccProfileURL = NSBundle.mainBundle().resourceURL?.URLByAppendingPathComponent("CoatedGRACoL2006.icc") else {
    return;
}
guard let iccProfileData = NSData(contentsOfURL: iccProfileURL) else {
    return;
}
guard let colorSpace = CGColorSpaceCreateWithICCProfile(iccProfileData) else {
    return;
}
let components = UIColor.redColor().colorComponentsByMatchingToColorSpace(colorSpace);
print("\(components)")

Note that the above loads the profile from your bundle resources and not from the asset catalog. (So you have to make sure that the .icc file is copied into your bundle during the "Copy Bundle Resources" build phase). If you want to use the asset catalog and you target iOS 9.0 or later, you can look into NSDataAsset.

0
emrys57 On

I find CFDataRef somewhat confusing, but I looked at CGDataProvider works the first time, returns an empty image the second time and that has an extra step. I tried this code:

guard let iccPath = NSBundle.mainBundle().pathForResource("CoatedGRACoL2006", ofType: "icc") else { print("bad file"); return (0,0,0,0) }
guard let iccProfile = CGDataProviderCreateWithFilename(iccPath) else { print("bad profile"); return (0,0,0,0); }
let iccCFDataRef = CGDataProviderCopyData(iccProfile) // extra step
guard let colorSpace = CGColorSpaceCreateWithICCProfile(iccCFDataRef) else { print ("bad colorSpace"); return (0,0,0,0); }
let intent = CGColorRenderingIntent.RenderingIntentPerceptual
guard let cmykColor = CGColorCreateCopyByMatchingToColorSpace(colorSpace, intent, rgbColor.CGColor, nil) else { print("Bad color"); return (0,0,0,0) }

and that does create a cmyk color without any errors. However, on generating the CMYK components to return, I found them to be exactly the same numbers as the components you generated without the icc profile. However however, I tried

print("Recovered profile: \(CGColorSpaceCopyICCProfile(colorSpace))")

and that definitely delivered a profile, so why the numbers did not change, I do not know. That may be a different problem.

If you are able to make progress with this, I think you should take care with memory management of these unmanaged CFthings. The copy I created above has to be released; I didn't write that code. Good Luck!

0
mredig On

@thomash-zoechling 's answer updated for Swift 5:

extension UIColor {
    func colorComponentsByMatchingToColorSpace(colorSpace: CGColorSpace) -> (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) {
        let intent = CGColorRenderingIntent.perceptual
        guard let cmykColor = self.cgColor.converted(to: colorSpace, intent: intent, options: nil) else { fatalError("Couldn't convert color to CMYK.") }
        let c: CGFloat = (cmykColor.components?[0] ?? 0) * 100
        let m: CGFloat = (cmykColor.components?[1] ?? 0) * 100
        let y: CGFloat = (cmykColor.components?[2] ?? 0) * 100
        let k: CGFloat = (cmykColor.components?[3] ?? 0) * 100
        return (c,m,y,k)
    }
}

and loading the icc profile:

guard let profileURL = Bundle.main.url(forResource: "filname", withExtension: "icc") else { return }
guard let data = try? Data(contentsOf: profileURL) else { return }
guard let colorspace = CGColorSpace(iccProfileData: data as CFData) else { return }