2

TL;DR: In legacy Obj-C code, the color space param value was NULL. That is not allowed in the Swift equivalent. What value to use?

I have inherited code that reads:

unsigned char pixel[1] = {0};
CGContextRef context = CGBitmapContextCreate(
    pixel,1, 1, 8, 1, NULL, (CGBitmapInfo)kCGImageAlphaOnly
);

The port to Swift 4 CGContext is straightforward, except for that NULL color space value. Using a plausible value, I am getting nil back from CGContext.init?(). My translation is:

var pixelValue = UInt8(0)
var pixel = Data(buffer: UnsafeBufferPointer(start:&pixelValue, count:1))
let context = CGContext(
    data            : &pixel,
    width           : 1,
    height          : 1,
    bitsPerComponent: 8,
    bytesPerRow     : 1,
    space           : CGColorSpace(name:CGColorSpace.genericRGBLinear)!,
    bitmapInfo      : CGImageAlphaInfo.alphaOnly.rawValue
)! // Returns nil; unwrapping crashes

Q: What is the appropriate value for space? (The value I provide is not returning nil; it's the CGContext() call itself.

Setting the environment variable CGBITMAP_CONTEXT_LOG_ERRORS yields an error log like this:

Assertion failed: (0), function get_color_model_name, 
file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Quartz2D_Sim/
Quartz2D-1129.2.1/CoreGraphics/API/CGBitmapContextInfo.c, line 210.

For some more backstory, the context was used to find the alpha value of a single pixel in a UIImage in the following way:

unsigned char pixel[1] = {0};
CGContextRef context = CGBitmapContextCreate(pixel,1, 1, 8, 1, NULL, (CGBitmapInfo)kCGImageAlphaOnly);
UIGraphicsPushContext(context);
[image drawAtPoint:CGPointMake(-point.x, -point.y)];
UIGraphicsPopContext();
CGContextRelease(context);
CGFloat alpha = pixel[0]/255.0;

(I do have possible alternatives for finding alpha, but in the interest of leaving legacy code alone, would like to keep it this way.)

Andrew Duncan
  • 3,553
  • 4
  • 28
  • 55
  • Have you looked at: https://developer.apple.com/documentation/coregraphics/cgcolorspace/1408921-init – DonMag Jan 10 '18 at 20:55
  • Try the `linearGray` color space. – rmaddy Jan 10 '18 at 22:11
  • @DonMag, I did read the documentation. That's how I found the `ColorSpace` names. Which I did use in the call as shown above. – Andrew Duncan Jan 10 '18 at 23:58
  • 1
    @rmaddy, thank you, that did allow the initialization of CGContext. Alas, the Swift 4 version of the (rest of) the code calculates 0 for every alpha. But I do believe you answered my question. – Andrew Duncan Jan 11 '18 at 00:00
  • @AndrewDuncan, did you ever find a solution for the wrong alpha calculation? – Marmoy Jul 27 '18 at 11:58

3 Answers3

1

I recently worked with similar topic, maybe this code sample will help someone:

let image = UIImage(named: "2.png")
guard let cgImage = image?.cgImage else {
    fatalError()
}

let width = cgImage.width
let height = cgImage.height
//CGColorSpaceCreateDeviceGray - 1 component, 8 bits
//i.e. 1px = 1byte
let bytesPerRow = width
let bitmapByteCount = width * height

let bitmapData: UnsafeMutablePointer<UInt8> = .allocate(capacity: bitmapByteCount)
defer {
    bitmapData.deallocate()
}
bitmapData.initialize(repeating: 0, count: bitmapByteCount)

guard let context = CGContext(data: bitmapData, width: width, height: height,
                              bitsPerComponent: 8, bytesPerRow: bytesPerRow,
                              space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.alphaOnly.rawValue) else {
                                fatalError()
}
//draw image to context
var rect = CGRect(x: 0, y: 0, width: width, height: height)
context.draw(cgImage, in: rect)

// Enumerate through all pixels
for row in 0..<height {
    for col in 0..<width {
        let alphaValue = bitmapData[row * width + col]
        if alphaValue != 0 {
            //visible pixel
        }
    }
}
Kirow
  • 1,077
  • 12
  • 25
0

Here’s how to determine whether a pixel is transparent:

    let info = CGImageAlphaInfo.alphaOnly.rawValue
    let pixel = UnsafeMutablePointer<UInt8>.allocate(capacity:1)
    defer {
        pixel.deinitialize(count: 1)
        pixel.deallocate()
    }
    pixel[0] = 0
    let sp = CGColorSpaceCreateDeviceGray()
    let context = CGContext(data: pixel,
        width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 1,
        space: sp, bitmapInfo: info)!
    UIGraphicsPushContext(context)
    im.draw(at:CGPoint(-point.x, -point.y))
    UIGraphicsPopContext()
    let p = pixel[0]
    let alpha = Double(p)/255.0
    let transparent = alpha < 0.01
matt
  • 515,959
  • 87
  • 875
  • 1,141
0

For the record, here is how I wound up doing it. It hasn't (yet) misbehaved, so on the principle of "If it ain't broke, don't fix it" I'll leave it. (I have added self for clarity.) But you can be sure that I will paste Matt's code right in there, in case I need it in the future. Thanks Matt!

// Note that "self" is a UIImageView; "point" is the point under consideration.

let im = self.image!

// TODO: Why is this clamping necessary? We get points outside our size.
var x = point.x
var y = point.y
if x < 0 { x = 0 } else if x > im.size.width  - 1 { x = im.size.width  - 1 }
if y < 0 { y = 0 } else if y > im.size.height - 1 { y = im.size.height - 1 }

let screenWidth    = self.bounds.width
let intrinsicWidth = im.size.width

x *= im.scale * intrinsicWidth/screenWidth
y *= im.scale * intrinsicWidth/screenWidth

let pixData = im.cgImage?.dataProvider?.data
let data    = CFDataGetBytePtr(pixData!)

let pixIndex = Int(((Int(im.size.width*im.scale) * Int(y)) + Int(x)) * 4)

let r = data?[pixIndex]
let g = data?[pixIndex + 1]
let b = data?[pixIndex + 2]
let α = data?[pixIndex + 3]

let red    = CGFloat(r!)/255
let green  = CGFloat(g!)/255
let blue   = CGFloat(b!)/255
let alpha  = CGFloat(α!)/255
Andrew Duncan
  • 3,553
  • 4
  • 28
  • 55