import Cocoa import ZIPFoundation import CoreFoundation import Accelerate struct SilicaChunk { var x: Int = 0 var y: Int = 0 var image: NSImage = NSImage() } struct SilicaLayerData { var blendMode: Int = 0 var extendedBlend: Int = 0 var chunks: [SilicaChunk] = [] var opacity: Double = 1.0 var hidden: Bool = false } struct SilicaLayer { 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] = [] 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 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) } /* 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 to integer coordinates. */ func parseChunkFilename(filename: String) -> (Int, Int)? { let pathURL = URL(fileURLWithPath: filename) let pathComponents = pathURL.lastPathComponent.replacingOccurrences(of: ".chunk", with: "").components(separatedBy: "~") let x = Int(pathComponents[0]) let y = Int(pathComponents[1]) if x != nil && y != nil { return (x!, y! + 1) } else { return nil } } 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: Archive.AccessMode.read) else { return } let threadEntry = threadArchive[chunkPaths[i]] guard let (x, y) = parseChunkFilename(filename: threadEntry!.path) else { return } let (width, height) = getTileSize(x: x, y: y) var numChannels = 4 if isMask { numChannels = 1 } let byteSize = width * height * numChannels let uncompressedMemory = UnsafeMutablePointer.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 var rgbColorSpace = info.colorSpace if isMask { rgbColorSpace = CGColorSpaceCreateDeviceGray() } var bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue).union(.byteOrder32Big) if isMask { bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.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 } let image = NSImage(cgImage: cgimage, size: NSZeroSize) queue.async(group: dispatchGroup) { layer.data.chunks[i].image = image layer.data.chunks[i].x = x layer.data.chunks[i].y = y } } dispatchGroup.wait() return layer } 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 colorProfileClassKey = dict["colorProfile"] 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)! } 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! String info.name = nameString let authorClassKey = dict[AuthorNameKey] 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 sizeComponents = sizeString.replacingOccurrences(of: "{", with: "").replacingOccurrences(of: "}", with: "").components(separatedBy: ", ") let width = Int(sizeComponents[0]) let height = Int(sizeComponents[1]) 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 //dump(dict, indent: 5) 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 { Swift.print("This is not a valid document!") 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 { return } guard let documentEntry = archive[DocumentArchivePath] else { return } guard let documentData = readData(archive: archive, entry: documentEntry) else { return } var plistFormat = PropertyListSerialization.PropertyListFormat.binary guard let propertyList = try? PropertyListSerialization.propertyList(from: documentData, options: [], format: &plistFormat) else { return } parseDocument(archive: archive, dict: propertyList as! NSDictionary) } 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) var masterImage = ccgContext?.makeImage() let context = CIContext() var previousImage: CGImage? for layer in info.layers.reversed() { // 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) let grayColorSpace = CGColorSpaceCreateDeviceGray() let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) let 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) var kernel: CIBlendKernel? = .sourceOver if layer.data.blendMode == 1 { kernel = .multiply } if layer.data.blendMode == 10 { kernel = .colorBurn } if layer.data.blendMode == 19 { kernel = .darken } if layer.data.blendMode == 8 { kernel = .linearBurn } if layer.data.blendMode == 4 { kernel = .lighten } if layer.data.blendMode == 2 { kernel = .screen } if layer.data.blendMode == 13 { kernel = .hardLight } if layer.data.blendMode == 9 { kernel = .colorDodge } if layer.data.blendMode == 3 { kernel = .subtract } if layer.data.blendMode == 0 && layer.data.blendMode != layer.data.extendedBlend { if layer.data.extendedBlend == 25 { kernel = .darkerColor } if layer.data.extendedBlend == 24 { kernel = .lighterColor } if layer.data.extendedBlend == 21 { kernel = .vividLight } if layer.data.extendedBlend == 22 { kernel = .linearLight } if layer.data.extendedBlend == 23 { kernel = .pinLight } if layer.data.extendedBlend == 20 { kernel = .hardMix } if layer.data.extendedBlend == 26 { kernel = .divide } } if layer.data.blendMode == 11 { kernel = .overlay } if layer.data.blendMode == 17 { kernel = .softLight } if layer.data.blendMode == 12 { kernel = .hardLight } if layer.data.blendMode == 6 { kernel = .difference } if layer.data.blendMode == 5 { kernel = .exclusion } if layer.data.blendMode == 7 { kernel = .componentAdd } if layer.data.blendMode == 15 { kernel = .hue } if layer.data.blendMode == 16 { kernel = .saturation } if layer.data.blendMode == 13 { kernel = .color } if layer.data.blendMode == 14 { kernel = .luminosity } if layer.mask != nil { for chunk in layer.mask!.chunks { let x = chunk.x var y = chunk.y let (width, height) = getTileSize(x: x, y: y) if y == rows { y = 0 } let rect = NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height) if !layer.data.hidden { maskContext?.draw(chunk.image.cgImage(forProposedRect: nil, context: NSGraphicsContext.current, hints: nil)!, in: rect) } } } for chunk in layer.data.chunks { let x = chunk.x var y = chunk.y let (width, height) = getTileSize(x: x, y: y) if y == rows { y = 0 } let rect = NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height) layerContext?.setAlpha(CGFloat(layer.data.opacity)) layerContext?.setBlendMode(.copy) if !layer.data.hidden { layerContext?.draw(chunk.image.cgImage(forProposedRect: nil, context: NSGraphicsContext(cgContext: layerContext!, flipped: false), hints: nil)!, in: rect) } } let layerImage = (layerContext?.makeImage())! if layer.clipped { guard let result = previousImage?.toGrayscale() else { return nil } let newImage = layerImage.masking(result)! previousImage = newImage } else if layer.mask != nil { let maskImage = (maskContext?.makeImage())! let newImage = layerImage.masking(maskImage)! previousImage = newImage } else { previousImage = layerImage } // apply image let ciImage = CIImage(cgImage: (masterImage)!) let newCiImage = kernel!.apply(foreground: CIImage(cgImage: previousImage!), background: ciImage, colorSpace: info.colorSpace) masterImage = context.createCGImage(newCiImage!, from: info.cgRect, format: .RGBA8, colorSpace: info.colorSpace)! } var image = NSImage(cgImage: masterImage!, 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 } } public extension CGImage { func toGrayscale() -> CGImage? { guard let format = vImage_CGImageFormat(cgImage: self) else { return nil } var sourceBuffer: vImage_Buffer = { guard var sourceImageBuffer = try? vImage_Buffer(cgImage: self, format: format), var scaledBuffer = try? vImage_Buffer(width: Int(sourceImageBuffer.height / 3), height: Int(sourceImageBuffer.width / 3), bitsPerPixel: format.bitsPerPixel) else { fatalError("Unable to create source buffers.") } defer { sourceImageBuffer.free() } vImageScale_ARGB8888(&sourceImageBuffer, &scaledBuffer, nil, vImage_Flags(kvImageNoFlags)) return scaledBuffer }() guard var destinationBuffer = try? vImage_Buffer(width: Int(sourceBuffer.width), height: Int(sourceBuffer.height), bitsPerPixel: 8) else { fatalError("Unable to create destination buffers.") } let redCoefficient: Float = 0.2126 let greenCoefficient: Float = 0.7152 let blueCoefficient: Float = 0.0722 let divisor: Int32 = 0x1000 let fDivisor = Float(divisor) var coefficientsMatrix = [ Int16(redCoefficient * fDivisor), Int16(greenCoefficient * fDivisor), Int16(blueCoefficient * fDivisor) ] // Use the matrix of coefficients to compute the scalar luminance by // returning the dot product of each RGB pixel and the coefficients // matrix. let preBias: [Int16] = [0, 0, 0, 0] let postBias: Int32 = 0 vImageMatrixMultiply_ARGB8888ToPlanar8(&sourceBuffer, &destinationBuffer, &coefficientsMatrix, divisor, preBias, postBias, vImage_Flags(kvImageNoFlags)) // Create a 1-channel, 8-bit grayscale format that's used to // generate a displayable image. let monoFormat = vImage_CGImageFormat( bitsPerComponent: 8, bitsPerPixel: 8, colorSpace: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue), renderingIntent: .defaultIntent)! // Create a Core Graphics image from the grayscale destination buffer. return try? destinationBuffer.createCGImage(format: monoFormat) } }