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 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