diff --git a/DocumentTests.swift b/DocumentTests.swift new file mode 100644 index 0000000..f0e6c39 --- /dev/null +++ b/DocumentTests.swift @@ -0,0 +1,33 @@ +import XCTest + +class DocumentTests: XCTestCase { + + let document: Document() + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + document.parsePairString("{255, 255}") + document.parsePairString("{255}") + + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/ProcreateViewer.xcodeproj/project.pbxproj b/ProcreateViewer.xcodeproj/project.pbxproj index 47e0cdb..521763b 100644 --- a/ProcreateViewer.xcodeproj/project.pbxproj +++ b/ProcreateViewer.xcodeproj/project.pbxproj @@ -14,7 +14,16 @@ 030F700E2415C6B500A43F01 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F700D2415C6B500A43F01 /* PreviewViewController.swift */; }; 030F70112415C6B500A43F01 /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 030F700F2415C6B500A43F01 /* PreviewViewController.xib */; }; 030F70162415C6B500A43F01 /* QuickLook.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 030F70082415C6B500A43F01 /* QuickLook.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 033F94F92648B8E200099FB7 /* TimelapseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033F94F82648B8E200099FB7 /* TimelapseViewController.swift */; }; + 0357A94D26F9C7370075D5BC /* minilzo.c in Sources */ = {isa = PBXBuildFile; fileRef = 0357A94A26F9C7370075D5BC /* minilzo.c */; }; + 0357A94E26F9C7370075D5BC /* lzoconf.h in Headers */ = {isa = PBXBuildFile; fileRef = 0357A94C26F9C7370075D5BC /* lzoconf.h */; }; + 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 */; }; + 035D1A0726F0927200B332BE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 035D19FD26F0927200B332BE /* Main.storyboard */; }; + 035D1A0826F0927200B332BE /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035D19FF26F0927200B332BE /* InfoViewController.swift */; }; + 035D1A0926F0927200B332BE /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035D1A0026F0927200B332BE /* Document.swift */; }; + 035D1A0A26F0927200B332BE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035D1A0126F0927200B332BE /* AppDelegate.swift */; }; + 035D1A0B26F0927200B332BE /* TimelapseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035D1A0226F0927200B332BE /* TimelapseViewController.swift */; }; 036AFBB8241687680075400A /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 036AFBB7241687680075400A /* ZIPFoundation */; }; 036AFBBA24168C030075400A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036AFBB924168C030075400A /* ViewController.swift */; }; 036AFBE32417F0A00075400A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 036AFBE12417F0A00075400A /* Main.storyboard */; }; @@ -24,8 +33,11 @@ 036AFC11241800350075400A /* ThumbnailProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036AFC10241800350075400A /* ThumbnailProvider.swift */; }; 036AFC16241800350075400A /* Thumbnail.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 036AFC0B241800350075400A /* Thumbnail.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 036AFC1B241800850075400A /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 036AFC1A241800850075400A /* ZIPFoundation */; }; - 037B4042241821D200392452 /* InfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037B4041241821D200392452 /* InfoViewController.swift */; }; - 03CB382424191F620078B3E5 /* cbridge.c in Sources */ = {isa = PBXBuildFile; fileRef = 03CB382324191F620078B3E5 /* cbridge.c */; }; + 0371996027BAC5D800EE1DFD /* Silica_ViewerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0371995F27BAC5D800EE1DFD /* Silica_ViewerTests.swift */; }; + 03C39DE726F90734005555AE /* lzodefs.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C39DE226F90733005555AE /* lzodefs.h */; }; + 03C39DE826F90734005555AE /* Makefile in Sources */ = {isa = PBXBuildFile; fileRef = 03C39DE426F90734005555AE /* Makefile */; }; + 03C39DE926F90734005555AE /* testmini.c in Sources */ = {isa = PBXBuildFile; fileRef = 03C39DE526F90734005555AE /* testmini.c */; }; + 03C39DEA26F90734005555AE /* minilzo.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C39DE626F90734005555AE /* minilzo.h */; }; 03CB383B2419CA2D0078B3E5 /* libLZO.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03CB382E2419C9DB0078B3E5 /* libLZO.a */; }; 03CB3840241A5AED0078B3E5 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CB383F241A5AED0078B3E5 /* Shared.swift */; }; 03CB3841241A5AED0078B3E5 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CB383F241A5AED0078B3E5 /* Shared.swift */; }; @@ -51,6 +63,13 @@ remoteGlobalIDString = 036AFC0A241800350075400A; remoteInfo = Thumbnail; }; + 0371996127BAC5D800EE1DFD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 030F6FE62415C5E300A43F01 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 030F6FED2415C5E300A43F01; + remoteInfo = SilicaViewer; + }; 03CB383C2419CA2D0078B3E5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 030F6FE62415C5E300A43F01 /* Project object */; @@ -76,29 +95,41 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 030F6FEE2415C5E300A43F01 /* Procreate Viewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Procreate Viewer.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 030F6FF12415C5E300A43F01 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 030F6FF32415C5E300A43F01 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; - 030F6FF82415C5E400A43F01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 030F6FFD2415C5E400A43F01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 030F6FFE2415C5E400A43F01 /* ProcreateViewer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ProcreateViewer.entitlements; sourceTree = ""; }; + 030F6FEE2415C5E300A43F01 /* Silica Viewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Silica Viewer.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 030F70082415C6B500A43F01 /* QuickLook.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QuickLook.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 030F700A2415C6B500A43F01 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; 030F700D2415C6B500A43F01 /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; 030F70102415C6B500A43F01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreviewViewController.xib; sourceTree = ""; }; 030F70122415C6B500A43F01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 030F70132415C6B500A43F01 /* Quicklook.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Quicklook.entitlements; sourceTree = ""; }; - 033F94F82648B8E200099FB7 /* TimelapseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelapseViewController.swift; sourceTree = ""; }; - 036AFBB924168C030075400A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 036AFBE22417F0A00075400A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 0357A94926F9C7370075D5BC /* README.LZO */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.LZO; sourceTree = ""; }; + 0357A94A26F9C7370075D5BC /* minilzo.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = minilzo.c; sourceTree = ""; }; + 0357A94B26F9C7370075D5BC /* testmini */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = testmini; sourceTree = ""; }; + 0357A94C26F9C7370075D5BC /* lzoconf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lzoconf.h; sourceTree = ""; }; + 035D19F826F0927200B332BE /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 035D19F926F0927200B332BE /* SilicaViewer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = SilicaViewer.entitlements; sourceTree = ""; }; + 035D19FA26F0927200B332BE /* cbridge.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = cbridge.c; sourceTree = ""; }; + 035D19FB26F0927200B332BE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 035D19FC26F0927200B332BE /* SilicaViewer-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SilicaViewer-Bridging-Header.h"; sourceTree = ""; }; + 035D19FE26F0927200B332BE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 035D19FF26F0927200B332BE /* InfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = ""; }; + 035D1A0026F0927200B332BE /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; + 035D1A0126F0927200B332BE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 035D1A0226F0927200B332BE /* TimelapseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelapseViewController.swift; sourceTree = ""; }; + 035D1A0326F0927200B332BE /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 036AFC0B241800350075400A /* Thumbnail.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Thumbnail.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 036AFC0C241800350075400A /* QuickLookThumbnailing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookThumbnailing.framework; path = System/Library/Frameworks/QuickLookThumbnailing.framework; sourceTree = SDKROOT; }; 036AFC10241800350075400A /* ThumbnailProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailProvider.swift; sourceTree = ""; }; 036AFC12241800350075400A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 036AFC13241800350075400A /* Thumbnail.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Thumbnail.entitlements; sourceTree = ""; }; - 037B4041241821D200392452 /* InfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = ""; }; - 03CB382224191F610078B3E5 /* ProcreateViewer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ProcreateViewer-Bridging-Header.h"; sourceTree = ""; }; - 03CB382324191F620078B3E5 /* cbridge.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = cbridge.c; sourceTree = ""; }; + 0371995827BAC52900EE1DFD /* SilicaViewer.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SilicaViewer.xctestplan; sourceTree = ""; }; + 0371995D27BAC5D800EE1DFD /* Silica ViewerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Silica ViewerTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 0371995F27BAC5D800EE1DFD /* Silica_ViewerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Silica_ViewerTests.swift; sourceTree = ""; }; + 03C39DE226F90733005555AE /* lzodefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lzodefs.h; sourceTree = ""; }; + 03C39DE326F90733005555AE /* COPYING */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = COPYING; sourceTree = ""; }; + 03C39DE426F90734005555AE /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; + 03C39DE526F90734005555AE /* testmini.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = testmini.c; sourceTree = ""; }; + 03C39DE626F90734005555AE /* minilzo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = minilzo.h; sourceTree = ""; }; 03CB382E2419C9DB0078B3E5 /* libLZO.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libLZO.a; sourceTree = BUILT_PRODUCTS_DIR; }; 03CB383F241A5AED0078B3E5 /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; 03CB3843241A5D600078B3E5 /* minilzo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = minilzo.h; path = "Dependencies/minilzo-2.10/minilzo.h"; sourceTree = SOURCE_ROOT; }; @@ -136,6 +167,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 0371995A27BAC5D800EE1DFD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 03CB382C2419C9DB0078B3E5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -149,11 +187,15 @@ 030F6FE52415C5E300A43F01 = { isa = PBXGroup; children = ( + 0371995827BAC52900EE1DFD /* SilicaViewer.xctestplan */, + 0357A94826F9C71E0075D5BC /* LZO */, + 035D19F726F0927200B332BE /* SilicaViewer */, 03CB383E241A5ACD0078B3E5 /* Shared */, 03CB38322419C9F80078B3E5 /* LZO */, 030F6FF02415C5E300A43F01 /* ProcreateViewer */, 030F700C2415C6B500A43F01 /* Quicklook */, 036AFC0F241800350075400A /* Thumbnail */, + 0371995E27BAC5D800EE1DFD /* Silica ViewerTests */, 030F70092415C6B500A43F01 /* Frameworks */, 030F6FEF2415C5E300A43F01 /* Products */, ); @@ -162,10 +204,11 @@ 030F6FEF2415C5E300A43F01 /* Products */ = { isa = PBXGroup; children = ( - 030F6FEE2415C5E300A43F01 /* Procreate Viewer.app */, + 030F6FEE2415C5E300A43F01 /* Silica Viewer.app */, 030F70082415C6B500A43F01 /* QuickLook.appex */, 036AFC0B241800350075400A /* Thumbnail.appex */, 03CB382E2419C9DB0078B3E5 /* libLZO.a */, + 0371995D27BAC5D800EE1DFD /* Silica ViewerTests.xctest */, ); name = Products; sourceTree = ""; @@ -208,6 +251,40 @@ path = Quicklook; sourceTree = ""; }; + 0357A94826F9C71E0075D5BC /* LZO */ = { + isa = PBXGroup; + children = ( + 0357A94C26F9C7370075D5BC /* lzoconf.h */, + 0357A94A26F9C7370075D5BC /* minilzo.c */, + 0357A94926F9C7370075D5BC /* README.LZO */, + 0357A94B26F9C7370075D5BC /* testmini */, + 03C39DE326F90733005555AE /* COPYING */, + 03C39DE226F90733005555AE /* lzodefs.h */, + 03C39DE426F90734005555AE /* Makefile */, + 03C39DE626F90734005555AE /* minilzo.h */, + 03C39DE526F90734005555AE /* testmini.c */, + ); + name = LZO; + sourceTree = ""; + }; + 035D19F726F0927200B332BE /* SilicaViewer */ = { + isa = PBXGroup; + children = ( + 035D19F826F0927200B332BE /* ViewController.swift */, + 035D19F926F0927200B332BE /* SilicaViewer.entitlements */, + 035D19FA26F0927200B332BE /* cbridge.c */, + 035D19FB26F0927200B332BE /* Assets.xcassets */, + 035D19FC26F0927200B332BE /* SilicaViewer-Bridging-Header.h */, + 035D19FD26F0927200B332BE /* Main.storyboard */, + 035D19FF26F0927200B332BE /* InfoViewController.swift */, + 035D1A0026F0927200B332BE /* Document.swift */, + 035D1A0126F0927200B332BE /* AppDelegate.swift */, + 035D1A0226F0927200B332BE /* TimelapseViewController.swift */, + 035D1A0326F0927200B332BE /* Info.plist */, + ); + path = SilicaViewer; + sourceTree = ""; + }; 036AFC0F241800350075400A /* Thumbnail */ = { isa = PBXGroup; children = ( @@ -218,15 +295,12 @@ path = Thumbnail; sourceTree = ""; }; - 03CB38322419C9F80078B3E5 /* LZO */ = { + 0371995E27BAC5D800EE1DFD /* Silica ViewerTests */ = { isa = PBXGroup; children = ( - 03CB3846241A5D600078B3E5 /* lzoconf.h */, - 03CB3844241A5D600078B3E5 /* lzodefs.h */, - 03CB3845241A5D600078B3E5 /* minilzo.c */, - 03CB3843241A5D600078B3E5 /* minilzo.h */, + 0371995F27BAC5D800EE1DFD /* Silica_ViewerTests.swift */, ); - path = LZO; + path = "Silica ViewerTests"; sourceTree = ""; }; 03CB383E241A5ACD0078B3E5 /* Shared */ = { @@ -244,18 +318,18 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 03CB3848241A5D600078B3E5 /* lzodefs.h in Headers */, - 03CB384A241A5D600078B3E5 /* lzoconf.h in Headers */, - 03CB3847241A5D600078B3E5 /* minilzo.h in Headers */, + 03C39DE726F90734005555AE /* lzodefs.h in Headers */, + 0357A94E26F9C7370075D5BC /* lzoconf.h in Headers */, + 03C39DEA26F90734005555AE /* minilzo.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 030F6FED2415C5E300A43F01 /* ProcreateViewer */ = { + 030F6FED2415C5E300A43F01 /* SilicaViewer */ = { isa = PBXNativeTarget; - buildConfigurationList = 030F70012415C5E400A43F01 /* Build configuration list for PBXNativeTarget "ProcreateViewer" */; + buildConfigurationList = 030F70012415C5E400A43F01 /* Build configuration list for PBXNativeTarget "SilicaViewer" */; buildPhases = ( 030F6FEA2415C5E300A43F01 /* Sources */, 030F6FEB2415C5E300A43F01 /* Frameworks */, @@ -269,12 +343,12 @@ 036AFC15241800350075400A /* PBXTargetDependency */, 03CB383D2419CA2D0078B3E5 /* PBXTargetDependency */, ); - name = ProcreateViewer; + name = SilicaViewer; packageProductDependencies = ( 036AFBB7241687680075400A /* ZIPFoundation */, ); productName = ProcreateViewer; - productReference = 030F6FEE2415C5E300A43F01 /* Procreate Viewer.app */; + productReference = 030F6FEE2415C5E300A43F01 /* Silica Viewer.app */; productType = "com.apple.product-type.application"; }; 030F70072415C6B500A43F01 /* QuickLook */ = { @@ -317,6 +391,24 @@ productReference = 036AFC0B241800350075400A /* Thumbnail.appex */; productType = "com.apple.product-type.app-extension"; }; + 0371995C27BAC5D800EE1DFD /* Silica ViewerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0371996327BAC5D800EE1DFD /* Build configuration list for PBXNativeTarget "Silica ViewerTests" */; + buildPhases = ( + 0371995927BAC5D800EE1DFD /* Sources */, + 0371995A27BAC5D800EE1DFD /* Frameworks */, + 0371995B27BAC5D800EE1DFD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0371996227BAC5D800EE1DFD /* PBXTargetDependency */, + ); + name = "Silica ViewerTests"; + productName = "Silica ViewerTests"; + productReference = 0371995D27BAC5D800EE1DFD /* Silica ViewerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 03CB382D2419C9DB0078B3E5 /* LZO */ = { isa = PBXNativeTarget; buildConfigurationList = 03CB382F2419C9DB0078B3E5 /* Build configuration list for PBXNativeTarget "LZO" */; @@ -340,8 +432,10 @@ 030F6FE62415C5E300A43F01 /* Project object */ = { isa = PBXProject; attributes = { +==== BASE ==== LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1130; + LastUpgradeCheck = 1250; +==== BASE ==== ORGANIZATIONNAME = Josh; TargetAttributes = { 030F6FED2415C5E300A43F01 = { @@ -354,6 +448,10 @@ 036AFC0A241800350075400A = { CreatedOnToolsVersion = 11.3.1; }; + 0371995C27BAC5D800EE1DFD = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 030F6FED2415C5E300A43F01; + }; 03CB382D2419C9DB0078B3E5 = { CreatedOnToolsVersion = 11.3.1; }; @@ -379,6 +477,7 @@ 030F70072415C6B500A43F01 /* QuickLook */, 036AFC0A241800350075400A /* Thumbnail */, 03CB382D2419C9DB0078B3E5 /* LZO */, + 0371995C27BAC5D800EE1DFD /* Silica ViewerTests */, ); }; /* End PBXProject section */ @@ -408,6 +507,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 0371995B27BAC5D800EE1DFD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -443,6 +549,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 0371995927BAC5D800EE1DFD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0371996027BAC5D800EE1DFD /* Silica_ViewerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 03CB382B2419C9DB0078B3E5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -464,6 +578,11 @@ target = 036AFC0A241800350075400A /* Thumbnail */; targetProxy = 036AFC14241800350075400A /* PBXContainerItemProxy */; }; + 0371996227BAC5D800EE1DFD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 030F6FED2415C5E300A43F01 /* SilicaViewer */; + targetProxy = 0371996127BAC5D800EE1DFD /* PBXContainerItemProxy */; + }; 03CB383D2419CA2D0078B3E5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 03CB382D2419C9DB0078B3E5 /* LZO */; @@ -727,6 +846,44 @@ }; name = Release; }; + 0371996427BAC5D800EE1DFD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JM5LKVKH48; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.redstrate.Silica-ViewerTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Silica Viewer.app/Contents/MacOS/Silica Viewer"; + }; + name = Debug; + }; + 0371996527BAC5D800EE1DFD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JM5LKVKH48; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.redstrate.Silica-ViewerTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Silica Viewer.app/Contents/MacOS/Silica Viewer"; + }; + name = Release; + }; 03CB38302419C9DB0078B3E5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -788,6 +945,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 0371996327BAC5D800EE1DFD /* Build configuration list for PBXNativeTarget "Silica ViewerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0371996427BAC5D800EE1DFD /* Debug */, + 0371996527BAC5D800EE1DFD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 03CB382F2419C9DB0078B3E5 /* Build configuration list for PBXNativeTarget "LZO" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Silica ViewerTests/Silica_ViewerTests.swift b/Silica ViewerTests/Silica_ViewerTests.swift new file mode 100644 index 0000000..e59b2a0 --- /dev/null +++ b/Silica ViewerTests/Silica_ViewerTests.swift @@ -0,0 +1,35 @@ +// +// Silica_ViewerTests.swift +// Silica ViewerTests +// +// Created by Joshua on 2/14/22. +// Copyright © 2022 Josh. All rights reserved. +// + +import XCTest + +@testable import Silica_Viewer + +class Silica_ViewerTests: XCTestCase { + + let document = Document() + + override func setUpWithError() throws { + + } + + override func tearDownWithError() throws { + + } + + func testExample() throws { + XCTAssert(document.parsePairString("{-1, -1}")! == (-1, -1)) + XCTAssert(document.parsePairString("{255, 255}")! == (255, 255)) + XCTAssert(document.parsePairString("{255}") == nil) + } + + func testPerformanceExample() throws { + + } + +} diff --git a/SilicaViewer.xctestplan b/SilicaViewer.xctestplan new file mode 100644 index 0000000..3a33217 --- /dev/null +++ b/SilicaViewer.xctestplan @@ -0,0 +1,35 @@ +{ + "configurations" : [ + { + "id" : "6342A531-0B85-4DC8-89B3-6061172C5E9C", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "environmentVariableEntries" : [ + { + "key" : "CGBITMAP_CONTEXT_LOG_ERRORS", + "value" : "1" + } + ], + "targetForVariableExpansion" : { + "containerPath" : "container:SilicaViewer.xcodeproj", + "identifier" : "030F6FED2415C5E300A43F01", + "name" : "SilicaViewer" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:SilicaViewer.xcodeproj", + "identifier" : "0371995C27BAC5D800EE1DFD", + "name" : "Silica ViewerTests" + } + } + ], + "version" : 1 +} diff --git a/SilicaViewer/Document.swift b/SilicaViewer/Document.swift new file mode 100644 index 0000000..7d845d1 --- /dev/null +++ b/SilicaViewer/Document.swift @@ -0,0 +1,745 @@ +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 { + 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 + + 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" + } + + /* + 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 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 .hardLight + } + if layer.data.blendMode == 9 { + return .colorDodge + } + if layer.data.blendMode == 3 { + return .subtract + } + + 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 .componentAdd + } + 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(filename: threadEntry!.path) else { + return + } + + let (width, height) = getTileSize(x, y) + + let numChannels = isMask ? 1 : 4 + 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 + 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 + } + + // this parses a string of form "{255, 255}" + 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"] + 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 (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 + + //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) + } + } + + 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 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 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 { + // 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 layerImage = layerContext?.makeImage() + + if layer.clipped && previousImage != nil { + let result = previousImage!.toGrayscale() + let newImage = layerImage!.masking(result!) + + previousImage = newImage + } else 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) + + 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? { + let ciImage = CIImage(cgImage: self) + + let filter = CIFilter(name: "CIColorControls") + filter?.setValue(ciImage, forKey: kCIInputImageKey) + filter?.setValue(5.0, forKey: kCIInputBrightnessKey) + filter?.setValue(0.0, forKey: kCIInputSaturationKey) + filter?.setValue(1.1, forKey: kCIInputContrastKey) + + guard let intermediateImage = filter?.outputImage else { + return nil + } + + guard let image = CIContext().createCGImage(intermediateImage, from: CGRect(origin: .zero, size: CGSize(width: self.width, height: self.height))) else { + return nil + } + + let grayColorSpace = CGColorSpaceCreateDeviceGray() + let maskBitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue).union(.byteOrder16Big) + + let maskContext = CGContext(data: nil, width: self.width, height: self.height, bitsPerComponent: 16, bytesPerRow: 0, space: grayColorSpace, bitmapInfo: maskBitmapInfo.rawValue) + + maskContext?.setFillColor(.black) + maskContext?.fill(CGRect(origin: .zero, size: CGSize(width: self.width, height: self.height))) + + maskContext?.draw(image, in: CGRect(origin: .zero, size: CGSize(width: self.width, height: self.height))) + + return maskContext?.makeImage() + } +}