Skip to content

Instantly share code, notes, and snippets.

@JoshuaSullivan
Last active April 25, 2023 16:45
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JoshuaSullivan/5951e08ff0f3e155ef52220a181864e8 to your computer and use it in GitHub Desktop.
Save JoshuaSullivan/5951e08ff0f3e155ef52220a181864e8 to your computer and use it in GitHub Desktop.
The Color Cube Image Creator creates the specially formatted images needed to create data for the CIColorCube filter.
//
// ColorCubeImageCreator.swift
// ColorCubeImageCreator
//
// Created by Joshua Sullivan on 4/25/16.
// Copyright © 2016 Joshua Sullivan. All rights reserved.
//
import UIKit
public final class ColorCubeImageCreator {
enum Dimension: Int {
/// A very small color cube. May exhibit posterization.
case four = 4
/// This size is good enough for many applications using noisy or lower-quality input.
case sixteen = 16
/// Higher quality. There is rarely a need to go beyond this setting.
case sixtyFour = 64
/// Excessive quality. Image is 4096x4096 and almost 35MB.
case twoHundredFiftySix = 256
}
/// Creates a reference color cube image for the indicated size and writes it to disk.
/// It is necessary to write to disk as part of this method in order to avoid either
/// leaking the image buffer or having it garbage collected before the file can be written.
///
/// - Parameter size: A value from the `ColorCubeImageCreator.Dimension` enum.
/// - Parameter saveLocation: An optional URL for where to store the created image.
/// If left `nil` the method will place the image in the Documents folder in the app sandbox.
/// - Returns: A Bool indicating the success or failure of the operation.
static func createColorCube(size: Dimension, saveLocation: NSURL?) -> Bool {
let cubeSize = size.rawValue
/// The total number of pixels we'll need to represnt all points in the cube.
let pixels = cubeSize * cubeSize * cubeSize
/// The square dimensions of the image that will store exactly enough pixels.
/// This is why I limit the available dimensions. The other powers of 2 have
/// non-integral square roots.
let imageSize = Int(sqrt(Double(pixels)))
/// We're only encoding RGB. No alpha.
let channels = 3
/// A UInt8 is pretty obviously single-byte.
let bytesPerChannel = 1
/// The total number of bytes we will need to encode all of the data.
let memorySize = pixels * channels * bytesPerChannel
/// Pre-calculate the square of the cube size.
let cubeSizeSquared = cubeSize * cubeSize
/// This is the storage we'll be writing our RGB values to.
let imageBuffer = UnsafeMutablePointer<UInt8>.alloc(memorySize)
// For the 2 smallest sizes, the dealloc command can have the effect of destroying the bytes
// backing the image before it can be written to disk, resulting in a corrupted image. That
// is why we save the image as part of this block.
defer { imageBuffer.dealloc(memorySize) }
/// The amount to vary the red, green, and blue channel values by at each step of the calculation.
let colorStep = 255.0 / Float(cubeSize - 1)
/// The offset in the imageBuffer we're working on.
var offset = 0
for i in 0..<pixels {
offset = i * channels
// Red value
imageBuffer[offset + 0] = UInt8(round(Float(i % cubeSize) * colorStep))
// Green value
imageBuffer[offset + 1] = UInt8(round(Float((i / cubeSize) % cubeSize) * colorStep))
// Blue value
imageBuffer[offset + 2] = UInt8(round(Float(i / cubeSizeSquared) * colorStep))
}
/// Data provide created with the calculated values in the imageBuffer.
let dataProvider = CGDataProviderCreateWithData(nil, imageBuffer, memorySize, nil)
// The following are a bunch of setup values for the CGImageCreate() function.
let bitsPerComponent = 8 * bytesPerChannel
let bitsPerPixel = channels * bitsPerComponent
let bytesPerRow = imageSize * channels
let colorSpace = CGColorSpaceCreateDeviceRGB()
let alphaInfo = CGImageAlphaInfo.None.rawValue
let bitmapInfo: CGBitmapInfo = [.ByteOrderDefault, CGBitmapInfo(rawValue: alphaInfo)]
let renderingIntent: CGColorRenderingIntent = .RenderingIntentDefault
// Attempt to process the buffer of bytes into an image.
guard let imageRef = CGImageCreate(imageSize, imageSize, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, bitmapInfo, dataProvider, nil, false, renderingIntent) else {
assertionFailure("Unable to create CGImage.")
return false
}
/// The UIImage created from the buffer data.
let cubeImage = UIImage(CGImage: imageRef)
/// The PNG representation of the `cubeImage`.
let imageData = UIImagePNGRepresentation(cubeImage)
// Check that we can establish a URL for saving.
guard let imageURL = saveLocation ?? defaultLocationForSize(size) else {
assertionFailure("Can't access save location.")
return false
}
// Try to write the image to disk.
do {
try imageData?.writeToURL(imageURL, options: [])
print("Image successfully written to:", imageURL.absoluteString)
return true
} catch let error {
print("Failed to write image:", error)
return false
}
}
/// Generates default URLs for the various Dimension values.
private static func defaultLocationForSize(size: Dimension) -> NSURL? {
let imageName = "ColorCubeReferenceImage\(size.rawValue).png"
guard let docsDir = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first else {
assertionFailure("Can't get documents directory!")
return nil
}
let imageURL = docsDir.URLByAppendingPathComponent(imageName)
return imageURL
}
}
//
// ColorCubeImageCreator.swift
// ColorCubeImageCreator
//
// Created by Joshua Sullivan on 10/01/16.
// Copyright © 2016 Joshua Sullivan. All rights reserved.
//
import UIKit
public final class ColorCubeImageCreator {
enum Dimension: Int {
/// A very small color cube. May exhibit posterization.
case four = 4
/// This size is good enough for many applications using noisy or lower-quality input.
case sixteen = 16
/// Higher quality. There is rarely a need to go beyond this setting.
case sixtyFour = 64
/// Excessive quality. Image is 4096x4096 and almost 35MB.
case twoHundredFiftySix = 256
}
/// Creates a reference color cube image for the indicated size and writes it to disk.
/// It is necessary to write to disk as part of this method in order to avoid either
/// leaking the image buffer or having it garbage collected before the file can be written.
///
/// - Parameter size: A value from the `ColorCubeImageCreator.Dimension` enum.
/// - Parameter saveLocation: An optional URL for where to store the created image.
/// If left `nil` the method will place the image in the Documents folder in the app sandbox.
/// - Returns: A Bool indicating the success or failure of the operation.
static func createColorCube(size: Dimension, saveLocation: URL?) -> Bool {
let cubeSize = size.rawValue
/// The total number of pixels we'll need to represnt all points in the cube.
let pixels = cubeSize * cubeSize * cubeSize
/// The square dimensions of the image that will store exactly enough pixels.
/// This is why I limit the available dimensions. The other powers of 2 have
/// non-integral square roots.
let imageSize = Int(sqrt(Double(pixels)))
/// We're only encoding RGB. No alpha.
let channels = 3
/// A UInt8 is pretty obviously single-byte.
let bytesPerChannel = 1
/// The total number of bytes we will need to encode all of the data.
let memorySize = pixels * channels * bytesPerChannel
/// Pre-calculate the square of the cube size.
let cubeSizeSquared = cubeSize * cubeSize
/// This is the storage we'll be writing our RGB values to.
let imageBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: memorySize)
// For the 2 smallest sizes, the dealloc command can have the effect of destroying the bytes
// backing the image before it can be written to disk, resulting in a corrupted image. That
// is why we save the image as part of this block.
defer { imageBuffer.deallocate(capacity: memorySize) }
/// The amount to vary the red, green, and blue channel values by at each step of the calculation.
let colorStep = 255.0 / Float(cubeSize - 1)
/// The offset in the imageBuffer we're working on.
var offset = 0
for i in 0..<pixels {
offset = i * channels
// Red value
imageBuffer[offset + 0] = UInt8(round(Float(i % cubeSize) * colorStep))
// Green value
imageBuffer[offset + 1] = UInt8(round(Float((i / cubeSize) % cubeSize) * colorStep))
// Blue value
imageBuffer[offset + 2] = UInt8(round(Float(i / cubeSizeSquared) * colorStep))
}
/// Data provide created with the calculated values in the imageBuffer.
let callback: CGDataProviderReleaseDataCallback = { _,_,_ in }
guard let dataProvider = CGDataProvider(dataInfo: nil, data: imageBuffer, size: memorySize, releaseData: callback) else {
preconditionFailure("Couldn't create CGDataProvider.")
}
// The following are a bunch of setup values for the CGImageCreate() function.
let bitsPerComponent = 8 * bytesPerChannel
let bitsPerPixel = channels * bitsPerComponent
let bytesPerRow = imageSize * channels
let colorSpace = CGColorSpaceCreateDeviceRGB()
let alphaInfo = CGImageAlphaInfo.none.rawValue
let bitmapInfo: CGBitmapInfo = [CGBitmapInfo(rawValue: alphaInfo)]
let renderingIntent: CGColorRenderingIntent = .defaultIntent
// Attempt to process the buffer of bytes into an image.
guard let imageRef = CGImage(width: imageSize, height: imageSize, bitsPerComponent: bitsPerComponent, bitsPerPixel: bitsPerPixel, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo, provider: dataProvider, decode: nil, shouldInterpolate: false, intent: renderingIntent) else {
assertionFailure("Unable to create CGImage.")
return false
}
/// The UIImage created from the buffer data.
let cubeImage = UIImage(cgImage: imageRef)
/// The PNG representation of the `cubeImage`.
let imageData = UIImagePNGRepresentation(cubeImage)
// Check that we can establish a URL for saving.
guard let imageURL = saveLocation ?? defaultLocationForSize(size: size) else {
assertionFailure("Can't access save location.")
return false
}
// Try to write the image to disk.
do {
try imageData?.write(to: imageURL, options: [])
print("Image successfully written to:", imageURL.absoluteString)
return true
} catch let error {
print("Failed to write image:", error)
return false
}
}
/// Generates default URLs for the various Dimension values.
private static func defaultLocationForSize(size: Dimension) -> URL? {
let imageName = "ColorCubeReferenceImage\(size.rawValue).png"
guard let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
assertionFailure("Can't get documents directory!")
return nil
}
let imageURL = docsDir.appendingPathComponent(imageName)
return imageURL
}
}
//
// ViewController.swift
// ColorCubeImageCreator
//
// Created by Joshua Sullivan on 4/25/16.
// Copyright © 2016 Joshua Sullivan. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let sizes: [ColorCubeImageCreator.Dimension] = [.four, .sixteen, .sixtyFour, .twoHundredFiftySix]
let results = sizes.map {
ColorCubeImageCreator.createColorCube($0, saveLocation: nil)
}
print("results:", results)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment