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