2021-09-14 12:24:08 -04:00
|
|
|
import Cocoa
|
|
|
|
import ZIPFoundation
|
|
|
|
import CoreFoundation
|
2021-09-21 04:27:20 -04:00
|
|
|
import Accelerate
|
2021-09-30 07:53:41 -04:00
|
|
|
import CoreMedia
|
2021-09-14 12:24:08 -04:00
|
|
|
|
|
|
|
func objectRefGetValue2(_ objectRef: CFTypeRef) -> UInt32 {
|
|
|
|
let val = unsafeBitCast(objectRef, to: UInt64.self)
|
|
|
|
|
|
|
|
return valueForKeyedArchiverUID(val)
|
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// NSDocument for reading Silica Document files
|
2021-09-14 12:24:08 -04:00
|
|
|
class Document: NSDocument {
|
2022-06-15 11:30:53 -04:00
|
|
|
// TODO: don't keep the entire file in memory, especially since we also store the image data raw
|
|
|
|
var data: Data?
|
2021-09-14 12:24:08 -04:00
|
|
|
|
|
|
|
var dict: NSDictionary?
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
var info = SilicaDocument()
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-02-14 12:36:08 -04:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-09-30 07:53:41 -04:00
|
|
|
override class func canConcurrentlyReadDocuments(ofType: String) -> Bool {
|
|
|
|
return ofType == "com.procreate"
|
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// Figures out the best filename for the document, depending on what information we know.
|
|
|
|
/// It prefers the actual name defined in the document, unless it's missing - where we'll fall back to the filename.
|
|
|
|
/// - Returns: The best filename.
|
2022-02-28 12:03:08 -04:00
|
|
|
func getIdealFilename() -> String {
|
|
|
|
if (!(info.name.isEmpty)) {
|
|
|
|
return info.name
|
|
|
|
} else {
|
|
|
|
return fileURL!.deletingPathExtension().lastPathComponent
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// Pass in an object from the `$object` array, which always contains a `$class` key.
|
|
|
|
/// - Parameter dict: The NSDictionary to parse.
|
|
|
|
/// - Returns: The class name.
|
2021-09-14 12:24:08 -04:00
|
|
|
func getDocumentClassName(dict: NSDictionary) -> String? {
|
|
|
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// 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`.
|
2021-09-14 12:24:08 -04:00
|
|
|
func getClassID(id: Any?) -> Int {
|
|
|
|
return Int(objectRefGetValue2(id! as CFTypeRef))
|
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// Parses a chunk filename to integer coordinates.
|
|
|
|
/// "1~1.chunk" -> (1, 2).
|
|
|
|
/// - Parameter filename: The chunk filename.
|
|
|
|
/// - Returns: A tuple containing the parsed coordinates.
|
2022-02-14 12:30:08 -04:00
|
|
|
func parseChunkFilename(_ filename: String) -> (Int, Int)? {
|
2021-09-14 12:24:08 -04:00
|
|
|
let pathURL = URL(fileURLWithPath: filename)
|
|
|
|
let pathComponents = pathURL.lastPathComponent.replacingOccurrences(of: ".chunk", with: "").components(separatedBy: "~")
|
|
|
|
|
2022-02-14 12:31:08 -04:00
|
|
|
if let x = Int(pathComponents[0]), let y = Int(pathComponents[1]) {
|
|
|
|
return (x, y + 1)
|
2021-09-14 12:24:08 -04:00
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// Parses a raw integer blend mode from the Silica Document, into a `BlendMode` enumeration.
|
|
|
|
/// - Parameters:
|
|
|
|
/// - blendMode: The integer value for the `blendMode` property.
|
|
|
|
/// - extendedBlend: The integer value for the `extendedBlend` property.
|
|
|
|
/// - Returns: If the provided integer representations are correct, a `BlendMode` enumeration.
|
2022-06-14 11:17:58 -04:00
|
|
|
func parseRawBlendMode(blendMode: Int, extendedBlend: Int) -> BlendMode? {
|
|
|
|
if blendMode == 0 && blendMode != extendedBlend {
|
|
|
|
return BlendMode(rawValue: extendedBlend)
|
|
|
|
} else {
|
|
|
|
return BlendMode(rawValue: blendMode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-16 18:13:57 -04:00
|
|
|
func parseSilicaLayer(archive: Archive, dict: NSDictionary, isMask: Bool) -> SilicaLayer? {
|
2021-09-14 12:24:08 -04:00
|
|
|
if getDocumentClassName(dict: dict) == LayerClassName {
|
|
|
|
var layer = SilicaLayer()
|
2021-09-27 11:31:19 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
// TODO: when does a layer actually not have a name? i think i hit this case before
|
2021-09-27 11:31:19 -04:00
|
|
|
if let val = dict["name"] {
|
2022-06-15 13:25:34 -04:00
|
|
|
if let name = parseBasicObject(key: val) as? String {
|
|
|
|
layer.name = name
|
|
|
|
}
|
2021-09-27 11:31:19 -04:00
|
|
|
}
|
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
guard let uuid = parseBasicObject(key: dict["UUID"]) as? String else {
|
|
|
|
throwError(.invalid)
|
|
|
|
return nil
|
|
|
|
}
|
2021-09-14 12:24:08 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
// sometimes mask is missing
|
|
|
|
if let maskDict = parseBasicObject(key: dict["mask"]) as? NSDictionary {
|
|
|
|
layer.mask = parseSilicaLayer(archive: archive, dict: maskDict, isMask: true)?.data
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
guard let blendMode = (dict["blend"] as? NSNumber)?.intValue,
|
|
|
|
let extendedBlendMode = (dict["extendedBlend"] as? NSNumber)?.intValue,
|
|
|
|
let parsedBlendMode = parseRawBlendMode(blendMode: blendMode, extendedBlend: extendedBlendMode) else {
|
|
|
|
throwError(.invalid)
|
|
|
|
return nil
|
|
|
|
}
|
2022-06-14 11:17:58 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
layer.data.blendMode = parsedBlendMode
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let opacity = (dict["opacity"] as? NSNumber)?.doubleValue {
|
|
|
|
layer.data.opacity = opacity
|
|
|
|
}
|
|
|
|
|
|
|
|
if let hidden = dict["hidden"] as? Bool {
|
|
|
|
layer.data.hidden = hidden
|
|
|
|
}
|
|
|
|
|
|
|
|
if let clipped = dict["clipped"] as? Bool {
|
|
|
|
layer.clipped = clipped
|
2021-09-16 18:13:57 -04:00
|
|
|
}
|
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
var chunkPaths: [String] = []
|
|
|
|
|
|
|
|
archive.forEach { (entry: Entry) in
|
2022-06-15 13:25:34 -04:00
|
|
|
if entry.path.contains(uuid) {
|
2021-09-14 12:24:08 -04:00
|
|
|
chunkPaths.append(entry.path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-16 18:13:57 -04:00
|
|
|
layer.data.chunks = Array(repeating: SilicaChunk(), count: chunkPaths.count)
|
2021-09-14 12:24:08 -04:00
|
|
|
|
|
|
|
let dispatchGroup = DispatchGroup()
|
|
|
|
let queue = DispatchQueue(label: "imageWork")
|
|
|
|
|
|
|
|
DispatchQueue.concurrentPerform(iterations: chunkPaths.count) { (i: Int) in
|
2021-09-29 13:02:32 -04:00
|
|
|
guard let threadArchive = Archive(data: self.data!, accessMode: .read) else {
|
2021-09-14 12:24:08 -04:00
|
|
|
return
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
let threadEntry = threadArchive[chunkPaths[i]]
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-06-15 12:32:56 -04:00
|
|
|
guard let (tileX, tileY) = parseChunkFilename(threadEntry!.path) else {
|
2021-09-14 12:24:08 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-15 12:32:56 -04:00
|
|
|
let (tileWidth, tileHeight) = info.getTileSize(tileX, tileY)
|
2021-09-16 18:13:57 -04:00
|
|
|
|
2021-09-27 11:48:16 -04:00
|
|
|
let numChannels = isMask ? 1 : 4
|
2022-06-15 12:32:56 -04:00
|
|
|
let byteSize = tileWidth * tileHeight * numChannels
|
2021-09-14 12:24:08 -04:00
|
|
|
|
|
|
|
let uncompressedMemory = UnsafeMutablePointer<UInt8>.allocate(capacity: byteSize)
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
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
|
2021-09-27 11:48:16 -04:00
|
|
|
let rgbColorSpace = isMask ? CGColorSpaceCreateDeviceGray() : info.colorSpace
|
2021-09-16 18:13:57 -04:00
|
|
|
|
2021-09-29 16:30:57 -04:00
|
|
|
let bitmapInfo = CGBitmapInfo(rawValue: (isMask ? CGImageAlphaInfo.none : CGImageAlphaInfo.premultipliedLast).rawValue).union(.byteOrder32Big)
|
2021-09-14 12:24:08 -04:00
|
|
|
let providerRef: CGDataProvider? = CGDataProvider(data: imageData as CFData)
|
|
|
|
|
2022-06-15 12:32:56 -04:00
|
|
|
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 {
|
2021-09-14 12:24:08 -04:00
|
|
|
return
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-16 18:41:54 -04:00
|
|
|
queue.async(group: dispatchGroup) {
|
2021-09-29 13:02:32 -04:00
|
|
|
layer.data.chunks[i].image = cgimage
|
2022-06-15 12:32:56 -04:00
|
|
|
layer.data.chunks[i].x = tileX
|
|
|
|
layer.data.chunks[i].y = tileY
|
2021-09-14 12:24:08 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatchGroup.wait()
|
|
|
|
|
2021-09-16 18:13:57 -04:00
|
|
|
return layer
|
2021-09-14 12:24:08 -04:00
|
|
|
}
|
2021-09-16 18:13:57 -04:00
|
|
|
|
|
|
|
return nil
|
2021-09-14 12:24:08 -04:00
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// Parses a pair string into a proper typed tuple.
|
|
|
|
/// For example, {0, 0} -> (0, 0).
|
|
|
|
/// - Parameter str: The pair string.
|
|
|
|
/// - Returns: If it can be parsed correctly, a tuple containing the integer values found in the string.
|
2021-09-29 17:27:13 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
func parseBasicObject(key: Any?) -> Any? {
|
|
|
|
if let objectsArray = self.dict?["$objects"] as? NSArray {
|
|
|
|
let classID = getClassID(id: key)
|
|
|
|
|
|
|
|
return objectsArray[classID]
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
func parseSilicaDocument(archive: Archive, dict: NSDictionary) {
|
|
|
|
if getDocumentClassName(dict: dict) == DocumentClassName {
|
2022-06-15 13:25:34 -04:00
|
|
|
// failing to parse these isn't a failing case, thus we don't throw errors here.
|
|
|
|
if let trackedTime = (dict[TrackedTimeKey] as? NSNumber)?.intValue {
|
|
|
|
info.trackedTime = trackedTime
|
|
|
|
}
|
2021-09-29 17:27:13 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let tileSize = (dict[TileSizeKey] as? NSNumber)?.intValue {
|
|
|
|
info.tileSize = tileSize
|
|
|
|
}
|
2021-09-29 17:27:13 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let orientation = (dict[OrientationKey] as? NSNumber)?.intValue {
|
|
|
|
info.orientation = orientation
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let flippedHorizontally = (dict[FlippedHorizontallyKey] as? NSNumber)?.boolValue {
|
|
|
|
info.flippedHorizontally = flippedHorizontally
|
|
|
|
}
|
2022-05-09 20:46:59 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let flippedVertically = (dict[FlippedVerticallyKey] as? NSNumber)?.boolValue {
|
|
|
|
info.flippedVertically = flippedVertically
|
|
|
|
}
|
|
|
|
|
|
|
|
if let videoResolution = parseBasicObject(key: dict["SilicaDocumentVideoSegmentInfoKey"]) as? NSDictionary {
|
|
|
|
guard let frameSize = parseBasicObject(key: videoResolution["frameSize"]) as? String,
|
|
|
|
let videoFrame = parsePairString(frameSize) else {
|
|
|
|
throwError(.invalid)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
info.videoFrame = videoFrame
|
|
|
|
}
|
|
|
|
|
|
|
|
if let colorProfileClassKey = dict["colorProfile"] {
|
|
|
|
let objectsArray = self.dict?["$objects"] as! NSArray
|
|
|
|
|
2022-05-09 20:46:59 -04:00
|
|
|
let colorProfileClassID = getClassID(id: colorProfileClassKey)
|
|
|
|
let colorProfile = objectsArray[colorProfileClassID] as! NSDictionary
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-05-09 20:46:59 -04:00
|
|
|
let colorProfileNameClassKey = colorProfile["SiColorProfileArchiveICCNameKey"]
|
|
|
|
let colorProfileNameClassID = getClassID(id: colorProfileNameClassKey)
|
|
|
|
let colorProfileName = objectsArray[colorProfileNameClassID] as! NSString
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-05-09 20:46:59 -04:00
|
|
|
// 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 {
|
2021-09-27 11:31:19 -04:00
|
|
|
info.colorSpace = CGColorSpace(name: CGColorSpace.displayP3)!
|
|
|
|
}
|
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let background = parseBasicObject(key: dict["backgroundColor"]) 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)!
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let strokeCount = parseBasicObject(key: dict[StrokeCountKey]) as? NSNumber {
|
|
|
|
info.strokeCount = Int(truncating: strokeCount)
|
2022-02-28 12:04:08 -04:00
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let name = parseBasicObject(key: dict[NameKey]) as? String,
|
|
|
|
name != "$null" {
|
|
|
|
info.name = name
|
2021-09-20 14:13:58 -04:00
|
|
|
}
|
2021-09-14 12:24:08 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
if let authorName = parseBasicObject(key: dict[AuthorNameKey]) as? String,
|
|
|
|
authorName != "$null" {
|
|
|
|
info.authorName = authorName
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
guard let size = parseBasicObject(key: dict[SizeKey]) as? String,
|
|
|
|
let (parsedWidth, parsedHeight) = parsePairString(size) else {
|
|
|
|
throwError(.invalid)
|
2022-06-15 12:32:56 -04:00
|
|
|
return
|
|
|
|
}
|
2021-09-14 12:24:08 -04:00
|
|
|
|
2022-06-15 12:32:56 -04:00
|
|
|
info.width = parsedWidth
|
|
|
|
info.height = parsedHeight
|
2022-06-15 13:25:34 -04:00
|
|
|
|
2022-06-15 12:07:09 -04:00
|
|
|
info.columns = Int(ceil(Float(info.width) / Float(info.tileSize)))
|
|
|
|
info.rows = Int(ceil(Float(info.height) / Float(info.tileSize))) + 1 // TODO: lol why
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
if info.width % info.tileSize != 0 {
|
2022-06-15 12:07:09 -04:00
|
|
|
info.remainderWidth = (info.columns * info.tileSize) - info.width
|
2021-09-14 12:24:08 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if info.height % info.tileSize != 0 {
|
2022-06-15 12:07:09 -04:00
|
|
|
info.remainderHeight = (info.rows * info.tileSize) - info.height
|
2021-09-14 12:24:08 -04:00
|
|
|
}
|
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
guard let layersDict = parseBasicObject(key: dict[LayersKey]) as? NSDictionary,
|
|
|
|
let layersArray = layersDict["NS.objects"] as? NSArray else {
|
|
|
|
throwError(.invalid)
|
|
|
|
return
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
for object in layersArray {
|
|
|
|
guard let layerDict = parseBasicObject(key: object) as? NSDictionary,
|
|
|
|
let layer = parseSilicaLayer(archive: archive, dict: layerDict, isMask: false) else {
|
|
|
|
throwError(.invalid)
|
|
|
|
return
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-16 18:13:57 -04:00
|
|
|
info.layers.append(layer)
|
2021-09-14 12:24:08 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseDocument(archive: Archive, dict: NSDictionary) {
|
|
|
|
// double check if this archive is really correct
|
|
|
|
if let value = dict["$version"] {
|
|
|
|
if (value as! Int) != NSKeyedArchiveVersion {
|
2022-02-14 12:36:08 -04:00
|
|
|
throwError(.invalid)
|
2021-09-14 12:24:08 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.dict = dict
|
|
|
|
|
2022-06-15 13:25:34 -04:00
|
|
|
guard let objectsArray = dict["$objects"] as? NSArray else {
|
|
|
|
throwError(.invalid)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// let's begin by reading $top, which is always going to be SilicaDocument type.
|
|
|
|
|
|
|
|
guard let topObject = dict["$top"] as? NSDictionary else {
|
|
|
|
throwError(.invalid)
|
|
|
|
return
|
|
|
|
}
|
2021-09-14 12:24:08 -04:00
|
|
|
|
|
|
|
let topClassID = objectRefGetValue2(topObject["root"] as CFTypeRef)
|
2022-06-15 13:25:34 -04:00
|
|
|
|
|
|
|
guard let topObjectClass = objectsArray[Int(topClassID)] as? NSDictionary else {
|
|
|
|
throwError(.invalid)
|
|
|
|
return
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
parseSilicaDocument(archive: archive, dict: topObjectClass)
|
|
|
|
}
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
override func read(from data: Data, ofType typeName: String) throws {
|
|
|
|
self.data = data
|
|
|
|
|
2021-09-30 07:53:41 -04:00
|
|
|
guard let archive = Archive(data: data, accessMode: Archive.AccessMode.read) else {
|
|
|
|
throwError(.invalid)
|
2021-09-14 12:24:08 -04:00
|
|
|
return
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
guard let documentEntry = archive[DocumentArchivePath] else {
|
2021-09-30 07:53:41 -04:00
|
|
|
throwError(.invalid)
|
2021-09-14 12:24:08 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let documentData = readData(archive: archive, entry: documentEntry) else {
|
2021-09-30 07:53:41 -04:00
|
|
|
throwError(.invalid)
|
2021-09-14 12:24:08 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var plistFormat = PropertyListSerialization.PropertyListFormat.binary
|
|
|
|
guard let propertyList = try? PropertyListSerialization.propertyList(from: documentData, options: [], format: &plistFormat) else {
|
2021-09-30 07:53:41 -04:00
|
|
|
throwError(.invalid)
|
2021-09-14 12:24:08 -04:00
|
|
|
return
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
parseDocument(archive: archive, dict: propertyList as! NSDictionary)
|
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// 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.
|
2021-09-21 04:41:05 -04:00
|
|
|
func makeComposite() -> NSImage? {
|
2022-06-15 12:32:56 -04:00
|
|
|
let engine = SilicaEngine()
|
2021-09-30 07:35:50 -04:00
|
|
|
|
2022-06-15 12:32:56 -04:00
|
|
|
var image = NSImage(cgImage: engine.draw(document: info)!, size: info.nsSize())
|
2021-09-14 12:24:08 -04:00
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
return image
|
|
|
|
}
|
|
|
|
|
2022-06-15 11:30:53 -04:00
|
|
|
/// Extracts the thumbnail image generated by Procreate itself.
|
|
|
|
/// - Returns: If it's able to find the thumbnail, an `NSImage`.
|
2021-09-14 12:24:08 -04:00
|
|
|
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
|
|
|
|
}
|
2022-06-15 11:09:17 -04:00
|
|
|
|
2021-09-14 12:24:08 -04:00
|
|
|
return NSImage(data: thumbnailData)
|
|
|
|
}
|
|
|
|
}
|