swiftmacoscocoansdatacgimage

Turning an array of RGB pixel data into CGImage in Swift


I'm really really really close here but I just don't know enough with why this is going wrong. It looks like either NSData or CGDataProvider or CGImage is skipping through every 8th row and then filling the remaining canvas with garbage so I'm only getting 1/8th of my image bunched at the top.

pixeldata.png rendered

I think this must be some math error, but after going through the code several times, I'm not sure where this might be.

I'm trying to learn some Swift on MacOS and after a couple weeks I've managed to get this far and start doing some interesting things. If someone could help me out here I'd really appreciate it!

Also, it'd be good to know if I'm the right track for efficiently plotting individual pixels. It doesn't seem to be a thing people do so hard to find good examples of this.

import Foundation
import Cocoa
import UniformTypeIdentifiers

extension CGImage
{
    //https://stackoverflow.com/questions/1320988/saving-cgimageref-to-a-png-file
    var png: Data?
    {
        let cfdata: CFMutableData = CFDataCreateMutable(nil, 0)
        if let destination = CGImageDestinationCreateWithData(cfdata, String(describing: UTType.png) as CFString, 1, nil)
        {
            CGImageDestinationAddImage(destination, self, nil)
            if CGImageDestinationFinalize(destination)
            {
                return cfdata as Data
            }
        }
        return nil
    }
}

//https://blog.human-friendly.com/drawing-images-from-pixel-data-in-swift
struct PixelData {
    var a:UInt8 = 255
    var r:UInt8
    var g:UInt8
    var b:UInt8
}

func pixeldata_to_image(pixels: [PixelData], width: Int, height: Int)->CGImage
{
    assert(pixels.count == Int(width * height))
    let bitsPerComponent: Int = 8
    let bitsPerPixel: Int = 32
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)

    var data = pixels
    guard
        let providerRef = CGDataProvider(data: NSData(bytes: &data, length: data.count * bitsPerPixel))
    else
    {
        fatalError("CGDataProvider failure")
    }
    guard
        let image = CGImage(
            width: width,
            height: height,
            bitsPerComponent:bitsPerComponent,
            bitsPerPixel: bitsPerPixel,
            bytesPerRow: width * bitsPerPixel,
            space: rgbColorSpace,
            bitmapInfo: bitmapInfo,
            provider: providerRef,
            decode: nil,
            shouldInterpolate: true,
            intent: .defaultIntent
        )
    else
    {
        fatalError("CGImage failure")
    }
    return image
}

func testUsage()
{
    let range_x: Int = 200
    let range_y: Int = 200
    let greyPixel = PixelData(a: 255, r: 128, g: 128, b: 128)
    let blackPixel = PixelData(a: 255, r: 0, g: 0, b: 0)
    let whitePixel = PixelData(a: 255, r: 255, g: 255, b: 255)
    let yellowPixel = PixelData(a: 255, r: 255, g: 255, b: 0)
    let greenPixel = PixelData(a: 255, r: 0, g: 255, b: 0)
    let redPixel = PixelData(a: 255, r: 255, g: 0, b: 0)
    var pixelData = [PixelData](repeating: greyPixel, count: Int(range_x * range_y))
    var index: Int = 0
    //populate pixelData
    for x in 0 ..< range_x
    {
        for y in 0 ..< range_y
        {
            let pixel: PixelData
            if x == 0 && y == 0 || x == 0 && y == range_x - 1 || x == 8 && y == 8
            {
                //grey draw specfic pixels
                pixel = greyPixel
            }
            else if x == y
            {
                //yellow draw diagonal from top-left to bottom-right
                pixel = yellowPixel
            }
            else if y == 0 || x == 0
            {
                //dblack raw top line and draw left line
                pixel = blackPixel
            }
            else if y == range_y - 1 || x == range_y - 1
            {
                //white draw bottom line and draw right line
                pixel = whitePixel
            }
            else if y == range_y / 2 || x == range_x / 2
            {
                //green draw horizontal center line and draw vertical center line
                pixel = greenPixel
            }
            else
            {
                pixel = redPixel
            }
            //print("x:", x, "y:", y, "index:", index, "pixel:", pixel) //sanity check
            pixelData[index] = pixel
            index += 1
        }
    }
    let image: CGImage = pixeldata_to_image(pixels: pixelData, width: range_x, height: range_y)
    try? image.png!.write(to: URL(fileURLWithPath: "pixeldata.png"))
}


Solution

  • As pointed out in a comment I confused some bits and bytes. I added these two lines:

        let bitsPerByte = 8
        let bytesPerPixel: Int = bitsPerPixel/bitsPerByte
    

    Then modified these two lines:

            -let providerRef = CGDataProvider(data: NSData(bytes: &data, length: data.count * bitsPerPixel))
            +let providerRef = CGDataProvider(data: NSData(bytes: &data, length: data.count * bytesPerPixel))
    

    and

                -bytesPerRow: width * bitsPerPixel,
                +bytesPerRow: width * bytesPerPixel,
    

    Works like a charm now!

    enter image description here