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 | 
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 | 
71 |
72 | This is how the Picture is rendered in Preview Version 11.0 on Mac OS X 14.4 (Sonoma).
73 |
74 |
75 |
76 | This is how it is rendered in QuickDraw Viewer:
77 |
78 |
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
--------------------------------------------------------------------------------