1
Fork 0

Overhaul documentation in the Document class

This commit is contained in:
Joshua Goins 2022-06-15 11:30:53 -04:00
parent 897df62a49
commit 5dd85f1989

View file

@ -4,13 +4,14 @@ import CoreFoundation
import Accelerate import Accelerate
import CoreMedia import CoreMedia
/// Represents a image chunk
struct SilicaChunk { struct SilicaChunk {
var x: Int = 0 var x: Int = 0
var y: Int = 0 var y: Int = 0
var image: CGImage? var image: CGImage?
} }
// all supported Procreate blend modes /// Supported Silica blend modes, including extended blend modes
enum BlendMode : Int { enum BlendMode : Int {
case Normal = 0, case Normal = 0,
Multiply = 1, Multiply = 1,
@ -30,7 +31,7 @@ enum BlendMode : Int {
Hue = 15, Hue = 15,
Saturation = 16, Saturation = 16,
SoftLight = 17, SoftLight = 17,
// what is this mysterious 18? // TODO: where is 18?
Darken = 19, Darken = 19,
// extended modes // extended modes
@ -43,6 +44,7 @@ enum BlendMode : Int {
Divide = 26 Divide = 26
} }
/// Represents the image data for a Silica Layer, may not be an actual layer present to the user, such as a mask.
struct SilicaLayerData { struct SilicaLayerData {
var blendMode: BlendMode = .Normal var blendMode: BlendMode = .Normal
var chunks: [SilicaChunk] = [] var chunks: [SilicaChunk] = []
@ -50,6 +52,7 @@ struct SilicaLayerData {
var hidden: Bool = false var hidden: Bool = false
} }
/// A Silica Layer, which equates to a real layer present in the document
struct SilicaLayer : Equatable { struct SilicaLayer : Equatable {
static func == (lhs: SilicaLayer, rhs: SilicaLayer) -> Bool { static func == (lhs: SilicaLayer, rhs: SilicaLayer) -> Bool {
return lhs.name == rhs.name && lhs.clipped == rhs.clipped return lhs.name == rhs.name && lhs.clipped == rhs.clipped
@ -61,6 +64,7 @@ struct SilicaLayer : Equatable {
var clipped: Bool = false var clipped: Bool = false
} }
/// Container for the Silica Document format
struct SilicaDocument { struct SilicaDocument {
var trackedTime: Int = 0 var trackedTime: Int = 0
var tileSize: Int = 0 var tileSize: Int = 0
@ -100,8 +104,10 @@ func objectRefGetValue2(_ objectRef: CFTypeRef) -> UInt32 {
return valueForKeyedArchiverUID(val) return valueForKeyedArchiverUID(val)
} }
/// NSDocument for reading Silica Document files
class Document: NSDocument { class Document: NSDocument {
var data: Data? // oh no... // TODO: don't keep the entire file in memory, especially since we also store the image data raw
var data: Data?
var dict: NSDictionary? var dict: NSDictionary?
@ -149,6 +155,9 @@ class Document: NSDocument {
return ofType == "com.procreate" return ofType == "com.procreate"
} }
/// Figures out the best filename for the document, depending on what information we know.
/// It prefers the actual name defined in the document, unless it's missing - where we'll fall back to the filename.
/// - Returns: The best filename.
func getIdealFilename() -> String { func getIdealFilename() -> String {
if (!(info.name.isEmpty)) { if (!(info.name.isEmpty)) {
return info.name return info.name
@ -157,9 +166,9 @@ class Document: NSDocument {
} }
} }
/* /// Pass in an object from the `$object` array, which always contains a `$class` key.
Pass in an object from the $object array, which always contains a $class key. /// - Parameter dict: The NSDictionary to parse.
*/ /// - Returns: The class name.
func getDocumentClassName(dict: NSDictionary) -> String? { func getDocumentClassName(dict: NSDictionary) -> String? {
let objectsArray = self.dict?["$objects"] as! NSArray let objectsArray = self.dict?["$objects"] as! NSArray
@ -173,9 +182,11 @@ class Document: NSDocument {
return nil return nil
} }
/* /// Calculates the correct tile size, taking into account the remainder between tile size and image size.
Returns the correct tile size, taking into account the remainder between tile size and image size. /// - Parameters:
*/ /// - x: The X position of the tile.
/// - y: The Y position of the tile.
/// - Returns: A tuple containing the correct tile size.
func getTileSize(_ x: Int, _ y: Int) -> (Int, Int) { func getTileSize(_ x: Int, _ y: Int) -> (Int, Int) {
var width: Int = info.tileSize var width: Int = info.tileSize
var height: Int = info.tileSize var height: Int = info.tileSize
@ -191,16 +202,17 @@ class Document: NSDocument {
return (width, height) return (width, height)
} }
/* /// Converts a `CFKeyedArchiveUID` from a `NSKeyedArchive` to a Int for indexing an array or `NSDictionary`.
Converts a CFKeyedArchiveUID from a NSKeyedArchive to a Int for indexing an array or dictionary. /// - Parameter id: The `CFKeyedArchiveUID` to parse.
*/ /// - Returns: An integer representation fo the `CFKeyedArchiveUID`.
func getClassID(id: Any?) -> Int { func getClassID(id: Any?) -> Int {
return Int(objectRefGetValue2(id! as CFTypeRef)) return Int(objectRefGetValue2(id! as CFTypeRef))
} }
/* /// Parses a chunk filename to integer coordinates.
Parses the chunk filename, ex. "1~1.chunk" to integer coordinates (1, 2). /// "1~1.chunk" -> (1, 2).
*/ /// - Parameter filename: The chunk filename.
/// - Returns: A tuple containing the parsed coordinates.
func parseChunkFilename(_ filename: String) -> (Int, Int)? { func parseChunkFilename(_ filename: String) -> (Int, Int)? {
let pathURL = URL(fileURLWithPath: filename) let pathURL = URL(fileURLWithPath: filename)
let pathComponents = pathURL.lastPathComponent.replacingOccurrences(of: ".chunk", with: "").components(separatedBy: "~") let pathComponents = pathURL.lastPathComponent.replacingOccurrences(of: ".chunk", with: "").components(separatedBy: "~")
@ -212,6 +224,9 @@ class Document: NSDocument {
} }
} }
/// Calculates a `NSRect` for a `SilicaChunk`.
/// - Parameter chunk: The `SilicaChunk` to return a `NSRect` for.
/// - Returns: A `NSRect` containg the rectangle for the chunk.
func getChunkRect(_ chunk: SilicaChunk) -> NSRect { func getChunkRect(_ chunk: SilicaChunk) -> NSRect {
let x = chunk.x let x = chunk.x
var y = chunk.y var y = chunk.y
@ -225,6 +240,11 @@ class Document: NSDocument {
return NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height) return NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height)
} }
/// Parses a raw integer blend mode from the Silica Document, into a `BlendMode` enumeration.
/// - Parameters:
/// - blendMode: The integer value for the `blendMode` property.
/// - extendedBlend: The integer value for the `extendedBlend` property.
/// - Returns: If the provided integer representations are correct, a `BlendMode` enumeration.
func parseRawBlendMode(blendMode: Int, extendedBlend: Int) -> BlendMode? { func parseRawBlendMode(blendMode: Int, extendedBlend: Int) -> BlendMode? {
if blendMode == 0 && blendMode != extendedBlend { if blendMode == 0 && blendMode != extendedBlend {
return BlendMode(rawValue: extendedBlend) return BlendMode(rawValue: extendedBlend)
@ -233,6 +253,9 @@ class Document: NSDocument {
} }
} }
/// Figures out the correct `CIBlendKernel` corresponding to the `SilicaLayer`'s blending mode.
/// - Parameter layer: The `SilicaLayer` to fetch the blend kernel for.
/// - Returns: If a blend kernel is found, returns a `CIBlendKernel`.
func getBlendKernel(_ layer: SilicaLayer) -> CIBlendKernel? { func getBlendKernel(_ layer: SilicaLayer) -> CIBlendKernel? {
switch(layer.data.blendMode) { switch(layer.data.blendMode) {
case .Normal: case .Normal:
@ -389,6 +412,10 @@ class Document: NSDocument {
return nil return nil
} }
/// Parses a pair string into a proper typed tuple.
/// For example, {0, 0} -> (0, 0).
/// - Parameter str: The pair string.
/// - Returns: If it can be parsed correctly, a tuple containing the integer values found in the string.
func parsePairString(_ str: String) -> (Int, Int)? { func parsePairString(_ str: String) -> (Int, Int)? {
let sizeComponents = str.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ") let sizeComponents = str.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ")
@ -578,6 +605,9 @@ class Document: NSDocument {
return (maskContext?.makeImage())! return (maskContext?.makeImage())!
} }
/// Draws the layer data into a CGImage, without taking into account the opacity or the blending of the layer.
/// - Parameter layer: The layer data to draw.
/// - Returns: If no errors occured during drawing, a `CGImage` of the layer data.
func simpleDrawLayer(_ layer : SilicaLayerData) -> CGImage? { func simpleDrawLayer(_ layer : SilicaLayerData) -> CGImage? {
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big)
@ -598,6 +628,11 @@ class Document: NSDocument {
return layerContext?.makeImage() return layerContext?.makeImage()
} }
/// Draws the layer into a CIImage, taking into account the layer opacity, blending and previous image - if any.
/// - Parameters:
/// - layer: The `SilicaLayer` to draw.
/// - previousImage: Previous `CGImage` to draw on top of, can be null.
/// - Returns: A `CIImage` of the new image.
func blendLayer(_ layer : SilicaLayer, previousImage : inout CGImage?) -> CIImage { func blendLayer(_ layer : SilicaLayer, previousImage : inout CGImage?) -> CIImage {
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big)
@ -623,7 +658,9 @@ class Document: NSDocument {
return kernel!.apply(foreground: CIImage(cgImage: layerImage!), background: previousImage == nil ? CIImage(color: .clear) : CIImage(cgImage: previousImage!), colorSpace: info.colorSpace)! return kernel!.apply(foreground: CIImage(cgImage: layerImage!), background: previousImage == nil ? CIImage(color: .clear) : CIImage(cgImage: previousImage!), colorSpace: info.colorSpace)!
} }
// this returns all of the layers that are clipping onto this one /// Calculates all of the layers that are clipping onto this one
/// - Parameter layer: The layer to figure out the clipping layers of.
/// - Returns: An array of clipping layers.
func getAllClippingLayers(layer: SilicaLayer) -> [SilicaLayer] { func getAllClippingLayers(layer: SilicaLayer) -> [SilicaLayer] {
var clippingLayers : [SilicaLayer] = [] var clippingLayers : [SilicaLayer] = []
@ -646,35 +683,8 @@ class Document: NSDocument {
return clippingLayers return clippingLayers
} }
func blendLayer(_ layer : SilicaLayer, previousImage : inout CGImage?, applyOpacity : Bool) -> CIImage { /// Renders a full, composite image of the Silica Document - emulating the Procreate drawing engine.
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) /// - Returns: If the image suceeds in drawing, then a `NSImage` of the canvas.
// start by creating a new layer composite image, needed for image masking
let layerContext = CGContext(data: nil, width: info.width, height: info.height, bitsPerComponent: 8, bytesPerRow: info.width * 4, space: info.colorSpace, bitmapInfo: bitmapInfo.rawValue)
layerContext?.clear(info.cgRect)
let kernel = getBlendKernel(layer)
for chunk in layer.data.chunks {
if applyOpacity {
layerContext?.setAlpha(CGFloat(layer.data.opacity))
} else {
layerContext?.setAlpha(1.0)
}
layerContext?.setBlendMode(.normal)
if !layer.data.hidden {
layerContext?.draw(chunk.image!, in: getChunkRect(chunk))
}
}
let layerImage = layerContext?.makeImage()
// apply image
return kernel!.apply(foreground: CIImage(cgImage: layerImage!), background: previousImage == nil ? CIImage(color: .clear) : CIImage(cgImage: previousImage!), colorSpace: info.colorSpace)!
}
func makeComposite() -> NSImage? { func makeComposite() -> NSImage? {
// create the final composite output image // create the final composite output image
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big)
@ -787,6 +797,8 @@ class Document: NSDocument {
return image return image
} }
/// Extracts the thumbnail image generated by Procreate itself.
/// - Returns: If it's able to find the thumbnail, an `NSImage`.
func makeThumbnail() -> NSImage? { func makeThumbnail() -> NSImage? {
guard let archive = Archive(data: data!, accessMode: Archive.AccessMode.read) else { guard let archive = Archive(data: data!, accessMode: Archive.AccessMode.read) else {
return nil return nil