Skip to content
This repository has been archived by the owner on Dec 7, 2018. It is now read-only.

[Sad Trombone] Image pixel data access #57

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
27 changes: 27 additions & 0 deletions Prototope/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,33 @@ public struct Color {
init(_ uiColor: UIColor) {
self.uiColor = uiColor
}

public func getRGBAValues() -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
let colorSpace = CGColorGetColorSpace(self.uiColor.CGColor);
let colorSpaceModel = CGColorSpaceGetModel(colorSpace);

var r = 0 as CGFloat
var g = 0 as CGFloat
var b = 0 as CGFloat
var a = 0 as CGFloat

switch colorSpaceModel.value {

case kCGColorSpaceModelMonochrome.value:
var w = 0 as CGFloat
self.uiColor.getWhite(&w, alpha: &a)
r = w
g = w
b = w
case kCGColorSpaceModelRGB.value:
self.uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)

default:
fatalError("unsupported color space for color: \(self)")
}

return (r, g, b, a)
}

public static var black: Color { return Color(UIColor.blackColor()) }
public static var darkGray: Color { return Color(UIColor.darkGrayColor()) }
Expand Down
104 changes: 104 additions & 0 deletions Prototope/Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,108 @@ public struct Image {
init(_ image: UIImage) {
uiImage = image
}

public func toPixels() -> PixelBitmap {
return PixelBitmap(image: self)
}
}

public struct PixelBitmap : MutableCollectionType {
typealias Index = Int

init(image: Image) {
width = Int(image.size.width)
height = Int(image.size.height)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful: Image.size reports sizes in points, but for these CG APIs, you'll need to work in pixels.


data = UnsafeMutablePointer<Pixel>.alloc(width*height)

let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.rawValue)
let bytesPerRow = width * 4

let context = CGBitmapContextCreate(data, width, height, 8, bytesPerRow, colorSpace, bitmapInfo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This context is leaking :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right you are, @NachoSoto, thanks!
’tis not the context that is leaking (CF objects are automatically memory-managed), but the data allocated on #57 (diff).

I added a PixelDataDestroyer class to properly clean up the underlying PixelBitmap data on deinit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you're right, I was thinking of UIGraphicsEndImageContext()


CGContextDrawImage(context, CGRect(origin: CGPointZero, size: image.uiImage.size), image.uiImage.CGImage)
}

var data: UnsafeMutablePointer<Pixel>
let width: Int
let height: Int

public let startIndex: Int = 0

public var endIndex: Int { return height * width }

public subscript (position: Int) -> Pixel {
get { return data[position] }
set { data[position] = newValue }
}

public func pixelAt(row: Int, column: Int) -> Pixel {
let idx = row * width + column
return self[idx]
}

public func generate() -> IndexingGenerator<PixelBitmap> {
return IndexingGenerator(self)
}

public func toImage() -> Image {
let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.rawValue)
let context = CGBitmapContextCreate(data, width, height, 8, width * 4, colorSpace, bitmapInfo)
let cgImage = CGBitmapContextCreateImage(context)

return Image(UIImage(CGImage: cgImage)!)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto note about scale here.

}

public func transform(transform: Pixel -> Pixel) -> PixelBitmap {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we call these map for consistency?

var newBitmap = self
for idx in startIndex..<endIndex {
newBitmap[idx] = transform(self[idx])
}

return newBitmap
}

public func transform(transform: (position: Int, Pixel) -> Pixel) -> PixelBitmap {
var newBitmap = self
for idx in startIndex..<endIndex {
newBitmap[idx] = transform(position: idx, self[idx])
}

return newBitmap
}

public func transform(transform: (row: Int, column: Int, Pixel) -> Pixel) -> PixelBitmap {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the multiple API endpoints here.

var newBitmap = self
for idx in startIndex..<endIndex {
let row = idx % width
let column = idx - (row * width)
newBitmap[idx] = transform(row: row, column:column, self[idx])
}

return newBitmap
}
}


/** A representation of a single pixel in an RGBA bitmap image. */
public struct Pixel {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To hide the UInt8 ugliness, you could name these properties redRaw and expose red: Float as a computed property.

public var red: UInt8
public var green: UInt8
public var blue: UInt8
public var alpha: UInt8

public var color: Color {
get { return Color(red: Double(red/255), green: Double(green/255), blue: Double(blue/255), alpha: Double(alpha/255)) }
set {
let (r, g, b, a) = newValue.getRGBAValues()
self.red = UInt8(r*255)
self.green = UInt8(g*255)
self.blue = UInt8(b*255)
self.alpha = UInt8(a*255)
}
}
}

39 changes: 36 additions & 3 deletions PrototopeTestApp/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ class ViewController: UIViewController {
Environment.currentEnvironment = Environment.defaultEnvironmentWithRootView(view)

// You might write a prototype in Swift...
//runSwiftPrototype()
runSwiftPrototype()

// ... or in JavaScript. (uncomment one; comment out the other)
runJSPrototope()
// runJSPrototope()
}

func runSwiftPrototype() {
Expand All @@ -34,7 +34,40 @@ class ViewController: UIViewController {

func makeRedLayer(name: String, y: Double) -> Layer {
let redLayer = Layer(parent: Layer.root, name: name)
redLayer.image = Image(name: "paint")

let redImage = Image(name: "paint")
let pixels = redImage.toPixels()

let width = redImage.size.width

var modified = pixels

// White-out every pixel in even rows and columns
modified = modified.transform { row, column, pixel in
var newPixel = pixel
newPixel.red = (row % 2 == 0) || (column % 2 == 0) ? 255 : newPixel.red
newPixel.green = (row % 2 == 0) || (column % 2 == 0) ? 255 : newPixel.green
newPixel.blue = (row % 2 == 0) || (column % 2 == 0) ? 255 : newPixel.blue
return newPixel
}

// Make every tenth pixel blue
modified = modified.transform { idx, pixel in
var newPixel = pixel
newPixel.blue = idx % 10 == 0 ? 255 : pixel.blue
return newPixel
}

// Invert all pixels
modified = modified.transform { (pixel: Pixel) -> Pixel in
var newPixel = pixel
newPixel.red = 255 - pixel.red
newPixel.green = 255 - pixel.green
newPixel.blue = 255 - pixel.blue
return newPixel
}

redLayer.image = modified.toImage()
tunable(50, name: "x") { value in redLayer.frame.origin = Point(x: value, y: y) }
redLayer.backgroundColor = Color.red
redLayer.cornerRadius = 10
Expand Down