2022-06-15 12:32:56 -04:00
|
|
|
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?
|
|
|
|
|
2022-11-21 11:40:05 -05:00
|
|
|
guard let kernel = getBlendKernel(layer) else {
|
|
|
|
return nil
|
|
|
|
}
|
2022-06-15 12:32:56 -04:00
|
|
|
|
2022-11-21 11:40:05 -05:00
|
|
|
if let mask = layer.mask {
|
2022-06-15 12:32:56 -04:00
|
|
|
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())
|
|
|
|
|
2022-11-21 11:40:05 -05:00
|
|
|
for chunk in mask.chunks {
|
2022-11-21 11:09:28 -05:00
|
|
|
if let image = chunk.image {
|
|
|
|
maskContext?.draw(image, in: document.getChunkRect(chunk))
|
|
|
|
}
|
2022-06-15 12:32:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for chunk in layer.data.chunks {
|
|
|
|
layerContext?.setAlpha(CGFloat(layer.data.opacity))
|
|
|
|
layerContext?.setBlendMode(.normal)
|
|
|
|
|
|
|
|
if !layer.data.hidden {
|
2022-11-21 11:40:05 -05:00
|
|
|
if let image = chunk.image {
|
|
|
|
layerContext?.draw(image, in: document.getChunkRect(chunk))
|
|
|
|
}
|
2022-06-15 12:32:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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...
|
2022-11-21 11:40:05 -05:00
|
|
|
if let temporaryClippedMaster = blendLayer(document: document, layer: layer, previousImage: &clippedMaster) {
|
|
|
|
clippedMaster = context.createCGImage(temporaryClippedMaster, from: document.cgRect(), format: .RGBA8, colorSpace: document.colorSpace)
|
|
|
|
}
|
2022-06-15 12:32:56 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2022-11-21 11:40:05 -05:00
|
|
|
masterImage = kernel.apply(foreground: CIImage(cgImage: previousImage!),
|
|
|
|
background: masterImage,
|
|
|
|
colorSpace: document.colorSpace)!
|
2022-06-15 12:32:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return context.createCGImage(masterImage, from: document.cgRect(), format: .RGBA8, colorSpace: document.colorSpace)
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeBlendImage(document: SilicaDocument, layer: SilicaLayer) -> CGImage {
|
|
|
|
var maskContext: CGContext?
|
|
|
|
|
2022-11-21 11:40:05 -05:00
|
|
|
if let mask = layer.mask {
|
2022-06-15 12:32:56 -04:00
|
|
|
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())
|
|
|
|
|
2022-11-21 11:40:05 -05:00
|
|
|
for chunk in mask.chunks {
|
|
|
|
if let image = chunk.image {
|
|
|
|
maskContext?.draw(image, in: document.getChunkRect(chunk))
|
|
|
|
}
|
2022-06-15 12:32:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2022-11-21 11:40:05 -05:00
|
|
|
if let image = chunk.image {
|
|
|
|
layerContext?.draw(image, in: document.getChunkRect(chunk))
|
|
|
|
}
|
2022-06-15 12:32:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
2022-11-21 11:40:05 -05:00
|
|
|
func blendLayer(document: SilicaDocument, layer : SilicaLayer, previousImage : inout CGImage?) -> CIImage? {
|
2022-06-15 12:32:56 -04:00
|
|
|
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())
|
|
|
|
|
2022-11-21 11:40:05 -05:00
|
|
|
guard let kernel = getBlendKernel(layer) else {
|
|
|
|
return nil
|
|
|
|
}
|
2022-06-15 12:32:56 -04:00
|
|
|
|
|
|
|
for chunk in layer.data.chunks {
|
|
|
|
layerContext?.setAlpha(CGFloat(layer.data.opacity))
|
|
|
|
layerContext?.setBlendMode(.normal)
|
|
|
|
|
|
|
|
if !layer.data.hidden {
|
2022-11-21 11:40:05 -05:00
|
|
|
if let image = chunk.image {
|
|
|
|
layerContext?.draw(image, in: document.getChunkRect(chunk))
|
|
|
|
}
|
2022-06-15 12:32:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let layerImage = layerContext?.makeImage()
|
|
|
|
|
|
|
|
// apply image
|
2022-11-21 11:40:05 -05:00
|
|
|
return kernel.apply(foreground: CIImage(cgImage: layerImage!), background: previousImage == nil ? CIImage(color: .clear) : CIImage(cgImage: previousImage!), colorSpace: document.colorSpace)!
|
2022-06-15 12:32:56 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
}
|
|
|
|
}
|