1
Fork 0

Separate drawing code into SilicaEngine class

This commit is contained in:
Joshua Goins 2022-06-15 12:32:56 -04:00
parent beac4f568e
commit 8672a69e8b
5 changed files with 368 additions and 305 deletions

View file

@ -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 = "<group>"; };
03328513285A2AB700AEEBF3 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
03328515285A32FD00AEEBF3 /* SilicaDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilicaDocument.swift; sourceTree = "<group>"; };
03328517285A33DA00AEEBF3 /* SilicaEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilicaEngine.swift; sourceTree = "<group>"; };
035D19F826F0927200B332BE /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
035D19F926F0927200B332BE /* SilicaViewer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = SilicaViewer.entitlements; sourceTree = "<group>"; };
035D19FA26F0927200B332BE /* cbridge.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = cbridge.c; sourceTree = "<group>"; };
@ -251,6 +253,7 @@
0371996827BACDE800EE1DFD /* ExportAccessoryView.swift */,
03328513285A2AB700AEEBF3 /* Extensions.swift */,
03328515285A32FD00AEEBF3 /* SilicaDocument.swift */,
03328517285A33DA00AEEBF3 /* SilicaEngine.swift */,
);
path = SilicaViewer;
sourceTree = "<group>";
@ -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 */,

View file

@ -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)
}

View file

@ -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<UInt8>.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)

View file

@ -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)
}
}

View file

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