3

Is there a way I can change the color space of an NSimage or NSBitmapImageRep/CGimage or the like. I am open to any way. Preferably the way photoshop does it.

Alex Zielenski
  • 3,591
  • 1
  • 26
  • 44

2 Answers2

5

The problem with the code you see below

CGImageRef CGImageCreateCopyWithColorSpace (
CGImageRef image,
CGColorSpaceRef colorspace
);

is not working because Quartz2D does not support alpha for CMYK images or gray images, it is supported only by RGB images. What you should do is creating 2 images then combine then in case of a CMYK color space and image with alpha. I have searched a lot about this topic and finally found. May be author of this question doesn't need it anymore but someone else may need it.

  1. Create new NSBitmapImageRep

    let imageRep = NSBitmapImageRep(bitmapDataPlanes: nil,
                                          pixelsWide: Int(round(imageSize.width)),
                                          pixelsHigh: Int(round(imageSize.height)),
                                       bitsPerSample: 8,
                                     samplesPerPixel: 4,
                                            hasAlpha: false,
                                            isPlanar: false,
                                      colorSpaceName: NSDeviceCMYKColorSpace,
                                        bitmapFormat: NSBitmapFormat(rawValue: 0),
                                         bytesPerRow: Int(round(imageSize.width) * CGFloat(4)),
                                        bitsPerPixel: 0)
    
  2. You need to draw your image into the new bitmap lets say you have colorSpace: NSColorSpace

    let context = NSGraphicsContext(bitmapImageRep: imageRep)
    NSGraphicsContext.saveGraphicsState()
    NSGraphicsContext.setCurrentContext(context)
    imageRep.setProperty(NSImageColorSyncProfileData, withValue: colorSpace.ICCProfileData)
    // Do your drawing here
    NSGraphicsContext.restoreGraphicsState()
    
  3. After this you will have an imageRep that is the image with correct color space but no alpha (transparency).

  4. You need a mask bitmap. Obtaining mask is tricky.

    let imageRep = NSBitmapImageRep(bitmapDataPlanes: nil,
                                          pixelsWide: Int(round(imageSize.width)),
                                          pixelsHigh: Int(round(imageSize.height)),
                                       bitsPerSample: 8,
                                     samplesPerPixel: 1,
                                            hasAlpha: false,
                                            isPlanar: false,
                                      colorSpaceName: NSDeviceWhiteColorSpace,
                                        bitmapFormat: NSBitmapFormat(rawValue: 0),
                                         bytesPerRow: Int(round(imageSize.width)),
                                        bitsPerPixel: 0)
    
  5. Clip white image to your image with transparency

    if let graphicsPort = NSGraphicsContext.currentContext()?.graphicsPort {
        let context = unsafeBitCast(graphicsPort, CGContextRef.self)
        let imgRect = NSRect(origin: NSPoint.zero, size: image.extent.size)
        let ciContext = CIContext()
        let cgImage = ciContext.createCGImage(image, fromRect: image.extent)
        CGContextClipToMask(context, imgRect, cgImage)
    }
    
  6. Colorize all pixel in white color, this will be clipped to image.

    let context = NSGraphicsContext(bitmapImageRep: imageRep)
    NSGraphicsContext.saveGraphicsState()
    NSGraphicsContext.setCurrentContext(context)
    imageRep.setProperty(NSImageColorSyncProfileData, withValue: colorSpace.ICCProfileData)
    NSColor.whiteColor().setFill()
    NSBezierPath.fillRect(NSRect(origin: NSPoint.zero, size: imageSize))
    NSGraphicsContext.restoreGraphicsState()
    
  7. At this step you have a image and a mask. Next we need to combine them. I am using this little algorithm (you need to verify that mask and source image have same sizes and same colorSpaceModel):

    func createCMYKAImageRepByApplingAlphaMask(srcImageRep: NSBitmapImageRep, alphaMaskImageRep alphaMask: NSBitmapImageRep) -> NSBitmapImageRep? {
        if canApplyMaskRepOnImageRep(srcImageRep, maskRep: alphaMask) == false {
            return nil
        }
        let alphaData = alphaMask.bitmapData
        let srcData = srcImageRep.bitmapData
        if let imageWithAlphaMaskRep = createEmptyCMYKABitmapImageRep(alphaMask.size) {
            if let colorSpaceData = imageColorSpace?.ICCProfileData {
                 imageWithAlphaMaskRep.setProperty(NSImageColorSyncProfileData, withValue: colorSpaceData)
            }
            fillPixelsWithComponentsData(imageWithAlphaMaskRep, components: { (pixelIdx: Int) -> (UInt8, UInt8, UInt8, UInt8, UInt8) in
                let cyan = srcData[pixelIdx * 4 + 0]
                let magenta = srcData[pixelIdx * 4 + 1]
                let yellow = srcData[pixelIdx * 4 + 2]
                let black = srcData[pixelIdx * 4 + 3]
                let alpha = alphaData[pixelIdx]
                return (cyan, magenta, yellow, black, alpha)
            })
            return imageWithAlphaMaskRep
        }
        return nil
    }
    
    func createEmptyBitmapImageRep() -> NSBitmapImageRep? {
        let imageRep = NSBitmapImageRep(bitmapDataPlanes: nil,
                                              pixelsWide: Int(round(imageSize.width)),
                                              pixelsHigh: Int(round(imageSize.height)),
                                           bitsPerSample: 8,
                                         samplesPerPixel: 5,
                                                hasAlpha: true,
                                                isPlanar: false,
                                          colorSpaceName: NSDeviceCMYKColorSpace,
                                            bitmapFormat: NSBitmapFormat(rawValue: 0),
                                             bytesPerRow: Int(round(imageSize.width) * CGFloat(5)),
                                            bitsPerPixel: 0)
        return imageRep
    }
    
    private func fillPixelsWithComponentsData(imgRep: NSBitmapImageRep,
    components: (Int) -> (cyan:UInt8, magenta:UInt8, yellow:UInt8, black:UInt8, alpha:UInt8)) {
        let imageRawData = imgRep.bitmapData
        let imageWidth = Int(imgRep.size.width)
        let imageHeight = Int(imgRep.size.height)
        for pixelIdx in 0 ..< (imageWidth * imageHeight) {
            let (cyan, magenta, yellow, black, alpha) = components(pixelIdx)
            let fAlpha = Float(alpha) / 255
            imageRawData[pixelIdx * 5 + 0] = UInt8(Float(cyan) * fAlpha)
            imageRawData[pixelIdx * 5 + 1] = UInt8(Float(magenta) * fAlpha)
            imageRawData[pixelIdx * 5 + 2] = UInt8(Float(yellow) * fAlpha)
            imageRawData[pixelIdx * 5 + 3] = UInt8(Float(black) * fAlpha)
            imageRawData[pixelIdx * 5 + 4] = alpha
        }
    }
    
HAS
  • 19,140
  • 6
  • 31
  • 53
ion
  • 819
  • 10
  • 8
  • @HAS Hello mates, how about if I want to create an greyscaled image instead of a CYMK? Do I still need to create an alpha mask? But it seems doesnt make sense for a greyscaled image to have a alpha channel.. I tried simple without doing a mask, but it returns a black image.. Or, should I just simply access to and take average from the colour channels ? – user3390652 Feb 04 '17 at 19:12
  • @user3390652 "Or, should I just simply access to and take average from the colour channels ? " I think that's a very good option! (didn't really think about it, but probably only the average of the RGB channels and leave the alpha as-is). – HAS Feb 04 '17 at 19:14
  • @HAS By the way, would you mind give an example of your step two above please? `//do your drawing here` I tried, with a `NSImage image`, `image.representation.first.draw()`... It seems only work with within a `lock focus` but not a `graphic state`. This is hence I am struggling why I have got an blank image with all pixels are zeros(I loaded the image into MATLAB and found this out..).. – user3390652 Feb 04 '17 at 19:29
  • @user3390652 This is not my answer, I just edited some time ago to make it more readable, maybe @ostafi-ion can chime in? Have you tried [`image.draw(in: CGRect(origin: .zero, size: imageSize)`](https://developer.apple.com/reference/appkit/nsimage/1519863-draw)? – HAS Feb 04 '17 at 19:33
  • 1
    @HAS I have found a way to grayscale an image. I have posted my code and my references in [My post](http://stackoverflow.com/questions/42036448/swift-3-colour-space-macos-not-ios/42046118#42046118), although I don't know if there can still be a better way of doing it. – user3390652 Feb 04 '17 at 22:03
4
CGImageRef CGImageCreateCopyWithColorSpace (
   CGImageRef image,
   CGColorSpaceRef colorspace
);

To get a CGColorSpaceRef, you can do things like

CGColorSpaceRef CGColorSpaceCreateDeviceRGB();

OR see this page for how to create other color spaces.

Rethunk
  • 3,976
  • 18
  • 32
Amy Worrall
  • 16,250
  • 3
  • 42
  • 65
  • Hmm. I tried that and it didn't work when I tried to use a GrayScale colorspace. – Alex Zielenski Jul 15 '10 at 17:20
  • This is a good thing to know, but not an answer to the question. The question was about how to change the colour space of an image, not how to create a colour space. – danpalmer Aug 17 '11 at 08:33
  • 5
    I did answer the question. The first function I posted was how to create a new image in a specific colour space. The idea is you make yourself a colour space ref for the desired colour space, then you use `CGImageCreateCopyWithColorSpace()` to create a new copy of your image in the new colour space. – Amy Worrall Aug 17 '11 at 10:14
  • I hadn't seen that the question included CGImage. The above method does not work for NSImage or NSBitmapImageRep. – danpalmer Aug 17 '11 at 10:36
  • 1
    @danpalmer: It's possible to go from NSBitmapImageRep to CGImage and back on 10.5 and later, and from NSImage to CGImage and back on 10.6 and later. – Peter Hosey Apr 22 '13 at 08:51