diff --git a/SilicaViewer/Document.swift b/SilicaViewer/Document.swift index abb3c3e..b660db6 100644 --- a/SilicaViewer/Document.swift +++ b/SilicaViewer/Document.swift @@ -4,13 +4,14 @@ import CoreFoundation import Accelerate import CoreMedia +/// Represents a image chunk struct SilicaChunk { var x: Int = 0 var y: Int = 0 var image: CGImage? } -// all supported Procreate blend modes +/// Supported Silica blend modes, including extended blend modes enum BlendMode : Int { case Normal = 0, Multiply = 1, @@ -30,7 +31,7 @@ enum BlendMode : Int { Hue = 15, Saturation = 16, SoftLight = 17, - // what is this mysterious 18? + // TODO: where is 18? Darken = 19, // extended modes @@ -43,6 +44,7 @@ enum BlendMode : Int { 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 { var blendMode: BlendMode = .Normal var chunks: [SilicaChunk] = [] @@ -50,6 +52,7 @@ struct SilicaLayerData { var hidden: Bool = false } +/// A Silica Layer, which equates to a real layer present in the document struct SilicaLayer : Equatable { static func == (lhs: SilicaLayer, rhs: SilicaLayer) -> Bool { return lhs.name == rhs.name && lhs.clipped == rhs.clipped @@ -61,6 +64,7 @@ struct SilicaLayer : Equatable { var clipped: Bool = false } +/// Container for the Silica Document format struct SilicaDocument { var trackedTime: Int = 0 var tileSize: Int = 0 @@ -100,8 +104,10 @@ func objectRefGetValue2(_ objectRef: CFTypeRef) -> UInt32 { return valueForKeyedArchiverUID(val) } +/// NSDocument for reading Silica Document files 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? @@ -149,6 +155,9 @@ class Document: NSDocument { 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 { if (!(info.name.isEmpty)) { 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? { let objectsArray = self.dict?["$objects"] as! NSArray @@ -173,9 +182,11 @@ class Document: NSDocument { return nil } - /* - Returns the correct tile size, taking into account the remainder between tile size and image size. - */ + /// Calculates 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) { var width: Int = info.tileSize var height: Int = info.tileSize @@ -191,16 +202,17 @@ class Document: NSDocument { return (width, height) } - /* - Converts a CFKeyedArchiveUID from a NSKeyedArchive to a Int for indexing an array or dictionary. - */ + /// Converts a `CFKeyedArchiveUID` from a `NSKeyedArchive` to a Int for indexing an array or `NSDictionary`. + /// - Parameter id: The `CFKeyedArchiveUID` to parse. + /// - Returns: An integer representation fo the `CFKeyedArchiveUID`. func getClassID(id: Any?) -> Int { return Int(objectRefGetValue2(id! as CFTypeRef)) } - /* - Parses the chunk filename, ex. "1~1.chunk" to integer coordinates (1, 2). - */ + /// Parses a chunk filename to integer coordinates. + /// "1~1.chunk" -> (1, 2). + /// - Parameter filename: The chunk filename. + /// - Returns: A tuple containing the parsed coordinates. func parseChunkFilename(_ filename: String) -> (Int, Int)? { let pathURL = URL(fileURLWithPath: filename) 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 { let x = chunk.x 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) } + /// 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? { if blendMode == 0 && blendMode != 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? { switch(layer.data.blendMode) { case .Normal: @@ -389,6 +412,10 @@ class Document: NSDocument { 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)? { let sizeComponents = str.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ") @@ -578,6 +605,9 @@ class Document: NSDocument { 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? { let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) @@ -598,6 +628,11 @@ class Document: NSDocument { 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 { 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)! } - // 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] { var clippingLayers : [SilicaLayer] = [] @@ -646,35 +683,8 @@ class Document: NSDocument { return clippingLayers } - func blendLayer(_ layer : SilicaLayer, previousImage : inout CGImage?, applyOpacity : Bool) -> CIImage { - let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) - - // 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)! - } - + /// Renders a full, composite image of the Silica Document - emulating the Procreate drawing engine. + /// - Returns: If the image suceeds in drawing, then a `NSImage` of the canvas. func makeComposite() -> NSImage? { // create the final composite output image let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) @@ -787,6 +797,8 @@ class Document: NSDocument { return image } + /// Extracts the thumbnail image generated by Procreate itself. + /// - Returns: If it's able to find the thumbnail, an `NSImage`. func makeThumbnail() -> NSImage? { guard let archive = Archive(data: data!, accessMode: Archive.AccessMode.read) else { return nil