From 8672a69e8bd4a6e2193c2276547b258655708e77 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 15 Jun 2022 12:32:56 -0400 Subject: [PATCH] Separate drawing code into SilicaEngine class --- SilicaViewer.xcodeproj/project.pbxproj | 4 + SilicaViewer/AppDelegate.swift | 12 +- SilicaViewer/Document.swift | 317 ++----------------------- SilicaViewer/SilicaDocument.swift | 53 ++++- SilicaViewer/SilicaEngine.swift | 287 ++++++++++++++++++++++ 5 files changed, 368 insertions(+), 305 deletions(-) create mode 100644 SilicaViewer/SilicaEngine.swift diff --git a/SilicaViewer.xcodeproj/project.pbxproj b/SilicaViewer.xcodeproj/project.pbxproj index 9333a00..23006cb 100644 --- a/SilicaViewer.xcodeproj/project.pbxproj +++ b/SilicaViewer.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 030F70162415C6B500A43F01 /* QuickLook.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 030F70082415C6B500A43F01 /* QuickLook.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 03328514285A2AB700AEEBF3 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03328513285A2AB700AEEBF3 /* Extensions.swift */; }; 03328516285A32FD00AEEBF3 /* SilicaDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03328515285A32FD00AEEBF3 /* SilicaDocument.swift */; }; + 03328518285A33DA00AEEBF3 /* SilicaEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03328517285A33DA00AEEBF3 /* SilicaEngine.swift */; }; 035D1A0426F0927200B332BE /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035D19F826F0927200B332BE /* ViewController.swift */; }; 035D1A0526F0927200B332BE /* cbridge.c in Sources */ = {isa = PBXBuildFile; fileRef = 035D19FA26F0927200B332BE /* cbridge.c */; }; 035D1A0626F0927200B332BE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 035D19FB26F0927200B332BE /* Assets.xcassets */; }; @@ -105,6 +106,7 @@ 030F70132415C6B500A43F01 /* Quicklook.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Quicklook.entitlements; sourceTree = ""; }; 03328513285A2AB700AEEBF3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 03328515285A32FD00AEEBF3 /* SilicaDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilicaDocument.swift; sourceTree = ""; }; + 03328517285A33DA00AEEBF3 /* SilicaEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilicaEngine.swift; sourceTree = ""; }; 035D19F826F0927200B332BE /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 035D19F926F0927200B332BE /* SilicaViewer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = SilicaViewer.entitlements; sourceTree = ""; }; 035D19FA26F0927200B332BE /* cbridge.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = cbridge.c; sourceTree = ""; }; @@ -251,6 +253,7 @@ 0371996827BACDE800EE1DFD /* ExportAccessoryView.swift */, 03328513285A2AB700AEEBF3 /* Extensions.swift */, 03328515285A32FD00AEEBF3 /* SilicaDocument.swift */, + 03328517285A33DA00AEEBF3 /* SilicaEngine.swift */, ); path = SilicaViewer; sourceTree = ""; @@ -517,6 +520,7 @@ 035D1A0A26F0927200B332BE /* AppDelegate.swift in Sources */, 03328514285A2AB700AEEBF3 /* Extensions.swift in Sources */, 035D1A0B26F0927200B332BE /* TimelapseViewController.swift in Sources */, + 03328518285A33DA00AEEBF3 /* SilicaEngine.swift in Sources */, 03328516285A32FD00AEEBF3 /* SilicaDocument.swift in Sources */, 035D1A0426F0927200B332BE /* ViewController.swift in Sources */, 035D1A0526F0927200B332BE /* cbridge.c in Sources */, diff --git a/SilicaViewer/AppDelegate.swift b/SilicaViewer/AppDelegate.swift index fcdc6b2..0c4d060 100644 --- a/SilicaViewer/AppDelegate.swift +++ b/SilicaViewer/AppDelegate.swift @@ -144,9 +144,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { flipHoriz = true } - var rect = document!.info.cgRect - rect.size.width = document!.info.cgSize.height - rect.size.height = document!.info.cgSize.width + var rect = document!.info.cgRect() + rect.size.width = document!.info.cgSize().height + rect.size.height = document!.info.cgSize().width let writer = PSDWriter(documentSize: rect.size) writer?.shouldUnpremultiplyLayerData = true @@ -160,11 +160,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations { writer?.addLayer(with: ccgContext?.makeImage(), andName: "Background", andOpacity: 1.0, andOffset: .zero) + let engine = SilicaEngine() + for layer in document!.info.layers.reversed() { - let finalCgImage = document!.simpleDrawLayer(layer.data)!.rotate(angle: self.degreesToRadians(degreesToRotate), flipHorizontally: flipHoriz, flipVertically: flipVert) + let finalCgImage = engine.simpleDrawLayer(document: document!.info, layer: layer.data)!.rotate(angle: self.degreesToRadians(degreesToRotate), flipHorizontally: flipHoriz, flipVertically: flipVert) if(layer.mask != nil) { - let mask = document!.simpleDrawLayer(layer.mask!)!.rotate(angle: self.degreesToRadians(degreesToRotate), flipHorizontally: flipHoriz, flipVertically: flipVert) + let mask = engine.simpleDrawLayer(document: document!.info, layer: layer.data)!.rotate(angle: self.degreesToRadians(degreesToRotate), flipHorizontally: flipHoriz, flipVertically: flipVert) writer?.addLayer(with: mask, andName: layer.name + " (Mask)", andOpacity: 1.0, andOffset: .zero) } diff --git a/SilicaViewer/Document.swift b/SilicaViewer/Document.swift index 75e1f93..50aefd4 100644 --- a/SilicaViewer/Document.swift +++ b/SilicaViewer/Document.swift @@ -82,26 +82,6 @@ class Document: NSDocument { return nil } - /// 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 - - if((x + 1) == info.columns) { - width = info.tileSize - info.remainderWidth - } - - if(y == info.rows) { - height = info.tileSize - info.remainderHeight - } - - return (width, height) - } - /// 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`. @@ -124,22 +104,6 @@ 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 - - let (width, height) = getTileSize(x, y) - - if y == info.rows { - y = 0 - } - - 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. @@ -153,66 +117,6 @@ 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: - return .sourceOver - case .Multiply: - return .multiply - case .Screen: - return .screen - case .Add: - return .componentAdd - case .Lighten: - return .lighten - case .Exclusion: - return .exclusion - case .Difference: - return .difference - case .Subtract: - return .subtract - case .LinearBurn: - return .linearBurn - case .ColorDodge: - return .colorDodge - case .ColorBurn: - return .colorBurn - case .Overlay: - return .overlay - case .HardLight: - return .hardLight - case .Color: - return .color - case .Luminosity: - return .luminosity - case .Hue: - return .hue - case .Saturation: - return .saturation - case .SoftLight: - return .softLight - case .Darken: - return .darken - case .HardMix: - return .hardMix - case .VividLight: - return .vividLight - case .LinearLight: - return .linearLight - case .PinLight: - return .pinLight - case .LighterColor: - return .lighterColor - case .DarkerColor: - return .darkerColor - case .Divide: - return .divide - } - } - func parseSilicaLayer(archive: Archive, dict: NSDictionary, isMask: Bool) -> SilicaLayer? { let objectsArray = self.dict?["$objects"] as! NSArray @@ -264,14 +168,14 @@ class Document: NSDocument { let threadEntry = threadArchive[chunkPaths[i]] - guard let (x, y) = parseChunkFilename(threadEntry!.path) else { + guard let (tileX, tileY) = parseChunkFilename(threadEntry!.path) else { return } - let (width, height) = getTileSize(x, y) + let (tileWidth, tileHeight) = info.getTileSize(tileX, tileY) let numChannels = isMask ? 1 : 4 - let byteSize = width * height * numChannels + let byteSize = tileWidth * tileHeight * numChannels let uncompressedMemory = UnsafeMutablePointer.allocate(capacity: byteSize) @@ -293,14 +197,24 @@ class Document: NSDocument { let bitmapInfo = CGBitmapInfo(rawValue: (isMask ? CGImageAlphaInfo.none : CGImageAlphaInfo.premultipliedLast).rawValue).union(.byteOrder32Big) let providerRef: CGDataProvider? = CGDataProvider(data: imageData as CFData) - guard let cgimage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 8 * numChannels, bytesPerRow: width * numChannels, space: rgbColorSpace, bitmapInfo: bitmapInfo, provider: providerRef!, decode: nil, shouldInterpolate: false, intent: render) else { + guard let cgimage = CGImage(width: tileWidth, + height: tileHeight, + bitsPerComponent: 8, + bitsPerPixel: 8 * numChannels, + bytesPerRow: tileWidth * numChannels, + space: rgbColorSpace, + bitmapInfo: bitmapInfo, + provider: providerRef!, + decode: nil, + shouldInterpolate: false, + intent: render) else { return } queue.async(group: dispatchGroup) { layer.data.chunks[i].image = cgimage - layer.data.chunks[i].x = x - layer.data.chunks[i].y = y + layer.data.chunks[i].x = tileX + layer.data.chunks[i].y = tileY } } @@ -405,10 +319,13 @@ class Document: NSDocument { let sizeClassID = getClassID(id: sizeClassKey) let sizeString = objectsArray[sizeClassID] as! String - let (width, height) = parsePairString(sizeString)! - info.width = width - info.height = height + guard let (parsedWidth, parsedHeight) = parsePairString(sizeString) else { + return + } + info.width = parsedWidth + info.height = parsedHeight + info.columns = Int(ceil(Float(info.width) / Float(info.tileSize))) info.rows = Int(ceil(Float(info.height) / Float(info.tileSize))) + 1 // TODO: lol why @@ -485,198 +402,12 @@ class Document: NSDocument { parseDocument(archive: archive, dict: propertyList as! NSDictionary) } - func makeBlendImage(_ layer: SilicaLayer) -> CGImage { - var maskContext: CGContext? - - if layer.mask != nil { - let grayColorSpace = CGColorSpaceCreateDeviceGray() - let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) - - maskContext = CGContext(data: nil, width: info.width, height: info.height, bitsPerComponent: 16, bytesPerRow: 0, space: grayColorSpace, bitmapInfo: maskBitmapInfo.rawValue) - - maskContext?.setFillColor(.white) - maskContext?.fill(info.cgRect) - - for chunk in layer.mask!.chunks { - maskContext?.draw(chunk.image!, in: getChunkRect(chunk)) - } - } - - 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) - - // 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) - - for chunk in layer.chunks { - layerContext?.setAlpha(1.0) - layerContext?.setBlendMode(.normal) - - if !layer.hidden { - layerContext?.draw(chunk.image!, in: getChunkRect(chunk)) - } - } - - 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) - - // 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 { - layerContext?.setAlpha(CGFloat(layer.data.opacity)) - 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)! - } - - /// 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] = [] - - let layers : [SilicaLayer] = info.layers.reversed() - let index = layers.firstIndex(of: layer)! + 1 - if index >= layers.count { - return clippingLayers - } - - for layerIndex in index...layers.count - 1 { - //Swift.print("checking " + layers[layerIndex].name + " is clipping = " + layers[layerIndex].clipped.description) - - if(layers[layerIndex].clipped) { - clippingLayers.append(layers[layerIndex]) - } else { - break - } - } - - return clippingLayers - } - /// 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) + let engine = SilicaEngine() - let ccgContext = CGContext(data: nil, width: info.width, height: info.height, bitsPerComponent: 8, bytesPerRow: info.width * 4, space: info.colorSpace, bitmapInfo: bitmapInfo.rawValue) - - ccgContext?.setFillColor(info.backgroundColor) - ccgContext?.fill(info.cgRect) - - let context = CIContext() - - guard let cgImage = ccgContext?.makeImage() else { - return nil - } - - var masterImage = CIImage(cgImage: cgImage) - - var previousImage: CGImage? = nil - - for layer in info.layers.reversed() { - if !layer.data.hidden && !layer.clipped { - // 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) - - var maskContext: CGContext? - - let kernel = getBlendKernel(layer) - - if layer.mask != nil { - let grayColorSpace = CGColorSpaceCreateDeviceGray() - let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) - - maskContext = CGContext(data: nil, width: info.width, height: info.height, bitsPerComponent: 16, bytesPerRow: 0, space: grayColorSpace, bitmapInfo: maskBitmapInfo.rawValue) - - maskContext?.setFillColor(.white) - maskContext?.fill(info.cgRect) - - for chunk in layer.mask!.chunks { - maskContext?.draw(chunk.image!, in: getChunkRect(chunk)) - } - } - - for chunk in layer.data.chunks { - layerContext?.setAlpha(CGFloat(layer.data.opacity)) - layerContext?.setBlendMode(.normal) - - if !layer.data.hidden { - layerContext?.draw(chunk.image!, in: getChunkRect(chunk)) - } - } - - let clippingLayers = getAllClippingLayers(layer: layer) - if !clippingLayers.isEmpty { - let layerImage = layerContext?.makeImage() - - var clippedMaster: CGImage? = layerImage - for layer in clippingLayers { - // so we if we want to clip, we want to gather all of the clipping layers in order first... - let temporaryClippedMaster = blendLayer(layer, previousImage: &clippedMaster) - - clippedMaster = context.createCGImage(temporaryClippedMaster, from: info.cgRect, format: .RGBA8, colorSpace: info.colorSpace) - } - - layerContext?.setAlpha(1.0) - layerContext?.setBlendMode(.sourceAtop) - - layerContext?.draw(clippedMaster!, in: info.cgRect) - } - - let layerImage = layerContext?.makeImage() - - if layer.mask != nil && maskContext != nil { - let maskImage = (maskContext?.makeImage())! - let newImage = layerImage!.masking(maskImage)! - - previousImage = newImage - } else { - previousImage = layerImage - } - - // apply image - masterImage = kernel!.apply(foreground: CIImage(cgImage: previousImage!), background: masterImage, colorSpace: info.colorSpace)! - } - } - - guard let finalCgImage = context.createCGImage(masterImage, from: info.cgRect, format: .RGBA8, colorSpace: info.colorSpace) else { - return nil - } - - var image = NSImage(cgImage: finalCgImage, size: info.nsSize) + var image = NSImage(cgImage: engine.draw(document: info)!, size: info.nsSize()) if info.orientation == 3 { image = image.imageRotatedByDegreess(degrees: 90) diff --git a/SilicaViewer/SilicaDocument.swift b/SilicaViewer/SilicaDocument.swift index cfac6e1..f7da015 100644 --- a/SilicaViewer/SilicaDocument.swift +++ b/SilicaViewer/SilicaDocument.swift @@ -87,15 +87,54 @@ struct SilicaDocument { var videoFrame: (Int, Int) = (0, 0) - lazy var nsSize = { + func nsSize() -> NSSize { return NSSize(width: width, height: height) - }() + } - lazy var cgSize = { + func cgSize() -> CGSize { return CGSize(width: width, height: height) - }() + } - lazy var cgRect = { - return CGRect(origin: .zero, size: cgSize) - }() + func cgRect() -> CGRect { + return CGRect(origin: .zero, size: cgSize()) + } + + /// 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 tileWidth: Int = tileSize + var tileHeight: Int = tileSize + + if((x + 1) == columns) { + tileWidth -= remainderWidth + } + + if(y == rows) { + tileHeight -= remainderHeight + } + + return (tileWidth, tileHeight) + } + + /// 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 + + let (tileWidth, tileHeight) = getTileSize(x, y) + + if y == rows { + y = 0 + } + + return NSRect(x: tileSize * x, + y: height - (tileSize * y), + width: tileWidth, + height: tileHeight) + } } diff --git a/SilicaViewer/SilicaEngine.swift b/SilicaViewer/SilicaEngine.swift new file mode 100644 index 0000000..d9c8d69 --- /dev/null +++ b/SilicaViewer/SilicaEngine.swift @@ -0,0 +1,287 @@ +import Foundation +import CoreGraphics +import CoreImage + +class SilicaEngine { + func draw(document: SilicaDocument) -> CGImage? { + // create the final composite output image + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big) + + let ccgContext = CGContext(data: nil, + width: document.width, + height: document.height, + bitsPerComponent: 8, + bytesPerRow: document.width * 4, + space: document.colorSpace, + bitmapInfo: bitmapInfo.rawValue) + + ccgContext?.setFillColor(document.backgroundColor) + ccgContext?.fill(document.cgRect()) + + let context = CIContext() + + guard let cgImage = ccgContext?.makeImage() else { + return nil + } + + var masterImage = CIImage(cgImage: cgImage) + + var previousImage: CGImage? = nil + + for layer in document.layers.reversed() { + if !layer.data.hidden && !layer.clipped { + // start by creating a new layer composite image, needed for image masking + let layerContext = CGContext(data: nil, + width: document.width, + height: document.height, + bitsPerComponent: 8, + bytesPerRow: document.width * 4, + space: document.colorSpace, + bitmapInfo: bitmapInfo.rawValue) + + layerContext?.clear(document.cgRect()) + + var maskContext: CGContext? + + let kernel = getBlendKernel(layer) + + if layer.mask != nil { + let grayColorSpace = CGColorSpaceCreateDeviceGray() + let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) + + maskContext = CGContext(data: nil, + width: document.width, + height: document.height, + bitsPerComponent: 16, + bytesPerRow: 0, + space: grayColorSpace, + bitmapInfo: maskBitmapInfo.rawValue) + + maskContext?.setFillColor(.white) + maskContext?.fill(document.cgRect()) + + for chunk in layer.mask!.chunks { + maskContext?.draw(chunk.image!, in: document.getChunkRect(chunk)) + } + } + + for chunk in layer.data.chunks { + layerContext?.setAlpha(CGFloat(layer.data.opacity)) + layerContext?.setBlendMode(.normal) + + if !layer.data.hidden { + layerContext?.draw(chunk.image!, in: document.getChunkRect(chunk)) + } + } + + let clippingLayers = getAllClippingLayers(document: document, layer: layer) + if !clippingLayers.isEmpty { + let layerImage = layerContext?.makeImage() + + var clippedMaster: CGImage? = layerImage + for layer in clippingLayers { + // so we if we want to clip, we want to gather all of the clipping layers in order first... + let temporaryClippedMaster = blendLayer(document: document, layer: layer, previousImage: &clippedMaster) + + clippedMaster = context.createCGImage(temporaryClippedMaster, from: document.cgRect(), format: .RGBA8, colorSpace: document.colorSpace) + } + + layerContext?.setAlpha(1.0) + layerContext?.setBlendMode(.sourceAtop) + + layerContext?.draw(clippedMaster!, in: document.cgRect()) + } + + let layerImage = layerContext?.makeImage() + + if layer.mask != nil && maskContext != nil { + let maskImage = (maskContext?.makeImage())! + let newImage = layerImage!.masking(maskImage)! + + previousImage = newImage + } else { + previousImage = layerImage + } + + // apply image + masterImage = kernel!.apply(foreground: CIImage(cgImage: previousImage!), background: masterImage, colorSpace: document.colorSpace)! + } + } + + return context.createCGImage(masterImage, from: document.cgRect(), format: .RGBA8, colorSpace: document.colorSpace) + } + + func makeBlendImage(document: SilicaDocument, layer: SilicaLayer) -> CGImage { + var maskContext: CGContext? + + if layer.mask != nil { + let grayColorSpace = CGColorSpaceCreateDeviceGray() + let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) + + maskContext = CGContext(data: nil, + width: document.width, + height: document.height, + bitsPerComponent: 16, + bytesPerRow: 0, + space: grayColorSpace, + bitmapInfo: maskBitmapInfo.rawValue) + + maskContext?.setFillColor(.white) + maskContext?.fill(document.cgRect()) + + for chunk in layer.mask!.chunks { + maskContext?.draw(chunk.image!, in: document.getChunkRect(chunk)) + } + } + + 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(document: SilicaDocument, layer : SilicaLayerData) -> CGImage? { + 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: document.width, + height: document.height, + bitsPerComponent: 8, + bytesPerRow: document.width * 4, + space: document.colorSpace, + bitmapInfo: bitmapInfo.rawValue) + + layerContext?.clear(document.cgRect()) + + for chunk in layer.chunks { + layerContext?.setAlpha(1.0) + layerContext?.setBlendMode(.normal) + + if !layer.hidden { + layerContext?.draw(chunk.image!, in: document.getChunkRect(chunk)) + } + } + + 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(document: SilicaDocument, layer : SilicaLayer, previousImage : inout CGImage?) -> 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: document.width, + height: document.height, + bitsPerComponent: 8, + bytesPerRow: document.width * 4, + space: document.colorSpace, + bitmapInfo: bitmapInfo.rawValue) + + layerContext?.clear(document.cgRect()) + + let kernel = getBlendKernel(layer) + + for chunk in layer.data.chunks { + layerContext?.setAlpha(CGFloat(layer.data.opacity)) + layerContext?.setBlendMode(.normal) + + if !layer.data.hidden { + layerContext?.draw(chunk.image!, in: document.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: document.colorSpace)! + } + + /// 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: + return .sourceOver + case .Multiply: + return .multiply + case .Screen: + return .screen + case .Add: + return .componentAdd + case .Lighten: + return .lighten + case .Exclusion: + return .exclusion + case .Difference: + return .difference + case .Subtract: + return .subtract + case .LinearBurn: + return .linearBurn + case .ColorDodge: + return .colorDodge + case .ColorBurn: + return .colorBurn + case .Overlay: + return .overlay + case .HardLight: + return .hardLight + case .Color: + return .color + case .Luminosity: + return .luminosity + case .Hue: + return .hue + case .Saturation: + return .saturation + case .SoftLight: + return .softLight + case .Darken: + return .darken + case .HardMix: + return .hardMix + case .VividLight: + return .vividLight + case .LinearLight: + return .linearLight + case .PinLight: + return .pinLight + case .LighterColor: + return .lighterColor + case .DarkerColor: + return .darkerColor + case .Divide: + return .divide + } + } + + /// 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(document: SilicaDocument, layer: SilicaLayer) -> [SilicaLayer] { + var clippingLayers : [SilicaLayer] = [] + + let layers : [SilicaLayer] = document.layers.reversed() + let index = layers.firstIndex(of: layer)! + 1 + if index >= layers.count { + return clippingLayers + } + + for layerIndex in index...layers.count - 1 { + if(layers[layerIndex].clipped) { + clippingLayers.append(layers[layerIndex]) + } else { + break + } + } + + return clippingLayers + } +}