1
Fork 0
This repository has been archived on 2025-04-12. You can view files and clone it, but cannot push or open issues or pull requests.
silica-viewer/SilicaViewer/SilicaEngine.swift

304 lines
12 KiB
Swift
Raw Normal View History

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-11-21 11:40:05 -05:00
if let mask = layer.mask {
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))
}
}
}
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))
}
}
}
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)
}
}
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)!
}
}
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 {
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))
}
}
}
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))
}
}
}
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? {
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
}
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))
}
}
}
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)!
}
/// 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
}
}