This is not the final blending code, it will go through another refactor but it's an improvement from before.
876 lines
32 KiB
Swift
876 lines
32 KiB
Swift
import Cocoa
|
|
import ZIPFoundation
|
|
import CoreFoundation
|
|
import Accelerate
|
|
import CoreMedia
|
|
|
|
struct SilicaChunk {
|
|
var x: Int = 0
|
|
var y: Int = 0
|
|
var image: CGImage?
|
|
}
|
|
|
|
struct SilicaLayerData {
|
|
var blendMode: Int = 0
|
|
var extendedBlend: Int = 0
|
|
var chunks: [SilicaChunk] = []
|
|
var opacity: Double = 1.0
|
|
var hidden: Bool = false
|
|
}
|
|
|
|
struct SilicaLayer : Equatable {
|
|
static func == (lhs: SilicaLayer, rhs: SilicaLayer) -> Bool {
|
|
return lhs.name == rhs.name && lhs.clipped == rhs.clipped
|
|
}
|
|
|
|
var name: String = ""
|
|
var data: SilicaLayerData = SilicaLayerData()
|
|
var mask: SilicaLayerData?
|
|
var clipped: Bool = false
|
|
}
|
|
|
|
struct SilicaDocument {
|
|
var trackedTime: Int = 0
|
|
var tileSize: Int = 0
|
|
var orientation: Int = 0
|
|
var flippedHorizontally: Bool = false
|
|
var flippedVertically: Bool = false
|
|
var name: String = ""
|
|
var authorName: String = ""
|
|
var strokeCount: Int = 0
|
|
|
|
var backgroundColor: CGColor = .white
|
|
var colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
|
|
|
|
var width: Int = 0
|
|
var height: Int = 0
|
|
|
|
var layers: [SilicaLayer] = []
|
|
|
|
var videoFrame: (Int, Int) = (0, 0)
|
|
|
|
lazy var nsSize = {
|
|
return NSSize(width: width, height: height)
|
|
}()
|
|
|
|
lazy var cgSize = {
|
|
return CGSize(width: width, height: height)
|
|
}()
|
|
|
|
lazy var cgRect = {
|
|
return CGRect(origin: .zero, size: cgSize)
|
|
}()
|
|
}
|
|
|
|
func objectRefGetValue2(_ objectRef: CFTypeRef) -> UInt32 {
|
|
let val = unsafeBitCast(objectRef, to: UInt64.self)
|
|
|
|
return valueForKeyedArchiverUID(val)
|
|
}
|
|
|
|
class Document: NSDocument {
|
|
var data: Data? // oh no...
|
|
|
|
var dict: NSDictionary?
|
|
|
|
var info = SilicaDocument()
|
|
|
|
var rows: Int = 0
|
|
var columns: Int = 0
|
|
|
|
var remainderWidth: Int = 0
|
|
var remainderHeight: Int = 0
|
|
|
|
struct SilicaParsingError: Error, LocalizedError {
|
|
enum Kind {
|
|
case invalid
|
|
}
|
|
|
|
let kind: Kind
|
|
let filename: URL?
|
|
|
|
public var errorDescription: String? {
|
|
switch self.kind {
|
|
case .invalid:
|
|
return filename!.lastPathComponent + " is an invalid Silica Document."
|
|
}
|
|
}
|
|
}
|
|
|
|
func throwError(_ error: SilicaParsingError.Kind) {
|
|
DispatchQueue.main.sync {
|
|
let _ = presentError(SilicaParsingError(kind: error, filename: fileURL))
|
|
}
|
|
}
|
|
|
|
override init() {
|
|
super.init()
|
|
}
|
|
|
|
override func makeWindowControllers() {
|
|
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
|
|
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
|
|
self.addWindowController(windowController)
|
|
}
|
|
|
|
override class func canConcurrentlyReadDocuments(ofType: String) -> Bool {
|
|
return ofType == "com.procreate"
|
|
}
|
|
|
|
func getIdealFilename() -> String {
|
|
if (!(info.name.isEmpty)) {
|
|
return info.name
|
|
} else {
|
|
return fileURL!.deletingPathExtension().lastPathComponent
|
|
}
|
|
}
|
|
|
|
/*
|
|
Pass in an object from the $object array, which always contains a $class key.
|
|
*/
|
|
func getDocumentClassName(dict: NSDictionary) -> String? {
|
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
|
|
|
if let value = dict["$class"] {
|
|
let classObjectId = objectRefGetValue2(value as CFTypeRef)
|
|
let classObject = objectsArray[Int(classObjectId)] as! NSDictionary
|
|
|
|
return classObject["$classname"] as? String
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
Returns the correct tile size, taking into account the remainder between tile size and image size.
|
|
*/
|
|
func getTileSize(_ x: Int, _ y: Int) -> (Int, Int) {
|
|
var width: Int = info.tileSize
|
|
var height: Int = info.tileSize
|
|
|
|
if((x + 1) == columns) {
|
|
width = info.tileSize - remainderWidth
|
|
}
|
|
|
|
if(y == rows) {
|
|
height = info.tileSize - remainderHeight
|
|
}
|
|
|
|
return (width, height)
|
|
}
|
|
|
|
/*
|
|
Converts a CFKeyedArchiveUID from a NSKeyedArchive to a Int for indexing an array or dictionary.
|
|
*/
|
|
func getClassID(id: Any?) -> Int {
|
|
return Int(objectRefGetValue2(id! as CFTypeRef))
|
|
}
|
|
|
|
/*
|
|
Parses the chunk filename, ex. "1~1.chunk" to integer coordinates (1, 2).
|
|
*/
|
|
func parseChunkFilename(_ filename: String) -> (Int, Int)? {
|
|
let pathURL = URL(fileURLWithPath: filename)
|
|
let pathComponents = pathURL.lastPathComponent.replacingOccurrences(of: ".chunk", with: "").components(separatedBy: "~")
|
|
|
|
if let x = Int(pathComponents[0]), let y = Int(pathComponents[1]) {
|
|
return (x, y + 1)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func getChunkRect(_ chunk: SilicaChunk) -> NSRect {
|
|
let x = chunk.x
|
|
var y = chunk.y
|
|
|
|
let (width, height) = getTileSize(x, y)
|
|
|
|
if y == rows {
|
|
y = 0
|
|
}
|
|
|
|
return NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height)
|
|
}
|
|
|
|
// TODO: convert to switch/case
|
|
func getBlendKernel(_ layer: SilicaLayer) -> CIBlendKernel? {
|
|
if layer.data.blendMode == 1 {
|
|
return .multiply
|
|
}
|
|
if layer.data.blendMode == 10 {
|
|
return .colorBurn
|
|
}
|
|
if layer.data.blendMode == 19 {
|
|
return .darken
|
|
}
|
|
if layer.data.blendMode == 8 {
|
|
return .linearBurn
|
|
}
|
|
if layer.data.blendMode == 4 {
|
|
return .lighten
|
|
}
|
|
if layer.data.blendMode == 2 {
|
|
return .screen
|
|
}
|
|
if layer.data.blendMode == 13 {
|
|
return .color
|
|
}
|
|
if layer.data.blendMode == 9 {
|
|
return .colorDodge
|
|
}
|
|
if layer.data.blendMode == 3 {
|
|
return .componentAdd
|
|
}
|
|
|
|
if layer.data.blendMode == 0 && layer.data.blendMode != layer.data.extendedBlend {
|
|
if layer.data.extendedBlend == 25 {
|
|
return .darkerColor
|
|
}
|
|
if layer.data.extendedBlend == 24 {
|
|
return .lighterColor
|
|
}
|
|
if layer.data.extendedBlend == 21 {
|
|
return .vividLight
|
|
}
|
|
if layer.data.extendedBlend == 22 {
|
|
return .linearLight
|
|
}
|
|
if layer.data.extendedBlend == 23 {
|
|
return .pinLight
|
|
}
|
|
if layer.data.extendedBlend == 20 {
|
|
return .hardMix
|
|
}
|
|
if layer.data.extendedBlend == 26 {
|
|
return .divide
|
|
}
|
|
}
|
|
|
|
if layer.data.blendMode == 11 {
|
|
return .overlay
|
|
}
|
|
if layer.data.blendMode == 17 {
|
|
return .softLight
|
|
}
|
|
if layer.data.blendMode == 12 {
|
|
return .hardLight
|
|
}
|
|
if layer.data.blendMode == 6 {
|
|
return .difference
|
|
}
|
|
if layer.data.blendMode == 5 {
|
|
return .exclusion
|
|
}
|
|
if layer.data.blendMode == 7 {
|
|
return .subtract
|
|
}
|
|
if layer.data.blendMode == 15 {
|
|
return .hue
|
|
}
|
|
if layer.data.blendMode == 16 {
|
|
return .saturation
|
|
}
|
|
if layer.data.blendMode == 13 {
|
|
return .color
|
|
}
|
|
if layer.data.blendMode == 14 {
|
|
return .luminosity
|
|
}
|
|
|
|
return .sourceOver
|
|
}
|
|
|
|
func parseSilicaLayer(archive: Archive, dict: NSDictionary, isMask: Bool) -> SilicaLayer? {
|
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
|
|
|
if getDocumentClassName(dict: dict) == LayerClassName {
|
|
var layer = SilicaLayer()
|
|
|
|
if let val = dict["name"] {
|
|
let NameClassID = getClassID(id: val)
|
|
let NameClass = objectsArray[NameClassID] as! NSString
|
|
|
|
layer.name = NameClass as String
|
|
}
|
|
|
|
let UUIDKey = dict["UUID"]
|
|
let UUIDClassID = getClassID(id: UUIDKey)
|
|
let UUIDClass = objectsArray[UUIDClassID] as! NSString
|
|
|
|
let maskKey = dict["mask"]
|
|
let maskClassID = getClassID(id: maskKey)
|
|
let maskClass = objectsArray[maskClassID]
|
|
|
|
layer.data.blendMode = (dict["blend"] as? NSNumber)!.intValue
|
|
layer.data.extendedBlend = (dict["extendedBlend"] as? NSNumber)!.intValue
|
|
layer.data.opacity = (dict["opacity"] as? NSNumber)!.doubleValue
|
|
layer.data.hidden = (dict["hidden"] as? Bool)!
|
|
layer.clipped = (dict["clipped"] as? Bool)!
|
|
|
|
if maskClassID != 0 {
|
|
layer.mask = parseSilicaLayer(archive: archive, dict: maskClass as! NSDictionary, isMask: true)?.data
|
|
}
|
|
|
|
var chunkPaths: [String] = []
|
|
|
|
archive.forEach { (entry: Entry) in
|
|
if entry.path.contains(String(UUIDClass)) {
|
|
chunkPaths.append(entry.path)
|
|
}
|
|
}
|
|
|
|
layer.data.chunks = Array(repeating: SilicaChunk(), count: chunkPaths.count)
|
|
|
|
let dispatchGroup = DispatchGroup()
|
|
let queue = DispatchQueue(label: "imageWork")
|
|
|
|
DispatchQueue.concurrentPerform(iterations: chunkPaths.count) { (i: Int) in
|
|
guard let threadArchive = Archive(data: self.data!, accessMode: .read) else {
|
|
return
|
|
}
|
|
|
|
let threadEntry = threadArchive[chunkPaths[i]]
|
|
|
|
guard let (x, y) = parseChunkFilename(threadEntry!.path) else {
|
|
return
|
|
}
|
|
|
|
let (width, height) = getTileSize(x, y)
|
|
|
|
let numChannels = isMask ? 1 : 4
|
|
let byteSize = width * height * numChannels
|
|
|
|
let uncompressedMemory = UnsafeMutablePointer<UInt8>.allocate(capacity: byteSize)
|
|
|
|
guard let lzoData = readData(archive: threadArchive, entry: threadEntry!) else {
|
|
return
|
|
}
|
|
|
|
lzoData.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) -> Void in
|
|
var len = lzo_uint(byteSize)
|
|
|
|
lzo1x_decompress_safe(bytes.baseAddress!.assumingMemoryBound(to: uint8.self), lzo_uint(lzoData.count), uncompressedMemory, &len, nil)
|
|
})
|
|
|
|
let imageData = Data(bytes: uncompressedMemory, count: byteSize)
|
|
|
|
let render: CGColorRenderingIntent = .defaultIntent
|
|
let rgbColorSpace = isMask ? CGColorSpaceCreateDeviceGray() : info.colorSpace
|
|
|
|
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 {
|
|
return
|
|
}
|
|
|
|
queue.async(group: dispatchGroup) {
|
|
layer.data.chunks[i].image = cgimage
|
|
layer.data.chunks[i].x = x
|
|
layer.data.chunks[i].y = y
|
|
}
|
|
}
|
|
|
|
dispatchGroup.wait()
|
|
|
|
return layer
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parsePairString(_ str: String) -> (Int, Int)? {
|
|
let sizeComponents = str.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ")
|
|
|
|
if sizeComponents.count == 2 {
|
|
let width = Int(sizeComponents[0])
|
|
let height = Int(sizeComponents[1])
|
|
|
|
return (width!, height!)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func parseSilicaDocument(archive: Archive, dict: NSDictionary) {
|
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
|
|
|
if getDocumentClassName(dict: dict) == DocumentClassName {
|
|
info.trackedTime = (dict[TrackedTimeKey] as! NSNumber).intValue
|
|
info.tileSize = (dict[TileSizeKey] as! NSNumber).intValue
|
|
info.orientation = (dict[OrientationKey] as! NSNumber).intValue
|
|
info.flippedHorizontally = (dict[FlippedHorizontallyKey] as! NSNumber).boolValue
|
|
info.flippedVertically = (dict[FlippedVerticallyKey] as! NSNumber).boolValue
|
|
|
|
let videoResolutionClassKey = dict["SilicaDocumentVideoSegmentInfoKey"]
|
|
let videoResolutionClassID = getClassID(id: videoResolutionClassKey)
|
|
let videoResolution = objectsArray[videoResolutionClassID] as! NSDictionary
|
|
|
|
let frameSizeClassKey = videoResolution["frameSize"]
|
|
let frameSizeClassID = getClassID(id: frameSizeClassKey)
|
|
let frameSize = objectsArray[frameSizeClassID] as! String
|
|
|
|
info.videoFrame = parsePairString(frameSize)!
|
|
|
|
let colorProfileClassKey = dict["colorProfile"]
|
|
if colorProfileClassKey != nil {
|
|
let colorProfileClassID = getClassID(id: colorProfileClassKey)
|
|
let colorProfile = objectsArray[colorProfileClassID] as! NSDictionary
|
|
|
|
let colorProfileNameClassKey = colorProfile["SiColorProfileArchiveICCNameKey"]
|
|
let colorProfileNameClassID = getClassID(id: colorProfileNameClassKey)
|
|
let colorProfileName = objectsArray[colorProfileNameClassID] as! NSString
|
|
|
|
// we only support the basic "Display P3" color space... does Procreate actually store the ICC data??
|
|
if colorProfileName == "Display P3" {
|
|
info.colorSpace = CGColorSpace(name: CGColorSpace.displayP3)!
|
|
}
|
|
} else {
|
|
info.colorSpace = CGColorSpace(name: CGColorSpace.displayP3)!
|
|
}
|
|
|
|
let backgroundClassKey = dict["backgroundColor"]
|
|
let backgroundClassID = getClassID(id: backgroundClassKey)
|
|
let background = objectsArray[backgroundClassID] as! NSData
|
|
|
|
var backgroundArray: [Float] = [0.0, 0.0, 0.0, 0.0]
|
|
|
|
background.getBytes(&backgroundArray, length: 16)
|
|
let backgroundCgArray: [CGFloat] = [CGFloat(backgroundArray[0]), CGFloat(backgroundArray[1]), CGFloat(backgroundArray[2]), CGFloat(backgroundArray[3])]
|
|
|
|
info.backgroundColor = CGColor(colorSpace: info.colorSpace, components: backgroundCgArray)!
|
|
|
|
let strokeClassKey = dict[StrokeCountKey]
|
|
let strokeClassID = getClassID(id: strokeClassKey)
|
|
let strokeCount = objectsArray[strokeClassID] as! NSNumber
|
|
|
|
info.strokeCount = Int(truncating: strokeCount)
|
|
|
|
let nameClassKey = dict[NameKey]
|
|
let nameClassID = getClassID(id: nameClassKey)
|
|
let nameString = objectsArray[nameClassID] as! NSString
|
|
|
|
if nameString != "$null" {
|
|
info.name = nameString as String
|
|
}
|
|
|
|
let authorClassKey = dict[AuthorNameKey]
|
|
if authorClassKey != nil {
|
|
let authorClassID = getClassID(id: authorClassKey)
|
|
let authorString = objectsArray[authorClassID] as! String
|
|
|
|
if authorString != "$null" {
|
|
info.authorName = authorString
|
|
}
|
|
}
|
|
|
|
let sizeClassKey = dict[SizeKey]
|
|
let sizeClassID = getClassID(id: sizeClassKey)
|
|
let sizeString = objectsArray[sizeClassID] as! String
|
|
|
|
let (width, height) = parsePairString(sizeString)!
|
|
info.width = width
|
|
info.height = height
|
|
|
|
columns = Int(ceil(Float(info.width) / Float(info.tileSize)))
|
|
rows = Int(ceil(Float(info.height) / Float(info.tileSize))) + 1 // TODO: lol why
|
|
|
|
if info.width % info.tileSize != 0 {
|
|
remainderWidth = (columns * info.tileSize) - info.width
|
|
}
|
|
|
|
if info.height % info.tileSize != 0 {
|
|
remainderHeight = (rows * info.tileSize) - info.height
|
|
}
|
|
|
|
let layersClassKey = dict[LayersKey]
|
|
let layersClassID = getClassID(id: layersClassKey)
|
|
let layersClass = objectsArray[layersClassID] as! NSDictionary
|
|
|
|
let array = layersClass["NS.objects"] as! NSArray
|
|
|
|
for object in array {
|
|
let layerClassID = getClassID(id: object)
|
|
let layerClass = objectsArray[layerClassID] as! NSDictionary
|
|
|
|
guard let layer = parseSilicaLayer(archive: archive, dict: layerClass, isMask: false) else { return }
|
|
|
|
info.layers.append(layer)
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseDocument(archive: Archive, dict: NSDictionary) {
|
|
// double check if this archive is really correct
|
|
if let value = dict["$version"] {
|
|
if (value as! Int) != NSKeyedArchiveVersion {
|
|
throwError(.invalid)
|
|
return
|
|
}
|
|
|
|
self.dict = dict
|
|
|
|
let objectsArray = dict["$objects"] as! NSArray
|
|
|
|
// let's read the $top class, which is always going to be SilicaDocument type.
|
|
let topObject = dict["$top"] as! NSDictionary
|
|
let topClassID = objectRefGetValue2(topObject["root"] as CFTypeRef)
|
|
let topObjectClass = objectsArray[Int(topClassID)] as! NSDictionary
|
|
|
|
parseSilicaDocument(archive: archive, dict: topObjectClass)
|
|
}
|
|
}
|
|
|
|
override func read(from data: Data, ofType typeName: String) throws {
|
|
self.data = data
|
|
|
|
guard let archive = Archive(data: data, accessMode: Archive.AccessMode.read) else {
|
|
throwError(.invalid)
|
|
return
|
|
}
|
|
|
|
guard let documentEntry = archive[DocumentArchivePath] else {
|
|
throwError(.invalid)
|
|
return
|
|
}
|
|
|
|
guard let documentData = readData(archive: archive, entry: documentEntry) else {
|
|
throwError(.invalid)
|
|
return
|
|
}
|
|
|
|
var plistFormat = PropertyListSerialization.PropertyListFormat.binary
|
|
guard let propertyList = try? PropertyListSerialization.propertyList(from: documentData, options: [], format: &plistFormat) else {
|
|
throwError(.invalid)
|
|
return
|
|
}
|
|
|
|
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())!
|
|
}
|
|
|
|
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)
|
|
|
|
Swift.print(layer.name + " - " + kernel!.name)
|
|
|
|
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)!
|
|
}
|
|
|
|
// this returns all of the layers that are clipping onto this one
|
|
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
|
|
}
|
|
|
|
func makeComposite() -> NSImage? {
|
|
// create the final composite output image
|
|
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).union(.byteOrder32Big)
|
|
|
|
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?
|
|
|
|
var kernel = getBlendKernel(layer)
|
|
//kernel = .sourceOver
|
|
|
|
Swift.print(layer.name + " - " + kernel!.name)
|
|
|
|
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 {
|
|
Swift.print("- " + layer.name + " is clipping with us...")
|
|
|
|
// so we if we want to clip, we want to gather all of the clipping layers in order first...
|
|
|
|
Swift.print("processing clipped layer " + layer.name)
|
|
|
|
let temporaryClippedMaster = blendLayer(layer, previousImage: &clippedMaster)
|
|
|
|
clippedMaster = context.createCGImage(temporaryClippedMaster, from: info.cgRect, format: .RGBA8, colorSpace: info.colorSpace)
|
|
}
|
|
|
|
Swift.print("clipping " + layer.name + "...")
|
|
|
|
layerContext?.setAlpha(1.0)
|
|
layerContext?.setBlendMode(.sourceAtop)
|
|
|
|
layerContext?.draw(clippedMaster!, in: info.cgRect)
|
|
}
|
|
|
|
let layerImage = layerContext?.makeImage()
|
|
|
|
if layer.mask != nil && maskContext != nil {
|
|
Swift.print("masking " + layer.name + "...")
|
|
|
|
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)
|
|
|
|
if info.orientation == 3 {
|
|
image = image.imageRotatedByDegreess(degrees: 90)
|
|
} else if info.orientation == 4 {
|
|
image = image.imageRotatedByDegreess(degrees: -90)
|
|
}
|
|
|
|
if info.flippedHorizontally && (info.orientation == 1 || info.orientation == 2) {
|
|
image = image.flipHorizontally()
|
|
} else if info.flippedHorizontally && (info.orientation == 3 || info.orientation == 4) {
|
|
image = image.flipVertically()
|
|
} else if info.flippedVertically && (info.orientation == 1 || info.orientation == 2) {
|
|
image = image.flipVertically()
|
|
} else if !info.flippedVertically && (info.orientation == 3 || info.orientation == 4) {
|
|
image = image.flipHorizontally()
|
|
}
|
|
|
|
return image
|
|
}
|
|
|
|
func makeThumbnail() -> NSImage? {
|
|
guard let archive = Archive(data: data!, accessMode: Archive.AccessMode.read) else {
|
|
return nil
|
|
}
|
|
|
|
guard let entry = archive[ThumbnailPath] else {
|
|
return nil
|
|
}
|
|
|
|
guard let thumbnailData = readData(archive: archive, entry: entry) else {
|
|
return nil
|
|
}
|
|
|
|
return NSImage(data: thumbnailData)
|
|
}
|
|
}
|
|
|
|
public extension NSImage {
|
|
func imageRotatedByDegreess(degrees:CGFloat) -> NSImage {
|
|
var imageBounds = NSMakeRect(0.0, 0.0, size.width, size.height)
|
|
|
|
let pathBounds = NSBezierPath(rect: imageBounds)
|
|
var transform = NSAffineTransform()
|
|
transform.rotate(byDegrees: degrees)
|
|
pathBounds.transform(using: transform as AffineTransform)
|
|
|
|
let rotatedBounds:NSRect = NSMakeRect(NSZeroPoint.x, NSZeroPoint.y, pathBounds.bounds.size.width, pathBounds.bounds.size.height )
|
|
let rotatedImage = NSImage(size: rotatedBounds.size)
|
|
|
|
imageBounds.origin.x = NSMidX(rotatedBounds) - (NSWidth(imageBounds) / 2)
|
|
imageBounds.origin.y = NSMidY(rotatedBounds) - (NSHeight(imageBounds) / 2)
|
|
|
|
transform = NSAffineTransform()
|
|
transform.translateX(by: +(NSWidth(rotatedBounds) / 2 ), yBy: +(NSHeight(rotatedBounds) / 2))
|
|
transform.rotate(byDegrees: degrees)
|
|
transform.translateX(by: -(NSWidth(rotatedBounds) / 2 ), yBy: -(NSHeight(rotatedBounds) / 2))
|
|
|
|
rotatedImage.lockFocus()
|
|
transform.concat()
|
|
|
|
self.draw(in: imageBounds, from: .zero, operation: .copy, fraction: 1.0)
|
|
|
|
rotatedImage.unlockFocus()
|
|
|
|
return rotatedImage
|
|
}
|
|
|
|
func flipHorizontally() -> NSImage {
|
|
let flipedImage = NSImage(size: size)
|
|
flipedImage.lockFocus()
|
|
|
|
let transform = NSAffineTransform()
|
|
transform.translateX(by: size.width, yBy: 0.0)
|
|
transform.scaleX(by: -1.0, yBy: 1.0)
|
|
transform.concat()
|
|
|
|
let rect = NSMakeRect(0, 0, size.width, size.height)
|
|
self.draw(at: .zero, from: rect, operation: .sourceOver, fraction: 1.0)
|
|
|
|
flipedImage.unlockFocus()
|
|
|
|
return flipedImage
|
|
}
|
|
|
|
func flipVertically() -> NSImage {
|
|
let flipedImage = NSImage(size: size)
|
|
flipedImage.lockFocus()
|
|
|
|
let transform = NSAffineTransform()
|
|
transform.translateX(by: 0.0, yBy: size.height)
|
|
transform.scaleX(by: 1.0, yBy: -1.0)
|
|
transform.concat()
|
|
|
|
let rect = NSMakeRect(0, 0, size.width, size.height)
|
|
self.draw(at: .zero, from: rect, operation: .sourceOver, fraction: 1.0)
|
|
|
|
flipedImage.unlockFocus()
|
|
|
|
return flipedImage
|
|
}
|
|
}
|
|
|
|
class ColorFilter: CIFilter {
|
|
// 2
|
|
var inputImage: CIImage?
|
|
|
|
// 3
|
|
static var kernel: CIKernel = { () -> CIColorKernel in
|
|
guard let url = Bundle.main.url(forResource: "default",
|
|
withExtension: "metallib"),
|
|
let data = try? Data(contentsOf: url) else {
|
|
fatalError("Unable to load metallib")
|
|
}
|
|
|
|
guard let kernel = try? CIColorKernel(
|
|
functionName: "colorFilterKernel",
|
|
fromMetalLibraryData: data) else {
|
|
fatalError("Unable to create color kernel")
|
|
}
|
|
|
|
return kernel
|
|
}()
|
|
|
|
// 4
|
|
override var outputImage: CIImage? {
|
|
guard let inputImage = inputImage else { return nil }
|
|
return ColorFilter.kernel.apply(
|
|
extent: inputImage.extent,
|
|
roiCallback: { _, rect in
|
|
return rect
|
|
},
|
|
arguments: [inputImage])
|
|
}
|
|
}
|
|
|
|
extension CGImage {
|
|
func toGrayscale() -> CGImage? {
|
|
let rect = CGRect(x: 0, y: 0, width: width, height: height)
|
|
let grayColorSpace = CGColorSpaceCreateDeviceGray()
|
|
let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big)
|
|
|
|
let ccgContext = CGContext(data: nil, width: width, height: height, bitsPerComponent: 16, bytesPerRow: width * 2, space: grayColorSpace, bitmapInfo: maskBitmapInfo.rawValue)
|
|
|
|
ccgContext?.setFillColor(.black)
|
|
ccgContext?.fill(rect)
|
|
|
|
ccgContext?.draw(self, in: rect)
|
|
|
|
return ccgContext?.makeImage()
|
|
}
|
|
}
|