Separate drawing code into SilicaEngine class
This commit is contained in:
parent
beac4f568e
commit
8672a69e8b
5 changed files with 368 additions and 305 deletions
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
287
SilicaViewer/SilicaEngine.swift
Normal file
287
SilicaViewer/SilicaEngine.swift
Normal 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
|
||||
}
|
||||
}
|
Reference in a new issue