diff --git a/ProcreateViewer/Base.lproj/Main.storyboard b/ProcreateViewer/Base.lproj/Main.storyboard index 91fed50..cb2424d 100644 --- a/ProcreateViewer/Base.lproj/Main.storyboard +++ b/ProcreateViewer/Base.lproj/Main.storyboard @@ -198,14 +198,14 @@ DQ - - + + @@ -247,49 +247,14 @@ DQ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ProcreateViewer/Document.swift b/ProcreateViewer/Document.swift index 511b37a..af02aad 100644 --- a/ProcreateViewer/Document.swift +++ b/ProcreateViewer/Document.swift @@ -3,15 +3,9 @@ import ZIPFoundation import CoreFoundation struct SilicaChunk { - init() { - x = 0 - y = 0 - image = NSImage() - } - - var x: Int - var y: Int - var image: NSImage + var x: Int = 0 + var y: Int = 0 + var image: NSImage = NSImage() } struct SilicaLayer { @@ -38,6 +32,11 @@ class Document: NSDocument { var dict: NSDictionary? + let NSKeyedArchiveVersion = 100000 + + let ThumbnailPath = "QuickLook/Thumbnail.png" + let DocumentArchivePath = "Document.archive" + let DocumentClassName = "SilicaDocument" let TrackedTimeKey = "SilicaDocumentTrackedTimeKey" let LayersKey = "layers" @@ -47,9 +46,13 @@ class Document: NSDocument { let LayerClassName = "SilicaLayer" var info = SilicaDocument() + + var rows: Int = 0 + var columns: Int = 0 + + var remainderWidth: Int = 0 + var remainderHeight: Int = 0 - var thumbnail: NSImage? = nil - override init() { super.init() } @@ -76,6 +79,64 @@ class Document: NSDocument { 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(objectRefGetValue(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!) + } else { + return nil + } + } + + func readData(archive: Archive, entry: Entry) -> Data? { + var data = Data() + + do { + let _ = try archive.extract(entry, consumer: { (d) in + data.append(d) + }) + } catch { + Swift.print("Extracting entry from archive failed with error:\(error)") + + return nil + } + + return data + } + func parseSilicaLayer(archive: Archive, dict: NSDictionary) { let objectsArray = self.dict?["$objects"] as! NSArray @@ -83,70 +144,76 @@ class Document: NSDocument { var layer = SilicaLayer() let UUIDKey = dict["UUID"] - let UUIDClassID = objectRefGetValue(UUIDKey as CFTypeRef) - let UUIDClass = objectsArray[Int(UUIDClassID)] as! NSString + let UUIDClassID = getClassID(id: UUIDKey) + let UUIDClass = objectsArray[UUIDClassID] as! NSString - var chunkPaths: [Entry] = [] + var chunkPaths: [String] = [] archive.forEach { (entry: Entry) in if entry.path.contains(String(UUIDClass)) { - chunkPaths.append(entry) + chunkPaths.append(entry.path) } } layer.chunks = Array(repeating: SilicaChunk(), count: chunkPaths.count) + let dispatchGroup = DispatchGroup() + let queue = DispatchQueue(label: "imageWork") + DispatchQueue.concurrentPerform(iterations: chunkPaths.count) { (i: Int) in - let entry = chunkPaths[i] + dispatchGroup.enter() + + var threadArchive: Archive? + var threadEntry: Entry? - let pathURL = URL(fileURLWithPath: entry.path) - let pathComponents = pathURL.lastPathComponent.replacingOccurrences(of: ".chunk", with: "").components(separatedBy: "~") - - let x = Int(pathComponents[0]) - let y = Int(pathComponents[1]) - - layer.chunks[i].x = x! - layer.chunks[i].y = y! - - guard let archive = Archive(data: self.data!, accessMode: Archive.AccessMode.read) else { + queue.sync { + threadArchive = Archive(data: self.data!, accessMode: Archive.AccessMode.read) + threadEntry = threadArchive?[chunkPaths[i]] + } + + guard let (x, y) = parseChunkFilename(filename: threadEntry!.path) else { return } - var lzo_data = Data() + let (width, height) = getTileSize(x: x, y: y) + let byteSize = width * height * 4 - do { - try archive.extract(entry, consumer: { (d) in - lzo_data.append(d) - }) - } catch { - Swift.print("Extracting entry from archive failed with error:\(error)") + let uncompressedMemory = UnsafeMutablePointer.allocate(capacity: byteSize) + + guard let lzoData = readData(archive: threadArchive!, entry: threadEntry!) else { + return } - let uint8Pointer = UnsafeMutablePointer.allocate(capacity: info.tileSize * info.tileSize * 4) - - lzo_data.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) -> Void in - var len = lzo_uint(info.tileSize * info.tileSize * 4) + lzoData.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) -> Void in + var len = lzo_uint(byteSize) - lzo1x_decompress_safe(bytes.baseAddress!.assumingMemoryBound(to: uint8.self), lzo_uint(lzo_data.count), uint8Pointer, &len, nil) + lzo1x_decompress_safe(bytes.baseAddress!.assumingMemoryBound(to: uint8.self), lzo_uint(lzoData.count), uncompressedMemory, &len, nil) }) - let image_data = Data(bytes: uint8Pointer, count: info.tileSize * info.tileSize * 4) + let imageData = Data(bytes: uncompressedMemory, count: byteSize) let render: CGColorRenderingIntent = CGColorRenderingIntent.defaultIntent let rgbColorSpace = CGColorSpaceCreateDeviceRGB() - let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue) - .union(.byteOrder32Big) - let providerRef: CGDataProvider? = CGDataProvider(data: image_data as CFData) + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue).union(.byteOrder32Big) + let providerRef: CGDataProvider? = CGDataProvider(data: imageData as CFData) - let cgimage: CGImage? = CGImage(width: info.tileSize, height: info.tileSize, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: info.tileSize * 4, space: rgbColorSpace, bitmapInfo: bitmapInfo, provider: providerRef!, decode: nil, shouldInterpolate: false, intent: render) + guard let cgimage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: width * 4, space: rgbColorSpace, bitmapInfo: bitmapInfo, provider: providerRef!, decode: nil, shouldInterpolate: false, intent: render) else { + return + } - if cgimage != nil { - let image = NSImage(cgImage: cgimage!, size: NSZeroSize) - + let image = NSImage(cgImage: cgimage, size: NSZeroSize) + + queue.async(flags: .barrier) { layer.chunks[i].image = image + layer.chunks[i].x = x + layer.chunks[i].y = y + + dispatchGroup.leave() } } + dispatchGroup.wait() + info.layers.append(layer) } } @@ -159,8 +226,8 @@ class Document: NSDocument { info.tileSize = (dict[TileSizeKey] as! NSNumber).intValue let sizeClassKey = dict[SizeKey] - let sizeClassID = objectRefGetValue(sizeClassKey as CFTypeRef) - let sizeString = objectsArray[Int(sizeClassID)] as! String + 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]) @@ -169,15 +236,26 @@ class Document: NSDocument { info.width = width! info.height = height! + columns = Int(ceil(Float(info.width) / Float(info.tileSize))) + rows = Int(ceil(Float(info.height) / Float(info.tileSize))) + + 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 = objectRefGetValue(layersClassKey as CFTypeRef) - let layersClass = objectsArray[Int(layersClassID)] as! NSDictionary + let layersClassID = getClassID(id: layersClassKey) + let layersClass = objectsArray[layersClassID] as! NSDictionary let array = layersClass["NS.objects"] as! NSArray for object in array { - let layerClassID = objectRefGetValue(object as CFTypeRef) - let layerClass = objectsArray[Int(layerClassID)] as! NSDictionary + let layerClassID = getClassID(id: object) + let layerClass = objectsArray[layerClassID] as! NSDictionary parseSilicaLayer(archive: archive, dict: layerClass) } @@ -187,8 +265,9 @@ class Document: NSDocument { func parseDocument(archive: Archive, dict: NSDictionary) { // double check if this archive is really correct if let value = dict["$version"] { - if (value as! Int) != 100000 { + if (value as! Int) != NSKeyedArchiveVersion { Swift.print("This is not a valid document!") + return } self.dict = dict @@ -210,68 +289,42 @@ class Document: NSDocument { guard let archive = Archive(data: data, accessMode: Archive.AccessMode.read) else { return } - - // load thumbnail - guard let entry = archive["QuickLook/Thumbnail.png"] else { - return - } - - var top_data = Data() - - do { - try archive.extract(entry, consumer: { (d) in - top_data.append(d) - }) - } catch { - Swift.print("Extracting entry from archive failed with error:\(error)") - } - - thumbnail = NSImage(data: top_data) - // load doc info - guard let document_entry = archive["Document.archive"] else { + guard let documentEntry = archive[DocumentArchivePath] else { return } - var doc_data = Data() - - do { - try archive.extract(document_entry, consumer: { (d) in - doc_data.append(d) - }) - } catch { - Swift.print("Extracting entry from archive failed with error:\(error)") + guard let documentData = readData(archive: archive, entry: documentEntry) else { + return } - // Document.archive is a binary plist (specifically a NSKeyedArchive), luckily swift has a built-in solution to decode it var plistFormat = PropertyListSerialization.PropertyListFormat.binary - let plistBinary = doc_data - - guard let propertyList = try? PropertyListSerialization.propertyList(from: plistBinary, options: [], format: &plistFormat) else { - fatalError("failed to deserialize") + guard let propertyList = try? PropertyListSerialization.propertyList(from: documentData, options: [], format: &plistFormat) else { + return } - - let dict = (propertyList as! NSDictionary); - - parseDocument(archive: archive, dict: dict) + + parseDocument(archive: archive, dict: propertyList as! NSDictionary) } func makeComposite() -> NSImage { let image = NSImage(size: NSSize(width: info.width, height: info.height)) image.lockFocus() - let rows = Int(ceil(Float(info.height) / Float(info.tileSize))) + let color = NSColor.white + color.drawSwatch(in: NSRect(origin: .zero, size: image.size)) for layer in info.layers.reversed() { for chunk in layer.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: info.tileSize, height: info.tileSize) + let rect = NSRect(x: info.tileSize * x, y: info.height - (info.tileSize * y), width: width, height: height) chunk.image.draw(in: rect) } diff --git a/ProcreateViewer/ViewController.swift b/ProcreateViewer/ViewController.swift index 8ef5f5c..50469ba 100644 --- a/ProcreateViewer/ViewController.swift +++ b/ProcreateViewer/ViewController.swift @@ -3,62 +3,11 @@ import Cocoa class ViewController: NSViewController { @IBOutlet weak var imageView: NSImageView! - - @IBOutlet weak var layerPopup: NSPopUpButton! - @IBOutlet weak var chunkPopup: NSPopUpButton! - - var selectedLayer: Int = 0 - var selectedChunk: Int = 0 - - func parseName(str: String) -> Int? { - if let int = Int(str.components(separatedBy: " ")[1]) { - return int - } - - return nil - } - - func loadCanvas() { - let document = self.view.window?.windowController?.document as? Document - imageView.image = document?.info.layers[selectedLayer].chunks[selectedChunk].image - } - - @IBAction func selectLayerAction(_ sender: Any) { - selectedLayer = parseName(str: (layerPopup.titleOfSelectedItem)!)! - - selectedChunk = 0 - - loadLayerChunks() - loadCanvas() - } - - @IBAction func selectChunkAction(_ sender: Any) { - selectedChunk = parseName(str: (chunkPopup.titleOfSelectedItem)!)! - - loadCanvas() - } - - func loadLayerChunks() { - let document = self.view.window?.windowController?.document as? Document - - chunkPopup.removeAllItems() - - for (i, chunk) in (document?.info.layers[selectedLayer].chunks.enumerated())! { - chunkPopup.addItem(withTitle: "Chunk " + String(chunk.x) + " " + String(chunk.y)) - } - } - override func viewWillAppear() { let document = self.view.window?.windowController?.document as? Document imageView.image = document?.makeComposite() - - for (i, layer) in (document?.info.layers.enumerated())! { - layerPopup.addItem(withTitle: "Layer " + String(i)) - } - - loadLayerChunks() } override func prepare(for segue: NSStoryboardSegue, sender: Any?) {