@reekon-tools/react-native-pdf-canvas 0.1.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/PdfCanvas.podspec +80 -0
- package/README.md +1391 -0
- package/android/build.gradle +175 -0
- package/android/consumer-rules.pro +3 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +2 -0
- package/android/gradlew +248 -0
- package/android/gradlew.bat +98 -0
- package/android/settings.gradle +34 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
- package/android/src/jsi/cpp/CMakeLists.txt +33 -0
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
- package/android/tools/compile-gate.sh +289 -0
- package/dist/cache.d.ts +161 -0
- package/dist/cache.js +389 -0
- package/dist/clock.d.ts +21 -0
- package/dist/clock.js +31 -0
- package/dist/controller.d.ts +131 -0
- package/dist/controller.js +1002 -0
- package/dist/geometry.d.ts +96 -0
- package/dist/geometry.js +248 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +50 -0
- package/dist/index.native.d.ts +21 -0
- package/dist/index.native.js +23 -0
- package/dist/index.web.d.ts +24 -0
- package/dist/index.web.js +26 -0
- package/dist/layout.d.ts +78 -0
- package/dist/layout.js +152 -0
- package/dist/peer-check.d.ts +83 -0
- package/dist/peer-check.js +183 -0
- package/dist/planner.d.ts +35 -0
- package/dist/planner.js +530 -0
- package/dist/policy.d.ts +14 -0
- package/dist/policy.js +168 -0
- package/dist/rasterizer/android.d.ts +121 -0
- package/dist/rasterizer/android.js +126 -0
- package/dist/rasterizer/buffer.d.ts +18 -0
- package/dist/rasterizer/buffer.js +18 -0
- package/dist/rasterizer/fake.d.ts +92 -0
- package/dist/rasterizer/fake.js +433 -0
- package/dist/rasterizer/index.d.ts +38 -0
- package/dist/rasterizer/index.js +69 -0
- package/dist/rasterizer/index.native.d.ts +19 -0
- package/dist/rasterizer/index.native.js +80 -0
- package/dist/rasterizer/index.web.d.ts +63 -0
- package/dist/rasterizer/index.web.js +75 -0
- package/dist/rasterizer/ingest.d.ts +180 -0
- package/dist/rasterizer/ingest.js +307 -0
- package/dist/rasterizer/ios.d.ts +84 -0
- package/dist/rasterizer/ios.js +155 -0
- package/dist/rasterizer/native-bridge.d.ts +261 -0
- package/dist/rasterizer/native-bridge.js +413 -0
- package/dist/rasterizer/web/client.d.ts +84 -0
- package/dist/rasterizer/web/client.js +359 -0
- package/dist/rasterizer/web/engine.d.ts +69 -0
- package/dist/rasterizer/web/engine.js +308 -0
- package/dist/rasterizer/web/index.d.ts +26 -0
- package/dist/rasterizer/web/index.js +21 -0
- package/dist/rasterizer/web/pdfium.d.ts +213 -0
- package/dist/rasterizer/web/pdfium.js +170 -0
- package/dist/rasterizer/web/protocol.d.ts +138 -0
- package/dist/rasterizer/web/protocol.js +37 -0
- package/dist/rasterizer/web/session.d.ts +106 -0
- package/dist/rasterizer/web/session.js +413 -0
- package/dist/rasterizer/web/worker.d.ts +44 -0
- package/dist/rasterizer/web/worker.js +76 -0
- package/dist/react/PdfContentView.d.ts +98 -0
- package/dist/react/PdfContentView.js +189 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +3 -0
- package/dist/react/usePdfDocument.d.ts +134 -0
- package/dist/react/usePdfDocument.js +275 -0
- package/dist/react/usePdfLayer.d.ts +94 -0
- package/dist/react/usePdfLayer.js +388 -0
- package/dist/skia/index.d.ts +26 -0
- package/dist/skia/index.js +25 -0
- package/dist/skia/index.native.d.ts +14 -0
- package/dist/skia/index.native.js +16 -0
- package/dist/skia/index.web.d.ts +12 -0
- package/dist/skia/index.web.js +14 -0
- package/dist/skia/resolve.native.d.ts +18 -0
- package/dist/skia/resolve.native.js +22 -0
- package/dist/skia/resolve.web.d.ts +41 -0
- package/dist/skia/resolve.web.js +62 -0
- package/dist/skia/skia-api.d.ts +94 -0
- package/dist/skia/skia-api.js +113 -0
- package/dist/testing/index.d.ts +27 -0
- package/dist/testing/index.js +28 -0
- package/dist/testing/scenes.d.ts +100 -0
- package/dist/testing/scenes.js +267 -0
- package/dist/types.d.ts +611 -0
- package/dist/types.js +21 -0
- package/dist/web-init.d.ts +68 -0
- package/dist/web-init.js +64 -0
- package/ios/Package.swift +58 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
- package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
- package/package.json +144 -0
- package/react-native.config.js +40 -0
- package/scripts/check-skia-peer.mjs +86 -0
- package/scripts/skia-peer-check.mjs +363 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
@testable import PdfCanvasCore
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
Numbers, printed rather than asserted.
|
|
8
|
+
|
|
9
|
+
THESE ARE NOT PASS/FAIL TESTS AND MUST NOT BECOME ONES. Their counterpart is
|
|
10
|
+
`PdfCanvasDeviceTimingTest` on Android, whose own lesson is written into
|
|
11
|
+
`AndroidRasterizerOptions.poolSize`: a synthetic fixture measured two lanes at
|
|
12
|
+
1.06x-1.25x while a real drawing measured 0.82x, and the synthetic number would
|
|
13
|
+
have shipped the wrong default. So what these produce is a SHAPE, on whatever
|
|
14
|
+
machine ran them, and the numbers that decide anything come from a device with a
|
|
15
|
+
real document on it — `app/dev-pdf-canvas-probe.tsx` in the consuming app.
|
|
16
|
+
|
|
17
|
+
The one thing they do assert is that nothing failed, because a timing loop that
|
|
18
|
+
silently rendered zero tiles would still print a very fast number.
|
|
19
|
+
*/
|
|
20
|
+
final class TimingTests: XCTestCase {
|
|
21
|
+
|
|
22
|
+
private func measure(_ label: String, _ body: () -> Void) -> Double {
|
|
23
|
+
var best = Double.infinity
|
|
24
|
+
for _ in 0..<5 {
|
|
25
|
+
let started = Date()
|
|
26
|
+
body()
|
|
27
|
+
best = min(best, Date().timeIntervalSince(started) * 1000)
|
|
28
|
+
}
|
|
29
|
+
print("PDFCANVAS-TIMING \(label): \(String(format: "%.2f", best)) ms")
|
|
30
|
+
return best
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
Per-tile cost at the sizes the planner actually asks for.
|
|
35
|
+
|
|
36
|
+
`maxPieceDimension` is 1024, so 512² and 1024² are the two that matter, and
|
|
37
|
+
`minEpochIntervalMs` is defined as the measured p95 of one of them.
|
|
38
|
+
*/
|
|
39
|
+
func testTileCostBySize() throws {
|
|
40
|
+
let r = try openFixture(
|
|
41
|
+
TestPdfs.grid(width: 1728, height: 2592, pitch: 8), name: "timing-tiles.pdf")
|
|
42
|
+
var rendered = 0
|
|
43
|
+
for side in [256, 512, 1024] {
|
|
44
|
+
let scale = 4.0
|
|
45
|
+
_ = measure("tile \(side)px @4x") {
|
|
46
|
+
if (try? r.render(
|
|
47
|
+
request(
|
|
48
|
+
x: 100, y: 100, width: Double(side) / scale, height: Double(side) / scale,
|
|
49
|
+
scale: scale),
|
|
50
|
+
cancellation: nil, sink: nil)) != nil
|
|
51
|
+
{
|
|
52
|
+
rendered += 1
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
XCTAssertEqual(rendered, 15, "every measured render must have succeeded")
|
|
57
|
+
r.close()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
A whole ARCH-D base raster at a plausible fit scale.
|
|
62
|
+
|
|
63
|
+
The base layer is `fitZoom * pixelRatio * baseQuality` over the whole sheet, so
|
|
64
|
+
on a DPR-3 phone an ARCH-D page is roughly 1170x1755 — which is what this
|
|
65
|
+
approximates. It is the single largest raster the package ever asks a backend
|
|
66
|
+
for, and therefore the one that sets the worst-case first paint.
|
|
67
|
+
*/
|
|
68
|
+
func testWholePageBaseRasterCost() throws {
|
|
69
|
+
let r = try openFixture(
|
|
70
|
+
TestPdfs.grid(width: 1728, height: 2592, pitch: 8), name: "timing-base.pdf")
|
|
71
|
+
var rendered = 0
|
|
72
|
+
_ = measure("ARCH-D base @0.677x (1170x1755)") {
|
|
73
|
+
if (try? r.render(
|
|
74
|
+
request(width: 1728, height: 2592, scale: 0.677), cancellation: nil,
|
|
75
|
+
sink: nil)) != nil
|
|
76
|
+
{
|
|
77
|
+
rendered += 1
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
XCTAssertEqual(rendered, 5)
|
|
81
|
+
r.close()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
Lane scaling, over the schedule the controller actually runs: N lanes pulling
|
|
86
|
+
from one shared cursor of plan items.
|
|
87
|
+
|
|
88
|
+
ON THIS MACHINE CoreGraphics parallelises — the probe measured 1.53x at two lanes
|
|
89
|
+
and 2.28x at four on a dense ARCH-D page, which is the OPPOSITE of Android, where
|
|
90
|
+
AOSP serialises `PdfRenderer` behind a process-wide PDFium lock that convoys and
|
|
91
|
+
every lane count above one measures worse. That difference is why
|
|
92
|
+
`IosRasterizerOptions.poolSize` does not cite any Android evidence.
|
|
93
|
+
*
|
|
94
|
+
IT IS STILL NOT A REASON TO RAISE THE DEFAULT. This is a Mac with a lot of cores
|
|
95
|
+
and a fixture whose tiles cost about a millisecond; a phone under thermal
|
|
96
|
+
pressure with a real drawing is a different measurement, and it is the one that
|
|
97
|
+
decides. Printed here so the shape is on record and so a device run has something
|
|
98
|
+
to be compared against.
|
|
99
|
+
*/
|
|
100
|
+
func testLaneScaling() throws {
|
|
101
|
+
let data = TestPdfs.grid(width: 1728, height: 2592, pitch: 4)
|
|
102
|
+
var items: [(Double, Double)] = []
|
|
103
|
+
for index in 0..<24 {
|
|
104
|
+
let column = Double(index % 6) * 256.0
|
|
105
|
+
let row = Double(index / 6) * 256.0
|
|
106
|
+
items.append((column, row))
|
|
107
|
+
}
|
|
108
|
+
var baseline = 0.0
|
|
109
|
+
for lanes in [1, 2, 4] {
|
|
110
|
+
let r = try openFixture(
|
|
111
|
+
data, name: "timing-lanes-\(lanes).pdf", poolSize: lanes)
|
|
112
|
+
let lock = NSLock()
|
|
113
|
+
var completed = 0
|
|
114
|
+
let ms = measure("\(lanes) lane(s), 24x 1024px tiles @4x") {
|
|
115
|
+
var cursor = 0
|
|
116
|
+
DispatchQueue.concurrentPerform(iterations: lanes) { _ in
|
|
117
|
+
while true {
|
|
118
|
+
lock.lock()
|
|
119
|
+
let index = cursor
|
|
120
|
+
cursor += 1
|
|
121
|
+
lock.unlock()
|
|
122
|
+
if index >= items.count { return }
|
|
123
|
+
let (x, y) = items[index]
|
|
124
|
+
if (try? r.render(
|
|
125
|
+
request(x: x, y: y, width: 256, height: 256, scale: 4),
|
|
126
|
+
cancellation: nil, sink: nil)) != nil
|
|
127
|
+
{
|
|
128
|
+
lock.lock()
|
|
129
|
+
completed += 1
|
|
130
|
+
lock.unlock()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if lanes == 1 { baseline = ms }
|
|
136
|
+
print(
|
|
137
|
+
"PDFCANVAS-TIMING \(lanes) lane(s) relative: "
|
|
138
|
+
+ String(format: "%.2fx", baseline / ms))
|
|
139
|
+
XCTAssertEqual(
|
|
140
|
+
completed, items.count * 5, "every tile in every repetition must land")
|
|
141
|
+
r.close()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
The two engines' relative cost, which is what makes `annotations: true` a
|
|
147
|
+
deliberate choice rather than a free one.
|
|
148
|
+
|
|
149
|
+
PDFKit has to build and keep a `PDFDocument` alongside the `CGPDFDocumentRef`, so
|
|
150
|
+
the FIRST annotated render in a lane pays for a second parse. That is why the
|
|
151
|
+
PDFKit document is lazy — every tile the controller asks for passes
|
|
152
|
+
`annotations: false` and never triggers it.
|
|
153
|
+
*/
|
|
154
|
+
func testEngineCostComparison() throws {
|
|
155
|
+
let r = try openFixture(
|
|
156
|
+
TestPdfs.grid(width: 1728, height: 2592, pitch: 8), name: "timing-engines.pdf")
|
|
157
|
+
var rendered = 0
|
|
158
|
+
// Warm both, so neither measurement includes its parse.
|
|
159
|
+
_ = try? r.render(
|
|
160
|
+
request(width: 200, height: 200, scale: 1, annotations: false),
|
|
161
|
+
cancellation: nil, sink: nil)
|
|
162
|
+
_ = try? r.render(
|
|
163
|
+
request(width: 200, height: 200, scale: 1, annotations: true),
|
|
164
|
+
cancellation: nil, sink: nil)
|
|
165
|
+
|
|
166
|
+
for annotations in [false, true] {
|
|
167
|
+
_ = measure("512px @4x annotations=\(annotations)") {
|
|
168
|
+
if (try? r.render(
|
|
169
|
+
request(width: 128, height: 128, scale: 4, annotations: annotations),
|
|
170
|
+
cancellation: nil, sink: nil)) != nil
|
|
171
|
+
{
|
|
172
|
+
rendered += 1
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
XCTAssertEqual(rendered, 10)
|
|
177
|
+
r.close()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import CoreGraphics
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
@testable import PdfCanvasCore
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
The three affine transforms, checked by mapping corners — no PDF, no bitmap, no
|
|
8
|
+
CoreGraphics state.
|
|
9
|
+
|
|
10
|
+
THIS FILE IS DELIBERATELY FIRST. Every failure mode of a transform produces a
|
|
11
|
+
plausible-looking image rather than an error: a rotation applied the wrong way
|
|
12
|
+
round, a crop-box origin left in, a y-flip half a pixel off. Checking them here
|
|
13
|
+
means a rendering failure further down is a rendering failure, and not this.
|
|
14
|
+
*/
|
|
15
|
+
final class TransformTests: XCTestCase {
|
|
16
|
+
|
|
17
|
+
/// A 200x100 box at the origin, so a swap of width and height is visible.
|
|
18
|
+
private let box = CGRect(x: 0, y: 0, width: 200, height: 100)
|
|
19
|
+
|
|
20
|
+
private func mapped(_ point: CGPoint, _ transform: CGAffineTransform) -> CGPoint {
|
|
21
|
+
point.applying(transform)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private func assertPoint(
|
|
25
|
+
_ actual: CGPoint, _ expected: CGPoint, _ what: String,
|
|
26
|
+
file: StaticString = #filePath, line: UInt = #line
|
|
27
|
+
) {
|
|
28
|
+
XCTAssertEqual(
|
|
29
|
+
Double(actual.x), Double(expected.x), accuracy: 1e-9,
|
|
30
|
+
"\(what): x", file: file, line: line)
|
|
31
|
+
XCTAssertEqual(
|
|
32
|
+
Double(actual.y), Double(expected.y), accuracy: 1e-9,
|
|
33
|
+
"\(what): y", file: file, line: line)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------- //
|
|
37
|
+
// PDF user space -> doc space
|
|
38
|
+
// ---------------------------------------------------------------- //
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
Rotation 0 is a pure y-flip through the box's top edge.
|
|
42
|
+
|
|
43
|
+
The PDF's bottom-left corner is doc space's bottom-LEFT, and the PDF's top-left
|
|
44
|
+
is doc (0, 0). An implementation that forgot the flip entirely would map
|
|
45
|
+
bottom-left to (0, 0), which is the single most likely mistake here.
|
|
46
|
+
*/
|
|
47
|
+
func testRotationZeroFlipsYAndNothingElse() {
|
|
48
|
+
let t = PdfCanvasPdfToDocTransform(box, 0)
|
|
49
|
+
assertPoint(mapped(CGPoint(x: 0, y: 100), t), CGPoint(x: 0, y: 0), "PDF top-left")
|
|
50
|
+
assertPoint(
|
|
51
|
+
mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 0, y: 100), "PDF bottom-left")
|
|
52
|
+
assertPoint(
|
|
53
|
+
mapped(CGPoint(x: 200, y: 100), t), CGPoint(x: 200, y: 0), "PDF top-right")
|
|
54
|
+
assertPoint(
|
|
55
|
+
mapped(CGPoint(x: 200, y: 0), t), CGPoint(x: 200, y: 100), "PDF bottom-right")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
`/Rotate 90` is a CLOCKWISE quarter turn of the displayed page.
|
|
60
|
+
|
|
61
|
+
Turning a sheet of paper clockwise sends its bottom-left corner to the top-left
|
|
62
|
+
and its bottom-right to the bottom-left. Those two are what distinguish 90 from
|
|
63
|
+
270; a test that only checked the page's SIZE would pass for either.
|
|
64
|
+
*/
|
|
65
|
+
func testRotation90TurnsClockwise() {
|
|
66
|
+
let t = PdfCanvasPdfToDocTransform(box, 90)
|
|
67
|
+
assertPoint(
|
|
68
|
+
mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 0, y: 0), "PDF bottom-left")
|
|
69
|
+
assertPoint(
|
|
70
|
+
mapped(CGPoint(x: 200, y: 0), t), CGPoint(x: 0, y: 200), "PDF bottom-right")
|
|
71
|
+
assertPoint(
|
|
72
|
+
mapped(CGPoint(x: 200, y: 100), t), CGPoint(x: 100, y: 200), "PDF top-right")
|
|
73
|
+
assertPoint(
|
|
74
|
+
mapped(CGPoint(x: 0, y: 100), t), CGPoint(x: 100, y: 0), "PDF top-left")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func testRotation180TurnsHalfway() {
|
|
78
|
+
let t = PdfCanvasPdfToDocTransform(box, 180)
|
|
79
|
+
assertPoint(
|
|
80
|
+
mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 200, y: 0), "PDF bottom-left")
|
|
81
|
+
assertPoint(
|
|
82
|
+
mapped(CGPoint(x: 0, y: 100), t), CGPoint(x: 200, y: 100), "PDF top-left")
|
|
83
|
+
assertPoint(
|
|
84
|
+
mapped(CGPoint(x: 200, y: 100), t), CGPoint(x: 0, y: 100), "PDF top-right")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func testRotation270TurnsCounterClockwise() {
|
|
88
|
+
let t = PdfCanvasPdfToDocTransform(box, 270)
|
|
89
|
+
assertPoint(
|
|
90
|
+
mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 100, y: 200), "PDF bottom-left")
|
|
91
|
+
assertPoint(
|
|
92
|
+
mapped(CGPoint(x: 0, y: 100), t), CGPoint(x: 0, y: 200), "PDF top-left")
|
|
93
|
+
assertPoint(
|
|
94
|
+
mapped(CGPoint(x: 200, y: 100), t), CGPoint(x: 0, y: 0), "PDF top-right")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
Every rotation is a reflection, not a rotation, in raw coordinates.
|
|
99
|
+
|
|
100
|
+
Doc space is y-down and PDF space is y-up, so a determinant of +1 anywhere here
|
|
101
|
+
would mean the flip had been lost. Cheap, and it catches a whole class of sign
|
|
102
|
+
errors that the corner checks above could miss if two of them were wrong in
|
|
103
|
+
compensating ways.
|
|
104
|
+
*/
|
|
105
|
+
func testEveryRotationHasDeterminantMinusOne() {
|
|
106
|
+
for rotation in [0, 90, 180, 270] {
|
|
107
|
+
let t = PdfCanvasPdfToDocTransform(box, rotation)
|
|
108
|
+
XCTAssertEqual(
|
|
109
|
+
Double(t.a * t.d - t.b * t.c), -1, accuracy: 1e-9,
|
|
110
|
+
"rotation \(rotation) should be orientation-reversing")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
The crop box's own top-left lands at doc (0, 0), whatever its origin is.
|
|
116
|
+
|
|
117
|
+
The fixture this mirrors has MediaBox `0 0 400 400` and CropBox
|
|
118
|
+
`100 150 300 350`. Without the `-minX` / `minY + height` terms the marker at
|
|
119
|
+
the crop box's top-left would be drawn 100pt right and 50pt down, which on a
|
|
120
|
+
flat page is invisible.
|
|
121
|
+
*/
|
|
122
|
+
func testCropBoxOriginIsSubtracted() {
|
|
123
|
+
let crop = CGRect(x: 100, y: 150, width: 200, height: 200)
|
|
124
|
+
let t = PdfCanvasPdfToDocTransform(crop, 0)
|
|
125
|
+
assertPoint(
|
|
126
|
+
mapped(CGPoint(x: 100, y: 350), t), CGPoint(x: 0, y: 0), "crop top-left")
|
|
127
|
+
assertPoint(
|
|
128
|
+
mapped(CGPoint(x: 300, y: 150), t), CGPoint(x: 200, y: 200),
|
|
129
|
+
"crop bottom-right")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// The same, for a rotated page — where the two corrections interact.
|
|
133
|
+
func testCropBoxOriginIsSubtractedUnderRotation() {
|
|
134
|
+
let crop = CGRect(x: 100, y: 150, width: 200, height: 100)
|
|
135
|
+
// Rotated 90, the page is 100 wide and 200 tall.
|
|
136
|
+
let t = PdfCanvasPdfToDocTransform(crop, 90)
|
|
137
|
+
assertPoint(
|
|
138
|
+
mapped(CGPoint(x: 100, y: 150), t), CGPoint(x: 0, y: 0),
|
|
139
|
+
"crop bottom-left -> doc top-left")
|
|
140
|
+
assertPoint(
|
|
141
|
+
mapped(CGPoint(x: 300, y: 150), t), CGPoint(x: 0, y: 200),
|
|
142
|
+
"crop bottom-right -> doc bottom-left")
|
|
143
|
+
assertPoint(
|
|
144
|
+
mapped(CGPoint(x: 100, y: 250), t), CGPoint(x: 100, y: 0),
|
|
145
|
+
"crop top-left -> doc top-right")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// A rotation that is not a multiple of 90 is treated as 0 rather than rounded.
|
|
149
|
+
func testUnusableRotationsFallBackToZero() {
|
|
150
|
+
let zero = PdfCanvasPdfToDocTransform(box, 0)
|
|
151
|
+
for rotation in [45, 1, 359, 137] {
|
|
152
|
+
XCTAssertTrue(
|
|
153
|
+
PdfCanvasPdfToDocTransform(box, rotation) == zero,
|
|
154
|
+
"rotation \(rotation) should behave as 0")
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Negatives and out-of-range multiples normalise rather than falling back.
|
|
159
|
+
func testRotationsNormalise() {
|
|
160
|
+
XCTAssertEqual(PdfCanvasNormalizeRotation(-90), 270)
|
|
161
|
+
XCTAssertEqual(PdfCanvasNormalizeRotation(450), 90)
|
|
162
|
+
XCTAssertEqual(PdfCanvasNormalizeRotation(-450), 270)
|
|
163
|
+
XCTAssertEqual(PdfCanvasNormalizeRotation(720), 0)
|
|
164
|
+
XCTAssertEqual(PdfCanvasNormalizeRotation(45), 0)
|
|
165
|
+
XCTAssertTrue(
|
|
166
|
+
PdfCanvasPdfToDocTransform(box, -90) == PdfCanvasPdfToDocTransform(box, 270),
|
|
167
|
+
"-90 should behave as 270")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------- //
|
|
171
|
+
// Doc space -> raster space
|
|
172
|
+
// ---------------------------------------------------------------- //
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
The doc rect's top-left lands at the raster's top-left, and its bottom-right at
|
|
176
|
+
the bottom-right.
|
|
177
|
+
|
|
178
|
+
Stated in CG's own y-up coordinates, because that is what the transform emits:
|
|
179
|
+
the raster's top-left is (0, H).
|
|
180
|
+
*/
|
|
181
|
+
func testDocRectMapsOntoTheWholeRaster() {
|
|
182
|
+
let t = PdfCanvasDocToRasterTransform(37, 23, 4, 164)
|
|
183
|
+
assertPoint(
|
|
184
|
+
mapped(CGPoint(x: 37, y: 23), t), CGPoint(x: 0, y: 164), "doc top-left")
|
|
185
|
+
assertPoint(
|
|
186
|
+
mapped(CGPoint(x: 37 + 64, y: 23 + 41), t), CGPoint(x: 256, y: 0),
|
|
187
|
+
"doc bottom-right")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
TWO RASTERS AT THE SAME SCALE AGREE WHERE THEY OVERLAP — the property the cache
|
|
192
|
+
and the overlapping detail pieces depend on, and the one the rounded
|
|
193
|
+
`pixelHeight` in the flip looks like it should break.
|
|
194
|
+
*
|
|
195
|
+
The two rects below have heights that round in OPPOSITE directions at scale 1
|
|
196
|
+
(100.4 -> 100, 100.6 -> 101), so a transform that leaked its rounding into
|
|
197
|
+
content placement would put the same doc point on different rows. The check is
|
|
198
|
+
in ROWS FROM THE TOP, which is how a raster is indexed and where the `H` term
|
|
199
|
+
cancels.
|
|
200
|
+
*/
|
|
201
|
+
func testRastersAtTheSameScaleAgreeWhereTheyOverlap() {
|
|
202
|
+
let scale = 1.0
|
|
203
|
+
// Rect A: doc y 0 .. 100.4, 100 rows. Rect B: doc y 50 .. 150.6, 101 rows.
|
|
204
|
+
let a = PdfCanvasDocToRasterTransform(0, 0, scale, 100)
|
|
205
|
+
let b = PdfCanvasDocToRasterTransform(0, 50, scale, 101)
|
|
206
|
+
|
|
207
|
+
for docY in stride(from: 50.0, through: 100.0, by: 5.0) {
|
|
208
|
+
let rowFromTopInA = 100 - Double(mapped(CGPoint(x: 0, y: docY), a).y)
|
|
209
|
+
let rowFromTopInB = 101 - Double(mapped(CGPoint(x: 0, y: docY), b).y)
|
|
210
|
+
XCTAssertEqual(
|
|
211
|
+
rowFromTopInA, docY, accuracy: 1e-9,
|
|
212
|
+
"doc y \(docY) should sit at row \(docY) of A")
|
|
213
|
+
XCTAssertEqual(
|
|
214
|
+
rowFromTopInB, docY - 50, accuracy: 1e-9,
|
|
215
|
+
"doc y \(docY) should sit at row \(docY - 50) of B")
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
Scale is used exactly as requested, never re-derived from the pixel size.
|
|
221
|
+
|
|
222
|
+
A doc rect of 512.4 points at scale 4 is 2050 pixels wide (2049.6 rounded), and
|
|
223
|
+
a re-derived scale would be 2050/512.4 = 4.00078. Over 2050 pixels that is a
|
|
224
|
+
drift of 1.6 px at the far edge relative to a neighbour drawn at exactly 4 —
|
|
225
|
+
which is precisely the misregistration the overlap rule cannot absorb.
|
|
226
|
+
*/
|
|
227
|
+
func testScaleIsNotReDerivedFromThePixelSize() {
|
|
228
|
+
let scale = 4.0
|
|
229
|
+
let docWidth = 512.4
|
|
230
|
+
let r = request(width: docWidth, height: 100, scale: scale)
|
|
231
|
+
XCTAssertEqual(r.pixelWidth, 2050, "512.4 * 4 = 2049.6, rounds to 2050")
|
|
232
|
+
|
|
233
|
+
let t = PdfCanvasDocToRasterTransform(0, 0, scale, r.pixelHeight)
|
|
234
|
+
// The far edge lands 0.4 px short of the raster's right edge — that is the
|
|
235
|
+
// half-pixel the comment on `tileMatrix` describes, left showing the fill.
|
|
236
|
+
XCTAssertEqual(
|
|
237
|
+
Double(mapped(CGPoint(x: docWidth, y: 0), t).x), 2049.6, accuracy: 1e-9)
|
|
238
|
+
// A re-derived scale would have landed it at exactly 2050, agreeing with the
|
|
239
|
+
// raster and disagreeing with every neighbour.
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------- //
|
|
243
|
+
// Doc space -> PDFKit's space
|
|
244
|
+
// ---------------------------------------------------------------- //
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
PDFKit's transform is a pure flip through the POST-ROTATION page height.
|
|
248
|
+
|
|
249
|
+
Nothing else, because PDFKit applies `/Rotate` and the crop-box origin itself —
|
|
250
|
+
measured, and the asymmetry with `-[PDFPage boundsForBox:]` is the trap this
|
|
251
|
+
whole function exists to keep out of the rasterizer.
|
|
252
|
+
*/
|
|
253
|
+
func testPdfKitTransformIsAPureFlip() {
|
|
254
|
+
let t = PdfCanvasDocToPdfKitTransform(612)
|
|
255
|
+
assertPoint(mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 0, y: 612), "doc origin")
|
|
256
|
+
assertPoint(
|
|
257
|
+
mapped(CGPoint(x: 792, y: 612), t), CGPoint(x: 792, y: 0),
|
|
258
|
+
"doc bottom-right")
|
|
259
|
+
XCTAssertEqual(Double(t.a * t.d - t.b * t.c), -1, accuracy: 1e-9)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------- //
|
|
263
|
+
// Pixel sizing
|
|
264
|
+
// ---------------------------------------------------------------- //
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
`round`, at least 1, matching `planner.ts` and `RasterRequest.java`.
|
|
268
|
+
|
|
269
|
+
The three have to agree exactly: the planner declares the pixel size in the
|
|
270
|
+
`RasterPlanItem` the cache is keyed on, so a backend that used `ceil` would
|
|
271
|
+
return a raster of a different size than the plan promised.
|
|
272
|
+
*/
|
|
273
|
+
func testPixelExtentsRoundAndFloorAtOne() {
|
|
274
|
+
XCTAssertEqual(request(width: 100, height: 100, scale: 1).pixelWidth, 100)
|
|
275
|
+
XCTAssertEqual(request(width: 100.4, height: 1, scale: 1).pixelWidth, 100)
|
|
276
|
+
XCTAssertEqual(request(width: 100.5, height: 1, scale: 1).pixelWidth, 101)
|
|
277
|
+
XCTAssertEqual(request(width: 100.6, height: 1, scale: 1).pixelWidth, 101)
|
|
278
|
+
XCTAssertEqual(request(width: 0.1, height: 0.1, scale: 1).pixelWidth, 1)
|
|
279
|
+
XCTAssertEqual(request(width: 0.1, height: 0.1, scale: 1).pixelHeight, 1)
|
|
280
|
+
XCTAssertEqual(request(width: 512.4, height: 1, scale: 4).pixelWidth, 2050)
|
|
281
|
+
XCTAssertEqual(request(width: 512.5, height: 1, scale: 4).pixelWidth, 2050)
|
|
282
|
+
}
|
|
283
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reekon-tools/react-native-pdf-canvas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PDF pages as Skia images, drawn at exact doc-space rects inside somebody else's transform. Owns no canvas, no gestures, no viewport state.",
|
|
5
|
+
"author": "REEKON Tools",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://gitlab.com/reekon-tools/software/rock-pro/react-native-pdf-canvas.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"//files-scripts": "`scripts` ships because the peer-floor check runs from a CONSUMER'S postinstall, before/without any build of `dist`.",
|
|
17
|
+
"//files": "`*.podspec` SHIPS OR iOS IS SILENTLY ABSENT. Autolinking reads the podspec out of the INSTALLED package, so a podspec that is not in this list means `pod install` finds nothing, `PdfCanvas` never appears in Podfile.lock, and `NativeModules.PdfCanvas` is undefined at runtime with no build error anywhere. `ios/Package.swift` and `ios/Tests` ship as part of `ios` and cost a few KB; they are what lets a consumer run the core's host tests against the exact version they installed.",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"ios",
|
|
21
|
+
"android",
|
|
22
|
+
"scripts",
|
|
23
|
+
"react-native.config.js",
|
|
24
|
+
"PdfCanvas.podspec",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"bin": {
|
|
28
|
+
"pdf-canvas-check-skia": "./scripts/check-skia-peer.mjs"
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.web.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"//exports": "`./skia` IS THE ONLY SUBPATH THAT NEEDS A LIVE SKIA RUNTIME, and the root barrel is deliberately free of module-scope Skia so it can be evaluated in Node during rock-desktop's SSR pass — `Skia.web.js` is `export const Skia = JsiSkApi(global.CanvasKit)`, taken ONCE, so reaching it before LoadSkiaWeb resolves breaks it permanently and unrecoverably in-bundle. `src/__tests__/module-ordering.test.ts` bundles the entries below and greps the output, so this is enforced rather than described. `./skia` is platform-split for the same Metro reason `./rasterizer` is (an `exports` condition is matched LITERALLY, with no platform-extension expansion). BOTH of those subpaths put `types` INSIDE each condition branch and carry NO top-level `types` key, and that placement is load-bearing rather than stylistic: TypeScript walks a conditional-exports object IN KEY ORDER and always matches a bare `types`, so a top-level `types` short-circuits the walk and hands EVERY consumer the same declaration file no matter what conditions are configured — a host's `customConditions` cannot rescue it. That is not cosmetic where the two entries genuinely differ: it made `configureWebPdfium` invisible to a browser consumer (TS2305) and dragged React Native's global declarations into a DOM project behind it. `attw` reports 🟢 either way, because it checks that resolution SUCCEEDS and not that it lands on the right platform. The root `.` keeps a single top-level `types` on purpose — its two entries are pure re-exports of the same barrel, so there is only one declaration to point at. `./web-init` is WEB ONLY and has no `react-native` condition on purpose: there is nothing to initialise on native, and a native import should fail loudly rather than resolve to a no-op. `./worker` is the same, and is a bundler ENTRY POINT rather than a module to import for its values — it binds itself to `self` on evaluation, and a host names it once as a worker (`'…/worker?worker'` under Vite, `new Worker(new URL(…), {type: 'module'})` anywhere else). It is the ONLY file in the package that imports `@embedpdf/pdfium`, which is exactly why that peer can be optional: a native-only consumer never resolves this path. NOTE that a `//comment` key may not go INSIDE the exports object — node rejects an exports map whose keys do not all begin with '.'.",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"react-native": "./dist/index.native.js",
|
|
37
|
+
"browser": "./dist/index.web.js",
|
|
38
|
+
"default": "./dist/index.web.js"
|
|
39
|
+
},
|
|
40
|
+
"./rasterizer": {
|
|
41
|
+
"react-native": {
|
|
42
|
+
"types": "./dist/rasterizer/index.native.d.ts",
|
|
43
|
+
"default": "./dist/rasterizer/index.native.js"
|
|
44
|
+
},
|
|
45
|
+
"browser": {
|
|
46
|
+
"types": "./dist/rasterizer/index.web.d.ts",
|
|
47
|
+
"default": "./dist/rasterizer/index.web.js"
|
|
48
|
+
},
|
|
49
|
+
"default": {
|
|
50
|
+
"types": "./dist/rasterizer/index.web.d.ts",
|
|
51
|
+
"default": "./dist/rasterizer/index.web.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"./skia": {
|
|
55
|
+
"react-native": {
|
|
56
|
+
"types": "./dist/skia/index.native.d.ts",
|
|
57
|
+
"default": "./dist/skia/index.native.js"
|
|
58
|
+
},
|
|
59
|
+
"browser": {
|
|
60
|
+
"types": "./dist/skia/index.web.d.ts",
|
|
61
|
+
"default": "./dist/skia/index.web.js"
|
|
62
|
+
},
|
|
63
|
+
"default": {
|
|
64
|
+
"types": "./dist/skia/index.web.d.ts",
|
|
65
|
+
"default": "./dist/skia/index.web.js"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"./web-init": {
|
|
69
|
+
"types": "./dist/web-init.d.ts",
|
|
70
|
+
"default": "./dist/web-init.js"
|
|
71
|
+
},
|
|
72
|
+
"./worker": {
|
|
73
|
+
"types": "./dist/rasterizer/web/worker.d.ts",
|
|
74
|
+
"default": "./dist/rasterizer/web/worker.js"
|
|
75
|
+
},
|
|
76
|
+
"./testing": {
|
|
77
|
+
"types": "./dist/testing/index.d.ts",
|
|
78
|
+
"default": "./dist/testing/index.js"
|
|
79
|
+
},
|
|
80
|
+
"./package.json": "./package.json"
|
|
81
|
+
},
|
|
82
|
+
"scripts": {
|
|
83
|
+
"build": "tsc",
|
|
84
|
+
"dev": "tsc --watch --preserveWatchOutput",
|
|
85
|
+
"prepare": "tsc",
|
|
86
|
+
"prepack": "yarn run build",
|
|
87
|
+
"typecheck": "tsc --noEmit",
|
|
88
|
+
"test": "vitest",
|
|
89
|
+
"test:run": "vitest run",
|
|
90
|
+
"format": "prettier --write .",
|
|
91
|
+
"check-format": "prettier --check .",
|
|
92
|
+
"check-exports": "attw --pack . --profile esm-only",
|
|
93
|
+
"check-peers": "node scripts/check-skia-peer.mjs",
|
|
94
|
+
"ci": "yarn run check-format && yarn run typecheck && vitest run && yarn run build"
|
|
95
|
+
},
|
|
96
|
+
"keywords": [
|
|
97
|
+
"react-native",
|
|
98
|
+
"pdf",
|
|
99
|
+
"skia",
|
|
100
|
+
"reanimated"
|
|
101
|
+
],
|
|
102
|
+
"//peerDependencies": "THE SKIA RANGE IS THE SINGLE SOURCE OF TRUTH FOR THE FLOOR — scripts/skia-peer-check.mjs and src/peer-check.ts both parse it out of here rather than restating 2.8.0, so a bump is this one line. Keep it a bare '>=x.y.z': the checker refuses to guess at a union or a caret and degrades to a warning. 2.8.0 and not 2.11.0 because 2.8.0 is the exact release carrying the RN Web reconciler dispose fix (verified: Container.web.js goes 1 -> 6 dispose calls and Recorder/FrameScope.js first appears AT 2.8.0, not 2.7.0), and the entire web draw path this package touches — Container.web.js, Reconciler.js, HostConfig.js, FrameScope.js, web JsiSkDataFactory, the MakeImage body of web JsiSkImageFactory, and the ColorType/AlphaType enums — is BYTE-IDENTICAL from 2.8.0 through 2.11.0, with canvaskit-wasm pinned 0.41.0 across the whole range. Nothing after 2.8.0 is required. The floor is a memory-correctness claim, not a compile one: the full suite passes against 2.6.2 and 2.8.0 alike. BOLDR now pins 2.8.0 and satisfies it (it pinned 2.6.2 and did not, until the bump) — see 'Why the skia peer floor is >=2.8.0' in README.md for how the floor is enforced given that yarn 1's link: protocol evaluates neither a linked package's peers nor its lifecycle scripts.",
|
|
103
|
+
"//embedpdf-peer": "OPTIONAL, AND WEB ONLY. `@embedpdf/pdfium` is imported by exactly one file — `src/rasterizer/web/worker.ts`, reached through the `./worker` subpath — so a consumer who ships native never resolves it and must not be made to install a 4.6 MB wasm package. It is a PEER rather than a dependency for the reason the note above gives: the package must stay dependency-free, and yarn 1 installs none of a `link:` dependency's deps anyway. The range is `>=2.15.0` because that is the version every measurement in `src/rasterizer/web/` was taken against, and because `EPDF_GetPageRotationByIndex` — how `pageGeometry` reads `/Rotate` without loading a page — is an @embedpdf extension rather than stock PDFium. The wasm and the JS bindings must come from the SAME version, which is why the host supplies the locator (see `PdfiumWasmSource`).",
|
|
104
|
+
"peerDependencies": {
|
|
105
|
+
"@embedpdf/pdfium": ">=2.15.0",
|
|
106
|
+
"@shopify/react-native-skia": ">=2.8.0",
|
|
107
|
+
"react": ">=19.0.0",
|
|
108
|
+
"react-native": ">=0.78.0",
|
|
109
|
+
"react-native-reanimated": ">=3.19.1"
|
|
110
|
+
},
|
|
111
|
+
"peerDependenciesMeta": {
|
|
112
|
+
"@embedpdf/pdfium": {
|
|
113
|
+
"optional": true
|
|
114
|
+
},
|
|
115
|
+
"react-native": {
|
|
116
|
+
"optional": true
|
|
117
|
+
},
|
|
118
|
+
"react-native-reanimated": {
|
|
119
|
+
"optional": true
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"//devDependencies": "`vite` is DECLARED even though vitest already brings it: `src/__tests__/module-ordering.test.ts` imports its `build` API directly to bundle each entry point and inspect the module graph, and a test that depends on a transitive dep is one hoisting change away from breaking. The four versions the app ships are EXACT, not carets. A caret is what caused the skew this alignment fixed: '^2.6.2' had drifted to 2.11.0 while BOLDR ran 2.6.2, so 171 tests — every CanvasKit pixel test among them — had been validating a version the app does not ship. A library validated against a floating version is not validated. `semver` is here and NOT in `dependencies` on purpose: the package must stay dependency-free (yarn 1 installs none of a `link:` dependency's deps anyway, so declaring it would buy nothing for BOLDR), so the peer check carries its own comparator and uses semver only when a tree happens to expose one. semver's real job here is to be the REFEREE in peer-check.test.ts, which pins that comparator and the runtime one to its answers.",
|
|
123
|
+
"devDependencies": {
|
|
124
|
+
"@arethetypeswrong/cli": "^0.18.1",
|
|
125
|
+
"@embedpdf/pdfium": "2.15.0",
|
|
126
|
+
"@shopify/react-native-skia": "2.11.0",
|
|
127
|
+
"@types/emscripten": "^1.41.5",
|
|
128
|
+
"@types/node": "20.19.43",
|
|
129
|
+
"@types/react": "19.2.17",
|
|
130
|
+
"prettier": "^3.5.3",
|
|
131
|
+
"react": "19.2.3",
|
|
132
|
+
"react-native": "0.86.2",
|
|
133
|
+
"react-native-reanimated": "4.5.3",
|
|
134
|
+
"semver": "^7.7.4",
|
|
135
|
+
"typescript": "^5.8.3",
|
|
136
|
+
"vite": "^7.3.6",
|
|
137
|
+
"vitest": "^3.1.3"
|
|
138
|
+
},
|
|
139
|
+
"//engines": "Copied from BOLDR's own engines field rather than invented. @types/node tracks the LOWEST supported major (20.x), not the v24 this machine runs, so tsc rejects a Node 22+/24-only API that the declared floor could not provide.",
|
|
140
|
+
"engines": {
|
|
141
|
+
"node": "^20.19.4 || ^22.13.0 || >=24"
|
|
142
|
+
},
|
|
143
|
+
"packageManager": "yarn@1.22.22"
|
|
144
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autolinking metadata for the React Native CLI.
|
|
3
|
+
*
|
|
4
|
+
* BOTH PLATFORMS NEED AN EXPLICIT ENTRY, for two different reasons.
|
|
5
|
+
*
|
|
6
|
+
* ANDROID: `packageImportPath` / `packageInstance` are not optional here, because
|
|
7
|
+
* of the source-set split. Autolinking discovers a ReactPackage by scanning
|
|
8
|
+
* `android/src/main/java`, and this package's binding deliberately lives in
|
|
9
|
+
* `android/src/reactnative/java` so that `src/main` can be built and tested with no
|
|
10
|
+
* React Native on the classpath at all. Without these two lines the module compiles
|
|
11
|
+
* into the app and is never registered, and every `open()` fails with "the Android
|
|
12
|
+
* PdfCanvas native module was not found".
|
|
13
|
+
*
|
|
14
|
+
* iOS: `podspecPath` is named explicitly because the podspec is `PdfCanvas.podspec`
|
|
15
|
+
* rather than a file named after the npm package. Autolinking's default search
|
|
16
|
+
* takes the first `*.podspec` at the package root, which happens to work today —
|
|
17
|
+
* there is exactly one — but "happens to work because there is only one candidate"
|
|
18
|
+
* is the kind of thing that stops being true the moment somebody adds a second, and
|
|
19
|
+
* the failure is silent: the pod is simply absent from `Podfile.lock` and
|
|
20
|
+
* `NativeModules.PdfCanvas` is undefined at runtime.
|
|
21
|
+
*
|
|
22
|
+
* `ios` was `null` until the CoreGraphics/PDFKit backend landed. If it ever needs
|
|
23
|
+
* to be null again, the message in `src/rasterizer/index.ts` is what a host sees.
|
|
24
|
+
*/
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
dependency: {
|
|
29
|
+
platforms: {
|
|
30
|
+
android: {
|
|
31
|
+
sourceDir: 'android',
|
|
32
|
+
packageImportPath: 'import tools.reekon.pdfcanvas.rn.PdfCanvasPackage;',
|
|
33
|
+
packageInstance: 'new PdfCanvasPackage()',
|
|
34
|
+
},
|
|
35
|
+
ios: {
|
|
36
|
+
podspecPath: path.join(__dirname, 'PdfCanvas.podspec'),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|