@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +68 -39
- package/README.md +213 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +487 -0
- package/scripts/pdfium-manifest.json +46 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -1,639 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
PDFs built in memory, so a test has no asset to ship and the content is known
|
|
5
|
-
exactly rather than eyeballed.
|
|
6
|
-
|
|
7
|
-
A PORT OF `android/src/androidTest/.../TestPdfs.java`, and the port is the point:
|
|
8
|
-
the two backends are checked against the same geometry, the same markers and the
|
|
9
|
-
same encryption scheme, so a disagreement between platforms is a disagreement
|
|
10
|
-
about rendering rather than about fixtures.
|
|
11
|
-
|
|
12
|
-
One thing is written by hand here that Android gets from the platform:
|
|
13
|
-
`android.graphics.pdf.PdfDocument` can author a page but has no way to set
|
|
14
|
-
`/Rotate`, so the Java version hand-writes the rotated fixture and uses the
|
|
15
|
-
platform for the rest. There is no PDF *writer* in CoreGraphics that lets a test
|
|
16
|
-
set `/Rotate` or `/CropBox` either, and `CGPDFContext` would author through the
|
|
17
|
-
same drawing model being tested — so every fixture here is hand-written. That is
|
|
18
|
-
strictly better for a rendering test: the bytes are the input, and nothing about
|
|
19
|
-
what they mean depends on the code under test.
|
|
20
|
-
*/
|
|
21
|
-
enum TestPdfs {
|
|
22
|
-
static let letterW = 612
|
|
23
|
-
static let letterH = 792
|
|
24
|
-
/// ARCH-D, the sheet size this package exists for.
|
|
25
|
-
static let archDW = 1728
|
|
26
|
-
static let archDH = 2592
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------- //
|
|
29
|
-
// The writer
|
|
30
|
-
// ---------------------------------------------------------------- //
|
|
31
|
-
|
|
32
|
-
/// Assembles objects and writes a correct xref table, which every reader prefers.
|
|
33
|
-
final class Writer {
|
|
34
|
-
private var out = Data()
|
|
35
|
-
private var offsets: [Int] = []
|
|
36
|
-
|
|
37
|
-
init() {
|
|
38
|
-
write("%PDF-1.4\n")
|
|
39
|
-
// A binary comment line marks the file as binary, per the spec's advice.
|
|
40
|
-
out.append(contentsOf: [0x25, 0xE2, 0xE3, 0xCF, 0xD3, 0x0A])
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
func write(_ s: String) {
|
|
44
|
-
// Latin-1, not UTF-8: a PDF's syntax layer is bytes, and every string this
|
|
45
|
-
// writer emits is ASCII. Using UTF-8 would silently change the byte length
|
|
46
|
-
// of any non-ASCII content and invalidate a /Length.
|
|
47
|
-
out.append(s.data(using: .isoLatin1)!)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
func write(_ bytes: Data) {
|
|
51
|
-
out.append(bytes)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/// How many objects have been written. The next `add` returns this plus one.
|
|
55
|
-
var count: Int { offsets.count }
|
|
56
|
-
|
|
57
|
-
/// - Returns: the object number, 1-based.
|
|
58
|
-
@discardableResult
|
|
59
|
-
func add(_ body: String) -> Int {
|
|
60
|
-
add(body.data(using: .isoLatin1)!)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@discardableResult
|
|
64
|
-
func add(_ body: Data) -> Int {
|
|
65
|
-
let number = offsets.count + 1
|
|
66
|
-
offsets.append(out.count)
|
|
67
|
-
write("\(number) 0 obj\n")
|
|
68
|
-
write(body)
|
|
69
|
-
write("\nendobj\n")
|
|
70
|
-
return number
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
Writes at least `atLeast` bytes of PDF COMMENT lines.
|
|
75
|
-
|
|
76
|
-
A `%` line is legal anywhere between objects and is never drawn, so this
|
|
77
|
-
changes the file's LENGTH and nothing else — which is the only lever a
|
|
78
|
-
header-window test needs. Call it after the last `add()` and before
|
|
79
|
-
`finish()`, so the trailer moves while every recorded xref offset stays
|
|
80
|
-
correct.
|
|
81
|
-
*/
|
|
82
|
-
func pad(atLeast: Int) {
|
|
83
|
-
var s = ""
|
|
84
|
-
var line = 0
|
|
85
|
-
while s.utf8.count < atLeast {
|
|
86
|
-
s += "% padding line \(line) - legal PDF comment, never drawn\n"
|
|
87
|
-
line += 1
|
|
88
|
-
}
|
|
89
|
-
write(s)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
func finish(rootObject: Int, extraTrailer: String = "") -> Data {
|
|
93
|
-
let xref = out.count
|
|
94
|
-
let size = offsets.count + 1
|
|
95
|
-
write("xref\n0 \(size)\n")
|
|
96
|
-
write("0000000000 65535 f \n")
|
|
97
|
-
for offset in offsets {
|
|
98
|
-
write(String(format: "%010d 00000 n \n", offset))
|
|
99
|
-
}
|
|
100
|
-
write(
|
|
101
|
-
"trailer\n<< /Size \(size) /Root \(rootObject) 0 R \(extraTrailer) >>\n"
|
|
102
|
-
+ "startxref\n\(xref)\n%%EOF\n")
|
|
103
|
-
return out
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------- //
|
|
108
|
-
// Single-page fixtures
|
|
109
|
-
// ---------------------------------------------------------------- //
|
|
110
|
-
|
|
111
|
-
/// A single page with an explicit MediaBox, optional CropBox, `/Rotate` and content.
|
|
112
|
-
static func page(
|
|
113
|
-
mediaBox: (Int, Int, Int, Int),
|
|
114
|
-
cropBox: (Int, Int, Int, Int)? = nil,
|
|
115
|
-
rotate: Int = 0,
|
|
116
|
-
annots: String? = nil,
|
|
117
|
-
extraObjects: ((Writer) -> String)? = nil,
|
|
118
|
-
content: String
|
|
119
|
-
) -> Data {
|
|
120
|
-
let w = Writer()
|
|
121
|
-
let stream = content.data(using: .isoLatin1)!
|
|
122
|
-
let contents = w.add("<< /Length \(stream.count) >>\nstream\n\(content)endstream")
|
|
123
|
-
// Whatever the caller needs to exist BEFORE the page dictionary that names it
|
|
124
|
-
// — an annotation and its appearance stream, say. It is handed the writer, so
|
|
125
|
-
// it learns its own object numbers rather than being told them.
|
|
126
|
-
let extra = extraObjects?(w) ?? ""
|
|
127
|
-
// COMPUTED, not hardcoded. `/Parent` has to name the page tree, which is
|
|
128
|
-
// written two objects from now; an earlier version of this writer wrote
|
|
129
|
-
// "3 0 R" and broke the moment `extraObjects` added anything, because a page
|
|
130
|
-
// whose /Parent points at its own annotation makes readers rebuild the tree
|
|
131
|
-
// and a test would still pass for the wrong reason.
|
|
132
|
-
let pagesNumber = w.count + 2
|
|
133
|
-
var dict = "<< /Type /Page /Parent \(pagesNumber) 0 R /MediaBox "
|
|
134
|
-
+ "[\(mediaBox.0) \(mediaBox.1) \(mediaBox.2) \(mediaBox.3)]"
|
|
135
|
-
if let crop = cropBox {
|
|
136
|
-
dict += " /CropBox [\(crop.0) \(crop.1) \(crop.2) \(crop.3)]"
|
|
137
|
-
}
|
|
138
|
-
if rotate != 0 { dict += " /Rotate \(rotate)" }
|
|
139
|
-
if let annots { dict += " /Annots \(annots)" }
|
|
140
|
-
dict += extra
|
|
141
|
-
dict += " /Contents \(contents) 0 R /Resources << >> >>"
|
|
142
|
-
let pageObj = w.add(dict)
|
|
143
|
-
let pages = w.add("<< /Type /Pages /Kids [\(pageObj) 0 R] /Count 1 >>")
|
|
144
|
-
let catalog = w.add("<< /Type /Catalog /Pages \(pages) 0 R >>")
|
|
145
|
-
precondition(pages == pagesNumber, "Pages object is \(pages), not \(pagesNumber)")
|
|
146
|
-
return w.finish(rootObject: catalog)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
Four saturated quadrants in PDF user space (y-up): red bottom-left, green
|
|
151
|
-
bottom-right, blue top-left, black top-right.
|
|
152
|
-
|
|
153
|
-
Saturated is what makes a channel-order test possible at all — a grey or white
|
|
154
|
-
page reads identically in RGBA and BGRA, so the two orderings are
|
|
155
|
-
indistinguishable on it. Quadrants also make a STRIDE error visible: a raster
|
|
156
|
-
read back with the wrong row stride walks diagonally across the boundaries, so
|
|
157
|
-
the sampled colours come out wrong rather than merely shifted. And because the
|
|
158
|
-
four differ in both axes, a y-flip and an x/y transposition each move half the
|
|
159
|
-
pixels.
|
|
160
|
-
*/
|
|
161
|
-
static func quadrants(width: Int, height: Int, rotate: Int = 0) -> Data {
|
|
162
|
-
let hw = width / 2
|
|
163
|
-
let hh = height / 2
|
|
164
|
-
let content = """
|
|
165
|
-
1 0 0 rg 0 0 \(hw) \(hh) re f
|
|
166
|
-
0 1 0 rg \(hw) 0 \(hw) \(hh) re f
|
|
167
|
-
0 0 1 rg 0 \(hh) \(hw) \(hh) re f
|
|
168
|
-
0 0 0 rg \(hw) \(hh) \(hw) \(hh) re f
|
|
169
|
-
|
|
170
|
-
"""
|
|
171
|
-
return page(mediaBox: (0, 0, width, height), rotate: rotate, content: content)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
Detail at every spatial frequency: a `pitch`-point grid catches a scale error,
|
|
176
|
-
and the two diagonals catch an x/y transposition.
|
|
177
|
-
|
|
178
|
-
Also the fixture the timing tests use, because a page of 500+ stroked lines is
|
|
179
|
-
the shape a real drawing has — a flat colour fill measures the memory system
|
|
180
|
-
rather than the rasterizer.
|
|
181
|
-
*/
|
|
182
|
-
static func grid(width: Int, height: Int, pitch: Int = 8) -> Data {
|
|
183
|
-
var content = "1 1 1 rg 0 0 \(width) \(height) re f\n0 0 0 RG 0.5 w\n"
|
|
184
|
-
for x in stride(from: 0, through: width, by: pitch) {
|
|
185
|
-
content += "\(x) 0 m \(x) \(height) l S\n"
|
|
186
|
-
}
|
|
187
|
-
for y in stride(from: 0, through: height, by: pitch) {
|
|
188
|
-
content += "0 \(y) m \(width) \(y) l S\n"
|
|
189
|
-
}
|
|
190
|
-
content += "1 0 0 RG 2 w 0 0 m \(width) \(height) l S "
|
|
191
|
-
+ "\(width) 0 m 0 \(height) l S\n"
|
|
192
|
-
return page(mediaBox: (0, 0, width, height), content: content)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
A 612x792 page carrying `/Rotate 90`, red filling the UNROTATED top-left
|
|
197
|
-
112x100 points and blue the unrotated bottom-left.
|
|
198
|
-
|
|
199
|
-
`/Rotate` is "degrees the page shall be rotated CLOCKWISE when displayed", so a
|
|
200
|
-
correct renderer reports 792x612 and puts red at the displayed TOP-RIGHT and
|
|
201
|
-
blue at the displayed TOP-LEFT. Both markers matter: swapped dimensions alone
|
|
202
|
-
would also be produced by an implementation that swapped width and height and
|
|
203
|
-
drew the content unrotated, and a single marker cannot tell a 90 from a 270.
|
|
204
|
-
|
|
205
|
-
PDF user space is y-UP from the bottom-left, so the top-left 112x100 of the
|
|
206
|
-
page is the rect (0, 692) to (112, 792).
|
|
207
|
-
*/
|
|
208
|
-
static func rotated(_ degrees: Int) -> Data {
|
|
209
|
-
let content = """
|
|
210
|
-
1 1 1 rg 0 0 \(letterW) \(letterH) re f
|
|
211
|
-
1 0 0 rg 0 692 112 100 re f
|
|
212
|
-
0 0 1 rg 0 0 112 100 re f
|
|
213
|
-
|
|
214
|
-
"""
|
|
215
|
-
return page(mediaBox: (0, 0, letterW, letterH), rotate: degrees, content: content)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
A page whose CropBox is offset from its MediaBox, with a red marker at the crop
|
|
220
|
-
box's own top-left and a blue one at its bottom-right.
|
|
221
|
-
|
|
222
|
-
The only fixture that can tell "the box origin was subtracted" from "the box
|
|
223
|
-
origin was ignored": with MediaBox `0 0 400 400` and CropBox `100 150 300 350`,
|
|
224
|
-
a renderer that ignores the offset draws the markers 100pt too far right and
|
|
225
|
-
50pt too far down, which on a grey page is invisible unless something samples
|
|
226
|
-
the corners.
|
|
227
|
-
*/
|
|
228
|
-
static func offsetCropBox() -> Data {
|
|
229
|
-
// CropBox (100, 150) to (300, 350): 200 x 200 points, top-left at (100, 350).
|
|
230
|
-
let content = """
|
|
231
|
-
0.5 0.5 0.5 rg 0 0 400 400 re f
|
|
232
|
-
1 0 0 rg 100 310 40 40 re f
|
|
233
|
-
0 0 1 rg 260 150 40 40 re f
|
|
234
|
-
|
|
235
|
-
"""
|
|
236
|
-
return page(
|
|
237
|
-
mediaBox: (0, 0, 400, 400), cropBox: (100, 150, 300, 350), content: content)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/// A CropBox that overhangs its MediaBox, so the spec-mandated intersection is testable.
|
|
241
|
-
static func cropBoxOverhangingMediaBox() -> Data {
|
|
242
|
-
let content = "0.5 0.5 0.5 rg 0 0 200 200 re f\n"
|
|
243
|
-
return page(
|
|
244
|
-
mediaBox: (0, 0, 200, 200), cropBox: (50, 50, 400, 400), content: content)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
An antialiased opaque-white circle on an otherwise empty page.
|
|
249
|
-
|
|
250
|
-
Rendered with `background: transparent` this is the fixture that decides the
|
|
251
|
-
alpha encoding: a 62%-covered white edge reads `[255,255,255,158]` straight and
|
|
252
|
-
`[158,158,158,158]` premultiplied. Every pixel on the circle's boundary is
|
|
253
|
-
partially covered, so there is no need to hunt for one.
|
|
254
|
-
*/
|
|
255
|
-
static func antialiasedWhiteEdge() -> Data {
|
|
256
|
-
let content = """
|
|
257
|
-
1 1 1 rg
|
|
258
|
-
50 10 m
|
|
259
|
-
72.09 10 90 27.91 90 50 c
|
|
260
|
-
90 72.09 72.09 90 50 90 c
|
|
261
|
-
27.91 90 10 72.09 10 50 c
|
|
262
|
-
10 27.91 27.91 10 50 10 c
|
|
263
|
-
f
|
|
264
|
-
|
|
265
|
-
"""
|
|
266
|
-
return page(mediaBox: (0, 0, 100, 100), content: content)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
A page whose only green content is inside an ANNOTATION's appearance stream.
|
|
271
|
-
|
|
272
|
-
The fixture the `annotations` flag is decided by, and it has to be built this
|
|
273
|
-
way round: the content stream paints nothing green, so green in the raster can
|
|
274
|
-
only have come from compositing the annotation. `annotations: true` (PDFKit)
|
|
275
|
-
must show it and `annotations: false` (`CGContextDrawPDFPage`) must not — which
|
|
276
|
-
is the one capability difference between this backend and Android's, where the
|
|
277
|
-
flag cannot be honoured at all.
|
|
278
|
-
|
|
279
|
-
A `/Square` annotation with an explicit `/AP << /N ... >>` normal appearance
|
|
280
|
-
stream, because an annotation with no appearance stream is up to the reader to
|
|
281
|
-
synthesise and two readers will not agree on it.
|
|
282
|
-
*/
|
|
283
|
-
static func annotatedPage() -> Data {
|
|
284
|
-
let content = "1 1 1 rg 0 0 200 200 re f\n1 0 0 rg 10 10 30 30 re f\n"
|
|
285
|
-
// The annotation and its appearance stream have to be written BEFORE the page
|
|
286
|
-
// dictionary that names them, so the `/Annots` array cannot be a literal — it
|
|
287
|
-
// is built from the object number `extraObjects` actually got. An earlier
|
|
288
|
-
// version hardcoded `[5 0 R]` and pointed at the page tree instead, which made
|
|
289
|
-
// the annotation unreachable and would have let the test pass by drawing
|
|
290
|
-
// nothing.
|
|
291
|
-
var annotsArray = ""
|
|
292
|
-
let data = page(
|
|
293
|
-
mediaBox: (0, 0, 200, 200),
|
|
294
|
-
annots: nil,
|
|
295
|
-
extraObjects: { w in
|
|
296
|
-
let appearance = "0 1 0 rg 0 0 100 100 re f\n"
|
|
297
|
-
let stream = appearance.data(using: .isoLatin1)!
|
|
298
|
-
let ap = w.add(
|
|
299
|
-
"<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] "
|
|
300
|
-
+ "/Length \(stream.count) >>\nstream\n\(appearance)endstream")
|
|
301
|
-
let annot = w.add(
|
|
302
|
-
"<< /Type /Annot /Subtype /Square /Rect [50 50 150 150] /F 4 "
|
|
303
|
-
+ "/AP << /N \(ap) 0 R >> >>")
|
|
304
|
-
annotsArray = "[\(annot) 0 R]"
|
|
305
|
-
return " /Annots [\(annot) 0 R]"
|
|
306
|
-
},
|
|
307
|
-
content: content)
|
|
308
|
-
precondition(!annotsArray.isEmpty, "the annotation was never written")
|
|
309
|
-
return data
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ---------------------------------------------------------------- //
|
|
313
|
-
// Multi-page fixtures
|
|
314
|
-
// ---------------------------------------------------------------- //
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
Pages of DIFFERENT sizes in one document, so a geometry test can catch an
|
|
318
|
-
implementation that probes page 0 and reports its size for every page.
|
|
319
|
-
*/
|
|
320
|
-
static let mixedSizes: [(Int, Int)] = [
|
|
321
|
-
(letterW, letterH), (archDW, archDH), (200, 900),
|
|
322
|
-
]
|
|
323
|
-
|
|
324
|
-
static func mixedSizeDocument() -> Data {
|
|
325
|
-
multiPage(mixedSizes.map { size in
|
|
326
|
-
(size, 0, "0.5 0.5 0.5 rg 0 0 \(size.0) \(size.1) re f\n")
|
|
327
|
-
})
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
One flat colour per page.
|
|
332
|
-
|
|
333
|
-
Catches a backend that ignores `RasterRequest.page` and renders page 0 for every
|
|
334
|
-
request — which a single-page fixture cannot catch at all, and a multi-page
|
|
335
|
-
fixture with identical pages catches only by luck.
|
|
336
|
-
*/
|
|
337
|
-
static func coloredPages(_ colors: [(Double, Double, Double)], width: Int, height: Int)
|
|
338
|
-
-> Data
|
|
339
|
-
{
|
|
340
|
-
multiPage(
|
|
341
|
-
colors.map { color in
|
|
342
|
-
(
|
|
343
|
-
(width, height), 0,
|
|
344
|
-
"\(color.0) \(color.1) \(color.2) rg 0 0 \(width) \(height) re f\n"
|
|
345
|
-
)
|
|
346
|
-
})
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/// Pages with different `/Rotate` values, so post-rotation geometry is testable per page.
|
|
350
|
-
static func rotatedPages(_ rotations: [Int]) -> Data {
|
|
351
|
-
multiPage(
|
|
352
|
-
rotations.map { rotation in
|
|
353
|
-
((200, 100), rotation, "1 0 0 rg 0 0 40 40 re f\n0 0 1 rg 0 60 40 40 re f\n")
|
|
354
|
-
})
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/// - Parameter pages: `(size, rotate, content)` per page.
|
|
358
|
-
static func multiPage(_ pages: [((Int, Int), Int, String)]) -> Data {
|
|
359
|
-
let w = Writer()
|
|
360
|
-
// The page tree is object 1 and every page's /Parent points at it, so the
|
|
361
|
-
// pages have to be numbered afterwards. Object 1 is therefore RESERVED with a
|
|
362
|
-
// placeholder and rewritten at the end — which a linear writer cannot do, so
|
|
363
|
-
// instead the content streams and pages come first and the tree last, with
|
|
364
|
-
// /Parent naming a number computed up front.
|
|
365
|
-
let contentObjects = pages.map { page -> Int in
|
|
366
|
-
let stream = page.2.data(using: .isoLatin1)!
|
|
367
|
-
return w.add("<< /Length \(stream.count) >>\nstream\n\(page.2)endstream")
|
|
368
|
-
}
|
|
369
|
-
// After the content streams, the next `pages.count` objects are the pages, and
|
|
370
|
-
// the one after those is the tree.
|
|
371
|
-
let treeNumber = contentObjects.count + pages.count + 1
|
|
372
|
-
var pageObjects: [Int] = []
|
|
373
|
-
for (index, page) in pages.enumerated() {
|
|
374
|
-
var dict = "<< /Type /Page /Parent \(treeNumber) 0 R /MediaBox "
|
|
375
|
-
+ "[0 0 \(page.0.0) \(page.0.1)]"
|
|
376
|
-
if page.1 != 0 { dict += " /Rotate \(page.1)" }
|
|
377
|
-
dict += " /Contents \(contentObjects[index]) 0 R /Resources << >> >>"
|
|
378
|
-
pageObjects.append(w.add(dict))
|
|
379
|
-
}
|
|
380
|
-
let kids = pageObjects.map { "\($0) 0 R" }.joined(separator: " ")
|
|
381
|
-
let tree = w.add("<< /Type /Pages /Kids [\(kids)] /Count \(pages.count) >>")
|
|
382
|
-
precondition(tree == treeNumber, "tree is \(tree), predicted \(treeNumber)")
|
|
383
|
-
let catalog = w.add("<< /Type /Catalog /Pages \(tree) 0 R >>")
|
|
384
|
-
return w.finish(rootObject: catalog)
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ---------------------------------------------------------------- //
|
|
388
|
-
// Not-PDFs
|
|
389
|
-
// ---------------------------------------------------------------- //
|
|
390
|
-
|
|
391
|
-
/// A valid `%PDF-` header and nothing else that parses.
|
|
392
|
-
static func headerOnlyGarbage() -> Data {
|
|
393
|
-
var s = "%PDF-1.7\n"
|
|
394
|
-
for i in 0..<200 { s += "garbage line \(i)\n" }
|
|
395
|
-
return s.data(using: .isoLatin1)!
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/// No `%PDF-` header anywhere in the first 1024 bytes.
|
|
399
|
-
static func notAPdfAtAll() -> Data {
|
|
400
|
-
var s = ""
|
|
401
|
-
for i in 0..<200 { s += "this is a text file, line \(i)\n" }
|
|
402
|
-
s += "%PDF-1.7 too late to count\n"
|
|
403
|
-
return s.data(using: .isoLatin1)!
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// ---------------------------------------------------------------- //
|
|
407
|
-
// Encryption (standard security handler, R=2, V=1, RC4-40)
|
|
408
|
-
// ---------------------------------------------------------------- //
|
|
409
|
-
|
|
410
|
-
static let userPassword = "secret"
|
|
411
|
-
/// Non-empty, and that is load-bearing — see the note in `encrypted()`.
|
|
412
|
-
static let ownerPassword = "owner"
|
|
413
|
-
|
|
414
|
-
private static let pad: [UInt8] = [
|
|
415
|
-
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56,
|
|
416
|
-
0xFF, 0xFA, 0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80,
|
|
417
|
-
0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A,
|
|
418
|
-
]
|
|
419
|
-
|
|
420
|
-
private static func padded(_ password: String) -> [UInt8] {
|
|
421
|
-
let pw = Array(password.data(using: .isoLatin1)!)
|
|
422
|
-
var out = [UInt8](repeating: 0, count: 32)
|
|
423
|
-
let n = min(pw.count, 32)
|
|
424
|
-
for i in 0..<n { out[i] = pw[i] }
|
|
425
|
-
for i in n..<32 { out[i] = pad[i - n] }
|
|
426
|
-
return out
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/// RC4, by hand, because CommonCrypto's RC4 is deprecated and gated.
|
|
430
|
-
private static func rc4(key: [UInt8], data: [UInt8]) -> [UInt8] {
|
|
431
|
-
var s = Array(0..<256)
|
|
432
|
-
var j = 0
|
|
433
|
-
for i in 0..<256 {
|
|
434
|
-
j = (j + s[i] + Int(key[i % key.count])) & 0xFF
|
|
435
|
-
s.swapAt(i, j)
|
|
436
|
-
}
|
|
437
|
-
var out = [UInt8](repeating: 0, count: data.count)
|
|
438
|
-
var i = 0
|
|
439
|
-
j = 0
|
|
440
|
-
for k in 0..<data.count {
|
|
441
|
-
i = (i + 1) & 0xFF
|
|
442
|
-
j = (j + s[i]) & 0xFF
|
|
443
|
-
s.swapAt(i, j)
|
|
444
|
-
out[k] = data[k] ^ UInt8(s[(s[i] + s[j]) & 0xFF])
|
|
445
|
-
}
|
|
446
|
-
return out
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private static func md5(_ parts: [[UInt8]]) -> [UInt8] {
|
|
450
|
-
// Insertion-sort-simple MD5, written out rather than taken from CommonCrypto:
|
|
451
|
-
// `CC_MD5` is deprecated with an availability warning that fails a
|
|
452
|
-
// warnings-as-errors build, and CryptoKit's `Insecure.MD5` is available but
|
|
453
|
-
// pulls a framework into a test target that otherwise needs none.
|
|
454
|
-
var message = [UInt8]()
|
|
455
|
-
for part in parts { message += part }
|
|
456
|
-
return MD5.digest(message)
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
A 200x200 red page encrypted with the standard security handler, R=2, V=1,
|
|
461
|
-
RC4-40, user password `userPassword` and owner password `ownerPassword`.
|
|
462
|
-
|
|
463
|
-
Hand-written because nothing in CoreGraphics or PDFKit writes an encrypted PDF.
|
|
464
|
-
Without it, "password-protected produces a distinguishable error" is an
|
|
465
|
-
assertion about code that has never met an encrypted file — and on iOS it is
|
|
466
|
-
also the only way to test the `password-incorrect` branch, which Android cannot
|
|
467
|
-
reach at all (`PdfRendererPool` refuses any password with `unsupported`,
|
|
468
|
-
because `LoadParams` landed at API 35).
|
|
469
|
-
*/
|
|
470
|
-
static func encrypted() -> Data {
|
|
471
|
-
let contentString = "1 0 0 rg 0 0 200 200 re f\n"
|
|
472
|
-
let content = Array(contentString.data(using: .isoLatin1)!)
|
|
473
|
-
|
|
474
|
-
// A fixed file ID, so the fixture is byte-stable across runs.
|
|
475
|
-
let fileId: [UInt8] = [
|
|
476
|
-
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
|
|
477
|
-
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
|
|
478
|
-
]
|
|
479
|
-
// -1 as a 32-bit little-endian signed int: every permission granted.
|
|
480
|
-
let permissions: Int32 = -1
|
|
481
|
-
let pBytes: [UInt8] = [
|
|
482
|
-
UInt8(truncatingIfNeeded: permissions),
|
|
483
|
-
UInt8(truncatingIfNeeded: permissions >> 8),
|
|
484
|
-
UInt8(truncatingIfNeeded: permissions >> 16),
|
|
485
|
-
UInt8(truncatingIfNeeded: permissions >> 24),
|
|
486
|
-
]
|
|
487
|
-
|
|
488
|
-
// Algorithm 3: /O is the padded USER password, RC4'd with the MD5 of the
|
|
489
|
-
// padded OWNER password.
|
|
490
|
-
//
|
|
491
|
-
// THE OWNER PASSWORD MUST NOT BE EMPTY, and getting that wrong is how this
|
|
492
|
-
// fixture first shipped as a document that opens with no password at all.
|
|
493
|
-
// MEASURED: with `ownerPassword = ""`, CoreGraphics reported
|
|
494
|
-
// `isEncrypted = true` and `isUnlocked = true` before any password was
|
|
495
|
-
// supplied, and PDFKit reported `isLocked = false`. The reason is Algorithm 7,
|
|
496
|
-
// the OWNER authentication path every reader tries after the user one: it
|
|
497
|
-
// decrypts /O with MD5(padded(supplied)) and runs Algorithm 6 on the result.
|
|
498
|
-
// An empty owner password makes the empty string decrypt /O correctly, which
|
|
499
|
-
// recovers the real user password, which authenticates. The spec's own
|
|
500
|
-
// instruction — "if there is no owner password, use the user password
|
|
501
|
-
// instead" — exists precisely to close that hole. `"owner"` here matches the
|
|
502
|
-
// Android fixture exactly, so the two platforms are tested against the same
|
|
503
|
-
// bytes.
|
|
504
|
-
let ownerKey = Array(md5([padded(ownerPassword)])[0..<5])
|
|
505
|
-
let o = rc4(key: ownerKey, data: padded(userPassword))
|
|
506
|
-
|
|
507
|
-
// Algorithm 2: the file encryption key.
|
|
508
|
-
let userPad = padded(userPassword)
|
|
509
|
-
let fileKey = Array(md5([userPad, o, pBytes, fileId])[0..<5])
|
|
510
|
-
|
|
511
|
-
// Algorithm 4 (R=2): /U is the pad string RC4'd with the file key.
|
|
512
|
-
let u = rc4(key: fileKey, data: pad)
|
|
513
|
-
|
|
514
|
-
// Each string and stream is encrypted with a key derived from the file key
|
|
515
|
-
// plus its own object and generation number (Algorithm 1).
|
|
516
|
-
func objectKey(_ number: Int) -> [UInt8] {
|
|
517
|
-
let extra: [UInt8] = [
|
|
518
|
-
UInt8(number & 0xFF), UInt8((number >> 8) & 0xFF),
|
|
519
|
-
UInt8((number >> 16) & 0xFF), 0, 0,
|
|
520
|
-
]
|
|
521
|
-
let digest = md5([fileKey, extra])
|
|
522
|
-
return Array(digest[0..<min(fileKey.count + 5, 16)])
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
let w = Writer()
|
|
526
|
-
// Object 1 is the content stream, so its key uses 1.
|
|
527
|
-
let encryptedContent = rc4(key: objectKey(1), data: content)
|
|
528
|
-
var streamObject = Data("<< /Length \(encryptedContent.count) >>\nstream\n"
|
|
529
|
-
.data(using: .isoLatin1)!)
|
|
530
|
-
streamObject.append(contentsOf: encryptedContent)
|
|
531
|
-
streamObject.append("\nendstream".data(using: .isoLatin1)!)
|
|
532
|
-
let contents = w.add(streamObject)
|
|
533
|
-
let pageObj = w.add(
|
|
534
|
-
"<< /Type /Page /Parent 3 0 R /MediaBox [0 0 200 200] /Contents "
|
|
535
|
-
+ "\(contents) 0 R /Resources << >> >>")
|
|
536
|
-
let pages = w.add("<< /Type /Pages /Kids [\(pageObj) 0 R] /Count 1 >>")
|
|
537
|
-
let catalog = w.add("<< /Type /Catalog /Pages \(pages) 0 R >>")
|
|
538
|
-
let encrypt = w.add(
|
|
539
|
-
"<< /Filter /Standard /V 1 /R 2 /O <\(hex(o))> /U <\(hex(u))> /P "
|
|
540
|
-
+ "\(permissions) >>")
|
|
541
|
-
precondition(pages == 3, "Pages object is \(pages), not 3")
|
|
542
|
-
return w.finish(
|
|
543
|
-
rootObject: catalog,
|
|
544
|
-
extraTrailer: "/Encrypt \(encrypt) 0 R /ID [<\(hex(fileId))> <\(hex(fileId))>]")
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
private static func hex(_ bytes: [UInt8]) -> String {
|
|
548
|
-
bytes.map { String(format: "%02X", $0) }.joined()
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// ---------------------------------------------------------------- //
|
|
552
|
-
// Files
|
|
553
|
-
// ---------------------------------------------------------------- //
|
|
554
|
-
|
|
555
|
-
/// Writes a fixture to a temporary file and returns its URL.
|
|
556
|
-
static func write(_ data: Data, name: String) throws -> URL {
|
|
557
|
-
let dir = URL(fileURLWithPath: NSTemporaryDirectory())
|
|
558
|
-
.appendingPathComponent("pdfcanvas-tests", isDirectory: true)
|
|
559
|
-
try FileManager.default.createDirectory(
|
|
560
|
-
at: dir, withIntermediateDirectories: true)
|
|
561
|
-
let url = dir.appendingPathComponent(name)
|
|
562
|
-
try data.write(to: url)
|
|
563
|
-
return url
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/// MD5, 60 lines, because CommonCrypto's is deprecated and CryptoKit is a framework.
|
|
568
|
-
enum MD5 {
|
|
569
|
-
private static let s: [UInt32] = [
|
|
570
|
-
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
|
571
|
-
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
|
572
|
-
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
|
573
|
-
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
|
574
|
-
]
|
|
575
|
-
|
|
576
|
-
private static let k: [UInt32] = (0..<64).map {
|
|
577
|
-
UInt32(truncatingIfNeeded: Int64(abs(sin(Double($0 + 1))) * 4294967296.0))
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
static func digest(_ message: [UInt8]) -> [UInt8] {
|
|
581
|
-
var msg = message
|
|
582
|
-
let bitLength = UInt64(message.count) * 8
|
|
583
|
-
msg.append(0x80)
|
|
584
|
-
while msg.count % 64 != 56 { msg.append(0) }
|
|
585
|
-
for i in 0..<8 { msg.append(UInt8(truncatingIfNeeded: bitLength >> (8 * UInt64(i)))) }
|
|
586
|
-
|
|
587
|
-
var a0: UInt32 = 0x6745_2301
|
|
588
|
-
var b0: UInt32 = 0xefcd_ab89
|
|
589
|
-
var c0: UInt32 = 0x98ba_dcfe
|
|
590
|
-
var d0: UInt32 = 0x1032_5476
|
|
591
|
-
|
|
592
|
-
for chunk in stride(from: 0, to: msg.count, by: 64) {
|
|
593
|
-
var m = [UInt32](repeating: 0, count: 16)
|
|
594
|
-
for i in 0..<16 {
|
|
595
|
-
let o = chunk + i * 4
|
|
596
|
-
m[i] =
|
|
597
|
-
UInt32(msg[o]) | (UInt32(msg[o + 1]) << 8) | (UInt32(msg[o + 2]) << 16)
|
|
598
|
-
| (UInt32(msg[o + 3]) << 24)
|
|
599
|
-
}
|
|
600
|
-
var a = a0
|
|
601
|
-
var b = b0
|
|
602
|
-
var c = c0
|
|
603
|
-
var d = d0
|
|
604
|
-
for i in 0..<64 {
|
|
605
|
-
var f: UInt32
|
|
606
|
-
var g: Int
|
|
607
|
-
switch i / 16 {
|
|
608
|
-
case 0:
|
|
609
|
-
f = (b & c) | (~b & d)
|
|
610
|
-
g = i
|
|
611
|
-
case 1:
|
|
612
|
-
f = (d & b) | (~d & c)
|
|
613
|
-
g = (5 * i + 1) % 16
|
|
614
|
-
case 2:
|
|
615
|
-
f = b ^ c ^ d
|
|
616
|
-
g = (3 * i + 5) % 16
|
|
617
|
-
default:
|
|
618
|
-
f = c ^ (b | ~d)
|
|
619
|
-
g = (7 * i) % 16
|
|
620
|
-
}
|
|
621
|
-
f = f &+ a &+ k[i] &+ m[g]
|
|
622
|
-
a = d
|
|
623
|
-
d = c
|
|
624
|
-
c = b
|
|
625
|
-
b = b &+ ((f << s[i]) | (f >> (32 - s[i])))
|
|
626
|
-
}
|
|
627
|
-
a0 = a0 &+ a
|
|
628
|
-
b0 = b0 &+ b
|
|
629
|
-
c0 = c0 &+ c
|
|
630
|
-
d0 = d0 &+ d
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
var out = [UInt8]()
|
|
634
|
-
for word in [a0, b0, c0, d0] {
|
|
635
|
-
for i in 0..<4 { out.append(UInt8(truncatingIfNeeded: word >> (8 * UInt32(i)))) }
|
|
636
|
-
}
|
|
637
|
-
return out
|
|
638
|
-
}
|
|
639
|
-
}
|