@reekon-tools/react-native-pdf-canvas 0.1.0 → 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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/PdfCanvas.podspec +68 -39
  4. package/README.md +213 -1316
  5. package/android/build.gradle +67 -54
  6. package/android/consumer-rules.pro +7 -0
  7. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
  8. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
  9. package/android/src/main/cpp/CMakeLists.txt +54 -0
  10. package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
  11. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
  12. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
  13. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
  14. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
  15. package/android/tools/compile-gate.sh +81 -101
  16. package/dist/controller.d.ts +14 -0
  17. package/dist/controller.js +4 -3
  18. package/dist/index.native.d.ts +5 -0
  19. package/dist/index.native.js +5 -0
  20. package/dist/index.web.d.ts +10 -0
  21. package/dist/index.web.js +10 -0
  22. package/dist/rasterizer/index.native.d.ts +8 -12
  23. package/dist/rasterizer/index.native.js +15 -21
  24. package/dist/rasterizer/native-bridge.d.ts +51 -75
  25. package/dist/rasterizer/native-bridge.js +49 -33
  26. package/dist/rasterizer/native.d.ts +45 -0
  27. package/dist/rasterizer/native.js +123 -0
  28. package/dist/react/usePdfDocument.d.ts +5 -5
  29. package/dist/react/usePdfLayer.d.ts +10 -1
  30. package/dist/react/usePdfLayer.js +10 -2
  31. package/dist/skia/index.native.d.ts +7 -3
  32. package/dist/skia/index.native.js +7 -3
  33. package/dist/skia/index.web.d.ts +4 -2
  34. package/dist/skia/index.web.js +4 -2
  35. package/dist/testing/scenes.d.ts +3 -3
  36. package/dist/testing/scenes.js +4 -4
  37. package/dist/types.d.ts +36 -42
  38. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  39. package/native/.clangd +5 -0
  40. package/native/CMakeLists.txt +89 -0
  41. package/native/core/include/pdfcanvas/document.h +90 -0
  42. package/native/core/include/pdfcanvas/error.h +47 -0
  43. package/native/core/include/pdfcanvas/jsi.h +18 -0
  44. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  45. package/native/core/include/pdfcanvas/library.h +28 -0
  46. package/native/core/include/pdfcanvas/pixels.h +92 -0
  47. package/native/core/include/pdfcanvas/service.h +87 -0
  48. package/native/core/include/pdfcanvas/slots.h +113 -0
  49. package/native/core/include/pdfcanvas/types.h +110 -0
  50. package/native/core/pdfcanvas-core.cmake +22 -0
  51. package/native/core/src/document.cpp +516 -0
  52. package/native/core/src/error.cpp +29 -0
  53. package/native/core/src/jsi.cpp +66 -0
  54. package/native/core/src/library.cpp +52 -0
  55. package/native/core/src/pixels.cpp +45 -0
  56. package/native/core/src/service.cpp +156 -0
  57. package/native/core/src/slots.cpp +139 -0
  58. package/native/tests/fixtures.cpp +451 -0
  59. package/native/tests/fixtures.h +96 -0
  60. package/native/tests/harness.h +122 -0
  61. package/native/tests/main.cpp +64 -0
  62. package/native/tests/pixels_util.h +126 -0
  63. package/native/tests/test_document.cpp +485 -0
  64. package/native/tests/test_service.cpp +130 -0
  65. package/native/tests/test_slots.cpp +250 -0
  66. package/package.json +38 -8
  67. package/react-native.config.js +5 -2
  68. package/scripts/fetch-pdfium.mjs +487 -0
  69. package/scripts/pdfium-manifest.json +46 -0
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  72. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  73. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  74. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  75. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  76. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  77. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  78. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  79. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  80. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  81. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  82. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  83. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  84. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  85. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  86. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  87. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  88. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  89. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  90. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  91. package/dist/rasterizer/android.d.ts +0 -121
  92. package/dist/rasterizer/android.js +0 -126
  93. package/dist/rasterizer/ios.d.ts +0 -84
  94. package/dist/rasterizer/ios.js +0 -155
  95. package/ios/Package.swift +0 -58
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  98. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  99. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  100. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  101. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  102. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  103. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  104. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  105. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  109. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  110. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  111. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  112. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  113. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  114. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  115. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  116. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  117. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  118. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  119. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  120. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  121. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  122. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  123. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  124. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  125. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  126. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  127. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  128. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  129. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -1,262 +0,0 @@
1
- import Foundation
2
- import XCTest
3
-
4
- @testable import PdfCanvasCore
5
-
6
- /**
7
- The error codes, and the preflight that makes them distinguishable.
8
-
9
- THE POINT IS THE CODE, NOT THE FAILURE. `CGPDFDocumentCreateWithURL` returning
10
- NULL is one signal for "no such file", "no permission", "zero bytes" and "not a
11
- PDF", and a host showing "corrupt" for a path that simply does not exist sends
12
- somebody looking at the wrong thing. Every case below asserts which code came back,
13
- because the codes are the whole contract across the bridge — `PdfCanvasErrorCodeWire`
14
- is the TypeScript union member verbatim and there is no translation table in
15
- between.
16
- */
17
- final class FailureTests: XCTestCase {
18
-
19
- private func expectFailure(
20
- _ url: URL,
21
- password: String? = nil,
22
- _ expected: PdfCanvasErrorCode,
23
- _ what: String,
24
- file: StaticString = #filePath,
25
- line: UInt = #line
26
- ) {
27
- XCTAssertThrowsError(
28
- try PdfCanvasRasterizer.open(fileURL: url, password: password, poolSize: 1),
29
- what, file: file, line: line
30
- ) { error in
31
- let ns = error as NSError
32
- XCTAssertEqual(
33
- PdfCanvasErrorCodeOf(ns), expected,
34
- "\(what): got \(PdfCanvasErrorCodeWire(PdfCanvasErrorCodeOf(ns))) — "
35
- + ns.localizedDescription,
36
- file: file, line: line)
37
- }
38
- }
39
-
40
- // ---------------------------------------------------------------- //
41
- // Not there / not readable / not a PDF
42
- // ---------------------------------------------------------------- //
43
-
44
- func testAMissingFileIsNotFound() {
45
- let url = URL(fileURLWithPath: NSTemporaryDirectory())
46
- .appendingPathComponent("pdfcanvas-does-not-exist-\(UUID().uuidString).pdf")
47
- expectFailure(url, .notFound, "a path with nothing at it")
48
- }
49
-
50
- func testADirectoryIsNotFound() {
51
- expectFailure(
52
- URL(fileURLWithPath: NSTemporaryDirectory()), .notFound,
53
- "a directory is not a document")
54
- }
55
-
56
- /// A non-file URL is `not-found` with a message that says why, rather than a hang.
57
- func testARemoteUrlIsNotFound() {
58
- expectFailure(
59
- URL(string: "https://example.com/a.pdf")!, .notFound,
60
- "this backend does no networking")
61
- }
62
-
63
- func testAnEmptyFileIsCorrupt() throws {
64
- let url = try TestPdfs.write(Data(), name: "empty.pdf")
65
- expectFailure(url, .corrupt, "0 bytes")
66
- }
67
-
68
- /**
69
- A file with no `%PDF-` header in its first 1024 bytes is `corrupt`, and the
70
- message says what was looked for.
71
-
72
- The window matters in both directions, so the fixture puts a real header at
73
- ~5 KB in: a scan of the whole file would ACCEPT this and hand CoreGraphics
74
- something it will refuse with no explanation.
75
- */
76
- func testAFileWithNoHeaderInTheWindowIsCorrupt() throws {
77
- let url = try TestPdfs.write(TestPdfs.notAPdfAtAll(), name: "not-a-pdf.pdf")
78
- expectFailure(url, .corrupt, "no header in the first 1024 bytes")
79
- XCTAssertThrowsError(
80
- try PdfCanvasRasterizer.open(fileURL: url, password: nil, poolSize: 1)
81
- ) { error in
82
- XCTAssertTrue(
83
- (error as NSError).localizedDescription.contains("%PDF-"),
84
- "the message should name what was looked for: "
85
- + (error as NSError).localizedDescription)
86
- }
87
- }
88
-
89
- /**
90
- Leading junk BEFORE the header is tolerated, because every other reader
91
- tolerates it.
92
-
93
- Real files carry it — an HTTP preamble, a shell wrapper — and the spec allows
94
- it. A preflight that demanded the header at byte 0 would reject documents that
95
- open everywhere else, which is a worse failure than the one it prevents.
96
- */
97
- func testLeadingJunkBeforeTheHeaderIsTolerated() throws {
98
- var data = Data("this is some junk a proxy prepended\n".utf8)
99
- data.append(TestPdfs.quadrants(width: 50, height: 50))
100
- let url = try TestPdfs.write(data, name: "junk-prefix.pdf")
101
- // CoreGraphics is the one that decides whether it can still parse it; the
102
- // point here is that PREFLIGHT does not reject it out of hand. Either it opens
103
- // or it fails as `corrupt` from CoreGraphics — never with the "no %PDF- header"
104
- // message, which is what would mean preflight had refused it.
105
- do {
106
- let r = try PdfCanvasRasterizer.open(fileURL: url, password: nil, poolSize: 1)
107
- XCTAssertEqual(r.pageCount, 1)
108
- r.close()
109
- } catch {
110
- XCTAssertFalse(
111
- (error as NSError).localizedDescription.contains("no %PDF- header"),
112
- "preflight must not be the one rejecting this")
113
- }
114
- }
115
-
116
- /**
117
- A `%PDF-` header over garbage is `corrupt`, and it does not poison anything.
118
-
119
- THIS IS THE TEST ANDROID CANNOT PASS WITHOUT A GUARD. On API 24 a `PdfRenderer`
120
- constructor that throws permanently poisons PDFium for the whole process — one
121
- failed open plus a garbage collection and the next successful open segfaults in
122
- `CPDF_Document`, which is why `PdfPreflight.java` exists as a correctness
123
- mechanism. CoreGraphics returns NULL and carries on, so the second half of this
124
- test — a good document opening cleanly afterwards, in the same process — is the
125
- evidence that no equivalent guard is needed here.
126
- */
127
- func testHeaderOnlyGarbageIsCorruptAndPoisonsNothing() throws {
128
- let bad = try TestPdfs.write(TestPdfs.headerOnlyGarbage(), name: "garbage.pdf")
129
- expectFailure(bad, .corrupt, "a header over garbage")
130
-
131
- // Again, several times, interleaved with good opens — the shape that breaks on
132
- // API 24.
133
- for i in 0..<5 {
134
- expectFailure(bad, .corrupt, "repeat \(i)")
135
- let good = try openFixture(
136
- TestPdfs.quadrants(width: 40, height: 40), name: "good-after-bad-\(i).pdf")
137
- let raster = try renderWholePage(good)
138
- assertPixel(raster, 10, 30, .red, "a good document still renders after \(i + 1) failures")
139
- good.close()
140
- }
141
- }
142
-
143
- // ---------------------------------------------------------------- //
144
- // Encryption
145
- // ---------------------------------------------------------------- //
146
-
147
- /**
148
- An encrypted document with no password is `password-required`; with the wrong
149
- one, `password-incorrect`; with the right one, it opens and RENDERS.
150
-
151
- The third part is what makes this more than an error-code test: iOS is the only
152
- native backend that can actually open an encrypted document.
153
- `PdfRendererPool.open` on Android REJECTS any password with `unsupported`,
154
- because `android.graphics.pdf.LoadParams` landed at API 35 — so the same file is
155
- a hard failure there and a working document here.
156
- */
157
- func testEncryptedDocuments() throws {
158
- let url = try TestPdfs.write(TestPdfs.encrypted(), name: "encrypted-cases.pdf")
159
- expectFailure(url, password: nil, .passwordRequired, "no password")
160
- expectFailure(url, password: "", .passwordRequired, "an empty password")
161
- expectFailure(url, password: "wrong", .passwordIncorrect, "a wrong password")
162
-
163
- let ok = try PdfCanvasRasterizer.open(
164
- fileURL: url, password: TestPdfs.userPassword, poolSize: 1)
165
- XCTAssertEqual(ok.pageCount, 1)
166
- let raster = try renderWholePage(ok, scale: 1)
167
- assertPixel(raster, 100, 100, .red, "the decrypted content stream must draw")
168
- ok.close()
169
- }
170
-
171
- /**
172
- And an encrypted document renders through PDFKit too, which unlocks separately.
173
-
174
- The two engines keep separate locks, so the password has to be presented twice —
175
- once to `CGPDFDocumentUnlockWithPassword` at open, once to
176
- `-[PDFDocument unlockWithPassword:]` on the first `annotations: true` render. A
177
- backend that only did the first would fail the SECOND render of a document that
178
- had already opened fine, which is about the most confusing shape a bug can have.
179
- */
180
- func testEncryptedDocumentsRenderThroughPdfKitToo() throws {
181
- let url = try TestPdfs.write(TestPdfs.encrypted(), name: "encrypted-pdfkit.pdf")
182
- let ok = try PdfCanvasRasterizer.open(
183
- fileURL: url, password: TestPdfs.userPassword, poolSize: 1)
184
- let raster = try renderWholePage(ok, scale: 1, annotations: true)
185
- assertPixel(raster, 100, 100, .red)
186
- ok.close()
187
- }
188
-
189
- // ---------------------------------------------------------------- //
190
- // The wire format
191
- // ---------------------------------------------------------------- //
192
-
193
- /**
194
- The wire strings are the TypeScript union members, verbatim.
195
-
196
- Restated here rather than derived, which is the point: this list and the one in
197
- `src/types.ts` are the contract, and if one is edited without the other a
198
- `PdfError` arrives in JS with a code no consumer can switch on. Android has a
199
- third copy for the same reason.
200
- */
201
- func testWireStringsMatchTheTypeScriptUnion() {
202
- let expected: [(PdfCanvasErrorCode, String)] = [
203
- (.notFound, "not-found"),
204
- (.passwordRequired, "password-required"),
205
- (.passwordIncorrect, "password-incorrect"),
206
- (.corrupt, "corrupt"),
207
- (.unsupported, "unsupported"),
208
- (.cancelled, "cancelled"),
209
- (.outOfMemory, "out-of-memory"),
210
- (.backendFailure, "backend-failure"),
211
- ]
212
- for (code, wire) in expected {
213
- XCTAssertEqual(PdfCanvasErrorCodeWire(code), wire)
214
- }
215
- XCTAssertEqual(expected.count, 8, "the union has eight members")
216
- }
217
-
218
- /// A foreign error reads as `backend-failure` rather than as a wrong code.
219
- func testAForeignErrorIsBackendFailure() {
220
- let foreign = NSError(domain: NSCocoaErrorDomain, code: 4, userInfo: nil)
221
- XCTAssertEqual(PdfCanvasErrorCodeOf(foreign), .backendFailure)
222
- }
223
-
224
- /// The underlying error is kept, because a `corrupt` verdict needs its cause.
225
- func testTheUnderlyingErrorIsKept() {
226
- let cause = NSError(domain: "test", code: 7, userInfo: nil)
227
- let error = PdfCanvasMakeError(.corrupt, "wrapped", cause) as NSError
228
- XCTAssertEqual(error.localizedDescription, "wrapped")
229
- XCTAssertEqual(
230
- (error.userInfo[NSUnderlyingErrorKey] as? NSError)?.code, 7)
231
- }
232
-
233
- // ---------------------------------------------------------------- //
234
- // Preflight, directly
235
- // ---------------------------------------------------------------- //
236
-
237
- func testPreflightAcceptsARealDocument() throws {
238
- let url = try TestPdfs.write(
239
- TestPdfs.quadrants(width: 40, height: 40), name: "preflight-ok.pdf")
240
- XCTAssertNoThrow(try PdfCanvasPreflight.check(fileURL: url))
241
- }
242
-
243
- func testPreflightOnDataChecksTheHeaderWindow() {
244
- XCTAssertNoThrow(
245
- try PdfCanvasPreflight.check(TestPdfs.quadrants(width: 10, height: 10)))
246
- XCTAssertThrowsError(try PdfCanvasPreflight.check(Data())) { error in
247
- XCTAssertEqual(PdfCanvasErrorCodeOf(error as NSError), .corrupt)
248
- }
249
- XCTAssertThrowsError(try PdfCanvasPreflight.check(TestPdfs.notAPdfAtAll())) {
250
- error in
251
- XCTAssertEqual(PdfCanvasErrorCodeOf(error as NSError), .corrupt)
252
- }
253
- // A header at the very edge of the window is still found.
254
- var edge = Data(repeating: 0x20, count: 1019)
255
- edge.append(Data("%PDF-".utf8))
256
- XCTAssertNoThrow(try PdfCanvasPreflight.check(edge))
257
- // One byte further out is not.
258
- var past = Data(repeating: 0x20, count: 1020)
259
- past.append(Data("%PDF-".utf8))
260
- XCTAssertThrowsError(try PdfCanvasPreflight.check(past))
261
- }
262
- }
@@ -1,173 +0,0 @@
1
- import CoreGraphics
2
- import XCTest
3
-
4
- @testable import PdfCanvasCore
5
-
6
- /**
7
- Page geometry, which is where a wrong answer is worst.
8
-
9
- A `PageGeometry` is what the layout function turns into the document's whole
10
- coordinate space, so a wrong one is not a missing page — it is every page after it
11
- in the wrong place. And nothing about it throws: a swapped width and height on a
12
- rotated page produces a document that lays out cleanly and draws sideways.
13
- */
14
- final class GeometryTests: XCTestCase {
15
-
16
- /**
17
- Geometry is POST-ROTATION, and this backend computes it rather than inheriting
18
- it.
19
-
20
- MEASURED and worth restating, because it is the opposite of Android: on a
21
- `/Rotate 90` letter page, `CGPDFPageGetBoxRect(page, kCGPDFCropBox)` returns
22
- `(0, 0, 612, 792)` — UNROTATED — and `CGPDFPageGetRotationAngle` returns 90.
23
- Android's `PdfRenderer.Page.getWidth()` is already post-rotation and cannot
24
- report `/Rotate` at all. So the swap below is arithmetic this library owns.
25
- */
26
- func testPageGeometryIsPostRotation() throws {
27
- for (rotation, expected) in [
28
- (0, (612.0, 792.0)), (90, (792.0, 612.0)), (180, (612.0, 792.0)),
29
- (270, (792.0, 612.0)),
30
- ] {
31
- let rasterizer = try openFixture(
32
- TestPdfs.rotated(rotation), name: "rotated-\(rotation).pdf")
33
- let geometry = try rasterizer.pageGeometry(at: 0)
34
- XCTAssertEqual(
35
- Double(geometry.widthPoints), expected.0, accuracy: 1e-9,
36
- "/Rotate \(rotation) width")
37
- XCTAssertEqual(
38
- Double(geometry.heightPoints), expected.1, accuracy: 1e-9,
39
- "/Rotate \(rotation) height")
40
- XCTAssertEqual(
41
- geometry.rotationDegrees, rotation, "/Rotate \(rotation) should be reported")
42
- rasterizer.close()
43
- }
44
- }
45
-
46
- /**
47
- `/Rotate` is REPORTED, not normalised away — which is what the web PDFium
48
- backend does and the opposite of Android.
49
-
50
- Android reports 0 because `PdfRenderer` gives it no way to read the value; that
51
- is an inability, not a policy, and `native-bridge.ts` documents the divergence
52
- at the field. This test exists so nobody "fixes" iOS to match Android.
53
- */
54
- func testRotationIsReportedPerPage() throws {
55
- let rasterizer = try openFixture(
56
- TestPdfs.rotatedPages([0, 90, 180, 270]), name: "rotated-pages.pdf")
57
- XCTAssertEqual(rasterizer.pageCount, 4)
58
- for (index, rotation) in [0, 90, 180, 270].enumerated() {
59
- let geometry = try rasterizer.pageGeometry(at: index)
60
- XCTAssertEqual(geometry.rotationDegrees, rotation, "page \(index)")
61
- // The fixture's pages are 200x100, so 90/270 must report 100x200.
62
- let quarter = rotation == 90 || rotation == 270
63
- XCTAssertEqual(Double(geometry.widthPoints), quarter ? 100 : 200, accuracy: 1e-9)
64
- XCTAssertEqual(Double(geometry.heightPoints), quarter ? 200 : 100, accuracy: 1e-9)
65
- }
66
- rasterizer.close()
67
- }
68
-
69
- /**
70
- Each page reports its OWN size.
71
-
72
- Catches an implementation that probes page 0 and reports its size for every
73
- page — which a single-page fixture cannot catch at all, and which would lay a
74
- three-page document out as three copies of its first page.
75
- */
76
- func testEachPageReportsItsOwnSize() throws {
77
- let rasterizer = try openFixture(
78
- TestPdfs.mixedSizeDocument(), name: "mixed-sizes.pdf")
79
- XCTAssertEqual(rasterizer.pageCount, TestPdfs.mixedSizes.count)
80
- for (index, size) in TestPdfs.mixedSizes.enumerated() {
81
- let geometry = try rasterizer.pageGeometry(at: index)
82
- XCTAssertEqual(Double(geometry.widthPoints), Double(size.0), accuracy: 1e-9)
83
- XCTAssertEqual(Double(geometry.heightPoints), Double(size.1), accuracy: 1e-9)
84
- XCTAssertEqual(geometry.index, index)
85
- }
86
- rasterizer.close()
87
- }
88
-
89
- /**
90
- Geometry is precomputed at open, not loaded on demand.
91
-
92
- Asked for AFTER `close()`, when every `CGPDFDocumentRef` is gone: a lazy
93
- implementation throws or crashes here. A page is read that was never asked about
94
- before the close, so a lazily-cached page 0 cannot pass by accident. This is the
95
- `RasterizerHandle` contract — "cheap: must not load a page" — held to.
96
- */
97
- func testGeometryIsPrecomputedAtOpenNotLoadedOnDemand() throws {
98
- let rasterizer = try openFixture(
99
- TestPdfs.mixedSizeDocument(), name: "mixed-sizes-closed.pdf")
100
- _ = try rasterizer.pageGeometry(at: 0)
101
- rasterizer.close()
102
- let last = try rasterizer.pageGeometry(at: TestPdfs.mixedSizes.count - 1)
103
- XCTAssertEqual(
104
- Double(last.widthPoints), Double(TestPdfs.mixedSizes.last!.0), accuracy: 1e-9)
105
- }
106
-
107
- /// An out-of-range index is `not-found`, not a crash and not a clamped page.
108
- func testOutOfRangePageIsNotFound() throws {
109
- let rasterizer = try openFixture(TestPdfs.quadrants(width: 100, height: 100), name: "one-page.pdf")
110
- for index in [-1, 1, 99] {
111
- XCTAssertThrowsError(try rasterizer.pageGeometry(at: index)) { error in
112
- XCTAssertEqual(PdfCanvasErrorCodeOf(error as NSError), .notFound)
113
- }
114
- }
115
- rasterizer.close()
116
- }
117
-
118
- /**
119
- The crop box is used, and it is INTERSECTED with the media box.
120
-
121
- `CGPDFPageGetBoxRect(kCGPDFCropBox)` already falls back to the media box when
122
- there is no crop box, so the fallback is not the interesting half. The
123
- intersection is: a file whose crop box overhangs its media box would otherwise
124
- report a page larger than the paper, and every rect the layout function derives
125
- from it would be wrong by the overhang.
126
- */
127
- func testCropBoxIsUsedAndIntersectedWithMediaBox() throws {
128
- let offset = try openFixture(TestPdfs.offsetCropBox(), name: "offset-crop.pdf")
129
- let cropped = try offset.pageGeometry(at: 0)
130
- XCTAssertEqual(Double(cropped.widthPoints), 200, accuracy: 1e-9)
131
- XCTAssertEqual(Double(cropped.heightPoints), 200, accuracy: 1e-9)
132
- XCTAssertEqual(Double(cropped.cropBox.origin.x), 100, accuracy: 1e-9)
133
- XCTAssertEqual(Double(cropped.cropBox.origin.y), 150, accuracy: 1e-9)
134
- offset.close()
135
-
136
- // MediaBox 0 0 200 200, CropBox 50 50 400 400 -> intersection is 50,50 150x150.
137
- let overhang = try openFixture(
138
- TestPdfs.cropBoxOverhangingMediaBox(), name: "overhang-crop.pdf")
139
- let clamped = try overhang.pageGeometry(at: 0)
140
- XCTAssertEqual(
141
- Double(clamped.widthPoints), 150, accuracy: 1e-9,
142
- "an un-intersected crop box would report 350")
143
- XCTAssertEqual(Double(clamped.heightPoints), 150, accuracy: 1e-9)
144
- overhang.close()
145
- }
146
-
147
- /// The page count comes from the document, and a one-page fixture is one page.
148
- func testPageCount() throws {
149
- let one = try openFixture(TestPdfs.quadrants(width: 50, height: 50), name: "count-1.pdf")
150
- XCTAssertEqual(one.pageCount, 1)
151
- one.close()
152
-
153
- let three = try openFixture(
154
- TestPdfs.coloredPages(
155
- [(1, 0, 0), (0, 1, 0), (0, 0, 1)], width: 60, height: 40),
156
- name: "count-3.pdf")
157
- XCTAssertEqual(three.pageCount, 3)
158
- three.close()
159
- }
160
-
161
- /// The pool size actually opened is what is reported, clamped rather than trusted.
162
- func testPoolSizeIsClamped() throws {
163
- let data = TestPdfs.quadrants(width: 50, height: 50)
164
- for (asked, expected) in [(-3, 1), (0, 1), (1, 1), (2, 2), (4, 4), (99, 4)] {
165
- let rasterizer = try openFixture(
166
- data, name: "pool-\(asked).pdf", poolSize: asked)
167
- XCTAssertEqual(
168
- rasterizer.poolSize, expected,
169
- "asked for \(asked) lanes, kPdfCanvasMaxPoolSize is \(kPdfCanvasMaxPoolSize)")
170
- rasterizer.close()
171
- }
172
- }
173
- }
@@ -1,114 +0,0 @@
1
- import CoreGraphics
2
- import Foundation
3
- import ImageIO
4
- import UniformTypeIdentifiers
5
- import XCTest
6
-
7
- @testable import PdfCanvasCore
8
-
9
- /**
10
- Writes what the rasterizer produced to PNG, so a human can look at it.
11
-
12
- NOT A GOLDEN-IMAGE TEST, and deliberately not one. Byte-exact reference images churn
13
- on every OS update — the spec says so where it rejects them for the reference scenes —
14
- and every property worth asserting is already asserted numerically elsewhere in this
15
- suite: quadrant placement, tile-vs-whole-page equality at maxDelta 0, rotation corners,
16
- the two engines' agreement, premultiplied alpha.
17
-
18
- What this adds is the thing no numeric assertion covers: somebody can open the files and
19
- see that a page looks like a page. It exists because the alternative on this toolchain is
20
- a simulator screenshot, and driving a dev-client simulator needs synthetic taps that need
21
- an accessibility grant — a worse dependency than a PNG in a temp directory.
22
-
23
- The only assertion is that every file was written and is non-trivial in size, because a
24
- preview test that silently wrote nothing would be worse than no preview test.
25
- */
26
- final class PreviewTests: XCTestCase {
27
-
28
- private func outputDirectory() throws -> URL {
29
- let dir = URL(fileURLWithPath: NSTemporaryDirectory())
30
- .appendingPathComponent("pdfcanvas-previews", isDirectory: true)
31
- try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
32
- return dir
33
- }
34
-
35
- /// Raster bytes -> PNG, through the same premultiplied RGBA layout the raster has.
36
- private func writePng(
37
- _ raster: PdfCanvasRasterPixels, to url: URL
38
- ) throws {
39
- let bytes = try XCTUnwrap(raster.bytes)
40
- let provider = try XCTUnwrap(CGDataProvider(data: bytes as CFData))
41
- let image = try XCTUnwrap(
42
- CGImage(
43
- width: raster.width, height: raster.height, bitsPerComponent: 8,
44
- bitsPerPixel: 32, bytesPerRow: raster.rowBytes,
45
- space: CGColorSpace(name: CGColorSpace.sRGB)!,
46
- bitmapInfo: CGBitmapInfo(
47
- rawValue: CGImageAlphaInfo.premultipliedLast.rawValue
48
- | CGBitmapInfo.byteOrder32Big.rawValue),
49
- provider: provider, decode: nil, shouldInterpolate: false,
50
- intent: .defaultIntent))
51
- let destination = try XCTUnwrap(
52
- CGImageDestinationCreateWithURL(
53
- url as CFURL, UTType.png.identifier as CFString, 1, nil))
54
- CGImageDestinationAddImage(destination, image, nil)
55
- XCTAssertTrue(CGImageDestinationFinalize(destination), "failed to write \(url.path)")
56
- }
57
-
58
- func testWritePreviews() throws {
59
- let dir = try outputDirectory()
60
- var written: [String] = []
61
-
62
- func emit(
63
- _ name: String, _ data: Data, page: Int = 0, scale: Double = 1,
64
- annotations: Bool = false, background: PdfCanvasBackground = .white,
65
- rect: (Double, Double, Double, Double)? = nil
66
- ) throws {
67
- let rasterizer = try openFixture(data, name: "preview-\(name).pdf")
68
- let geometry = try rasterizer.pageGeometry(at: page)
69
- let r = rect ?? (0, 0, geometry.widthPoints, geometry.heightPoints)
70
- let raster = try rasterizer.render(
71
- request(
72
- page: page, x: r.0, y: r.1, width: r.2, height: r.3, scale: scale,
73
- annotations: annotations, background: background),
74
- cancellation: nil, sink: nil)
75
- let url = dir.appendingPathComponent("\(name).png")
76
- try writePng(raster, to: url)
77
- let size = try FileManager.default.attributesOfItem(atPath: url.path)[.size]
78
- as? Int ?? 0
79
- XCTAssertGreaterThan(size, 200, "\(name).png is suspiciously small")
80
- written.append("\(name).png \(raster.width)x\(raster.height) \(size)B")
81
- rasterizer.close()
82
- }
83
-
84
- // A dense ARCH-D grid, whole page at a plausible fit scale: the base layer.
85
- try emit("base-archd", TestPdfs.grid(width: 1728, height: 2592), scale: 0.25)
86
- // One tile of the same sheet at a detail scale: the detail layer.
87
- try emit(
88
- "detail-tile", TestPdfs.grid(width: 1728, height: 2592), scale: 4,
89
- rect: (300, 400, 128, 128))
90
- // The doc-space contract, where a y-flip or channel swap is obvious.
91
- try emit("quadrants", TestPdfs.quadrants(width: 200, height: 100), scale: 2)
92
- // All four rotations, drawn upright.
93
- for rotation in [0, 90, 180, 270] {
94
- try emit("rotate-\(rotation)", TestPdfs.rotated(rotation), scale: 0.25)
95
- }
96
- // An offset crop box, through both engines.
97
- try emit("cropbox-cg", TestPdfs.offsetCropBox(), scale: 1)
98
- try emit("cropbox-pdfkit", TestPdfs.offsetCropBox(), scale: 1, annotations: true)
99
- // The annotation flag, both ways.
100
- try emit("annots-off", TestPdfs.annotatedPage(), scale: 1, annotations: false)
101
- try emit("annots-on", TestPdfs.annotatedPage(), scale: 1, annotations: true)
102
- // A transparent background, so the alpha is visible rather than inferred.
103
- try emit(
104
- "transparent", TestPdfs.antialiasedWhiteEdge(), scale: 4,
105
- background: .transparent)
106
- // A page from a multi-page document, so `request.page` is visibly honoured.
107
- try emit(
108
- "page-2", TestPdfs.coloredPages([(1, 0, 0), (0, 1, 0), (0, 0, 1)],
109
- width: 120, height: 80), page: 2, scale: 1)
110
-
111
- print("PDFCANVAS-PREVIEWS \(dir.path)")
112
- for line in written { print(" \(line)") }
113
- }
114
- }