├── .github └── FUNDING.yml ├── License.txt ├── QuickDrawViewer.xcodeproj ├── project.pbxproj ├── xcshareddata │ ├── xcbaselines │ │ └── E59CDF7B2B42C0E600D7BB7B.xcbaseline │ │ │ ├── 50B0F94A-D726-4662-A1A7-5AC3E6D82B13.plist │ │ │ └── Info.plist │ └── xcschemes │ │ └── QuickDrawViewer.xcscheme └── xcuserdata │ └── wiesmann.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── QuickDrawViewer ├── Animation.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon128.png │ │ ├── Icon16.png │ │ ├── Icon256 1.png │ │ ├── Icon256.png │ │ ├── Icon32 1.png │ │ ├── Icon32.png │ │ └── Icon64.png │ ├── Contents.json │ ├── Moof.imageset │ │ ├── Contents.json │ │ ├── Moof128.png │ │ └── Moof256.png │ └── license.dataset │ │ ├── Contents.json │ │ └── license.txt ├── Base.lproj │ └── Credits.rtf ├── Bits.swift ├── Blitting.swift ├── Cinepak.swift ├── FixedPoint.swift ├── Info.plist ├── InfoPlist.xcstrings ├── IntelRaw.swift ├── Localizable.xcstrings ├── MacPaint.swift ├── PackBit.swift ├── Planar.swift ├── Preview Content │ ├── Preview Assets.xcassets │ │ └── Contents.json │ └── demo.pict ├── QuickDrawBitMap.swift ├── QuickDrawColor.swift ├── QuickDrawComment.swift ├── QuickDrawErrors.swift ├── QuickDrawOpcodes.swift ├── QuickDrawParser.swift ├── QuickDrawPattern.swift ├── QuickDrawPoint.swift ├── QuickDrawPolygon.swift ├── QuickDrawPort.swift ├── QuickDrawReader.swift ├── QuickDrawRect.swift ├── QuickDrawRegions.swift ├── QuickDrawRender.swift ├── QuickDrawResolution.swift ├── QuickDrawText.swift ├── QuickDrawTypes.swift ├── QuickDrawViewer.entitlements ├── QuickDrawViewer.xctestplan ├── QuickDrawViewerRelease.entitlements ├── QuickTime.swift ├── QuickTimeGraphics.swift ├── RoadPizza.swift ├── Targa.swift ├── TypeCode.swift ├── UI │ ├── ContentView.swift │ ├── ImageWrapper.swift │ ├── QuickDrawViewerApp.swift │ └── QuickDrawViewerDocument.swift ├── Yuv2.swift ├── en.lproj │ └── Credits.rtf └── fr.lproj │ └── Credits.rtf ├── QuickDrawViewerTests ├── ColorTests.swift ├── PackBitTests.swift └── QuickDrawViewerTests.swift ├── README.md └── docs ├── GitTemplate.graffle ├── Icon.graffle ├── inside_macintosh.pict ├── inside_macintosh_listing_A5.png ├── inside_macintosh_pict.png └── inside_macintosh_preview.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | patreon: thias 3 | 4 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /QuickDrawViewer.xcodeproj/xcshareddata/xcbaselines/E59CDF7B2B42C0E600D7BB7B.xcbaseline/50B0F94A-D726-4662-A1A7-5AC3E6D82B13.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | PackBitTests 8 | 9 | testPerformance() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.845000 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /QuickDrawViewer.xcodeproj/xcshareddata/xcbaselines/E59CDF7B2B42C0E600D7BB7B.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 50B0F94A-D726-4662-A1A7-5AC3E6D82B13 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 0 13 | cpuCount 14 | 1 15 | cpuKind 16 | Apple M1 Max 17 | cpuSpeedInMHz 18 | 0 19 | logicalCPUCoresPerPackage 20 | 10 21 | modelCode 22 | MacBookPro18,4 23 | physicalCPUCoresPerPackage 24 | 10 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | arm64 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /QuickDrawViewer.xcodeproj/xcshareddata/xcschemes/QuickDrawViewer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 35 | 36 | 37 | 40 | 46 | 47 | 48 | 51 | 57 | 58 | 59 | 60 | 61 | 71 | 73 | 79 | 80 | 81 | 82 | 88 | 90 | 96 | 97 | 98 | 99 | 101 | 102 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /QuickDrawViewer.xcodeproj/xcuserdata/wiesmann.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | QuickDrawViewer.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | E59CDF672B42C0E500D7BB7B 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /QuickDrawViewer/Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animation.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 06.03.2024. 6 | // 7 | // Decoder for the `RLE ` "Animation" codec. 8 | // See https://wiki.multimedia.cx/index.php/Apple_QuickTime_RLE 9 | 10 | import Foundation 11 | 12 | enum AnimationCodecError : Error { 13 | case unknownPackBitStride(depth: Int); 14 | case unknownPackBitOpcode(code: Int8); 15 | case invalidXCoordinate(x: Int, dimensions: QDDelta); 16 | case outOfBoundWrite(x: Int, y: Int, length: Int); 17 | } 18 | 19 | 20 | // TODO: depth = 1 bits per pixels does not work. 21 | class AnimationImage : PixMapMetadata { 22 | init(dimensions: QDDelta, depth: Int, clut: QDColorTable?) throws { 23 | self.dimensions = dimensions; 24 | self.depth = depth; 25 | self.clut = clut; 26 | self.pixmap = []; 27 | let (_, componentSize) = try expandDepth(depth); 28 | self.cmpSize = componentSize; 29 | } 30 | 31 | var pixelSize: Int { 32 | return depth; 33 | } 34 | 35 | func packBitStride() throws -> Int { 36 | switch depth { 37 | case 1: return 2; 38 | case 2: return 4; 39 | case 4: return 4; 40 | case 8: return 4; 41 | case 16: return 2; 42 | case 24: return 3; 43 | case 32: return 4; 44 | default: 45 | throw AnimationCodecError.unknownPackBitStride(depth: depth); 46 | } 47 | } 48 | 49 | func writeStride(x: Int, y: Int, data: ArraySlice) throws { 50 | guard x < dimensions.dh.rounded else { 51 | throw AnimationCodecError.invalidXCoordinate(x:x, dimensions: dimensions); 52 | } 53 | let offset = (y * rowBytes) + (x * depth / 8); 54 | for (i, v) in data.enumerated() { 55 | guard offset + i < pixmap.count else { 56 | throw AnimationCodecError.outOfBoundWrite(x:x, y:y,length: i); 57 | } 58 | // In ARGB mode, the alpha is always 0, set it to 0xff 59 | if depth == 32 && i % 4 == 0 { 60 | pixmap[offset + i] = 0xff; 61 | } else { 62 | pixmap[offset + i] = v; 63 | } 64 | } 65 | } 66 | 67 | func parseRunLength(data : ArraySlice, x: inout Int, y: inout Int) throws -> Int { 68 | let stride = try packBitStride(); 69 | var index = data.startIndex; 70 | while index < data.endIndex - 1 { 71 | var decompressed : [UInt8] = []; 72 | let code = Int8(bitPattern: data[index]); 73 | index += 1; 74 | switch code { 75 | case 0: return index - data.startIndex ; 76 | case -1: 77 | x = 0; y += 1; return index - data.startIndex; 78 | case let v where v > 0: 79 | let tail = data[index...]; 80 | guard tail.count >= stride + 1 else { 81 | return index - data.startIndex; 82 | } 83 | index += try copyDiscrete(length: Int(code), src: tail, destination: &decompressed, byteNum: stride); 84 | case let v where v < -1: 85 | let tail = data[index...]; 86 | guard tail.count >= stride + 1 else { 87 | return index - data.startIndex; 88 | } 89 | index += try copyRepeated(length: -Int(code) , src: tail, destination: &decompressed, byteNum: stride); 90 | default: 91 | throw AnimationCodecError.unknownPackBitOpcode(code: code); 92 | } 93 | try writeStride(x: x, y:y, data: decompressed[0...]); 94 | x += (decompressed.count * 8 / depth); 95 | } 96 | return index - data.startIndex; 97 | } 98 | 99 | func load(data : consuming Data) throws { 100 | let s = rowBytes * (dimensions.dv.rounded + 1); 101 | pixmap = Array(repeating: UInt8.zero, count: s); 102 | let reader = try QuickDrawDataReader(data: data, position:0); 103 | let _ = try reader.readUInt32(); 104 | var x = 0; 105 | var y = 0; 106 | let head_flag = try reader.readUInt16(); 107 | if (head_flag & 0x0008) != 0{ 108 | y = Int(try reader.readInt16()); 109 | reader.skip(bytes: 6); 110 | } 111 | let encoded = try reader.readSlice(bytes: reader.remaining); 112 | var index = encoded.startIndex; 113 | 114 | while (index < encoded.count - 1) { 115 | let skip = Int(encoded[index]); 116 | if skip == 0 { 117 | return; 118 | } 119 | 120 | index += 1; 121 | x += (skip - 1); 122 | index += try parseRunLength(data: encoded[index...], x: &x, y: &y); 123 | } 124 | } 125 | 126 | let dimensions: QDDelta; 127 | let depth: Int; 128 | let cmpSize : Int; 129 | let clut: QDColorTable?; 130 | var pixmap : [UInt8]; 131 | 132 | var rowBytes: Int { 133 | return Int(ceil(dimensions.dh.value * Double(depth) / 8.0)); 134 | } 135 | 136 | var description: String { 137 | let desc = describePixMap(self); 138 | return "Animation: \(desc)"; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Icon32 1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Icon64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Icon256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "idiom" : "mac", 47 | "scale" : "2x", 48 | "size" : "256x256" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "512x512" 54 | }, 55 | { 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "512x512" 59 | } 60 | ], 61 | "info" : { 62 | "author" : "xcode", 63 | "version" : 1 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon128.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon16.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon256 1.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon256.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon32 1.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon32.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon64.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/Moof.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Moof128.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Moof256.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/Moof.imageset/Moof128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/Moof.imageset/Moof128.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/Moof.imageset/Moof256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/QuickDrawViewer/Assets.xcassets/Moof.imageset/Moof256.png -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/license.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "data" : [ 3 | { 4 | "filename" : "license.txt", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /QuickDrawViewer/Assets.xcassets/license.dataset/license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /QuickDrawViewer/Base.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2761 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fswiss\fcharset0 Helvetica-Bold;} 3 | {\colortbl;\red255\green255\blue255;\red251\green2\blue7;} 4 | {\*\expandedcolortbl;;\cssrgb\c100000\c14913\c0;} 5 | {\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}} 6 | {\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} 7 | \paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 8 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 9 | 10 | \f0\fs24 \cf0 This program displays images from the original Macintoshes. It supports the following formats:\ 11 | \pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural\partightenfactor0 12 | \ls1\ilvl0\cf0 {\listtext \uc0\u8226 } 13 | \f1\b \cf2 QuickDraw 14 | \f0\b0 \cf0 image files, commonly called PICT files. This was the image file format of the original Macintosh. \ 15 | {\listtext \uc0\u8226 } 16 | \f1\b \cf2 MacPaint\cf0 17 | \f0\b0 image files. This was the image file format of the MacPaint program.\ 18 | {\listtext \uc0\u8226 } 19 | \f1\b \cf2 QuickTime 20 | \f0\b0 \cf0 image files. \ 21 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 22 | \cf0 \ 23 | You can copy-paste the image into Preview to translate it into a modern format like PNG or PDF. \ 24 | \ 25 | This program is offered as is, you assume any risks, including in the event the destruction of the space-time continuum. If you find a bug, or have an image that fails parsing, {\field{\*\fldinst{HYPERLINK "https://github.com/wiesmann/QuickDrawViewer/issues"}}{\fldrslt consider filling a bug}}.\ 26 | \ 27 | This program was written by {\field{\*\fldinst{HYPERLINK "https://wiesmann.codiferes.net"}}{\fldrslt Matthias Wiesmann}} and is distributed under the Apache 2.0 license. } -------------------------------------------------------------------------------- /QuickDrawViewer/Bits.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bits.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 09.03.2024. 6 | // 7 | // Various low level bit and memory manipulation utilities. 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | var bytes: [UInt8] { 13 | return [UInt8](self) 14 | } 15 | } 16 | 17 | extension SIMD4 { 18 | var bytes: [UInt8] { 19 | return [self.x, self.y, self.z, self.w]; 20 | } 21 | } 22 | 23 | /// Convert some integer type into a sequence of boolean, starting from the most significant bit. 24 | /// - Parameter from: number to convert 25 | /// - Returns: an array of boolean 26 | func boolArray(_ from: T) -> [Bool] where T : FixedWidthInteger { 27 | var buffer = from; 28 | let mask : T = 1 << (T.bitWidth - 1); 29 | var result : [Bool] = []; 30 | for _ in 0..(bytes : ArraySlice) -> T where T: FixedWidthInteger { 39 | return bytes.reduce(0) { T($0) << 8 + T($1) } 40 | } 41 | 42 | func makeUInt24(bytes: (UInt8, UInt8, UInt8)) -> UInt32 { 43 | return UInt32(bytes.0) << 16 | UInt32(bytes.1) << 8 | UInt32(bytes.2); 44 | } 45 | 46 | func byteArrayLE(from value: T) -> [UInt8] where T: FixedWidthInteger { 47 | withUnsafeBytes(of: value.littleEndian, Array.init) 48 | } 49 | 50 | func byteArrayBE(from value: T) -> [UInt8] where T: FixedWidthInteger { 51 | withUnsafeBytes(of: value.bigEndian, Array.init) 52 | } 53 | -------------------------------------------------------------------------------- /QuickDrawViewer/Blitting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Blitting.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 10.03.2024. 6 | // 7 | // Various image processing utilities. 8 | 9 | import Foundation 10 | 11 | enum BlittingError : Error { 12 | case invalidBlockNumber(blockNumber: Int, totalBlockNumber: Int); 13 | case invalidBlockLine(lineNumber: Int, blockSize: Int); 14 | case badPixMapIndex(index: Int, pixMapSize: Int); 15 | case unsupportedDepth(depth : Int); 16 | } 17 | 18 | func roundTo(_ value: FixedPoint, multipleOf: Int) -> Int { 19 | return (value.rounded + (multipleOf - 1)) / multipleOf * multipleOf; 20 | } 21 | 22 | /// Convert YUV values to RGB bytes. 23 | /// - Parameters: 24 | /// - y: luminence in the 0-255 range 25 | /// - u: u chrominance in the -127 - 128 range 26 | /// - v: v chrominance in the -127 - 128 range 27 | /// - Returns: rgb bytes 28 | func yuv2Rgb(y : Double, u: Double, v: Double) -> RGB8 { 29 | let r = Int(y + (1.370705 * v)); 30 | let g = Int(y - (0.698001 * v) - 0.337633 * u); 31 | let b = Int(y + (1.732446 * u)); 32 | return [UInt8(clamping: r), UInt8(clamping: g), UInt8(clamping: b)]; 33 | } 34 | 35 | func yuv2Rgb(y: UInt8, u: UInt8, v: UInt8) -> RGB8 { 36 | let nu = Double(u) - 128; 37 | let nv = Double(v) - 128; 38 | let ny = Double(y); 39 | return yuv2Rgb(y: ny, u: nu, v: nv); 40 | } 41 | 42 | func interleaveRgb(planar : ArraySlice) -> [UInt8] { 43 | var result : [UInt8] = []; 44 | let w = planar.count / 3; 45 | let w2 = 2 * w ; 46 | for i in 0.. (Int, Int) { 60 | switch depth { 61 | case let d where d <= 8: 62 | return (1, d); 63 | case 16: 64 | return (3, 5); 65 | case 24: 66 | return (3, 8); 67 | case 32: 68 | return (3, 8); 69 | default: 70 | throw BlittingError.unsupportedDepth(depth: depth); 71 | } 72 | } 73 | 74 | /// Abstract view of a bitmap information 75 | protocol PixMapMetadata : CustomStringConvertible { 76 | 77 | var dimensions : QDDelta {get}; 78 | var rowBytes : Int {get}; 79 | var cmpSize : Int {get}; 80 | var pixelSize : Int {get}; 81 | 82 | var clut: QDColorTable? {get}; 83 | } 84 | 85 | func describePixMap(_ pm: PixMapMetadata) -> String { 86 | return "\(pm.dimensions) rowBytes: \(pm.rowBytes) pixelSize: \(pm.pixelSize)" 87 | } 88 | 89 | /// Parent class for block based pixmap formats. 90 | class BlockPixMap : PixMapMetadata { 91 | 92 | init(dimensions: QDDelta, blockSize: Int, pixelSize: Int, cmpSize: Int, clut: QDColorTable?) { 93 | self.dimensions = dimensions; 94 | self.blockSize = blockSize; 95 | self.pixelSize = pixelSize; 96 | self.cmpSize = cmpSize; 97 | self.clut = clut; 98 | let blockOffset = blockSize - 1; 99 | self.blocksPerLine = (dimensions.dh.rounded + blockOffset) / blockSize; 100 | let blockLines = (dimensions.dv.rounded + blockOffset) / blockSize; 101 | self.totalBlocks = blocksPerLine * blockLines; 102 | self.bufferDimensions = QDDelta(dv: FixedPoint(blockLines * blockSize), dh: FixedPoint(blocksPerLine * blockSize)); 103 | let blockBytes = blockSize * blockSize * pixelSize / 8; 104 | // Add one safety block because rounding up happens. 105 | self.pixmap = [UInt8].init(repeating: 0, count: (totalBlocks + 1) * blockBytes); 106 | } 107 | 108 | var description: String { 109 | let desc = describePixMap(self); 110 | return "BlockImage \(desc) \(blockSize)×\(blockSize)"; 111 | } 112 | 113 | var rowBytes: Int { 114 | return blocksPerLine * blockSize * pixelSize / 8; 115 | } 116 | 117 | func getOffset(block: Int, line: Int) throws -> Int { 118 | guard (0..); 110 | case twelve(y: SIMD4, u: Int8, v: Int8); 111 | case uninitialized(rgb: RGB8); // Should never been seen, for debugging 112 | } 113 | 114 | /// Initialize an entry with four 4 8-bit values, either intensity, or palette entries. 115 | /// - Parameter y4: 4 8-bit values, either intensity, or palette entries. 116 | init(y4 : [UInt8]) { 117 | assert(y4.count == 4); 118 | self.payload = .eight(y: SIMD4(y4[0], y4[1], y4[2], y4[3])) 119 | } 120 | 121 | /// Initialize an entry with four 6 8-bit values, four intensities and two chrominance bytes (u, v). 122 | /// - Parameters: 123 | /// - y4: intensities (unsigned) 124 | /// - u: u chrominance value (signed) 125 | /// - v: v chrominance value (signed) 126 | init(y4 : [UInt8], u : Int8, v: Int8) { 127 | assert(y4.count == 4); 128 | let y = SIMD4(y4[0], y4[1], y4[2], y4[3]); 129 | self.payload = .twelve(y: y, u: u, v: v); 130 | } 131 | 132 | private init(y: UInt8, u: Int8, v: Int8) { 133 | self.init(y4: [UInt8](repeating: y, count: 4), u:u, v:v); 134 | } 135 | 136 | private init(y: UInt8) { 137 | self.init(y4: [UInt8](repeating: y, count: 4)); 138 | } 139 | 140 | /// Create an uninitialized entry, used for debugging. 141 | /// - Parameter uninitialized: rgb value to makr unitialized entry. 142 | private init(uninitialized: RGB8) { 143 | self.payload = .uninitialized(rgb: uninitialized); 144 | } 145 | 146 | /// The intensities (or palette indexes) of the entry. 147 | var y : [UInt8] { 148 | switch payload { 149 | case .eight(let y): 150 | return y.bytes; 151 | case .twelve(let y, _, _): 152 | return y.bytes; 153 | case .uninitialized: 154 | return [0, 0, 0, 0]; 155 | } 156 | } 157 | 158 | /// The four pixels in RGB8 format. 159 | /// Note that cinepak uses a simplified version of yuv. 160 | var rgb : [RGB8] { 161 | switch payload { 162 | case .eight(let y): 163 | return y.bytes.map(){[$0, $0, $0]}; 164 | case .twelve(let y, let u, let v): 165 | // Decode cinepak YUV 166 | let u4 = SIMD4.init(repeating: Int16(u)); 167 | let v4 = SIMD4.init(repeating: Int16(v)); 168 | let one = SIMD4.one; 169 | let y4 = SIMD4.init(clamping: y); 170 | let r = SIMD4(clamping: y4 &+ (v4 &<< one)); 171 | let g = SIMD4(clamping: y4 &- (u4 &>> one) &- v4); 172 | let b = SIMD4(clamping: y4 &+ (u4 &<< one)); 173 | return toRGB8(r: r, g: g, b: b); 174 | case .uninitialized(let rgb): 175 | return [RGB8].init(repeating: rgb, count: 4); 176 | } 177 | } 178 | 179 | /// Return 4 codebook entries corresponding to this one. 180 | var doubled : [CinepakCodeBookEntry] { 181 | switch payload { 182 | case .eight(let y): 183 | return y.bytes.map(){CinepakCodeBookEntry(y:$0)}; 184 | case .twelve(let y, let u, let v): 185 | return y.bytes.map(){CinepakCodeBookEntry(y:$0, u: u, v: v)}; 186 | case .uninitialized: 187 | return [CinepakCodeBookEntry](repeating: self, count: 4); 188 | } 189 | } 190 | 191 | private let payload: CodeBookPayload; 192 | static let uninitialized = CinepakCodeBookEntry(uninitialized: [0xff, 0x00, 0xff]); 193 | } 194 | 195 | /// A code-book is a collection of 2×2 pixel patterns, see the CinepakCodeBookEntry struct . 196 | class CinepakCodeBook { 197 | 198 | init(name: String) { 199 | self.name = name; 200 | 201 | entries = [CinepakCodeBookEntry].init(repeating: CinepakCodeBookEntry.uninitialized, count: 256); 202 | } 203 | 204 | func readEntries(n: Int, chunkType: CinepakChunk.ChunkType, reader : QuickDrawDataReader) throws { 205 | for i in 0.. 4 { 214 | for v in boolArray(try reader.readUInt32()) { 215 | if v { 216 | let entry = try reader.readCinepakCodeBookEntry(chunkType: chunkType); 217 | entries[pos] = entry; 218 | } 219 | pos += 1; 220 | } 221 | } 222 | } 223 | 224 | func lookup(_ index : UInt8) throws -> CinepakCodeBookEntry { 225 | guard index < entries.count else { 226 | throw CinepakError.codeBookOutOfRange(index, max: entries.count, name: name); 227 | } 228 | return entries[Int(index)] 229 | } 230 | 231 | let name : String; 232 | var entries : [CinepakCodeBookEntry]; 233 | } 234 | 235 | extension QuickDrawDataReader { 236 | func readCinepakStripHeader(vOffset : FixedPoint) throws -> CinepakStripeDescriptor { 237 | let rawId = try readUInt16(); 238 | guard let stripeType = CinepakStripeDescriptor.StripeType(rawValue: rawId) else { 239 | throw CinepakError.invalidStripId(rawId); 240 | } 241 | let size = try readUInt16() - 12; 242 | var frame = try readRect(); 243 | frame = frame + QDDelta(dv: vOffset, dh: FixedPoint.zero); 244 | return CinepakStripeDescriptor(stripeType: stripeType, stripeSize: size, stripeFrame: frame); 245 | } 246 | 247 | func readCinepakCodeBookEntry(chunkType: CinepakChunk.ChunkType) throws -> CinepakCodeBookEntry{ 248 | let y4 = try readUInt8(bytes: 4); 249 | if chunkType.contains(.eightBpp) { 250 | return CinepakCodeBookEntry(y4: y4); 251 | } 252 | let u = try readInt8(); 253 | let v = try readInt8(); 254 | return CinepakCodeBookEntry(y4: y4, u: u, v: v); 255 | } 256 | } 257 | 258 | 259 | /// A cinepak image is composed of 4×4 blocks, which are filled either using one 2×2 codebook entries (doubled), 260 | /// or 4 2×2 code book entries, each block 261 | class Cinepak : BlockPixMap { 262 | init(dimensions: QDDelta, clut: QDColorTable?) { 263 | components = clut != nil ? CinepakComponents.index : CinepakComponents.rgb; 264 | super.init(dimensions: dimensions, blockSize: 4, pixelSize: components.rawValue * 8, cmpSize: 8, clut: clut); 265 | } 266 | 267 | enum CinepakComponents : Int { 268 | case index = 1; 269 | case rgb = 3; 270 | } 271 | 272 | func apply(entry: CinepakCodeBookEntry, pos: Int, offset: Int) throws { 273 | let max = offset + self.components.rawValue; 274 | guard max <= pixmap.count else { 275 | throw BlittingError.badPixMapIndex(index: max, pixMapSize: pixmap.count); 276 | } 277 | switch components { 278 | case .index: 279 | pixmap[offset] = entry.y[pos]; 280 | case .rgb: 281 | let rgb = entry.rgb[pos] 282 | for c in 0..<3 { 283 | pixmap[offset + c] = rgb[c]; 284 | } 285 | } 286 | } 287 | 288 | func applyEntries(block: Int, entries : [CinepakCodeBookEntry]) throws { 289 | let lines = [0, 1, 2, 3]; 290 | let lineOffsets = try lines.map(){try getOffset(block: block, line: $0);} 291 | let pixOffset = components.rawValue; 292 | // First line, entries 0 and 1. 293 | try apply(entry: entries[0], pos: 0, offset: lineOffsets[0]); 294 | try apply(entry: entries[0], pos: 1, offset: lineOffsets[0] + pixOffset); 295 | try apply(entry: entries[1], pos: 0, offset: lineOffsets[0] + pixOffset * 2); 296 | try apply(entry: entries[1], pos: 1, offset: lineOffsets[0] + pixOffset * 3); 297 | // Second line, entries 0 and 1. 298 | try apply(entry: entries[0], pos: 2, offset: lineOffsets[1]); 299 | try apply(entry: entries[0], pos: 3, offset: lineOffsets[1] + pixOffset); 300 | try apply(entry: entries[1], pos: 2, offset: lineOffsets[1] + pixOffset * 2); 301 | try apply(entry: entries[1], pos: 3, offset: lineOffsets[1] + pixOffset * 3); 302 | // Third line, entries 2 and 3. 303 | try apply(entry: entries[2], pos: 0, offset: lineOffsets[2]); 304 | try apply(entry: entries[2], pos: 1, offset: lineOffsets[2] + pixOffset); 305 | try apply(entry: entries[3], pos: 0, offset: lineOffsets[2] + pixOffset * 2); 306 | try apply(entry: entries[3], pos: 1, offset: lineOffsets[2] + pixOffset * 3); 307 | // Fourth line, entries 2 and 3. 308 | try apply(entry: entries[2], pos: 2, offset: lineOffsets[3]); 309 | try apply(entry: entries[2], pos: 3, offset: lineOffsets[3] + pixOffset); 310 | try apply(entry: entries[3], pos: 2, offset: lineOffsets[3] + pixOffset * 2); 311 | try apply(entry: entries[3], pos: 3, offset: lineOffsets[3] + pixOffset * 3); 312 | } 313 | 314 | func applyV4(block: Int, v4: [UInt8]) throws { 315 | assert(v4.count == 4); 316 | guard block < totalBlocks else { 317 | return; 318 | } 319 | let entries = try v4.map(){try v4Codebook.lookup($0)}; 320 | try applyEntries(block: block, entries: entries) 321 | } 322 | 323 | func applyV1(block: Int, v1: UInt8) throws { 324 | guard block < totalBlocks else { 325 | return; 326 | } 327 | let entry = try v1Codebook.lookup(v1); 328 | let entries = entry.doubled; 329 | try applyEntries(block: block, entries: entries) 330 | } 331 | 332 | func applyVectorBlock(reader : QuickDrawDataReader) throws -> Bool { 333 | let mask = try reader.readUInt32(); 334 | for v in boolArray(mask) { 335 | if v { 336 | guard reader.remaining >= 4 else { 337 | return false; 338 | } 339 | let v4 = try reader.readUInt8(bytes: 4); 340 | try applyV4(block: self.block, v4: v4); 341 | } else { 342 | guard reader.remaining >= 1 else { 343 | return false; 344 | } 345 | let v1 = try reader.readUInt8(); 346 | try applyV1(block: self.block, v1: v1); 347 | } 348 | self.block += 1; 349 | } 350 | return true; 351 | } 352 | 353 | func applyVectors(strip: CinepakStripeDescriptor, reader : QuickDrawDataReader) throws { 354 | while true { 355 | guard reader.remaining >= 4 else { 356 | return; 357 | } 358 | let fullRead = try applyVectorBlock(reader: reader); 359 | guard fullRead else { 360 | return; 361 | } 362 | } 363 | } 364 | 365 | /// Parse a single chunk 366 | func parseChunk(strip: CinepakStripeDescriptor, chunk: CinepakChunk) throws { 367 | let reader = try QuickDrawDataReader(data: chunk.chunkData, position: 0); 368 | switch chunk.chunkType { 369 | /// The chunk contains vectors, i.e. entry indexes, not exclusively for v1. 370 | case let c where c.contains(.vectors) && !c.contains(.v1): 371 | try applyVectors(strip: strip, reader: reader); 372 | /// The chunk contains a codebook definition (not update) 373 | case let c where c.contains(.codebook) && !c.contains(.update): 374 | let numEntries = c.contains(.eightBpp) ? (chunk.size / 4) : (chunk.size / 6); 375 | guard numEntries <= 256 else { 376 | throw CinepakError.tooManyCodebookEntries(number: numEntries); 377 | } 378 | if c.contains(.v1) { 379 | try v1Codebook.readEntries(n: numEntries, chunkType: chunk.chunkType, reader: reader); 380 | } else { 381 | try v4Codebook.readEntries(n: numEntries, chunkType: chunk.chunkType, reader: reader); 382 | } 383 | /// The chunk contains codebook updates. 384 | case let c where c.contains(.codebook) && c.contains(.update): 385 | if c.contains(.v1) { 386 | try v1Codebook.updateEntries(chunkType: chunk.chunkType, reader: reader); 387 | } else { 388 | try v4Codebook.updateEntries(chunkType: chunk.chunkType, reader: reader); 389 | } 390 | default: 391 | throw CinepakError.unsupportChunkType(chunk.chunkType); 392 | } 393 | } 394 | 395 | func loadStripe(stripe: CinepakStripeDescriptor, reader : QuickDrawDataReader) throws { 396 | assert (reader.data.count == stripe.stripeSize); 397 | while reader.remaining >= 16 { 398 | let rawId = try reader.readUInt16(); 399 | let chunkType = CinepakChunk.ChunkType(rawValue: rawId); 400 | let chunkSize = Int(try reader.readUInt16() - 4); 401 | let data = try reader.readData(bytes: chunkSize); 402 | let chunk = CinepakChunk(chunkType: chunkType, chunkData: data); 403 | stripe.chunks.append(chunk); 404 | try parseChunk(strip: stripe, chunk: chunk); 405 | } 406 | } 407 | 408 | func load(data : consuming Data) throws { 409 | let reader = try QuickDrawDataReader(data: data, position: 0); 410 | self.flags = try reader.readUInt8(); 411 | let sizeHigh = Int(try reader.readUInt8()); 412 | let sizeLow = Int(try reader.readUInt16()); 413 | let size = sizeLow & (sizeHigh << 16); 414 | guard size == 0 || size == reader.data.count else { 415 | throw CinepakError.inconsistentSize(container: reader.data.count, header: size); 416 | } 417 | let width = try reader.readUInt16(); 418 | guard Int(width) == self.bufferDimensions.dh.rounded else { 419 | throw CinepakError.inconsistentWidth(frame:self.bufferDimensions, width:width); 420 | } 421 | let height = try reader.readUInt16(); 422 | guard Int(height) == self.bufferDimensions.dv.rounded else { 423 | throw CinepakError.inconsistentHeight(frame:self.bufferDimensions, height:height); 424 | } 425 | let stripNumber = try reader.readUInt16(); 426 | guard stripNumber <= 32 else { 427 | throw CinepakError.tooManyStrips(strips: stripNumber); 428 | } 429 | var y = FixedPoint.zero; 430 | for _ in 0.. (_ value: T) { 30 | self.fixedValue = Int(value) * FixedPoint.multiplier; 31 | } 32 | 33 | private let fixedValue : Int; 34 | private static let multiplier : Int = 0x10000; 35 | private static let fractionMask : Int = multiplier - 1; 36 | 37 | public static let zero = FixedPoint(rawValue: 0); 38 | public static let one = FixedPoint(rawValue: multiplier); 39 | 40 | public var magnitude: FixedPoint { 41 | return FixedPoint(rawValue: Int(fixedValue.magnitude)); 42 | } 43 | 44 | /// Return a compact, description. 45 | /// Use Unicode fraction to denote _perfect_ fractions that are typically used for line width and such. 46 | public var description: String { 47 | switch fixedValue { 48 | case 0 : 49 | return "0"; 50 | case 0x8000: 51 | return "½"; 52 | // Quarters 53 | case 0x4000: 54 | return "¼"; 55 | case 0xc000: 56 | return "¾"; 57 | // Eights 58 | case 0x2000: 59 | return "⅛"; 60 | case 0x6000: 61 | return "⅜"; 62 | case 0xA000: 63 | return "⅝"; 64 | case 0xE000: 65 | return "⅞"; 66 | case let fixedValue where fixedValue & FixedPoint.fractionMask == 0: 67 | return "\(rounded)"; 68 | default: 69 | return "\(value)"; 70 | } 71 | } 72 | 73 | public var rounded : Int { 74 | return fixedValue / FixedPoint.multiplier; 75 | } 76 | 77 | public var fractionPart : FixedPoint { 78 | return FixedPoint(rawValue : self.fixedValue & FixedPoint.fractionMask); 79 | } 80 | 81 | public var value : Double { 82 | return Double(fixedValue) / Double(FixedPoint.multiplier); 83 | } 84 | 85 | public var isRound : Bool { 86 | return fixedValue & FixedPoint.fractionMask == 0; 87 | } 88 | 89 | /// Addition 90 | /// - Parameters: 91 | /// - a: left hand value to add 92 | /// - b: right hand value to add 93 | /// - Returns: A fixed point with the sum of a + b. 94 | public static func + (a: FixedPoint, b: FixedPoint) -> FixedPoint { 95 | return FixedPoint(rawValue: a.fixedValue + b.fixedValue); 96 | } 97 | 98 | /// Substraction 99 | /// - Parameters: 100 | /// - a: left hand value to add 101 | /// - b: right hand value to add 102 | /// - Returns: A fixed point with the difference a - b. 103 | public static func - (a: FixedPoint, b: FixedPoint) -> FixedPoint { 104 | return FixedPoint(rawValue: a.fixedValue - b.fixedValue); 105 | } 106 | 107 | /// Negation 108 | /// - Parameter v: value to negate 109 | /// - Returns: return the negative value of v. 110 | static prefix func -(v: FixedPoint) -> FixedPoint { 111 | return FixedPoint(rawValue: -v.fixedValue); 112 | } 113 | 114 | /// Shift right. 115 | /// - Parameters: 116 | /// - v: fixed point value to shift 117 | /// - s: number of right shifts 118 | /// - Returns: value shift to the right, Note that 1 >> 1 = 0.5 119 | static func >> (v: FixedPoint, s: Int) -> FixedPoint { 120 | let raw = v.fixedValue >> s; 121 | return FixedPoint(rawValue: raw); 122 | } 123 | 124 | /// Shift left 125 | /// - Parameters: 126 | /// - v: fixed point value to shift 127 | /// - s: number of left shifts 128 | /// - Returns: <#description#> 129 | static func << (v: FixedPoint, s: Int) -> FixedPoint { 130 | let raw = v.fixedValue << s; 131 | return FixedPoint(rawValue: raw); 132 | } 133 | 134 | public static func < (lhs: FixedPoint, rhs: FixedPoint) -> Bool { 135 | return lhs.fixedValue < rhs.fixedValue; 136 | } 137 | 138 | public static func * (lhs: FixedPoint, rhs: FixedPoint) -> FixedPoint { 139 | return FixedPoint(rawValue: lhs.fixedValue * rhs.fixedValue / multiplier); 140 | } 141 | 142 | public static func *= (lhs: inout FixedPoint, rhs: FixedPoint) { 143 | lhs = lhs * rhs; 144 | } 145 | 146 | /// We need division to parse `fract` types. 147 | /// - Parameters: 148 | /// - a: numerator 149 | /// - b: denominator 150 | /// - Returns: a/b 151 | public static func / (a : FixedPoint, b: FixedPoint) -> FixedPoint { 152 | return FixedPoint(a.value / b.value); 153 | } 154 | } 155 | 156 | extension QuickDrawDataReader { 157 | 158 | func readFixed() throws -> FixedPoint { 159 | let v = try readInt32(); 160 | return FixedPoint(rawValue: Int(v)); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /QuickDrawViewer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDocumentTypes 6 | 7 | 8 | CFBundleTypeIconSystemGenerated 9 | 1 10 | CFBundleTypeName 11 | Apple QuickTime picture 12 | CFBundleTypeRole 13 | Viewer 14 | LSHandlerRank 15 | Default 16 | LSItemContentTypes 17 | 18 | com.apple.quicktime-image 19 | 20 | 21 | 22 | CFBundleTypeIconSystemGenerated 23 | 1 24 | CFBundleTypeName 25 | Apple QuickDraw pictures 26 | CFBundleTypeRole 27 | Viewer 28 | LSHandlerRank 29 | Default 30 | LSItemContentTypes 31 | 32 | com.apple.pict 33 | 34 | 35 | 36 | CFBundleTypeIconSystemGenerated 37 | 1 38 | CFBundleTypeName 39 | Apple MacPaint pictures 40 | CFBundleTypeRole 41 | Viewer 42 | LSHandlerRank 43 | Default 44 | LSItemContentTypes 45 | 46 | com.apple.macpaint-image 47 | 48 | 49 | 50 | UTImportedTypeDeclarations 51 | 52 | 53 | UTTypeConformsTo 54 | 55 | public.image 56 | 57 | UTTypeDescription 58 | QuickTime Picture 59 | UTTypeIcons 60 | 61 | UTTypeIconBadgeName 62 | 63 | UTTypeIconText 64 | QuickTime 65 | 66 | UTTypeIdentifier 67 | com.apple.quicktime-image 68 | UTTypeTagSpecification 69 | 70 | public.filename-extension 71 | 72 | qtif 73 | qif 74 | 75 | public.mime-type 76 | 77 | Image/x-quicktime 78 | 79 | 80 | 81 | 82 | UTTypeConformsTo 83 | 84 | public.image 85 | 86 | UTTypeDescription 87 | Quickdraw Picture 88 | UTTypeIcons 89 | 90 | UTTypeIconText 91 | PICT 92 | 93 | UTTypeIdentifier 94 | com.apple.pict 95 | UTTypeTagSpecification 96 | 97 | public.filename-extension 98 | 99 | pict 100 | pct 101 | 102 | public.mime-type 103 | 104 | image/x-pict 105 | image/pict 106 | 107 | 108 | 109 | 110 | UTTypeConformsTo 111 | 112 | public.image 113 | 114 | UTTypeDescription 115 | MacPaint Picture 116 | UTTypeIcons 117 | 118 | UTTypeIdentifier 119 | com.apple.macpaint-image 120 | UTTypeTagSpecification 121 | 122 | public.filename-extension 123 | 124 | pntg 125 | 126 | public.mime-type 127 | 128 | Image/x-macpaint 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /QuickDrawViewer/InfoPlist.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "CFBundleDisplayName" : { 5 | "comment" : "Bundle display name", 6 | "extractionState" : "extracted_with_value", 7 | "localizations" : { 8 | "en" : { 9 | "stringUnit" : { 10 | "state" : "new", 11 | "value" : "QuickDraw Viewer" 12 | } 13 | } 14 | } 15 | }, 16 | "CFBundleName" : { 17 | "comment" : "Bundle name", 18 | "extractionState" : "extracted_with_value", 19 | "localizations" : { 20 | "en" : { 21 | "stringUnit" : { 22 | "state" : "new", 23 | "value" : "QuickDrawViewer" 24 | } 25 | } 26 | } 27 | }, 28 | "MacPaint Picture" : { 29 | "localizations" : { 30 | "fr" : { 31 | "stringUnit" : { 32 | "state" : "translated", 33 | "value" : "Image MacPaint" 34 | } 35 | } 36 | } 37 | }, 38 | "NSHumanReadableCopyright" : { 39 | "comment" : "Copyright (human-readable)", 40 | "extractionState" : "extracted_with_value", 41 | "localizations" : { 42 | "en" : { 43 | "stringUnit" : { 44 | "state" : "new", 45 | "value" : "© Matthias Wiesmann – Apache 2.0 License." 46 | } 47 | } 48 | } 49 | }, 50 | "Quickdraw Picture" : { 51 | "localizations" : { 52 | "fr" : { 53 | "stringUnit" : { 54 | "state" : "translated", 55 | "value" : "Image QuickDraw" 56 | } 57 | } 58 | } 59 | }, 60 | "QuickTime Picture" : { 61 | "localizations" : { 62 | "fr" : { 63 | "stringUnit" : { 64 | "state" : "translated", 65 | "value" : "Image QuickTime" 66 | } 67 | } 68 | } 69 | } 70 | }, 71 | "version" : "1.0" 72 | } -------------------------------------------------------------------------------- /QuickDrawViewer/IntelRaw.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntelRaw.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 01.04.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The YVU9 is a planar format, in which U and V are sampled every 4 pixels horizontally 11 | /// and vertically (sometimes referred to as 16:1:1). The V plane appears before the U plane. 12 | class IntelRawImage : PixMapMetadata { 13 | 14 | init(dimensions : QDDelta) { 15 | let h = roundTo(dimensions.dh, multipleOf: 4); 16 | let v = roundTo(dimensions.dv, multipleOf: 4); 17 | self.dimensions = QDDelta(dv:v ,dh:h); 18 | self.rowBytes = 3 * h; 19 | self.pixmap = []; 20 | } 21 | 22 | // This could probably be done using some optimised library. 23 | func load(data : consuming Data) throws { 24 | let lines = dimensions.dv.rounded; 25 | let columns = dimensions.dh.rounded; 26 | let ySize = lines * columns 27 | let yData = data[0.. some OpCode { 28 | let bitRect = BitRectOpcode(isPacked: true); 29 | bitRect.bitmapInfo.rowBytes = self.rowBytes; 30 | let frame = QDRect(topLeft: .zero, dimension: self.dimensions); 31 | bitRect.bitmapInfo.bounds = frame; 32 | bitRect.bitmapInfo.srcRect = frame; 33 | bitRect.bitmapInfo.dstRect = frame; 34 | bitRect.bitmapInfo.data = bitmap; 35 | return bitRect; 36 | } 37 | 38 | /// Convert a MacPaint images into a minimalistic picture. 39 | /// This is enough for this program, but a valid quickdraw file would have some header operations. 40 | func macPicture(filename: String?) -> QDPicture { 41 | let frame = QDRect(topLeft: .zero, dimension: self.dimensions); 42 | let picture = QDPicture(size: -1, frame: frame, filename: filename); 43 | picture.opcodes.append(makeOpcode()); 44 | return picture; 45 | } 46 | 47 | let rowBytes: Int = MacPaintImage.width / 8; 48 | var cmpSize: Int = 1; 49 | var pixelSize: Int = 1; 50 | let dimensions = QDDelta( 51 | dv: FixedPoint(MacPaintImage.height), 52 | dh: FixedPoint(MacPaintImage.width)); 53 | var clut: QDColorTable? = QDColorTable.palette1; 54 | var bitmap: [UInt8] = []; 55 | 56 | var description: String { 57 | let pm = describePixMap(self); 58 | return "MacPaint: \(pm)"; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /QuickDrawViewer/PackBit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PackBit.swift 3 | // QuickDrawKit 4 | // 5 | // Created by Matthias Wiesmann on 16.12.2023. 6 | // 7 | // Facilities to handle PackBit and PackBit like compression. 8 | 9 | import Foundation 10 | 11 | enum PackbitError: Error { 12 | case mismatchedLength(expectedLength: Int, actualLength: Int); 13 | case emptySlice; 14 | case outOfBoundRunStart(start: Int, dataSize: Int); 15 | case outOfBoundRunEnd(end: Int, data: ArraySlice); 16 | } 17 | 18 | /// Copy a repeated pattern of length `length`× `byteNum`. 19 | /// - Parameters: 20 | /// - length: number of time pattern is repeated 21 | /// - src: array where the pattern (`byteNum` bytes) is read. 22 | /// - destination: where the pattern is written. 23 | /// - byteNum: size of the pattern in bytes. 24 | /// - Throws: outOfBoundRunEnd if pattern is larger than `src`. 25 | /// - Returns: number of bytes read, always `byteNum`. 26 | func copyRepeated(length: Int, src : ArraySlice, destination: inout [UInt8], byteNum: Int) throws -> Int { 27 | guard !src.isEmpty else { 28 | throw PackbitError.emptySlice; 29 | } 30 | let start = src.startIndex; 31 | let end = start + byteNum; 32 | guard end <= src.endIndex else { 33 | throw PackbitError.outOfBoundRunEnd(end: byteNum, data: src); 34 | } 35 | let element = src[start.. src 48 | /// - Returns: number of bytes read, 49 | func copyDiscrete(length: Int, src : ArraySlice, destination : inout [UInt8], byteNum: Int) throws -> Int { 50 | guard !src.isEmpty else { 51 | throw PackbitError.emptySlice; 52 | } 53 | let start = src.startIndex; 54 | let end = start + (length * byteNum); 55 | guard end <= src.endIndex else { 56 | throw PackbitError.outOfBoundRunEnd(end: end, data: src); 57 | } 58 | let slice = src[start.., unpackedSize: Int, byteNum : Int = 1) throws -> [UInt8] { 70 | var decompressed : [UInt8] = []; 71 | decompressed.reserveCapacity(unpackedSize); 72 | var index = data.startIndex 73 | while index < data.endIndex { 74 | let tag = Int8(bitPattern: data[index]); 75 | index += 1; 76 | guard index < data.endIndex else { 77 | throw PackbitError.outOfBoundRunStart(start: index, dataSize: data.endIndex); 78 | } 79 | if (tag < 0) { 80 | let length = (Int(tag) * -1) + 1; 81 | index += try copyRepeated(length: length, src: data[index...], destination: &decompressed, byteNum: byteNum); 82 | } else { 83 | let length = Int(tag) + 1; 84 | index += try copyDiscrete(length: length, src: data[index...], destination: &decompressed, byteNum: byteNum); 85 | } 86 | } 87 | guard decompressed.count == unpackedSize else { 88 | throw PackbitError.mismatchedLength(expectedLength: unpackedSize, actualLength: decompressed.count ); 89 | } 90 | return decompressed; 91 | } 92 | -------------------------------------------------------------------------------- /QuickDrawViewer/Planar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Planar.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 09.02.2024. 6 | // 7 | // Decoder for the QuickTime `8BPS` "Planar" codec. 8 | 9 | import Foundation 10 | 11 | enum PlanarImageError : Error { 12 | case badDepth(depth: Int); 13 | case packbitError(line: Int, packbitError: PackbitError); 14 | } 15 | 16 | class PlanarImage : PixMapMetadata { 17 | 18 | init(dimensions: QDDelta, depth: Int, clut: QDColorTable?) throws { 19 | self.dimensions = dimensions; 20 | self.depth = depth; 21 | self.clut = clut; 22 | let (channels, pixSize) = try expandDepth(depth); 23 | self.channels = channels; 24 | if pixSize != 8 { 25 | throw PlanarImageError.badDepth(depth: depth); 26 | } 27 | self.pixmap = []; 28 | } 29 | 30 | func load(data : consuming Data) throws { 31 | let reader = try QuickDrawDataReader(data: data, position:0); 32 | let lines = (dimensions.dv.rounded * channels); 33 | var lineLengths : [Int] = []; 34 | for _ in 0.. QDPixMapInfo { 128 | let pixMapInfo = QDPixMapInfo(); 129 | pixMapInfo.version = Int(try readUInt16()); 130 | pixMapInfo.packType = QDPackType(rawValue:try readUInt16())!; 131 | pixMapInfo.packSize = Int(try readUInt32()); 132 | pixMapInfo.resolution = try readResolution(); 133 | pixMapInfo.pixelType = Int(try readUInt16()); 134 | pixMapInfo.pixelSize = Int(try readUInt16()); 135 | pixMapInfo.cmpCount = Int(try readUInt16()); 136 | pixMapInfo.cmpSize = Int(try readUInt16()); 137 | pixMapInfo.planeByte = Int64(try readUInt32()); 138 | pixMapInfo.clutId = try readInt32(); 139 | pixMapInfo.clutSeed = try readType(); 140 | return pixMapInfo; 141 | } 142 | } 143 | 144 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawComment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawComment.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 20.03.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | /// ---------------- 11 | /// Comment operations 12 | /// ---------------- 13 | 14 | enum CommentType : UInt16, CaseIterable { 15 | case groupBegin = 0; 16 | case groupEnd = 1; 17 | case proprietary = 100; 18 | case macDrawBegin = 130; 19 | case macDrawEnd = 131; 20 | case groupedBegin = 140 ; 21 | case groupedEnd = 141 ; 22 | case bitmapBegin = 142 ; 23 | case bitmapEnd = 143 ; 24 | case textBegin = 150; 25 | case textEnd = 151; 26 | case stringBegin = 152 ; 27 | case stringEnd = 153; 28 | case textCenter = 154; 29 | case lineLayoutOff = 155; 30 | case lineLayoutOn = 156; 31 | case lineLayoutClient = 157; 32 | case polyBegin = 160; 33 | case polyEnd = 161; 34 | case polyCurve = 162; 35 | case polyIgnore = 163; 36 | case polySmooth = 164; 37 | case polyClose = 165; 38 | case arrow1 = 170 ; 39 | case arrow2 = 171 ; 40 | case arrow3 = 172 ; 41 | case arrowEnd = 173; 42 | case dashedLineBegin = 180; 43 | case dashedLineEnd = 181; 44 | case setLineWidth = 182; 45 | case postscriptStart = 190 ; 46 | case postscriptEnd = 191 ; 47 | case postscriptHandle = 192 ; 48 | case postscriptFile = 193 ; 49 | case textIsPostscript = 194 ; 50 | case resourcePostscript = 195; 51 | case postscriptBeginNoSave = 196 ; 52 | case setGrayLevel = 197; 53 | case rotateBegin = 200; 54 | case rotateEnd = 201; 55 | case rotateCenter = 202; 56 | case formsPrinting = 210; 57 | case endFormsPrinting = 211; 58 | case iccColorProfile = 224; // https://www.color.org/icc32.pdf 59 | case creator = 498 ; 60 | case scale = 499; 61 | case bitmapThinBegin = 1000; 62 | case bitmapThinEnd = 1001; 63 | case picLasso = 12345; 64 | case unknown = 0xffff; 65 | } 66 | 67 | /// Define pen and font comment payloads as operations, so they can be executed like opcodes. 68 | /// This allows some generic processing on the renderer code. 69 | 70 | struct LineWidthPayload : PenStateOperation { 71 | func execute(penState: inout PenState) { 72 | penState.penWidth = width; 73 | } 74 | let width: FixedPoint; 75 | } 76 | 77 | struct TextCenterPayload : FontStateOperation { 78 | func execute(fontState: inout QDFontState) { 79 | fontState.textCenter = center; 80 | } 81 | let center : QDDelta; 82 | } 83 | 84 | struct TextPictPayload : FontStateOperation { 85 | func execute(fontState: inout QDFontState) { 86 | fontState.textPictRecord = textPictRecord; 87 | } 88 | let textPictRecord : QDTextPictRecord; 89 | } 90 | 91 | struct PostScript : CustomStringConvertible { 92 | var description: String { 93 | return ""; 94 | } 95 | let source : String; 96 | } 97 | 98 | enum CommentPayload { 99 | case noPayload; 100 | case dataPayload(creator: MacTypeCode, data: Data); 101 | case postScriptPayLoad(postscript: PostScript); 102 | case fontStatePayload(fontOperation: FontStateOperation); 103 | case penStatePayload(penOperation: PenStateOperation); 104 | case polySmoothPayload(verb: PolygonOptions); 105 | case canvasPayload(canvas: CanvasPayload); 106 | case colorPayload(creator: MacTypeCode, color: QDColor); 107 | case unknownPayload(rawType: Int, data: Data); 108 | } 109 | 110 | /// See Technote 091: Optimizing for the LaserWriter—Picture Comments 111 | func readTextPictRecord(reader: QuickDrawDataReader) throws -> QDTextPictRecord { 112 | let raw_justification = try reader.readUInt8(); 113 | guard let justification = QDTextJustification(rawValue: raw_justification) else { 114 | throw QuickDrawError.quickDrawIoError(message: "Could not parse justification value \(raw_justification)"); 115 | } 116 | let rawFlip = try reader.readUInt8(); 117 | guard let flip = QDTextFlip(rawValue: rawFlip) else { 118 | throw QuickDrawError.quickDrawIoError(message: "Could not parse flip value \(rawFlip)"); 119 | } 120 | let angle1 = FixedPoint(try reader.readInt16()); 121 | let rawLineHeight = try reader.readUInt8(); 122 | guard let lineHeight = QDTextLineHeight(rawValue: rawLineHeight) else { 123 | throw QuickDrawError.quickDrawIoError(message: "Could not parse line height value \(rawLineHeight)"); 124 | } 125 | reader.skip(bytes: 1); // Reserved 126 | // MacDraw 1 comments are shorter 127 | if reader.remaining < 4 { 128 | return QDTextPictRecord(justification: justification, flip: flip, angle: angle1, lineHeight: lineHeight); 129 | } 130 | let angle2 = try reader.readFixed(); 131 | let angle = angle2 + angle1 132 | return QDTextPictRecord( 133 | justification: justification, flip: flip, angle: angle, lineHeight: lineHeight); 134 | } 135 | 136 | enum CanvasPayload { 137 | case canvasEnd; 138 | case canvasUnknown(code: UInt16, data: Data); 139 | } 140 | 141 | func parseCanvasPayload(creator: MacTypeCode, data: Data) throws -> CommentPayload { 142 | let reader = try QuickDrawDataReader(data: data, position: 0); 143 | let code = try reader.readUInt16(); 144 | switch code { 145 | case 0x44: 146 | return .canvasPayload(canvas: .canvasEnd); 147 | case 0xF7D3: 148 | reader.skip(bytes :10); 149 | let cmyk = try reader.readCMKY(); 150 | let name = try reader.readPascalString(); 151 | return .colorPayload(creator: creator, color: .cmyk(cmyk: cmyk, name: name)); 152 | default: 153 | return .canvasPayload(canvas: .canvasUnknown(code: code, data: try reader.readFullData())); 154 | } 155 | } 156 | 157 | func parseProprietaryPayload(creator: MacTypeCode, data: Data) throws -> CommentPayload { 158 | switch creator.description { 159 | case "drw2": 160 | return try parseCanvasPayload(creator: creator, data: data); 161 | default: 162 | return .dataPayload(creator: creator, data: data); 163 | } 164 | } 165 | 166 | struct CommentOp : OpCode, CustomStringConvertible, CullableOpcode { 167 | 168 | mutating func load(reader: QuickDrawDataReader) throws { 169 | let value = try reader.readUInt16(); 170 | kind = CommentType(rawValue: value) ?? .unknown; 171 | let size = long_comment ? Data.Index(try reader.readUInt16()) : Data.Index(0); 172 | switch (kind, size) { 173 | case (.proprietary, let size) where size > 4: 174 | let creator = try reader.readType(); 175 | let data = try reader.readData(bytes: size - 4); 176 | payload = try parseProprietaryPayload(creator: creator, data: data); 177 | case (.postscriptBeginNoSave, _), 178 | (.postscriptStart, _), 179 | (.postscriptFile, _), 180 | (.postscriptHandle, _): 181 | let postscript = try reader.readPostScript(bytes: size); 182 | payload = .postScriptPayLoad(postscript : postscript); 183 | case (.textBegin, let size) where size > 0: 184 | let subreader = try reader.subReader(bytes: size); 185 | let fontOp = TextPictPayload(textPictRecord: try readTextPictRecord(reader: subreader)); 186 | payload = .fontStatePayload(fontOperation: fontOp); 187 | case (.setLineWidth, let size) where size > 0: 188 | let subreader = try reader.subReader(bytes: size); 189 | let point = try subreader.readPoint(); 190 | let penOp = LineWidthPayload(width: point.vertical / point.horizontal); 191 | payload = .penStatePayload(penOperation: penOp); 192 | case (.textCenter, let size) where size > 0: 193 | let subreader = try reader.subReader(bytes: size); 194 | // readDelta assumes integer, here we want to read fixed points. 195 | let v = try subreader.readFixed(); 196 | let h = try subreader.readFixed(); 197 | let fontOp = TextCenterPayload(center: QDDelta(dv: v, dh: h)); 198 | payload = .fontStatePayload(fontOperation: fontOp); 199 | case (.polySmooth, 1): 200 | var verb = PolygonOptions(rawValue: try reader.readUInt8()); 201 | verb.insert(.smooth); 202 | payload = .polySmoothPayload(verb: verb); 203 | case (_, 0): 204 | payload = .noPayload; 205 | case (.unknown, let size) where size > 0: 206 | payload = .unknownPayload(rawType: Int(value), data: try reader.readData(bytes: size)); 207 | case (_, let size) where size > 0: 208 | let creator = try MacTypeCode(fromString: "APPL"); 209 | payload = .dataPayload(creator: creator, data: try reader.readData(bytes: size)); 210 | default: 211 | payload = .unknownPayload(rawType: Int(value), data: try reader.readData(bytes: size)); 212 | } 213 | } 214 | 215 | var description: String { 216 | return "CommentOp \(kind): [\(payload)]" 217 | } 218 | 219 | var canCull: Bool { 220 | switch (kind, payload) { 221 | case (_, .postScriptPayLoad): return true; 222 | case (.postscriptEnd, _): return true; 223 | default: 224 | return false; 225 | } 226 | } 227 | 228 | let long_comment : Bool; 229 | var kind : CommentType = .unknown; 230 | var payload : CommentPayload = CommentPayload.noPayload; 231 | } 232 | 233 | extension QuickDrawDataReader { 234 | // PostScript is encoded as pure ASCII text. 235 | func readPostScript(bytes: Data.Index) throws -> PostScript { 236 | let data = try readUInt8(bytes: bytes); 237 | guard let str = String(bytes:data, encoding: String.Encoding.ascii) else { 238 | throw QuickDrawError.quickDrawIoError(message: "Failed decoding PostScript"); 239 | } 240 | return PostScript(source: str); 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawErrors.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 02.01.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum QuickDrawError: Error { 11 | case quickDrawIoError (message: String); 12 | case unknownOpcodeError (opcode: UInt16); 13 | case unknownQuickDrawVersionError (version: Int); 14 | case corruptColorTableError (message: String); 15 | case missingColorTableError; 16 | case invalidStr32(length: Int); 17 | case invalidClutError(clut: QDColorTable); 18 | case corruptPackbitLine(row: Int, expectedLength : Data.Index, actualLength: Data.Index); 19 | case corruptRegion(boundingBox: QDRect); 20 | case renderingError (message: String); 21 | case wrongComponentNumber(componentNumber : Int); 22 | case unsupportedVerb(verb: QDVerb); 23 | case unsupportedQD1Color(colorCode: UInt32); 24 | case corruptPayload(message: String); 25 | case invalidFract(message: String); 26 | case invalidPhotoShopDepth(depth: Int); 27 | case invalidCommentPayload(payload: CommentPayload); 28 | case invalidReservedSize(reservedType: ReservedOpType); 29 | case corruptRegionLine(line: Int); 30 | case missingDestinationRect(message: String); 31 | case unsupportedBlend(fg: QDColor, bg: QDColor); 32 | case cannotConvertToRGB(color: QDColor); 33 | } 34 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawParser.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 02.01.2024. 6 | // 7 | 8 | import os 9 | import Foundation 10 | 11 | /// Class that decodes a stream of QuickDraw opcodes. 12 | class QDParser { 13 | 14 | /// Setup the parser with the content of the file. 15 | /// No parsing will occur until `parse`is called. 16 | /// - Parameter contentsOf: url from where the data will be retrieved. 17 | public init(contentsOf: URL) throws { 18 | let options = Data.ReadingOptions(); 19 | let data = try Data(contentsOf: contentsOf, options: options); 20 | try dataReader = QuickDrawDataReader(data: data); 21 | } 22 | 23 | /// Setup the parser with some raw data. 24 | /// Note that the parser assumes the actual data starts at offset 512. 25 | /// - Parameter data: data containing the QuickDraw opcodes. 26 | public init(data : Data ) throws { 27 | try dataReader = QuickDrawDataReader(data: data); 28 | } 29 | 30 | /// Decode a single opcode 31 | /// - Parameter opcode: numerical opcode identifier 32 | /// - Returns: an OpCode instance 33 | func decode(opcode: UInt16) throws -> OpCode { 34 | switch opcode { 35 | case 0x00: return NoOp(); 36 | case 0x01: return RegionOp(same: false, verb: QDVerb.clip); 37 | case 0x03: return FontOp(longOp: false); 38 | case 0x04: return FontStyleOp(); 39 | case 0x05: return TextModeOp(); 40 | case 0x06: return SpaceExtraOp() 41 | case 0x07: return PenSizeOp(); 42 | case 0x08: return PenModeOp(); 43 | case 0x09: return PatternOp(verb: QDVerb.frame); 44 | case 0x0A: return PatternOp(verb: QDVerb.fill); 45 | case 0x0B: return OvalSizeOp(); 46 | case 0x0C: return OriginOp(); 47 | case 0x0D: return FontSizeOp(); 48 | case 0x0E: return ColorOp(rgb:false, selection: QDColorSelection.foreground); 49 | case 0x0F: return ColorOp(rgb:false, selection: QDColorSelection.background); 50 | case 0x10: return TextRatioOp(); 51 | case 0x11, 0x02FF: 52 | return VersionOp(); 53 | // case 0x12: return ColorPattern op 54 | case 0x15: return PnLocHFracOp(); 55 | case 0x16: return SpaceExtraOp(); 56 | // $0017, $0018, $0019 are reserved with no defined arg size 57 | case 0x1A: return ColorOp(rgb: true, selection: QDColorSelection.foreground); 58 | case 0x1B: return ColorOp(rgb: true, selection: QDColorSelection.background); 59 | case 0x1D: return ColorOp(rgb: true, selection: QDColorSelection.highlight); 60 | case 0x1E: return DefHiliteOp(); 61 | case 0x1F: return ColorOp(rgb: true, selection: QDColorSelection.operations); 62 | case 0x20: return LineOp(short: false, from:false); 63 | case 0x21: return LineOp(short: false, from:true); 64 | case 0x22: return LineOp(short: true, from: false); 65 | case 0x23: return LineOp(short: true, from: true); 66 | case 0x24...0x27: return ReservedOp(reservedType: .readLength(bytes: 2)); 67 | case 0x28: return LongTextOp(); 68 | case 0x29: return DHDVTextOp(readDh: true, readDv: false); 69 | case 0x2a: return DHDVTextOp(readDh: false, readDv: true); 70 | case 0x2b: return DHDVTextOp(readDh: true , readDv: true); 71 | case 0x2c: return FontOp(longOp: true); 72 | case 0x2e: return GlyphStateOp(); 73 | case 0x30...0x34: 74 | return RectOp(same: false, verb: QDVerb(rawValue: opcode - 0x30)!); 75 | case 0x35...0x37: 76 | return ReservedOp(reservedType: .fixedLength(bytes: 8)); 77 | case 0x38...0x3C: 78 | return RectOp(same: true, verb: QDVerb(rawValue: opcode - 0x38)!); 79 | case 0x3D...0x3F: 80 | return ReservedOp(reservedType: .fixedLength(bytes: 8)); 81 | case 0x40...0x44: 82 | return RoundRectOp(same: false, verb: QDVerb(rawValue: opcode - 0x40)!); 83 | case 0x45...0x47: 84 | return ReservedOp(reservedType: .fixedLength(bytes: 8)); 85 | case 0x48...0x4C: 86 | return RoundRectOp(same: true, verb: QDVerb(rawValue: opcode - 0x48)!); 87 | case 0x4D...0x4F: 88 | return ReservedOp(reservedType: .fixedLength(bytes: 0)); 89 | case 0x50...0x54: 90 | return OvalOp(same: false, verb: QDVerb(rawValue: opcode - 0x50)!); 91 | case 0x58...0x5C: 92 | return OvalOp(same: true, verb: QDVerb(rawValue: opcode - 0x58)!); 93 | case 0x60...0x64: 94 | return ArcOp(same: false, verb: QDVerb(rawValue: opcode - 0x60)!); 95 | case 0x65...0x67: 96 | return ReservedOp(reservedType: .fixedLength(bytes: 12)); 97 | case 0x68...0x6C: 98 | return ArcOp(same: true, verb: QDVerb(rawValue: opcode - 0x68)!); 99 | case 0x6D...0x6F: 100 | return ReservedOp(reservedType: .fixedLength(bytes: 4)); 101 | case 0x70...0x74: 102 | return PolygonOp(same: false, verb: QDVerb(rawValue: opcode - 0x70)!); 103 | case 0x80...0x84: 104 | return RegionOp(same: false, verb: QDVerb(rawValue: opcode - 0x80)!); 105 | case 0x85...0x87: 106 | return RegionOp(same: false, verb: .ignore); 107 | case 0x90: return BitRectOpcode(isPacked: false); 108 | case 0x98: return BitRectOpcode(isPacked: true); 109 | case 0x9A: return DirectBitOpcode(); 110 | case 0xA0: return CommentOp(long_comment:false); 111 | case 0xA1: return CommentOp(long_comment:true); 112 | case 0xA2...0xAF: return ReservedOp(reservedType: .readLength(bytes: 2)); 113 | case 0xB0...0xCF: return ReservedOp(reservedType: .fixedLength(bytes: 0)); 114 | case 0xD0...0xFE: return ReservedOp(reservedType: .readLength(bytes: 4)); 115 | case 0xFF: return EndOp(); 116 | case 0x0100...0x1ff: return ReservedOp(reservedType: .fixedLength(bytes: 2)); 117 | case 0x0200...0x2fe: return ReservedOp(reservedType: .fixedLength(bytes: 4)); 118 | case 0x0300...0x0bff: 119 | return ReservedOp(reservedType: .fixedLength(bytes: Int(opcode) / 0x80)); 120 | case 0x0c00: return Version2HeaderOp(); 121 | case 0x0c01...0x7fff: 122 | return ReservedOp(reservedType: .fixedLength(bytes: Int(opcode) / 0x80)); 123 | case 0x8000...0x80ff: 124 | return ReservedOp(reservedType: .fixedLength(bytes: 0)); 125 | case 0x8100...0x81ff: 126 | return ReservedOp(reservedType: .readLength(bytes: 4)); 127 | case 0x8200: return QuickTimeOpcode(); 128 | case 0x8202...0xffff: 129 | return ReservedOp(reservedType: .readLength(bytes: 4)); 130 | case 0xFFFF : return ReservedOp(reservedType: .readLength(bytes: 4)); 131 | default: 132 | throw QuickDrawError.unknownOpcodeError(opcode:opcode); 133 | } 134 | } 135 | 136 | /// Parse one opcode. 137 | /// - Parameters: 138 | /// - picture: the picture to execute picture operations into. 139 | /// - Returns: true if futher opcodes should be read. 140 | func parseOne(picture: inout QDPicture) throws -> Bool { 141 | 142 | let codeValue = try dataReader.readOpcode(version: picture.version); 143 | var opcode = try decode(opcode: codeValue); 144 | try opcode.load(reader: dataReader); 145 | if let picture_operation = opcode as? PictureOperation { 146 | picture_operation.execute(picture: &picture); 147 | } 148 | 149 | picture.opcodes.append(opcode); 150 | guard !(opcode is EndOp) else { 151 | return false; 152 | } 153 | return true; 154 | } 155 | 156 | /// Parse the actual QuickDraw picture 157 | /// - Returns: a picture object that can be rendered. 158 | public func parse() throws -> QDPicture { 159 | let startTime = CFAbsoluteTimeGetCurrent(); 160 | // Parse v1 header. 161 | let size = Int(try dataReader.readUInt16()); 162 | let frame = try dataReader.readRect(); 163 | // Create picture object 164 | var picture = QDPicture(size: size, frame:frame, filename: dataReader.filename); 165 | do { 166 | while (try parseOne(picture: &picture)) {} 167 | } catch { 168 | let message = String(localized: "Failed parsing QuickDraw file"); 169 | logger.log(level: .error, "\(message): \(error)"); 170 | } 171 | let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime; 172 | let name = picture.filename ?? "" 173 | logger.log(level: .debug, "Picture \(name) parsed in : \(timeElapsed) seconds"); 174 | return picture 175 | } 176 | 177 | var dataReader: QuickDrawDataReader; 178 | let logger : Logger = Logger(subsystem: "net.codiferes.wiesmann.QuickDraw", category: "parser"); 179 | 180 | var filename : String? { 181 | set (name) { 182 | dataReader.filename = name; 183 | } 184 | get { 185 | return dataReader.filename 186 | } 187 | } 188 | } 189 | 190 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawPattern.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawPattern.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 10.03.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Black and white pattern (8×8 pixels) 11 | struct QDPattern : Equatable, PixMapMetadata, RawRepresentable { 12 | 13 | init(rawValue: UInt64) { 14 | self.rawValue = rawValue; 15 | } 16 | 17 | init(bytes: [UInt8]) { 18 | var buffer : UInt64 = 0; 19 | for v in bytes { 20 | buffer = buffer << 8 | UInt64(v); 21 | } 22 | self.rawValue = buffer; 23 | } 24 | 25 | let rowBytes : Int = 1; 26 | let cmpSize: Int = 1; 27 | let pixelSize: Int = 1; 28 | let rawValue: UInt64; 29 | let dimensions = QDDelta(dv: FixedPoint(8), dh: FixedPoint(8)); 30 | let clut: QDColorTable? = nil; // Color table is only known at runtime. 31 | var description: String { 32 | return "Pat: \(bytes): \(isShade)"; 33 | } 34 | 35 | var bytes : [UInt8] { 36 | return byteArrayBE(from: rawValue); 37 | } 38 | 39 | /// Should the pattern represent a shade of color, i.e. the pattern was used for dither. 40 | public var isShade : Bool { 41 | return [ 42 | QDPattern.black, QDPattern.white, 43 | QDPattern.gray, QDPattern.darkGray, 44 | QDPattern.lightGray, QDPattern.batmanGray 45 | ].contains(where: {$0 == self} ); 46 | } 47 | 48 | /// Scalar intensity of the pattern, going from 0 (white) to 1.0 (black). 49 | var intensity : Double { 50 | let total = rawValue.nonzeroBitCount; 51 | return Double(total) / Double(UInt64.bitWidth); 52 | } 53 | 54 | // Blend fgColor and bgColor using the intensity of this pattern. 55 | func blendColors(fg: QDColor, bg: QDColor) throws -> QDColor { 56 | if intensity == 1.0 { 57 | return fg; 58 | } 59 | if intensity == 0.0 { 60 | return bg; 61 | } 62 | return .blend(colorA: fg, colorB: bg, weight: intensity); 63 | } 64 | 65 | static let black = QDPattern(bytes:[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); 66 | static let white = QDPattern(bytes:[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); 67 | static let gray = QDPattern(bytes:[0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55]); 68 | static let darkGray = QDPattern(bytes:[0x88, 0x00, 0x22, 0x00, 0x88, 0x00, 0x22, 0x00]); 69 | static let lightGray = QDPattern(bytes:[0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77]); 70 | static let batmanGray = QDPattern(bytes: [0x88, 0x00, 0x22, 0x88, 0x00, 0x22]); 71 | } 72 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawPoint.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 17.04.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Point in QuickDraw space. 11 | struct QDPoint : CustomStringConvertible, Equatable { 12 | 13 | init (vertical: FixedPoint, horizontal: FixedPoint) { 14 | self.vertical = vertical; 15 | self.horizontal = horizontal; 16 | } 17 | 18 | public init (vertical: T, horizontal: T) { 19 | self.init(vertical: FixedPoint(vertical), horizontal: FixedPoint(horizontal)); 20 | } 21 | 22 | public var description: String { 23 | return "<→\(horizontal),↓\(vertical)>"; 24 | } 25 | 26 | static func + (point: QDPoint, delta: QDDelta) -> QDPoint { 27 | let vertical = point.vertical + delta.dv; 28 | let horizontal = point.horizontal + delta.dh; 29 | return QDPoint(vertical: vertical, horizontal: horizontal); 30 | } 31 | 32 | static func - (point: QDPoint, delta: QDDelta) -> QDPoint { 33 | return point + (-delta); 34 | } 35 | 36 | static func - (p1: QDPoint, p2: QDPoint) -> QDDelta { 37 | let dv = p1.vertical - p2.vertical; 38 | let dh = p1.horizontal - p2.horizontal; 39 | return QDDelta(dv: dv, dh: dh); 40 | } 41 | 42 | let vertical: FixedPoint; 43 | let horizontal: FixedPoint; 44 | 45 | static let zero = QDPoint( 46 | vertical: FixedPoint.zero, horizontal: FixedPoint.zero); 47 | 48 | } 49 | 50 | /// Relative position in Quickdraw space, functionally, this is the same as a point, but we distinguish 51 | /// as adding deltas make sense, adding points does not. 52 | struct QDDelta : CustomStringConvertible, Equatable, AdditiveArithmetic { 53 | 54 | init(dv : FixedPoint, dh : FixedPoint) { 55 | self.dv = dv; 56 | self.dh = dh; 57 | } 58 | 59 | init (dv : T, dh : T) { 60 | self.dv = FixedPoint(dv); 61 | self.dh = FixedPoint(dh); 62 | } 63 | 64 | public var description: String { 65 | return "<∂→\(dh),∂↓\(dv)>"; 66 | } 67 | 68 | let dh: FixedPoint; 69 | let dv: FixedPoint; 70 | 71 | static func + (a: QDDelta, b: QDDelta) -> QDDelta { 72 | return QDDelta(dv: a.dv + b.dv, dh: a.dh + b.dh); 73 | } 74 | 75 | static func - (lhs: QDDelta, rhs: QDDelta) -> QDDelta { 76 | return lhs + (-rhs); 77 | } 78 | 79 | static prefix func -(d: QDDelta) -> QDDelta { 80 | return QDDelta(dv: -d.dv, dh: -d.dh); 81 | } 82 | 83 | static let zero : QDDelta = QDDelta(dv: Int8(0), dh: Int8(0)); 84 | } 85 | 86 | extension QuickDrawDataReader { 87 | 88 | func readPoint() throws -> QDPoint { 89 | let v = FixedPoint(try readInt16()); 90 | let h = FixedPoint(try readInt16()); 91 | return QDPoint(vertical: v, horizontal: h); 92 | } 93 | 94 | func readDelta() throws -> QDDelta { 95 | let h = try readInt16(); 96 | let v = try readInt16(); 97 | return QDDelta(dv:v, dh:h); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawPolygon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawPolygon.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 17.04.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PolygonOptions : OptionSet, CustomStringConvertible { 11 | var rawValue: UInt8; 12 | static let frame = PolygonOptions(rawValue: 1 << 0); 13 | static let fill = PolygonOptions(rawValue: 1 << 1); 14 | static let close = PolygonOptions(rawValue: 1 << 2); 15 | static let smooth = PolygonOptions(rawValue: 1 << 3); 16 | static let empty = PolygonOptions([]); 17 | 18 | var description: String { 19 | var result : String = "PolySmoothVerb " 20 | if contains(.frame) {result += " frame"} 21 | if contains(.fill) {result += " fill"} 22 | if contains(.close) {result += " close"} 23 | if contains(.smooth) {result += " smooth"} 24 | return result; 25 | } 26 | } 27 | 28 | class QDPolygon { 29 | 30 | init(boundingBox: QDRect?, points: [QDPoint]) { 31 | self.boundingBox = boundingBox; 32 | self.points = points; 33 | self.options = PolygonOptions.empty; 34 | } 35 | 36 | convenience init() { 37 | self.init(boundingBox: nil, points: []); 38 | } 39 | 40 | var boundingBox : QDRect?; 41 | var points : [QDPoint]; 42 | var options : PolygonOptions; 43 | 44 | func AddLine(line : [QDPoint]) { 45 | if points.isEmpty { 46 | self.points = line; 47 | return; 48 | } 49 | if line.first == points.last { 50 | points.removeLast(); 51 | } 52 | points.append(contentsOf: line); 53 | } 54 | } 55 | 56 | extension QuickDrawDataReader { 57 | func readPoly() throws -> QDPolygon { 58 | let raw_size = try readUInt16(); 59 | let boundingBox = try readRect(); 60 | 61 | let pointNumber = (raw_size - 10) / 4; 62 | var points : [QDPoint] = []; 63 | if pointNumber > 0 { 64 | for _ in 1...pointNumber { 65 | points.append(try readPoint()); 66 | } 67 | } 68 | return QDPolygon(boundingBox: boundingBox, points: points); 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawPort.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawPort.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 21.03.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | struct QDPortBits : OptionSet { 11 | let rawValue: UInt16; 12 | static let textEnable = QDPortBits(rawValue: 1 << 0); 13 | static let lineEnable = QDPortBits(rawValue: 1 << 1); 14 | static let rectEnable = QDPortBits(rawValue: 1 << 3); 15 | static let rRectEnable = QDPortBits(rawValue: 1 << 4); 16 | static let ovalEnable = QDPortBits(rawValue: 1 << 5); 17 | static let arcEnable = QDPortBits(rawValue: 1 << 6); 18 | static let polyEnable = QDPortBits(rawValue: 1 << 7); 19 | static let rgnEnable = QDPortBits(rawValue: 1 << 8); 20 | static let bitsEnable = QDPortBits(rawValue: 1 << 9); 21 | static let commentsEnable = QDPortBits(rawValue: 1 << 10); 22 | static let txtMeasEnable = QDPortBits(rawValue: 1 << 11); 23 | static let clipEnable = QDPortBits(rawValue: 1 << 12); 24 | static let quickTimeEnable = QDPortBits(rawValue: 1 << 13); 25 | static let defaultState = QDPortBits([ 26 | textEnable, lineEnable, rectEnable, rRectEnable, ovalEnable, 27 | arcEnable, polyEnable, rgnEnable, bitsEnable, commentsEnable, 28 | txtMeasEnable, clipEnable, quickTimeEnable 29 | ]); 30 | } 31 | 32 | protocol QuickDrawPort { 33 | // Bottleneck functions 34 | func stdPoly(polygon: QDPolygon, verb: QDVerb) throws -> Void; 35 | func stdRect(rect : QDRect, verb: QDVerb) throws -> Void; 36 | func stdOval(rect : QDRect, verb: QDVerb) throws -> Void; 37 | func stdText(text : String) throws -> Void; 38 | func stdRegion(region : QDRegion, verb: QDVerb) throws -> Void; 39 | func stdRoundRect(rect : QDRect, verb: QDVerb) throws -> Void; 40 | func stdLine(points: [QDPoint]) throws -> Void; 41 | func stdArc(rect: QDRect, startAngle : Int16, angle: Int16, verb: QDVerb) throws -> Void; 42 | 43 | // Port state 44 | var penState : PenState {get set }; 45 | var fontState : QDFontState {get set}; 46 | var portBits : QDPortBits {get set}; 47 | // Last values 48 | var lastPoly : QDPolygon { get set}; 49 | var lastRect : QDRect {get set}; 50 | var lastRegion : QDRegion { get set}; 51 | } 52 | 53 | 54 | protocol QuickDrawRenderer { 55 | func execute(opcode: OpCode) throws -> Void; 56 | func execute(picture: QDPicture, zoom: Double) throws -> Void; 57 | /// Bottleneck functions 58 | } 59 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawReader.swift 3 | // QuickDrawKit 4 | // 5 | // Created by Matthias Wiesmann on 20.12.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | 12 | /// Class that handles reading from the data object. 13 | /// Handles deserialisation of basic QuickDraw types. 14 | /// Reading of high-level objects (points, rectangles, regions), is handled in extensions along the type definitions. 15 | class QuickDrawDataReader { 16 | 17 | /// Initializes a reader from a data object. 18 | /// - Parameters: 19 | /// - data: object containing the QuickDraw pict data. 20 | /// - position: offset in the data object where reading should start, typically 512 as this is the standart offset. 21 | init(data: Data, position: Data.Index=512) throws { 22 | guard position <= data.count else { 23 | throw QuickDrawError.quickDrawIoError(message: "Initial position \(position) beyond \(data.count)"); 24 | } 25 | guard position >= 0 else { 26 | throw QuickDrawError.quickDrawIoError(message:"Initial positon \(position) is negative"); 27 | } 28 | self.data = data; 29 | self.position = position; 30 | } 31 | 32 | func peekUInt8() throws -> UInt8 { 33 | guard position < data.count else { 34 | throw QuickDrawError.quickDrawIoError(message:"Peek at \(position) beyond \(data.count)"); 35 | } 36 | return data[position]; 37 | } 38 | 39 | func readUInt8() throws -> UInt8 { 40 | let value = try peekUInt8(); 41 | position += 1; 42 | return value; 43 | } 44 | 45 | func readBool() throws -> Bool { 46 | let v = try readUInt8(); 47 | return v > 0; 48 | } 49 | 50 | func readUInt8(bytes: Data.Index) throws -> [UInt8] { 51 | let subdata = try readData(bytes: bytes); 52 | return subdata.bytes; 53 | } 54 | 55 | func readSlice(bytes: Data.Index) throws -> ArraySlice { 56 | let start = self.position; 57 | self.position += bytes; 58 | let end = self.position; 59 | guard end <= data.count else { 60 | throw QuickDrawError.quickDrawIoError(message:"ReadSlice \(bytes):\(end) beyond \(data.count)"); 61 | } 62 | return data.bytes[start.. Data { 66 | guard bytes >= 0 else { 67 | throw QuickDrawError.quickDrawIoError(message: "Negative amount of bytes \(bytes)"); 68 | } 69 | let end = position + bytes; 70 | guard end <= data.count else { 71 | throw QuickDrawError.quickDrawIoError(message:"Read \(bytes):\(end) beyond \(data.count)"); 72 | } 73 | let result = data.subdata(in: position.. Data { 79 | return try readData(bytes: remaining); 80 | } 81 | 82 | func subReader(bytes: Data.Index) throws -> QuickDrawDataReader { 83 | let data = try readData(bytes : bytes); 84 | let sub = try QuickDrawDataReader(data: data, position: 0); 85 | sub.filename = self.filename; 86 | return sub; 87 | } 88 | 89 | func readString(bytes: Data.Index) throws -> String { 90 | let data = try readUInt8(bytes: bytes); 91 | guard let str = String(bytes:data, encoding: String.Encoding.macOSRoman) else { 92 | throw QuickDrawError.quickDrawIoError(message: String(localized: "Failed reading string")); 93 | } 94 | return str; 95 | } 96 | 97 | /// Read a fixed length (31 bytes) pascal string 98 | /// - Returns: a string , with a maximum 31 characters. 99 | func readStr31() throws -> String { 100 | let length = Data.Index(try readUInt8()); 101 | let tail = 31 - length ; 102 | guard tail >= 0 else { 103 | throw QuickDrawError.invalidStr32(length: length); 104 | } 105 | let result = try readString(bytes:length); 106 | skip(bytes: tail); 107 | return result; 108 | } 109 | 110 | /// Read a Pascal string (length byte, followed by text bytes). 111 | /// - Returns: a string, with a maximum of 255 characters. 112 | func readPascalString() throws -> String { 113 | let length = Data.Index(try readUInt8()); 114 | return try readString(bytes:length); 115 | } 116 | 117 | func readInt8() throws -> Int8 { 118 | return try Int8(bitPattern: readUInt8()); 119 | } 120 | 121 | func readUInt16() throws -> UInt16 { 122 | let bytes = try readSlice(bytes: 2); 123 | return toScalar(bytes: bytes); 124 | } 125 | 126 | func readUInt16(bytes: Data.Index) throws -> [UInt16] { 127 | let num = bytes / 2; 128 | let raw = try readSlice(bytes: num * 2); 129 | var result :[UInt16] = []; 130 | result.reserveCapacity(num); 131 | for index in 0..<(num) { 132 | let p = index * 2 + raw.startIndex; 133 | let s = raw[p..

UInt16 { 141 | let v = try readUInt16(); 142 | return v.byteSwapped; 143 | } 144 | 145 | func readUInt32LE() throws -> UInt32 { 146 | let v = try readUInt32(); 147 | return v.byteSwapped; 148 | } 149 | 150 | func readInt16() throws -> Int16 { 151 | return Int16(bitPattern: try readUInt16()); 152 | } 153 | 154 | func readUInt32() throws -> UInt32 { 155 | let bytes = try readSlice(bytes: 4); 156 | return toScalar(bytes: bytes); 157 | } 158 | 159 | func readInt32() throws -> Int32 { 160 | return Int32(bitPattern: try readUInt32()); 161 | } 162 | 163 | func readUInt64() throws -> UInt64 { 164 | let bytes = try readSlice(bytes: 8); 165 | return toScalar(bytes: bytes); 166 | } 167 | 168 | func readOpcode(version: Int) throws -> UInt16 { 169 | switch version { 170 | case 1: 171 | let opcode = try readUInt8() 172 | return UInt16(opcode); 173 | case 2: 174 | if (position % 2) == 1 { 175 | position+=1; 176 | } 177 | let opcode = try readUInt16() 178 | return opcode; 179 | default: 180 | throw QuickDrawError.unknownQuickDrawVersionError(version:version); 181 | } 182 | } 183 | 184 | func skip(bytes: Data.Index) -> Void { 185 | position += bytes; 186 | } 187 | 188 | var remaining : Int { 189 | return data.count - position; 190 | } 191 | 192 | var position: Data.Index; 193 | var data: Data; 194 | var filename : String?; 195 | } 196 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawRect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawRect.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 17.04.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Rectangle 11 | struct QDRect : CustomStringConvertible, Equatable { 12 | 13 | init(topLeft: QDPoint, bottomRight: QDPoint) { 14 | self.topLeft = topLeft; 15 | self.bottomRight = bottomRight; 16 | } 17 | 18 | init(topLeft: QDPoint, dimension : QDDelta) { 19 | self.topLeft = topLeft; 20 | self.bottomRight = topLeft + dimension; 21 | } 22 | 23 | public var description: String { 24 | return "▭ ⌜\(topLeft),\(bottomRight)⌟" 25 | } 26 | 27 | let topLeft: QDPoint; 28 | let bottomRight: QDPoint; 29 | 30 | var dimensions : QDDelta { 31 | get { 32 | return bottomRight - topLeft; 33 | } 34 | } 35 | 36 | var center : QDPoint { 37 | get { 38 | let h = (topLeft.horizontal + bottomRight.horizontal) >> 1; 39 | let v = (topLeft.vertical + bottomRight.vertical) >> 1; 40 | return QDPoint(vertical: v, horizontal: h); 41 | } 42 | } 43 | 44 | var isEmpty : Bool { 45 | return topLeft == bottomRight; 46 | } 47 | 48 | static func + (rect: QDRect, delta: QDDelta) -> QDRect { 49 | let topleft = rect.topLeft + delta; 50 | let dimensions = rect.dimensions; 51 | return QDRect(topLeft: topleft, dimension: dimensions); 52 | } 53 | 54 | static let empty = QDRect(topLeft: QDPoint.zero, bottomRight: QDPoint.zero); 55 | } 56 | 57 | extension QuickDrawDataReader { 58 | func readRect() throws -> QDRect { 59 | let tl = try readPoint(); 60 | let br = try readPoint(); 61 | return QDRect(topLeft: tl, bottomRight: br); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawRegions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawRegions.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 02.03.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | /// This file contains all the code related to region processing. 11 | 12 | /// A single, raw line of the QuickDraw region. 13 | /// bitmap is one _byte_ per pixel. 14 | struct QDRegionLine { 15 | let lineNumber : Int; 16 | let bitmap : [UInt8]; 17 | } 18 | 19 | let QDRegionEndMark = 0x7fff; 20 | let QDRegionHeaderSize = UInt16(10); 21 | 22 | /// Decodes one line of the region data. 23 | /// - Parameters: 24 | /// - boundingBox: bounding box of the region 25 | /// - data: region data, as an array of shorts 26 | /// - index: position in the data, will be updated. 27 | /// - Returns: a decoded line (line number + byte array) 28 | func DecodeRegionLine(boundingBox: QDRect, data: [UInt16], index : inout Int) throws -> QDRegionLine? { 29 | var bitmap : [UInt8] = Array(repeating: 0, count: boundingBox.dimensions.dh.rounded); 30 | let lineNumber = Int(data[index]); 31 | if lineNumber == QDRegionEndMark { 32 | return nil; 33 | } 34 | index += 1 35 | while index < data.count { 36 | var start = Int(Int16(bitPattern: data[index])); 37 | index += 1; 38 | if (start == QDRegionEndMark) { 39 | return QDRegionLine(lineNumber: lineNumber, bitmap:bitmap); 40 | } 41 | var end = Int(data[index]); 42 | index += 1; 43 | if end == QDRegionEndMark { 44 | end = start; 45 | start = 0; 46 | } 47 | guard start <= end else { 48 | throw QuickDrawError.corruptRegionLine(line: lineNumber); 49 | } 50 | for i in start..= 0 && p < bitmap.count else { 53 | throw QuickDrawError.corruptRegionLine(line: lineNumber); 54 | } 55 | bitmap[p] = 0xff; 56 | } 57 | } 58 | return QDRegionLine(lineNumber: lineNumber, bitmap:bitmap); 59 | } 60 | 61 | /// Convert a line of pixels into a sequence of ranges. 62 | /// - Parameter line: pixels of one line, one byte per pixel 63 | /// - Returns: set of ranges of active (non zero) pixels. 64 | func BitLineToRanges(line: [UInt8]) -> Set> { 65 | var index = 0; 66 | var result = Set>(); 67 | while true { 68 | while index < line.count && line[index] == 0 { 69 | index += 1; 70 | } 71 | if index == line.count { 72 | return result; 73 | } 74 | let start = index; 75 | while index < line.count && line[index] != 0 { 76 | index += 1; 77 | } 78 | result.insert(start.. ([QDRect], [[UInt8]]) { 92 | /// Decode as bitmap 93 | let width = boundingBox.dimensions.dh.rounded; 94 | let height = boundingBox.dimensions.dv.rounded + 1; 95 | guard width > 0 && height > 0 else { 96 | throw QuickDrawError.corruptRegion(boundingBox: boundingBox); 97 | } 98 | let emptyLine : [UInt8] = Array(repeating: 0, count: width); 99 | var bitLines: [[UInt8]] = Array(repeating: emptyLine, count: height); 100 | var index : Int = 0; 101 | /// Decode the region lines. 102 | while index < data.count { 103 | let line = try DecodeRegionLine(boundingBox: boundingBox, data: data, index: &index); 104 | guard line != nil else { 105 | break; 106 | } 107 | let l = line!.lineNumber - boundingBox.topLeft.vertical.rounded; 108 | bitLines[l] = line!.bitmap; 109 | } 110 | /// Xor each line with the previous 111 | for y in 1.. 0 { 136 | result += " \(rects.count) rects"; 137 | } 138 | return result ; 139 | } 140 | 141 | var boundingBox: QDRect = QDRect.empty; 142 | var isRect : Bool { 143 | return rects.isEmpty; 144 | } 145 | 146 | static func forRect(rect: QDRect) -> QDRegion { 147 | return QDRegion(boundingBox: rect, rects:[], bitlines:[[]]); 148 | } 149 | 150 | static let empty = QDRegion( 151 | boundingBox: QDRect.empty, rects: [], bitlines: []); 152 | 153 | let rects : [QDRect]; 154 | 155 | func getRects() -> [QDRect] { 156 | if isRect { 157 | return [boundingBox]; 158 | } 159 | return rects; 160 | } 161 | 162 | let bitlines: [[UInt8]]; 163 | } 164 | 165 | extension QuickDrawDataReader { 166 | func readRegion() throws -> QDRegion { 167 | var len = UInt16(try readUInt16()); 168 | if len < QDRegionHeaderSize { 169 | len += QDRegionHeaderSize; 170 | } 171 | let rgnDataSize = Data.Index(len - QDRegionHeaderSize); 172 | let box = try readRect(); 173 | if rgnDataSize > 0 { 174 | let data = try readUInt16(bytes: rgnDataSize); 175 | let (rects, bitlines) = try DecodeRegionData(boundingBox: box, data: data); 176 | return QDRegion(boundingBox:box, rects:rects, bitlines: bitlines); 177 | } 178 | return QDRegion(boundingBox:box, rects: [], bitlines:[[]]); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawResolution.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawResolution.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 17.04.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Quickdraw picture resolution, in DPI. 11 | struct QDResolution : Equatable, CustomStringConvertible { 12 | let hRes : FixedPoint; 13 | let vRes : FixedPoint; 14 | 15 | public var description: String { 16 | return "\(hRes)×\(vRes)"; 17 | } 18 | 19 | /// Scale a delta as a function of the resolution, relative to the standard (72 DPI). 20 | /// - Parameters: 21 | /// - dim: dimension to scale 22 | /// - resolution: resolution description 23 | /// - Returns: scales dimension 24 | public static func ⨴ (dim : QDDelta, resolution: QDResolution) -> QDDelta { 25 | let h = dim.dh.value * defaultScalarResolution.value / resolution.hRes.value; 26 | let v = dim.dv.value * defaultScalarResolution.value / resolution.vRes.value; 27 | return QDDelta(dv: FixedPoint(v), dh: FixedPoint(h)); 28 | } 29 | 30 | /// Return a rectangle scaled for a given resolution 31 | /// - Parameters: 32 | /// - rect: rectangle to scale 33 | /// - resolution: resolution to use for scaling 34 | /// - Returns: a scaled rectangle. 35 | public static func ⨴ (rect: QDRect, resolution: QDResolution) -> QDRect { 36 | let d = rect.dimensions ⨴ resolution; 37 | return QDRect(topLeft: rect.topLeft, dimension: d); 38 | } 39 | 40 | static let defaultScalarResolution = FixedPoint(72); 41 | static let defaultResolution = QDResolution( 42 | hRes: defaultScalarResolution, vRes: defaultScalarResolution); 43 | static let zeroResolution = QDResolution(hRes: FixedPoint.zero, vRes: FixedPoint.zero); 44 | } 45 | 46 | extension QuickDrawDataReader { 47 | func readResolution() throws -> QDResolution { 48 | let hRes = try readFixed(); 49 | let vRes = try readFixed(); 50 | return QDResolution(hRes: hRes, vRes: vRes); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawText.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 17.04.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Text rendering options 11 | struct QDGlyphState : OptionSet { 12 | let rawValue: UInt8; 13 | static let outlinePreferred = QDGlyphState(rawValue: 1 << 0); 14 | static let preserveGlyphs = QDGlyphState(rawValue: 1 << 1); 15 | static let fractionalWidths = QDGlyphState(rawValue: 1 << 2); 16 | static let scalingDisabled = QDGlyphState(rawValue: 1 << 3); 17 | static let defaultState = QDGlyphState([]); 18 | } 19 | 20 | /// Various text related properties. 21 | class QDFontState { 22 | func getFontName() -> String? { 23 | if let name = self.fontName { 24 | return name; 25 | } 26 | /// List of classic fonts with their canonical IDs. 27 | switch fontId { 28 | case 2: return "New York"; 29 | case 3: return "Geneva"; 30 | case 4: return "Monaco"; 31 | case 5: return "Venice"; 32 | case 6: return "Venice"; 33 | case 7: return "Athens"; 34 | case 8: return "San Francisco"; 35 | case 9: return "Toronto"; 36 | case 11: return "Cairo"; 37 | case 12: return "Los Angeles"; 38 | case 20: return "Times"; 39 | case 21: return "Helvetica"; 40 | case 22: return "Courrier"; 41 | case 23: return "Symbol"; 42 | case 24: return "Mobile"; 43 | default: 44 | return nil; 45 | } // Switch 46 | } 47 | var fontId : Int = 0; 48 | var fontName : String?; 49 | var fontSize = FixedPoint(12); 50 | var fontMode : QuickDrawMode = QuickDrawMode.defaultMode; 51 | var location : QDPoint = QDPoint.zero; 52 | var fontStyle : QDFontStyle = QDFontStyle.defaultStyle; 53 | var glyphState : QDGlyphState = QDGlyphState.defaultState; 54 | var xRatio : FixedPoint = FixedPoint.one; 55 | var yRatio : FixedPoint = FixedPoint.one; 56 | var textCenter: QDDelta?; 57 | var textPictRecord : QDTextPictRecord?; 58 | var extraSpace : FixedPoint = FixedPoint.zero; 59 | } 60 | 61 | enum QDTextJustification : UInt8 { 62 | case justificationNone = 0; 63 | case justificationLeft = 1; 64 | case justificationCenter = 2; 65 | case justificationRight = 3; 66 | case justificationFull = 4; 67 | case justification5 = 5; // Found in MacDraw 1 68 | case justification6 = 6; // Found in MacDraw 1 69 | } 70 | 71 | enum QDTextFlip : UInt8 { 72 | case textFlipNone = 0; 73 | case textFlipHorizontal = 1; 74 | case textFlipVertical = 2; 75 | } 76 | 77 | enum QDTextLineHeight : UInt8 { 78 | case unknown = 0; 79 | case single = 1; 80 | case oneAndHalf = 2; 81 | case double = 3; 82 | case double2 = 4; 83 | } 84 | 85 | // Text annotation for text comments 86 | struct QDTextPictRecord { 87 | let justification : QDTextJustification; 88 | let flip : QDTextFlip; 89 | let angle : FixedPoint; 90 | let lineHeight : QDTextLineHeight; 91 | } 92 | 93 | struct QDFontStyle : OptionSet { 94 | let rawValue: UInt8; 95 | static let boldBit = QDFontStyle(rawValue: 1 << 0); 96 | static let italicBit = QDFontStyle(rawValue: 1 << 1); 97 | static let ulineBit = QDFontStyle(rawValue: 1 << 2); 98 | static let outlineBit = QDFontStyle(rawValue: 1 << 3); 99 | static let shadowBit = QDFontStyle(rawValue: 1 << 4); 100 | static let condenseBit = QDFontStyle(rawValue: 1 << 5); 101 | static let extendBit = QDFontStyle(rawValue: 1 << 6); 102 | 103 | static let defaultStyle = QDFontStyle([]); 104 | } 105 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawTypes.swift 3 | // QuickDrawKit 4 | // 5 | // Created by Matthias Wiesmann on 21.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | enum QDVerb : UInt16, CustomStringConvertible { 11 | var description: String { 12 | switch self { 13 | case .frame: return "frame"; 14 | case .paint: return "paint"; 15 | case .erase: return "erase"; 16 | case .fill: return "fill"; 17 | case .clip: return "clip"; 18 | case .ignore: return "ignore"; 19 | case .invert: return "invert"; 20 | } 21 | } 22 | 23 | case frame = 0; 24 | case paint = 1; 25 | case erase = 2; 26 | case invert = 3; 27 | case fill = 4; 28 | case clip = 50; 29 | case ignore = 0xFF; 30 | } 31 | 32 | enum QDColorSelection : UInt8 { 33 | case foreground = 0; 34 | case background = 1; 35 | case operations = 2; 36 | case highlight = 3; 37 | } 38 | 39 | 40 | // the 8 first transfer modes from QuickDraw.p 41 | // Patterns operation is bit 5. 42 | enum QuickDrawTransferMode : UInt16, CustomStringConvertible { 43 | public var description: String { 44 | return QuickDrawTransferMode.describeRaw(rawValue); 45 | } 46 | 47 | private static func describeRaw(_ rawValue : UInt16) -> String { 48 | if rawValue & 0x4 > 0 { 49 | return "!" + describeRaw(rawValue & 0x3); 50 | } 51 | switch rawValue % 4 { 52 | case 0 : return "copy"; 53 | case 1 : return "or"; 54 | case 2 : return "xor"; 55 | case 3 : return "bic"; 56 | default: 57 | assert(false); 58 | return "never"; 59 | } 60 | } 61 | 62 | case copyMode = 0; 63 | case orMode = 1; 64 | case xorMode = 2; 65 | case bicMode = 3; 66 | case notCopyMode = 4; 67 | case notOrMode = 5; 68 | case notXorMode = 6; 69 | case notBic = 7; 70 | } 71 | 72 | struct QuickDrawMode : RawRepresentable, CustomStringConvertible { 73 | 74 | let rawValue: UInt16; 75 | 76 | var mode : QuickDrawTransferMode { 77 | return QuickDrawTransferMode(rawValue: rawValue % 8)!; 78 | } 79 | 80 | var isPattern : Bool { 81 | rawValue & QuickDrawMode.patternMask != 0 82 | } 83 | 84 | var isDither: Bool { 85 | rawValue & QuickDrawMode.ditherMask != 0; 86 | } 87 | 88 | var description: String { 89 | var result = "[\(mode)"; 90 | if isPattern { 91 | result += " pattern"; 92 | } 93 | if isDither { 94 | result += " dither"; 95 | } 96 | result += "]"; 97 | return result; 98 | } 99 | 100 | static private let patternMask : UInt16 = 0x08; 101 | static private let ditherMask : UInt16 = 0x40; 102 | static let defaultMode : QuickDrawMode = QuickDrawMode(rawValue: 0); 103 | } 104 | 105 | /// Operator ⨴ is used for non commutative product between a structured type and a scalar or vector. 106 | precedencegroup ComparisonPrecedence { 107 | associativity: left 108 | higherThan: AdditionPrecedence 109 | } 110 | infix operator ⨴ : MultiplicationPrecedence 111 | 112 | 113 | /// All the state associated with drawing 114 | class PenState { 115 | var location : QDPoint = QDPoint.zero; 116 | var penSize: QDPoint = defaultPen; 117 | var mode: QuickDrawMode = QuickDrawMode.defaultMode; 118 | var fgColor : QDColor = QDColor.black; 119 | var bgColor : QDColor = QDColor.white; 120 | var opColor : QDColor = QDColor.black; 121 | var highlightColor : QDColor = .rgb(rgb: RGBColor(red: 0, green: 0, blue: 0xffff)); 122 | var drawPattern: QDPattern = QDPattern.black; 123 | var fillPattern: QDPattern = QDPattern.black; 124 | var ovalSize : QDDelta = QDDelta.zero; 125 | 126 | var drawColor : QDColor { 127 | get throws { 128 | return try drawPattern.blendColors(fg: fgColor, bg: bgColor); 129 | } 130 | } 131 | 132 | var fillColor : QDColor { 133 | get throws { 134 | return try fillPattern.blendColors(fg: fgColor, bg: bgColor); 135 | } 136 | } 137 | 138 | /// Pen width, assuming a square pen (height = width). 139 | var penWidth : FixedPoint { 140 | get { 141 | return (penSize.horizontal + penSize.vertical) >> 1; 142 | } 143 | set(width) { 144 | penSize = QDPoint(vertical: width, horizontal: width); 145 | } 146 | } 147 | 148 | static let defautPenWidth = FixedPoint.one; 149 | static let defaultPen = QDPoint(vertical: defautPenWidth, horizontal: defautPenWidth); 150 | } 151 | 152 | public class QDPicture : CustomStringConvertible { 153 | init(size: Int, frame:QDRect, filename: String?) { 154 | self.size = size; 155 | self.frame = frame; 156 | self.filename = filename; 157 | self.srcRect = frame; 158 | } 159 | 160 | let size: Int; 161 | var srcRect : QDRect; 162 | var frame: QDRect; 163 | var resolution : QDResolution = QDResolution.defaultResolution; 164 | var version: Int = 1; 165 | var opcodes: [OpCode] = []; 166 | var filename : String?; 167 | 168 | public var description : String { 169 | var result = "Picture size: \(size) bytes, version: \(version) "; 170 | if let name = filename { 171 | result += "filename: \(name) "; 172 | } 173 | result += "frame: \(frame) src: \(srcRect) @ \(resolution)\n"; 174 | result += "===========================\n"; 175 | for (index, opcode) in opcodes.enumerated() { 176 | result += "\(index): \(opcode)\n"; 177 | } 178 | result += "===========================\n"; 179 | return result; 180 | } 181 | } 182 | 183 | 184 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawViewer.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.pictures.read-only 8 | 9 | com.apple.security.cs.disable-library-validation 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.print 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawViewer.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "52A4598C-78BE-40B1-ACA7-5CCCCFD25AF2", 5 | "name" : "Test Scheme Action", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "targetForVariableExpansion" : { 13 | "containerPath" : "container:QuickDrawViewer.xcodeproj", 14 | "identifier" : "E59CDF852B42C0E600D7BB7B", 15 | "name" : "QuickDrawViewerUITests" 16 | } 17 | }, 18 | "testTargets" : [ 19 | { 20 | "parallelizable" : true, 21 | "target" : { 22 | "containerPath" : "container:QuickDrawViewer.xcodeproj", 23 | "identifier" : "E59CDF7B2B42C0E600D7BB7B", 24 | "name" : "QuickDrawViewerTests" 25 | } 26 | } 27 | ], 28 | "version" : 1 29 | } 30 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickDrawViewerRelease.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.assets.pictures.read-only 8 | 9 | com.apple.security.cs.disable-library-validation 10 | 11 | com.apple.security.files.downloads.read-only 12 | 13 | com.apple.security.files.user-selected.read-only 14 | 15 | com.apple.security.print 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /QuickDrawViewer/QuickTimeGraphics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickTimeGraphics.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 08.03.2024. 6 | // 7 | // Decode the QuickTime Graphics codec (SMC). 8 | // See https://wiki.multimedia.cx/index.php/Apple_SMC 9 | 10 | import Foundation 11 | 12 | enum QuickTimeGraphicsError : Error { 13 | case invalidBlockData(data: [UInt8]); 14 | case invalidCacheEntry(entry: [UInt8]); 15 | case unknownOpcode(opcode: UInt8); 16 | } 17 | 18 | class QuickTimeGraphicsColorCache { 19 | 20 | init(entrySize: Int) { 21 | self.entrySize = entrySize; 22 | let zeroEntry = [UInt8].init(repeating: 0, count: entrySize); 23 | self.entries = [[UInt8]].init(repeating: zeroEntry, count: 256); 24 | } 25 | 26 | func add(entry: [UInt8]) throws { 27 | guard entry.count == self.entrySize else { 28 | throw QuickTimeGraphicsError.invalidCacheEntry(entry: entry); 29 | } 30 | entries[pos] = entry; 31 | pos = (pos + 1) % 256; 32 | } 33 | 34 | func lookup(index: UInt8) -> [UInt8] { 35 | return entries[Int(index)]; 36 | } 37 | 38 | var entries : [[UInt8]]; 39 | 40 | let entrySize : Int; 41 | var pos : Int = 0; 42 | } 43 | 44 | /// QuickTime `Graphics` codec. 45 | class QuickTimeGraphicsImage : BlockPixMap { 46 | 47 | init(dimensions: QDDelta, clut: QDColorTable) { 48 | super.init(dimensions: dimensions, blockSize: 4, pixelSize: 8, cmpSize: 8, clut: clut); 49 | } 50 | 51 | func toRange(blockNum: Int, lineNum: Int) throws -> Range { 52 | let start = try getOffset(block: blockNum, line: lineNum); 53 | let end = start + blockSize; 54 | return start.. ArraySlice { 58 | let r = try toRange(blockNum: blockNum, lineNum: lineNum); 59 | return pixmap[r]; 60 | } 61 | 62 | func readBlock(blockNum: Int) throws -> [UInt8] { 63 | var block : [UInt8] = []; 64 | for line in 0..) throws { 71 | let r = try toRange(blockNum: blockNum, lineNum: lineNum); 72 | for (position, value) in zip(r, values) { 73 | pixmap[position] = value; 74 | } 75 | } 76 | 77 | func writeBlock(blockNum: Int, values: [UInt8]) throws { 78 | guard blockNum < totalBlocks else { 79 | return; 80 | } 81 | 82 | guard values.count == 16 else { 83 | throw QuickTimeGraphicsError.invalidBlockData(data: values); 84 | } 85 | 86 | for line in 0..> 15); 98 | assert(index < 2); 99 | buffer = buffer >> 1; 100 | values.append(colorIndexes[index]); 101 | } 102 | try writeBlock(blockNum: blockNum, values: values); 103 | } 104 | 105 | func write4ColorBlock(blockNum: Int, colorIndexes: [UInt8], data: UInt32) throws { 106 | var buffer = data; 107 | var values : [UInt8] = []; 108 | for _ in 0..<16 { 109 | let index = Int((buffer & 0xC000) >> 30); 110 | assert(index < 4); 111 | buffer = buffer >> 2; 112 | values.append(colorIndexes[index]); 113 | } 114 | try writeBlock(blockNum: blockNum, values: values); 115 | } 116 | 117 | func convert8ColorBlockWord(bytes: (UInt8, UInt8, UInt8), colorIndexes: [UInt8]) -> [UInt8]{ 118 | var word = makeUInt24(bytes: bytes); 119 | var values : [UInt8] = []; 120 | for _ in 0..<8 { 121 | let index = Int((0xe00000 & word) >> 21); 122 | let color = colorIndexes[index]; 123 | values.append(color); 124 | word = word << 3; 125 | } 126 | return values; 127 | } 128 | 129 | func write8ColorBlock(blockNum: Int, colorIndexes: [UInt8], data: [UInt8]) throws { 130 | let av = ( 131 | data[0], 132 | data[1] & 0xf0 | data[2] & 0x0f, 133 | (data[2] & 0x0f) << 4 | data[3] >> 4); 134 | let bv = ( 135 | data[4], 136 | data[5] & 0xf0 | data[1] & 0x0f, 137 | (data[3] & 0xf0) << 4 | (data[5] & 0xf0)); 138 | var values : [UInt8] = convert8ColorBlockWord(bytes: av, colorIndexes: colorIndexes); 139 | values.append(contentsOf: convert8ColorBlockWord(bytes: bv, colorIndexes: colorIndexes)); 140 | try writeBlock(blockNum: blockNum, values: values); 141 | } 142 | 143 | func load(data : consuming Data) throws { 144 | let reader = try QuickDrawDataReader(data: data, position: 4); 145 | var currentBlock = 0; 146 | while reader.remaining > 2 && currentBlock < totalBlocks { 147 | let v = try reader.readUInt8(); 148 | let opcode = v & 0xf0; 149 | let n = Int(v & 0x0f) + 1; 150 | switch opcode { 151 | case 0x00: 152 | currentBlock += n; 153 | case 0x10: 154 | let skip = try reader.readUInt8(); 155 | currentBlock += Int(skip); 156 | case 0x20: 157 | let last = try readBlock(blockNum: currentBlock - 1); 158 | for _ in 0..) throws { 31 | assert(color4.count == blockSize, "Invalid pixel line size"); 32 | let p = try getOffset(block: block, line: line); 33 | for (index, value) in color4.enumerated() { 34 | let rawValue = value.rawValue; 35 | pixmap[p + (index * 2)] = UInt8(rawValue >> 8); 36 | pixmap[p + (index * 2) + 1] = UInt8(rawValue & 0xff); 37 | } 38 | } 39 | 40 | private static let m21 = SIMD3.init(repeating: 21); 41 | private static let m11 = SIMD3.init(repeating: 11); 42 | private static let m5 = SIMD3.init(repeating: 5); 43 | 44 | /// Creates a ARGB555 color that is ⅔ color a and ⅓ color b. 45 | /// - Parameters: 46 | /// - a: color to mix ⅔ from 47 | /// - b: color to mix ⅓ from 48 | /// - Returns: a color which is on the line in RGB space between a and b. 49 | private func mix⅔(_ a: SIMD3, _ b: SIMD3) -> ARGB555 { 50 | let aa = SIMD3.init(clamping: a) &* RoadPizzaImage.m21; 51 | let bb = SIMD3.init(clamping: b) &* RoadPizzaImage.m11; 52 | let mix = SIMD3(clamping: (aa &+ bb) &>> RoadPizzaImage.m5); 53 | return ARGB555(simd: mix); 54 | } 55 | 56 | private func makeColorTable(colorA: ARGB555, colorB: ARGB555) -> [ARGB555] { 57 | let simda = colorA.simdValue; 58 | let simdb = colorB.simdValue; 59 | return [ 60 | colorB, mix⅔(simdb, simda), mix⅔(simda, simdb), colorA]; 61 | } 62 | 63 | func execute1Color(block: Int, color: ARGB555) throws { 64 | let color4 = [ARGB555].init(repeating: color, count: blockSize); 65 | for line in 0..> 6); 78 | color4.append(colorTable[index]); 79 | shiftedValue = shiftedValue << 2; 80 | } 81 | try writePixelLine(block: block, line: line, color4: color4[0..<4]); 82 | } 83 | } 84 | 85 | func executeDirectColor(block: Int, data: [ARGB555]) throws { 86 | assert(data.count == blockSize * blockSize, 87 | "Invalid direct color data size"); 88 | 89 | for line in 0.. 1 { 106 | let rawOpcode = try reader.readUInt8(); 107 | let opcode = rawOpcode & 0xe0; 108 | let blockCount = Int(rawOpcode & 0x1f) + 1; 109 | switch opcode { 110 | case let lowbit where lowbit & 0x80 == 0: 111 | /// Special case: colorA is encoded in rawOpcode + 1 byte 112 | let v = UInt16(rawOpcode) << 8 | UInt16(try reader.readUInt8()) | 0x8000; 113 | colorA = ARGB555(rawValue: v); 114 | if (try reader.peekUInt8() & 0x80) != 0 { 115 | /// Special case of palette block 116 | colorB = try reader.ReadRGB555(); 117 | let data = try reader.readUInt8(bytes: blockSize); 118 | try executeIndexColor(block: block, colorA: colorA, colorB: colorB, data: data); 119 | block += 1; 120 | } else { 121 | /// Direct AGRB data, colorA is the first. 122 | var data : [ARGB555] = [colorA]; 123 | for _ in 0..<15 { 124 | data.append(try reader.ReadRGB555()); 125 | } 126 | try executeDirectColor(block: block, data: data); 127 | block += 1; 128 | } 129 | case 0x80: /// Skip the block 130 | block += blockCount; 131 | case 0xa0: /// Single color block(s) 132 | colorA = try reader.ReadRGB555(); 133 | for i in block.., maxSize : Int, byteNum: Int) throws -> [UInt8] { 45 | var result : [UInt8] = []; 46 | var p = data.startIndex; 47 | while p < data.endIndex && result.count < maxSize { 48 | let c = data[p]; 49 | p += 1; 50 | if c & 0x80 > 0 { 51 | let run = Int(c & 0x7f) + 1; 52 | let end = p + byteNum; 53 | p += try copyRepeated(length: run, src: data[p.. 0 && self.clut == nil { 130 | if colorMapType == .noColorMap { 131 | throw TargaImageError.wrongColorMapType; 132 | } 133 | switch paletteDepth { 134 | case 8: 135 | clut = clutFromRgb(rgb: paletteData); 136 | default: 137 | throw TargaImageError.unsupportPaletteDepth(depth: paletteDepth); 138 | } 139 | } 140 | // Check the tail 141 | /* 142 | let tailPosition = data.count - 26 143 | let tailReader = try QuickDrawDataReader(data: data, position: tailPosition); 144 | let extensionAreaOffset = try tailReader.readUInt32LE(); 145 | let developperOffset = try tailReader.readUInt32LE(); 146 | let signature = try tailReader.readString(bytes: 16); 147 | */ 148 | // Decoding 149 | self.rowBytes = width * pixelSize / 8; 150 | let imageData = try reader.readSlice(bytes: reader.remaining); 151 | 152 | switch imageType { 153 | case .rleColorMap: 154 | pixmap = try decompressTarga(data: imageData, maxSize: rowBytes * height, byteNum: 1); 155 | case .rleTrueColor: 156 | pixmap = try decompressTarga(data: imageData, maxSize: rowBytes * height, byteNum: pixelSize / 8); 157 | if pixelSize == 16 { 158 | swap16BitColor(); 159 | } 160 | case .rleGrayScale: 161 | pixmap = try decompressTarga(data: imageData, maxSize: rowBytes * height, byteNum: 1); 162 | case .grayScale: 163 | pixmap = Array(imageData); 164 | case .colorMap: 165 | pixmap = Array(imageData); 166 | case .trueColor: 167 | pixmap = Array(imageData); 168 | if pixelSize == 16 { 169 | swap16BitColor(); 170 | } 171 | default: 172 | throw TargaImageError.unsupportedImageType(imageType: imageType); 173 | } 174 | } 175 | 176 | var description: String { 177 | return "Targa: " + describePixMap(self); 178 | } 179 | 180 | let dimensions: QDDelta 181 | var clut: QDColorTable? 182 | var imageType: TargaImageType = .noImageData; 183 | var rowBytes: Int = 0; 184 | var cmpSize: Int = 0; 185 | var pixelSize: Int = 0; 186 | 187 | var pixmap : [UInt8]; 188 | var imageIdData : [UInt8] = []; 189 | } 190 | -------------------------------------------------------------------------------- /QuickDrawViewer/TypeCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeCode.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 04.02.2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MacTypeError: Error { 11 | case notMacRoman(str: String); 12 | case invalidLength(length: Int); 13 | } 14 | 15 | struct MacTypeCode : RawRepresentable, CustomStringConvertible { 16 | init(rawValue: UInt32) { 17 | self.rawValue = rawValue; 18 | } 19 | 20 | init(fromString: String) throws { 21 | guard let data = fromString.cString(using: .macOSRoman) else { 22 | throw MacTypeError.notMacRoman(str: fromString); 23 | } 24 | // There could be a zero at the end 25 | guard data.count >= 4 else { 26 | throw MacTypeError.invalidLength(length: data.count); 27 | } 28 | rawValue = 29 | UInt32(data[0]) << 24 | 30 | UInt32(data[1]) << 16 | 31 | UInt32(data[2]) << 8 | 32 | UInt32(data[3]); 33 | } 34 | 35 | var description: String { 36 | let data = byteArrayBE(from: rawValue); 37 | return String(bytes:data, encoding: String.Encoding.macOSRoman) ?? "\(rawValue)"; 38 | } 39 | 40 | let rawValue: UInt32; 41 | static let zero = MacTypeCode(rawValue: 0); 42 | } 43 | 44 | extension QuickDrawDataReader { 45 | func readType() throws -> MacTypeCode { 46 | let data = try readUInt32(); 47 | return MacTypeCode(rawValue:data); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /QuickDrawViewer/UI/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 01.01.2024. 6 | // 7 | 8 | import os 9 | import SwiftUI 10 | import UniformTypeIdentifiers 11 | import PDFKit 12 | 13 | struct ContentView: View { 14 | 15 | @ObservedObject var document: QuickDrawViewerDocument; 16 | 17 | @State private var renderZoom = 1.0; 18 | @State private var isExporting = false; 19 | @State private var isAlerting = false; 20 | @State private var alertMessage : Alert? = nil; 21 | @GestureState private var zoom = 1.0; 22 | 23 | let logger : Logger = Logger(subsystem: "net.codiferes.wiesmann.QuickDraw", category: "view"); 24 | 25 | func renderCG(context : CGContext) -> Void { 26 | let picture = $document.picture.wrappedValue; 27 | do { 28 | try renderPicture(picture: picture, context: context, zoom: renderZoom, logger: self.logger); 29 | } catch { 30 | alert(title: String(localized: "Failed rendering picture"), message: "\(error)"); 31 | } 32 | } 33 | 34 | func render(context : inout GraphicsContext, dimension : CGSize) -> Void { 35 | context.withCGContext(content: self.renderCG); 36 | } 37 | 38 | func alert(title: String, message: String?) -> Void { 39 | isAlerting = true; 40 | if let msg = message { 41 | alertMessage = Alert(title:Text(title), message:Text(msg)); 42 | } else { 43 | alertMessage = Alert(title:Text(title)); 44 | } 45 | } 46 | 47 | func exportDone(result: Result ) -> Void { 48 | isExporting = false; 49 | } 50 | 51 | func doPrint(picture: QDPicture) -> Void { 52 | let printInfo = NSPrintInfo(); 53 | let pdfData = picture.pdfData(); 54 | guard let document = PDFDocument(data: pdfData as Data) else { 55 | alert(title: String(localized: "Failed to generate PDF document"), message: nil); 56 | return; 57 | } 58 | guard let operation = document.printOperation(for: printInfo, scalingMode: .pageScaleToFit, autoRotate: true) else { 59 | alert(title: String(localized: "Failed build print operation"), message: nil); 60 | return; 61 | } 62 | operation.run(); 63 | } 64 | 65 | func QDView() -> some View { 66 | let picture = $document.picture.wrappedValue; 67 | 68 | let width = picture.frame.dimensions.dh.value * renderZoom; 69 | let height = picture.frame.dimensions.dv.value * renderZoom; 70 | let canvas = Canvas(opaque: true, colorMode: ColorRenderingMode.linear, rendersAsynchronously: true, renderer: self.render).frame(width: width, height: height); 71 | return AnyView(canvas.focusable().copyable([picture]).draggable(picture).fileExporter(isPresented: $isExporting, item: picture, contentTypes: [.pdf], defaultFilename: MakePdfFilename(picture:picture), onCompletion: exportDone).toolbar { 72 | ToolbarItemGroup() { 73 | Button { 74 | isExporting = true 75 | } label: { 76 | Label(String(localized: "Export file"), systemImage: "square.and.arrow.up") 77 | } 78 | Button { 79 | doPrint(picture:picture); 80 | } label: { 81 | Label(String(localized: "Print"), systemImage: "printer") 82 | } 83 | } 84 | }.alert(isPresented:$isAlerting){return $alertMessage.wrappedValue!}).scaleEffect(zoom) 85 | .gesture( 86 | MagnifyGesture() 87 | .updating($zoom) { value, gestureState, transaction in 88 | gestureState = value.magnification 89 | } 90 | ) 91 | } 92 | 93 | var body: some View { 94 | ScrollView([.horizontal, .vertical]){QDView()}; 95 | } 96 | } 97 | 98 | #Preview { 99 | ContentView(document: QuickDrawViewerDocument(testMessage: "TestView")); 100 | } 101 | -------------------------------------------------------------------------------- /QuickDrawViewer/UI/ImageWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageWrapper.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 02.01.2024. 6 | // 7 | // Various utility functions to glue the QuickDraw renderer with UI-Kit. 8 | 9 | import os 10 | import Foundation 11 | import UniformTypeIdentifiers 12 | import SwiftUI 13 | 14 | 15 | /// Add UI related features. 16 | extension QDPicture { 17 | 18 | func pdfData() -> NSData { 19 | let data = NSMutableData(); 20 | let renderer = PDFRenderer(data: data); 21 | do { 22 | try renderer.execute(picture: self, zoom: 1.0); 23 | } catch { 24 | let logger : Logger = Logger(subsystem: "net.codiferes.wiesmann.QuickDraw", category: "imageWrapper"); 25 | logger.log(level: .error, "Failed rendering \(error)"); 26 | } 27 | return data; 28 | } 29 | } 30 | 31 | func MakePdfFilename(picture: QDPicture) -> String { 32 | let filename = picture.filename ?? "picture.pict"; 33 | return filename.replacingOccurrences(of: "pict", with: "pdf"); 34 | } 35 | 36 | /// Make it possible to transfer pictures into the clipboard, drag-and-drop. 37 | extension QDPicture : Transferable { 38 | public static var transferRepresentation: some TransferRepresentation { 39 | DataRepresentation(exportedContentType: .pdf) { 40 | picture in picture.pdfData() as Data }.suggestedFileName { MakePdfFilename(picture: $0) } 41 | } 42 | } 43 | 44 | /// Utility function that converts a picture into an image provider. 45 | /// - Parameter picture: picture to render 46 | /// - Returns: description 47 | func ProvidePicture(picture: QDPicture) -> [NSItemProvider] { 48 | let pdfProvider = NSItemProvider(item:picture.pdfData(), typeIdentifier: UTType.pdf.identifier); 49 | return [pdfProvider]; 50 | } 51 | 52 | func renderPicture(picture: QDPicture, context : CGContext, zoom: Double, logger: Logger) throws -> Void { 53 | let startTime = CFAbsoluteTimeGetCurrent(); 54 | let renderer = QuickdrawCGRenderer(context: context); 55 | try renderer.execute(picture: picture, zoom: zoom); 56 | let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime; 57 | let filename = picture.filename ?? ""; 58 | let frame = picture.frame; 59 | for (pos, opcode) in picture.opcodes.enumerated() { 60 | let entry = "\(pos): \(opcode)"; 61 | logger.log(level: .debug, "\(entry)"); 62 | } 63 | logger.log(level: .info, "\(filename) \(frame)\n rendered in : \(timeElapsed) seconds"); 64 | } 65 | -------------------------------------------------------------------------------- /QuickDrawViewer/UI/QuickDrawViewerApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawViewerApp.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 01.01.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HelpMenu: View { 11 | var body: some View { 12 | Group { 13 | Link(String(localized: "QuickDraw Viewer Project"), destination: URL( 14 | string: "https://github.com/wiesmann/QuickDrawViewer")!); 15 | Link(String(localized: "License"), destination: URL( 16 | string: "https://www.apache.org/licenses/LICENSE-2.0")!); 17 | } 18 | } 19 | } 20 | 21 | @main 22 | struct QuickDrawViewerApp: App { 23 | var body: some Scene { 24 | DocumentGroup(viewing: QuickDrawViewerDocument.self) { file in 25 | ContentView(document: file.document)} 26 | .commands { 27 | CommandGroup(replacing: .help) { 28 | HelpMenu() 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /QuickDrawViewer/UI/QuickDrawViewerDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickDrawViewerDocument.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 01.01.2024. 6 | // 7 | 8 | import os 9 | import SwiftUI 10 | import UniformTypeIdentifiers 11 | 12 | extension UTType { 13 | static var quickDrawImage: UTType { 14 | UTType(importedAs: "com.apple.pict") 15 | } 16 | static var quickTimeImage: UTType { 17 | UTType(importedAs: "com.apple.quicktime-image") 18 | } 19 | static var macPaintImage : UTType { 20 | UTType(importedAs: "com.apple.macpaint-image") 21 | } 22 | } 23 | 24 | /// Document wrapper for QuickDraw file types (also QuickTime and MacPaint). 25 | class QuickDrawViewerDocument: ReferenceFileDocument { 26 | 27 | typealias Snapshot = Data; 28 | @Published var picture: QDPicture; 29 | let logger = Logger(subsystem: "net.codiferes.wiesmann.QuickDraw", category: "document"); 30 | 31 | init(testMessage: String) { 32 | let frame = QDRect(topLeft: QDPoint.zero, dimension: QDDelta(dv: 120, dh: 120)); 33 | picture = QDPicture(size: 0, frame: frame, filename: testMessage + ".pict"); 34 | let rect = QDRect(topLeft: QDPoint(vertical: 20, horizontal: 20), dimension: QDDelta(dv: 100, dh: 100)); 35 | let frameOp = RectOp(same: false, verb: QDVerb.frame, rect: rect); 36 | picture.opcodes.append(frameOp); 37 | var magentaOp = ColorOp(rgb: false, selection: QDColorSelection.foreground); 38 | magentaOp.color = QDColor.qd1(qd1: QD1Color.magenta); 39 | picture.opcodes.append(magentaOp); 40 | let fillOp = RectOp(same: true, verb: QDVerb.fill); 41 | picture.opcodes.append(fillOp); 42 | var blueOp = ColorOp(rgb: false, selection: QDColorSelection.foreground); 43 | blueOp.color = QDColor.qd1(qd1: QD1Color.blue); 44 | picture.opcodes.append(blueOp); 45 | let textOp = LongTextOp(position: QDPoint(vertical: 70, horizontal: 40), text: testMessage); 46 | picture.opcodes.append(textOp); 47 | } 48 | 49 | init(path: String) throws { 50 | do { 51 | let input_url = URL(string: path); 52 | let parser = try QDParser(contentsOf: input_url!); 53 | parser.filename = path; 54 | picture = try parser.parse(); 55 | } 56 | catch { 57 | let message = String(localized: "Failed parsing QuickDraw file"); 58 | logger.log(level: .error, "\(message): \(error)"); 59 | throw CocoaError(.fileReadCorruptFile); 60 | } 61 | } 62 | 63 | required init(configuration: ReadConfiguration) throws { 64 | guard let data = configuration.file.regularFileContents else { 65 | throw CocoaError(.fileReadCorruptFile) 66 | } 67 | switch configuration.contentType { 68 | case .quickDrawImage: 69 | let parser = try QDParser(data: data); 70 | parser.filename = configuration.file.filename; 71 | picture = try parser.parse(); 72 | case .quickTimeImage: 73 | do { 74 | let reader = try QuickDrawDataReader(data: data, position: 0); 75 | reader.filename = configuration.file.filename; 76 | picture = try reader.readQuickTimeImage(); 77 | } catch { 78 | let message = String(localized: "Failed parsing QuickTime file"); 79 | logger.log(level: .error, "\(message): \(error)"); 80 | throw error; 81 | } 82 | case .macPaintImage: 83 | let macPaint = MacPaintImage(); 84 | try macPaint.load(data: data.subdata(in: 512.. Data { 97 | throw CocoaError(.fileWriteUnknown); 98 | } 99 | 100 | func fileWrapper(snapshot: Data, configuration: WriteConfiguration) throws -> FileWrapper { 101 | throw CocoaError(.fileWriteNoPermission); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /QuickDrawViewer/Yuv2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Yuv2.swift 3 | // QuickDrawViewer 4 | // 5 | // Created by Matthias Wiesmann on 31.01.2024. 6 | // 7 | // Decoder for the QuickTime `yuv2` codec. 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | /// It would be nice to use something like the accelerate framework to decode this. 13 | /// Sadly this particular version uses _signed_ int8 for u and v, not a cut-off of 128. 14 | /// So we code it explicitely. 15 | func signedYuv2Rgb(y: UInt8, u: UInt8, v: UInt8) -> RGB8 { 16 | let nu = Double(Int8(bitPattern: u)); 17 | let nv = Double(Int8(bitPattern: v)); 18 | let ny = Double(y); 19 | return yuv2Rgb(y: ny, u: nu, v: nv); 20 | } 21 | 22 | func convertYuv2Data(data: Data) -> [UInt8] { 23 | var rgb : [UInt8] = []; 24 | let pixelPairCount = data.count / 4; 25 | for i in 0..> 4), FixedPoint(16)); 75 | let half = FixedPoint(0.5); 76 | XCTAssertEqual(half, FixedPoint.one >> 1); 77 | } 78 | 79 | func testMultiply() throws { 80 | XCTAssertEqual(FixedPoint.one * FixedPoint.one, FixedPoint.one); 81 | XCTAssertEqual(FixedPoint.zero * FixedPoint.one, FixedPoint.zero); 82 | XCTAssertEqual(FixedPoint.one * FixedPoint.zero, FixedPoint.zero); 83 | XCTAssertEqual(FixedPoint.zero * FixedPoint.zero, FixedPoint.zero); 84 | XCTAssertEqual(FixedPoint(3) * FixedPoint(5), FixedPoint(15)); 85 | } 86 | 87 | func testDivide() throws { 88 | let half = FixedPoint(0.5); 89 | XCTAssertEqual(half, FixedPoint.one / FixedPoint(2)); 90 | XCTAssertEqual(half, FixedPoint(100) / FixedPoint(200)); 91 | } 92 | 93 | func testFixedPointRaw() throws { 94 | XCTAssertEqual(FixedPoint(rawValue: 0), FixedPoint.zero); 95 | XCTAssertEqual(FixedPoint(rawValue: 0x8000), FixedPoint(1) / FixedPoint(2)); 96 | // Quarters 97 | XCTAssertEqual(FixedPoint(rawValue: 0x4000), FixedPoint(1) / FixedPoint(4)); 98 | XCTAssertEqual(FixedPoint(rawValue: 0xc000), FixedPoint(3) / FixedPoint(4)); 99 | // Eights 100 | XCTAssertEqual(FixedPoint(rawValue: 0x2000), FixedPoint(1) / FixedPoint(8)); 101 | XCTAssertEqual(FixedPoint(rawValue: 0x6000), FixedPoint(3) / FixedPoint(8)); 102 | XCTAssertEqual(FixedPoint(rawValue: 0xA000), FixedPoint(5) / FixedPoint(8)); 103 | XCTAssertEqual(FixedPoint(rawValue: 0xE000), FixedPoint(7) / FixedPoint(8)); 104 | // Sixteenths 105 | XCTAssertEqual(FixedPoint(rawValue: 0x1000), FixedPoint(1) / FixedPoint(16)); 106 | } 107 | } 108 | 109 | final class QDGeometryTests: XCTestCase { 110 | 111 | func testDelta() throws { 112 | XCTAssertEqual(QDDelta.zero, QDDelta.zero); 113 | XCTAssertEqual(QDDelta.zero, -QDDelta.zero); 114 | XCTAssertEqual(QDDelta.zero, QDDelta.zero + QDDelta.zero); 115 | XCTAssertEqual(QDDelta.zero, QDDelta.zero - QDDelta.zero); 116 | let one = QDDelta(dv: FixedPoint.one, dh: FixedPoint.one); 117 | XCTAssertEqual(QDDelta.zero + one, one); 118 | XCTAssertEqual(one + QDDelta.zero, one); 119 | XCTAssertEqual(QDDelta.zero - one, -one); 120 | XCTAssertEqual(one - QDDelta.zero, one); 121 | XCTAssertEqual(one - one, QDDelta.zero); 122 | XCTAssertEqual(QDDelta(dv: FixedPoint(3), dh: FixedPoint(5)) + one, 123 | QDDelta(dv: FixedPoint(4), dh: FixedPoint(6))); 124 | } 125 | 126 | func testPointAndDelta() throws { 127 | XCTAssertEqual(QDPoint.zero.vertical, FixedPoint.zero); 128 | XCTAssertEqual(QDPoint.zero.horizontal, FixedPoint.zero); 129 | let p = QDPoint(vertical: FixedPoint(5), horizontal: FixedPoint(7)); 130 | XCTAssertEqual(p, p); 131 | XCTAssertEqual(p + QDDelta.zero, p); 132 | XCTAssertEqual(p - QDDelta.zero, p); 133 | let d = QDDelta(dv: FixedPoint(3), dh: FixedPoint(11)); 134 | let s = p + d; 135 | XCTAssertEqual(s.vertical, FixedPoint(8)); 136 | XCTAssertEqual(s.horizontal, FixedPoint(18)); 137 | XCTAssertEqual(s - d, p); 138 | } 139 | 140 | func testRects() throws { 141 | let p1 = QDPoint(vertical: FixedPoint(16), horizontal: FixedPoint(32)); 142 | let p2 = QDPoint(vertical: FixedPoint(256), horizontal: FixedPoint(512)); 143 | let r = QDRect(topLeft: p1, bottomRight: p2); 144 | XCTAssertEqual(r, r); 145 | XCTAssertNotEqual(r, QDRect.empty); 146 | XCTAssertFalse(r.isEmpty); 147 | XCTAssertNotEqual(r, QDRect.empty); 148 | XCTAssertTrue(QDRect.empty.isEmpty); 149 | // Dimensions 150 | XCTAssertEqual(r.dimensions.dh.value, 480, "\(r.dimensions)"); 151 | XCTAssertEqual(r.dimensions.dv.value, 240, "\(r.dimensions)"); 152 | XCTAssertEqual(QDRect.empty.dimensions, QDDelta.zero); 153 | // Center 154 | XCTAssertEqual(r.center.horizontal.value, 272, "\(r.center)"); 155 | XCTAssertEqual(r.center.vertical.value, 136, "\(r.center)"); 156 | XCTAssertEqual(QDRect.empty.center, QDPoint.zero); 157 | } 158 | 159 | func testRenderAngles() throws { 160 | XCTAssertEqual(deg2rad(0), -0.5 * .pi); 161 | XCTAssertEqual(deg2rad(90), -.pi); 162 | XCTAssertEqual(deg2rad(180), -1.5 * .pi); 163 | XCTAssertEqual(deg2rad(270), -0.0); 164 | } 165 | 166 | } 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickDraw Viewer 2 | 3 | ![QuickDraw Viewer Icon](QuickDrawViewer/Assets.xcassets/AppIcon.appiconset/Icon128.png) 4 | 5 | I wanted to teach myself Swift programming, and needed something a bit more involved than just _Hello World_, so I decided the write a program that would decode QuickDraw image files and display them. This was basically a rewrite of the [Java Quickdraw](https://github.com/wiesmann/JavaQuickDraw) code I wrote, many years back. 6 | 7 | This program is far from finished, but I decided to release it for the 40th anniversary of the original Macintosh computer: QuickDraw was the graphical language of the original Macintosh, and the format used to store and exchange images on the computer. Support for these files has been slowly decaying with newer versions of Mac OS X, and on my M1 PowerBook, Preview can only open a small subset of the files I have. 8 | 9 | ## Philosophy 10 | 11 | This program is not meant to be a pixel correct QuickDraw renderer, 12 | instead it behaves more like a _printer driver_ did under the classic Mac OS, 13 | and tries to render pictures as well as possible on a modern Mac OS X screen. 14 | 15 | The screen of my 2021 14" laptop has a resolution of around 264 DPI, 16 | closer to the resolution of the LaserWriter printers (300DPI) than that of the screen 17 | of a compact Macintosh (72 DPI) and well above the resolution of an ImageWriter dot matrix printer (144 DPI.) 18 | The rendering engine of Mac OS X is also closer to a PostScript printer than the QuickDraw model. 19 | 20 | So this program mostly translates QuickDraw instructions and delegates most of the actual rendering to Core Graphics. 21 | Instructions meant for printers (QuickDraw _comments_) are also used in the translation. 22 | 23 | ## Original Pict Example 24 | 25 | The decoder is mostly based on `Inside Macintosh - Imaging With QuickDraw` published in 1994. 26 | The book contains the resource definition of very simple QuickDraw picture. 27 | ``` 28 | data 'PICT' (128) { 29 | $"0078" /* picture size; don't use this value for picture size */ 30 | $"0000 0000 006C 00A8" /* bounding rectangle of picture at 72 dpi */ 31 | $"0011" /* VersionOp opcode; always $0011 for extended version 2 */ 32 | $"02FF" /* Version opcode; always $02FF for extended version 2 */ 33 | $"0C00" /* HeaderOp opcode; always $0C00 for extended version 2 */ 34 | /* next 24 bytes contain header information */ 35 | $"FFFE" /* version; always -2 for extended version 2 */ 36 | $"0000" /* reserved */ 37 | $"0048 0000" /* best horizontal resolution: 72 dpi */ 38 | $"0048 0000" /* best vertical resolution: 72 dpi */ 39 | $"0002 0002 006E 00AA" /* optimal source rectangle for 72 dpi horizontal 40 | and 72 dpi vertical resolutions */ 41 | $"0000" /* reserved */ 42 | $"001E" /* DefHilite opcode to use default hilite color */ 43 | $"0001" /* Clip opcode to define clipping region for picture */ 44 | $"000A" /* region size */ 45 | $"0002 0002 006E 00AA" /* bounding rectangle for clipping region */ 46 | $"000A" /* FillPat opcode; fill pattern specified in next 8 bytes */ 47 | $"77DD 77DD 77DD 77DD" /* fill pattern */ 48 | $"0034" /* fillRect opcode; rectangle specified in next 8 bytes */ 49 | $"0002 0002 006E 00AA" /* rectangle to fill */ 50 | $"000A" /* FillPat opcode; fill pattern specified in next 8 bytes */ 51 | $"8822 8822 8822 8822" /* fill pattern */ 52 | $"005C" 53 | $"0008" 54 | $"0008" 55 | $"0071" 56 | /* fillSameOval opcode */ 57 | /* PnMode opcode */ 58 | /* pen mode data */ 59 | /* paintPoly opcode */ 60 | $"001A" /* size of polygon */ 61 | $"0002 0002 006E 00AA" /* bounding rectangle for polygon */ 62 | $"006E 0002 0002 0054 006E 00AA 006E 0002" /* polygon points */ 63 | $"00FF" /* OpEndPic opcode; end of picture */ 64 | }; 65 | ``` 66 | 67 | You can [download the compiled Pict file](docs/inside_macintosh.pict). 68 | he rendering in the book looks like this: 69 | 70 | ![Example Pict](docs/inside_macintosh_listing_A5.png) 71 | 72 | This is how the Picture is rendered in Preview Version 11.0 on Mac OS X 14.4 (Sonoma). 73 | 74 | Example Pict in Preview (Broken) 75 | 76 | This is how it is rendered in QuickDraw Viewer: 77 | 78 | Example Pict (QuickDraw Viewer) 79 | 80 | ## Supported File types 81 | 82 | This application basically handles QuickDraw image files, but also two related (but distinct) image formats: 83 | 84 | * QuickTime images (`QTIF`) 85 | * MacPaint images (`PNTG`) 86 | 87 | These two formats are handled by converting them into QuickDraw at load time. 88 | QuickTime images are supported so far as the underlying codec is supported. 89 | MacPaint images are supported by virtue of being one of codecs that can be embedded inside QuickTime. 90 | 91 | ## Structure 92 | 93 | This program has basically three parts: 94 | 95 | * A library that parses QuickDraw files, which only depends on the `Foundation` framework. 96 | * A Library that renders into a CoreGraphics context, which depends on CoreGraphics, CoreText and CoreImage (AppKit is pulled in for some color logic, but could easily be removed). 97 | * A minimalistic Swift-UI application that shows the pictures. 98 | 99 | This means the code could be used in other applications that want to handle QuickDraw files. 100 | 101 | ## Features 102 | 103 | The library basically parses QuickDraw version 1 and version 2 files 104 | 105 | * Lines 106 | * Basic Shapes (Rectangles, Ovals, Round-Rectangles and Arcs) 107 | * Regions 108 | * Text 109 | * Patterns (black & white) 110 | * Colours 111 | * Palette images 112 | * Direct (RGB) images 113 | * QuickTime embedded images with the following codecs: 114 | * External image formats: JPEG, TIFF, PNG, BMP, JPEG-2000, GIF, SGI 115 | (these are handled natively by the renderer) 116 | * RAW (`raw `) 117 | * MacPaint 118 | * Targa (`tga `) for RLE 8-bit palette, RLE 24-bit RGB, RLE 8-bit grayscale. 119 | * Apple Video (`RPZA`) 120 | * Apple Component Video (`YUV2`) 121 | * Apple Graphics (`smc `) 122 | * Apple Animation (`RLE `) with depths of 2,4,8,16, 24 and 32 bits/pixel 123 | * Planar Video (`8BPS`) 124 | * Intel Raw (`YVU9`) 125 | * Cinepak (`CVID`) 126 | 127 | Some basic comment parsing is used to improve images, in particular: 128 | 129 | * Polygon annotations to [connect the lines](https://wiesmann.codiferes.net/wordpress/archives/37337) and close polygons 130 | * Fractional pen width 131 | * [Text rotation](https://wiesmann.codiferes.net/wordpress/archives/37285) 132 | * CMYK colors embedded in proprietary Deneba / Canvas comments. 133 | 134 | ## Unsupported features 135 | 136 | Currently, the following QuickDraw features don't work: 137 | 138 | * Some exotic compositing modes (which are typically not supported by printers) 139 | * Text alignement 140 | * Polygon smoothing 141 | * Color patterns 142 | * Exotic QuickTime codecs, like for instance Photo-CD 143 | 144 | ## User Interface Application 145 | 146 | The application is currently very simple, you can view pictures, copy-paste them to Preview. 147 | There is an export icon in the toolbar that allows you to export to PDF files. 148 | There is some primitive drag-drop that works when the target is Notes or Pages, but not when the target expects a file, like the Finder or Mail. 149 | 150 | ## License 151 | 152 | The code is distributed under the [Apache 2.0 License](License.txt). 153 | -------------------------------------------------------------------------------- /docs/GitTemplate.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/docs/GitTemplate.graffle -------------------------------------------------------------------------------- /docs/Icon.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/docs/Icon.graffle -------------------------------------------------------------------------------- /docs/inside_macintosh.pict: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/docs/inside_macintosh.pict -------------------------------------------------------------------------------- /docs/inside_macintosh_listing_A5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/docs/inside_macintosh_listing_A5.png -------------------------------------------------------------------------------- /docs/inside_macintosh_pict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/docs/inside_macintosh_pict.png -------------------------------------------------------------------------------- /docs/inside_macintosh_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiesmann/QuickDrawViewer/a217e5e4a5948368fc26f5b6015ece601b3104f7/docs/inside_macintosh_preview.png --------------------------------------------------------------------------------