Add unit tests
This just tests Document.parsePairString for now
This commit is contained in:
parent
c0f47191be
commit
636145e265
5 changed files with 1044 additions and 30 deletions
33
DocumentTests.swift
Normal file
33
DocumentTests.swift
Normal file
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = "<group>"; };
|
||||
030F6FF32415C5E300A43F01 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = "<group>"; };
|
||||
030F6FF82415C5E400A43F01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
030F6FFD2415C5E400A43F01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
030F6FFE2415C5E400A43F01 /* ProcreateViewer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ProcreateViewer.entitlements; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
030F70102415C6B500A43F01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreviewViewController.xib; sourceTree = "<group>"; };
|
||||
030F70122415C6B500A43F01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
030F70132415C6B500A43F01 /* Quicklook.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Quicklook.entitlements; sourceTree = "<group>"; };
|
||||
033F94F82648B8E200099FB7 /* TimelapseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelapseViewController.swift; sourceTree = "<group>"; };
|
||||
036AFBB924168C030075400A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
036AFBE22417F0A00075400A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
0357A94926F9C7370075D5BC /* README.LZO */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README.LZO; sourceTree = "<group>"; };
|
||||
0357A94A26F9C7370075D5BC /* minilzo.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = minilzo.c; sourceTree = "<group>"; };
|
||||
0357A94B26F9C7370075D5BC /* testmini */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = testmini; sourceTree = "<group>"; };
|
||||
0357A94C26F9C7370075D5BC /* lzoconf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lzoconf.h; sourceTree = "<group>"; };
|
||||
035D19F826F0927200B332BE /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
035D19F926F0927200B332BE /* SilicaViewer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = SilicaViewer.entitlements; sourceTree = "<group>"; };
|
||||
035D19FA26F0927200B332BE /* cbridge.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = cbridge.c; sourceTree = "<group>"; };
|
||||
035D19FB26F0927200B332BE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
035D19FC26F0927200B332BE /* SilicaViewer-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SilicaViewer-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
035D19FE26F0927200B332BE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
035D19FF26F0927200B332BE /* InfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = "<group>"; };
|
||||
035D1A0026F0927200B332BE /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = "<group>"; };
|
||||
035D1A0126F0927200B332BE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
035D1A0226F0927200B332BE /* TimelapseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelapseViewController.swift; sourceTree = "<group>"; };
|
||||
035D1A0326F0927200B332BE /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
036AFC12241800350075400A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
036AFC13241800350075400A /* Thumbnail.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Thumbnail.entitlements; sourceTree = "<group>"; };
|
||||
037B4041241821D200392452 /* InfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoViewController.swift; sourceTree = "<group>"; };
|
||||
03CB382224191F610078B3E5 /* ProcreateViewer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ProcreateViewer-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
03CB382324191F620078B3E5 /* cbridge.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = cbridge.c; sourceTree = "<group>"; };
|
||||
0371995827BAC52900EE1DFD /* SilicaViewer.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SilicaViewer.xctestplan; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
03C39DE226F90733005555AE /* lzodefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = lzodefs.h; sourceTree = "<group>"; };
|
||||
03C39DE326F90733005555AE /* COPYING */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = COPYING; sourceTree = "<group>"; };
|
||||
03C39DE426F90734005555AE /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = "<group>"; };
|
||||
03C39DE526F90734005555AE /* testmini.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = testmini.c; sourceTree = "<group>"; };
|
||||
03C39DE626F90734005555AE /* minilzo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = minilzo.h; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>";
|
||||
|
@ -208,6 +251,40 @@
|
|||
path = Quicklook;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
036AFC0F241800350075400A /* Thumbnail */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -218,15 +295,12 @@
|
|||
path = Thumbnail;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
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 = (
|
||||
|
|
35
Silica ViewerTests/Silica_ViewerTests.swift
Normal file
35
Silica ViewerTests/Silica_ViewerTests.swift
Normal file
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
35
SilicaViewer.xctestplan
Normal file
35
SilicaViewer.xctestplan
Normal file
|
@ -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
|
||||
}
|
745
SilicaViewer/Document.swift
Normal file
745
SilicaViewer/Document.swift
Normal file
|
@ -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<UInt8>.allocate(capacity: byteSize)
|
||||
|
||||
guard let lzoData = readData(archive: threadArchive, entry: threadEntry!) else {
|
||||
return
|
||||
}
|
||||
|
||||
lzoData.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) -> Void in
|
||||
var len = lzo_uint(byteSize)
|
||||
|
||||
lzo1x_decompress_safe(bytes.baseAddress!.assumingMemoryBound(to: uint8.self), lzo_uint(lzoData.count), uncompressedMemory, &len, nil)
|
||||
})
|
||||
|
||||
let imageData = Data(bytes: uncompressedMemory, count: byteSize)
|
||||
|
||||
let render: CGColorRenderingIntent = .defaultIntent
|
||||
let rgbColorSpace = isMask ? CGColorSpaceCreateDeviceGray() : info.colorSpace
|
||||
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: (isMask ? CGImageAlphaInfo.none : CGImageAlphaInfo.premultipliedLast).rawValue).union(.byteOrder32Big)
|
||||
let providerRef: CGDataProvider? = CGDataProvider(data: imageData as CFData)
|
||||
|
||||
guard let cgimage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 8 * numChannels, bytesPerRow: width * numChannels, space: rgbColorSpace, bitmapInfo: bitmapInfo, provider: providerRef!, decode: nil, shouldInterpolate: false, intent: render) else {
|
||||
return
|
||||
}
|
||||
|
||||
queue.async(group: dispatchGroup) {
|
||||
layer.data.chunks[i].image = cgimage
|
||||
layer.data.chunks[i].x = x
|
||||
layer.data.chunks[i].y = y
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
Reference in a new issue