swiftmacosnsimagecgimagensbitmapimagerep

setColor on NSBitmapImageRep not working in Swift


I'm trying to figure out how setColor works. I have the following code:

    
    
    lazy var imageView:NSImageView = {
        let imageView = NSImageView(frame: view.frame)
        return imageView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        createColorProjection()
        view.wantsLayer = true
        view.addSubview(imageView)
        
        view.needsDisplay = true
    }
    
    func createColorProjection() {
        var bitmap = NSBitmapImageRep(cgImage: cgImage!)
        var x = 0
        while x < bitmap.pixelsWide {
            var y = 0
            while y < bitmap.pixelsHigh {
                //pixels[Point(x: x, y: y)] = (getColor(x: x, y: y, bitmap: bitmap))
                bitmap.setColor(NSColor(cgColor: .black)!, atX: x, y: y)
                y += 1
            }
            x += 1
        }
        
        let image = createImage(bitmap: bitmap)
        imageView.image = image
        imageView.needsDisplay = true
    }
    
    
    func createImage(bitmap:NSBitmapImageRep) -> NSImage {
        let image = bitmap.cgImage
        return NSImage(cgImage: image! , size: CGSize(width: image!.width, height: image!.height))
    }

The intention of the code is to change a photo (a rainbow) to be entirely black (I'm just testing with black right now to make sure I understand how it works). However, when I run the program, the unchanged picture of the rainbow is shown, not a black photo.

I am getting these errors: Unrecognized colorspace number -1 and Unknown number of components for colorspace model -1.

Thanks.


Solution

  • First, you're right: setColor has been broken at least since Catalina. Apple hasn't fixed it probably because it's so slow and inefficient, and nobody ever used it.

    Second, docs say NSBitmapImageRep(cgImage: CGImage) produces a read-only bitmap so your code wouldn't have worked even if setColor worked.

    As Alexander says, making your own CIFilter is the best way to change a photo's pixels to different colors. Writing and implementing the OpenGL isn't easy, but it's the best.

    If you were to add an extension to NSBitmapImageRep like this:

    extension NSBitmapImageRep {
        func setColorNew(_ color: NSColor, atX x: Int, y: Int) {
            guard let data = bitmapData else { return }
            
            let ptr = data + bytesPerRow * y + samplesPerPixel * x
            
            ptr[0] = UInt8(color.redComponent * 255.1)
            ptr[1] = UInt8(color.greenComponent * 255.1)
            ptr[2] = UInt8(color.blueComponent * 255.1)
            
            if samplesPerPixel > 3 {
                ptr[3] = UInt8(color.alphaComponent * 255.1)
            }
        }
    }
    

    Then simply changing an image's pixels could be done like this:

    func changePixels(image: NSImage, newColor: NSColor) -> NSImage {
        guard let imgData = image.tiffRepresentation,
              let bitmap = NSBitmapImageRep(data: imgData),
              let color = newColor.usingColorSpace(.deviceRGB)
        else { return image }
        
        var y = 0
        while y < bitmap.pixelsHigh {
            var x = 0
            while x < bitmap.pixelsWide {
                bitmap.setColorNew(color, atX: x, y: y)
                x += 1
            }
            y += 1
        }
        
        let newImage = NSImage(size: image.size)
        newImage.addRepresentation(bitmap)
        
        return newImage
    }