@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.
Files changed (161) hide show
  1. package/PdfCanvas.podspec +80 -0
  2. package/README.md +1391 -0
  3. package/android/build.gradle +175 -0
  4. package/android/consumer-rules.pro +3 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +2 -0
  8. package/android/gradlew +248 -0
  9. package/android/gradlew.bat +98 -0
  10. package/android/settings.gradle +34 -0
  11. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
  12. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
  13. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
  14. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
  15. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
  16. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
  17. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
  18. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
  19. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
  20. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
  21. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
  22. package/android/src/jsi/cpp/CMakeLists.txt +33 -0
  23. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
  24. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
  25. package/android/src/main/AndroidManifest.xml +7 -0
  26. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
  27. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
  28. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
  29. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
  30. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
  31. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
  32. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
  33. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
  34. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
  35. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
  36. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
  37. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
  38. package/android/tools/compile-gate.sh +289 -0
  39. package/dist/cache.d.ts +161 -0
  40. package/dist/cache.js +389 -0
  41. package/dist/clock.d.ts +21 -0
  42. package/dist/clock.js +31 -0
  43. package/dist/controller.d.ts +131 -0
  44. package/dist/controller.js +1002 -0
  45. package/dist/geometry.d.ts +96 -0
  46. package/dist/geometry.js +248 -0
  47. package/dist/index.d.ts +52 -0
  48. package/dist/index.js +50 -0
  49. package/dist/index.native.d.ts +21 -0
  50. package/dist/index.native.js +23 -0
  51. package/dist/index.web.d.ts +24 -0
  52. package/dist/index.web.js +26 -0
  53. package/dist/layout.d.ts +78 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/peer-check.d.ts +83 -0
  56. package/dist/peer-check.js +183 -0
  57. package/dist/planner.d.ts +35 -0
  58. package/dist/planner.js +530 -0
  59. package/dist/policy.d.ts +14 -0
  60. package/dist/policy.js +168 -0
  61. package/dist/rasterizer/android.d.ts +121 -0
  62. package/dist/rasterizer/android.js +126 -0
  63. package/dist/rasterizer/buffer.d.ts +18 -0
  64. package/dist/rasterizer/buffer.js +18 -0
  65. package/dist/rasterizer/fake.d.ts +92 -0
  66. package/dist/rasterizer/fake.js +433 -0
  67. package/dist/rasterizer/index.d.ts +38 -0
  68. package/dist/rasterizer/index.js +69 -0
  69. package/dist/rasterizer/index.native.d.ts +19 -0
  70. package/dist/rasterizer/index.native.js +80 -0
  71. package/dist/rasterizer/index.web.d.ts +63 -0
  72. package/dist/rasterizer/index.web.js +75 -0
  73. package/dist/rasterizer/ingest.d.ts +180 -0
  74. package/dist/rasterizer/ingest.js +307 -0
  75. package/dist/rasterizer/ios.d.ts +84 -0
  76. package/dist/rasterizer/ios.js +155 -0
  77. package/dist/rasterizer/native-bridge.d.ts +261 -0
  78. package/dist/rasterizer/native-bridge.js +413 -0
  79. package/dist/rasterizer/web/client.d.ts +84 -0
  80. package/dist/rasterizer/web/client.js +359 -0
  81. package/dist/rasterizer/web/engine.d.ts +69 -0
  82. package/dist/rasterizer/web/engine.js +308 -0
  83. package/dist/rasterizer/web/index.d.ts +26 -0
  84. package/dist/rasterizer/web/index.js +21 -0
  85. package/dist/rasterizer/web/pdfium.d.ts +213 -0
  86. package/dist/rasterizer/web/pdfium.js +170 -0
  87. package/dist/rasterizer/web/protocol.d.ts +138 -0
  88. package/dist/rasterizer/web/protocol.js +37 -0
  89. package/dist/rasterizer/web/session.d.ts +106 -0
  90. package/dist/rasterizer/web/session.js +413 -0
  91. package/dist/rasterizer/web/worker.d.ts +44 -0
  92. package/dist/rasterizer/web/worker.js +76 -0
  93. package/dist/react/PdfContentView.d.ts +98 -0
  94. package/dist/react/PdfContentView.js +189 -0
  95. package/dist/react/index.d.ts +6 -0
  96. package/dist/react/index.js +3 -0
  97. package/dist/react/usePdfDocument.d.ts +134 -0
  98. package/dist/react/usePdfDocument.js +275 -0
  99. package/dist/react/usePdfLayer.d.ts +94 -0
  100. package/dist/react/usePdfLayer.js +388 -0
  101. package/dist/skia/index.d.ts +26 -0
  102. package/dist/skia/index.js +25 -0
  103. package/dist/skia/index.native.d.ts +14 -0
  104. package/dist/skia/index.native.js +16 -0
  105. package/dist/skia/index.web.d.ts +12 -0
  106. package/dist/skia/index.web.js +14 -0
  107. package/dist/skia/resolve.native.d.ts +18 -0
  108. package/dist/skia/resolve.native.js +22 -0
  109. package/dist/skia/resolve.web.d.ts +41 -0
  110. package/dist/skia/resolve.web.js +62 -0
  111. package/dist/skia/skia-api.d.ts +94 -0
  112. package/dist/skia/skia-api.js +113 -0
  113. package/dist/testing/index.d.ts +27 -0
  114. package/dist/testing/index.js +28 -0
  115. package/dist/testing/scenes.d.ts +100 -0
  116. package/dist/testing/scenes.js +267 -0
  117. package/dist/types.d.ts +611 -0
  118. package/dist/types.js +21 -0
  119. package/dist/web-init.d.ts +68 -0
  120. package/dist/web-init.js +64 -0
  121. package/ios/Package.swift +58 -0
  122. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
  123. package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
  124. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
  125. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
  126. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
  127. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
  128. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
  129. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
  130. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
  131. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
  132. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
  133. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
  134. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
  135. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
  136. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
  137. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
  138. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
  139. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
  140. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
  141. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
  142. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
  143. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
  144. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
  145. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
  146. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
  147. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
  148. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
  149. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
  150. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
  151. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
  152. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
  153. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
  154. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
  155. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
  156. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
  157. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
  158. package/package.json +144 -0
  159. package/react-native.config.js +40 -0
  160. package/scripts/check-skia-peer.mjs +86 -0
  161. package/scripts/skia-peer-check.mjs +363 -0
package/dist/layout.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Page layouts: pure functions from page sizes to doc-space rects.
3
+ *
4
+ * A layout describes where pages SIT, and says nothing about what is visible
5
+ * or what is rastered — it returns a rect for every page, always, so the host
6
+ * can hit-test and scroll to a page that has never been rendered.
7
+ *
8
+ * Each export is a factory so the tuning parameter (`gap`) is captured once
9
+ * and the resulting `PdfPageLayout` stays referentially stable in a host that
10
+ * memoizes on it.
11
+ *
12
+ * INVARIANT every layout upholds: a page's rect has exactly the page's own
13
+ * post-rotation extents. Layouts position pages; they never resize them. The
14
+ * planner derives raster scale from the rect, so a layout that scaled a page
15
+ * would silently rescale its bitmap.
16
+ */
17
+ import { isEmptyRect, rectIntersects } from './geometry.js';
18
+ /**
19
+ * Every page at the origin, stacked on top of each other.
20
+ *
21
+ * Overlap is the point: in a paged viewer, doc space contains ONE page and the
22
+ * host swaps which page index it draws. Page 4 having the same rect as page 0
23
+ * is what lets the host page forward without translating its world transform.
24
+ *
25
+ * PAIR IT WITH A ONE-PAGE SELECTION — `usePdfLayer({layout: singlePage(),
26
+ * pages: [n]})` — AND NOTHING ELSE. That is not a style preference; it is the
27
+ * only configuration in which a stacked layout is correct, and the reason is
28
+ * that "which pages may hold rasters" is the only thing standing between this
29
+ * layout and every page in the document:
30
+ *
31
+ * - The raster planner decides what is on screen by intersecting the viewport
32
+ * with each page rect. Here every page rect IS the viewport, so with the
33
+ * on-screen selection a 400-page document plans 400 bases — measured at
34
+ * exactly N times the bytes of one page in `__tests__/single-page.test.ts`.
35
+ * - `PdfContentView` draws pages in index order at the rects `content` carries,
36
+ * so those bases land on top of each other and the highest index wins. The
37
+ * cost is not merely memory: the wrong page is on screen.
38
+ *
39
+ * With `pages: [n]` none of that can happen — no page outside the allow-list is
40
+ * ever rastered, so no page outside it can ever be drawn. `usePdfLayer` warns in
41
+ * `__DEV__` when it is handed a stacked layout with a selection that admits more
42
+ * than one page, and `hasOverlappingPages` below is the predicate it uses.
43
+ */
44
+ export function singlePage() {
45
+ return pages => pages.map(p => ({ x: 0, y: 0, width: p.width, height: p.height }));
46
+ }
47
+ /**
48
+ * Does this layout put two pages on top of each other?
49
+ *
50
+ * Answered about the RECTS a layout produced for a specific document, never
51
+ * about the layout function, because it is a property of both: `singlePage()`
52
+ * over a one-page document stacks nothing, and a bespoke host layout can stack
53
+ * without being `singlePage()`.
54
+ *
55
+ * Overlap is STRICT — `rectIntersects` uses `<`, so a zero-gap continuous layout,
56
+ * where consecutive pages share an edge exactly, is not a stack. Empty rects are
57
+ * skipped: a layout that produced two zero-size rects at the same origin has a
58
+ * different problem than stacking, and reporting this one would send the reader
59
+ * to the wrong place.
60
+ *
61
+ * O(n^2) with an early exit, evaluated once per layout/document change and only
62
+ * behind a `__DEV__` guard at its one call site. Do not put it on a per-frame
63
+ * path.
64
+ */
65
+ export function hasOverlappingPages(rects) {
66
+ for (let i = 0; i < rects.length; i++) {
67
+ const a = rects[i];
68
+ if (a === undefined || isEmptyRect(a))
69
+ continue;
70
+ for (let j = i + 1; j < rects.length; j++) {
71
+ const b = rects[j];
72
+ if (b === undefined || isEmptyRect(b))
73
+ continue;
74
+ if (rectIntersects(a, b))
75
+ return true;
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+ /**
81
+ * Pages stacked top to bottom, horizontally centred on the widest page.
82
+ * Centring (rather than left-aligning) matters in mixed-size documents — a
83
+ * letter page bound into a set of plans should not hug the left edge.
84
+ */
85
+ export function continuousVertical(gap = 12) {
86
+ return pages => {
87
+ let maxWidth = 0;
88
+ for (const p of pages)
89
+ maxWidth = Math.max(maxWidth, p.width);
90
+ const rects = [];
91
+ let y = 0;
92
+ for (const p of pages) {
93
+ rects.push({
94
+ x: (maxWidth - p.width) / 2,
95
+ y,
96
+ width: p.width,
97
+ height: p.height,
98
+ });
99
+ // The cursor advances by the gap, so no trailing gap exists after the
100
+ // last page and the content height is exactly the union of the rects.
101
+ y += p.height + gap;
102
+ }
103
+ return rects;
104
+ };
105
+ }
106
+ /**
107
+ * Two-up: pages (0,1) form the first row, (2,3) the second, and so on. `gap`
108
+ * separates both the two pages of a row and consecutive rows.
109
+ *
110
+ * No cover-page special case. A viewer that wants page 0 alone can pad its own
111
+ * page list; baking that convention in here would make the layout lie about
112
+ * which page sits where for every document that does not follow it.
113
+ */
114
+ export function spread(gap = 12) {
115
+ return pages => {
116
+ let maxRowWidth = 0;
117
+ for (let i = 0; i < pages.length; i += 2) {
118
+ const left = pages[i];
119
+ if (!left)
120
+ continue;
121
+ const right = pages[i + 1];
122
+ const rowWidth = left.width + (right ? gap + right.width : 0);
123
+ maxRowWidth = Math.max(maxRowWidth, rowWidth);
124
+ }
125
+ const rects = [];
126
+ let y = 0;
127
+ for (let i = 0; i < pages.length; i += 2) {
128
+ const left = pages[i];
129
+ if (!left)
130
+ continue;
131
+ const right = pages[i + 1];
132
+ const rowWidth = left.width + (right ? gap + right.width : 0);
133
+ const x = (maxRowWidth - rowWidth) / 2;
134
+ rects.push({ x, y, width: left.width, height: left.height });
135
+ if (right) {
136
+ rects.push({
137
+ x: x + left.width + gap,
138
+ y,
139
+ width: right.width,
140
+ height: right.height,
141
+ });
142
+ }
143
+ // Rows are top-aligned; the row's height is its taller page. Vertical
144
+ // centring would break the spine alignment a spread exists to show.
145
+ y += rowHeight(left, right) + gap;
146
+ }
147
+ return rects;
148
+ };
149
+ }
150
+ function rowHeight(left, right) {
151
+ return right ? Math.max(left.height, right.height) : left.height;
152
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The runtime half of the skia peer-floor gate: a dev-only, once-per-process
3
+ * warning.
4
+ *
5
+ * ------------------------------------------------------------------------
6
+ * WHY THERE IS A RUNTIME LAYER AT ALL
7
+ * ------------------------------------------------------------------------
8
+ *
9
+ * It is the only layer that catches a drift no install ran. `peerDependencies`
10
+ * and the `postinstall` check both fire at install time; `yarn add
11
+ * @shopify/react-native-skia@2.6.2` afterwards, or an OTA update onto a build
12
+ * whose bundle was made elsewhere, is invisible to both.
13
+ *
14
+ * It is also the ecosystem idiom, not an invention. Skia does exactly this for
15
+ * ITS peer (`sksg/Container.native.ts`): `require(peer + '/package.json')
16
+ * .version`, compare, then throw in `__DEV__` and `console.error` once in
17
+ * production. Reanimated does the same via `assertWorkletsVersion`. Measured
18
+ * against BOLDR's 2020 installed packages, only six have any install hook at all
19
+ * and NONE of them checks a peer's version — the runtime check is where this
20
+ * genuinely lives in React Native.
21
+ *
22
+ * ------------------------------------------------------------------------
23
+ * WHY IT WARNS AND DOES NOT THROW, where skia and reanimated both throw
24
+ * ------------------------------------------------------------------------
25
+ *
26
+ * Because their check and ours are not the same kind of claim. Skia's reanimated
27
+ * mismatch means the feature does not work at all — animations silently freeze —
28
+ * so a dev throw converts a silent failure into a loud one. Our floor is a
29
+ * MEMORY POLICY claim about the WEB reconciler. Below it the package still
30
+ * renders correctly everywhere, and on native it is measurably fine: the full
31
+ * suite (181 tests, the offscreen CanvasKit pixel tests included) passes against
32
+ * skia 2.6.2, and BOLDR ships that combination on Android today.
33
+ *
34
+ * A throw here would therefore brick a configuration that works, from inside
35
+ * `imageFromPixels`, surfacing as a `backend-failure` against a raster key —
36
+ * i.e. reported as a backend bug. Warn once, name the fix, do no harm.
37
+ *
38
+ * ------------------------------------------------------------------------
39
+ * THE FLOOR IS NOT RESTATED HERE
40
+ * ------------------------------------------------------------------------
41
+ *
42
+ * It is read out of this package's own `peerDependencies`, the same single
43
+ * source of truth `scripts/skia-peer-check.mjs` reads. A floor bump is one line
44
+ * in package.json and both layers follow.
45
+ */
46
+ export type PeerCheckDeps = {
47
+ /** Installed peer version, or null if it could not be read. */
48
+ readonly peerVersion: () => string | null;
49
+ /** This package's declared floor range, or null. */
50
+ readonly floorRange: () => string | null;
51
+ readonly isDev: () => boolean;
52
+ readonly warn: (message: string) => void;
53
+ };
54
+ export declare const DEFAULT_PEER_CHECK_DEPS: PeerCheckDeps;
55
+ /**
56
+ * Below the floor, told apart from "cannot tell".
57
+ *
58
+ * `null` means no verdict — an unparseable version, an unparseable floor, or a
59
+ * manifest that could not be read. It is NOT folded into `false`, because the
60
+ * caller's only action on `true` is to shout, and shouting on a non-answer is
61
+ * how a version gate loses its credibility.
62
+ *
63
+ * Prerelease handling matches `scripts/skia-peer-check.mjs` exactly, including
64
+ * the rule that makes it more than a tuple compare: for an EQUAL tuple a
65
+ * prerelease is BELOW the floor, so `2.8.0-next.1` does not clear `>=2.8.0`
66
+ * while `2.12.0-next.1` does. `peer-check.test.ts` pins this function, that one
67
+ * and the real `semver` to the same table.
68
+ */
69
+ export declare const isBelowFloor: (version: string | null, floorRange: string | null) => boolean | null;
70
+ /**
71
+ * Called from `ingest.ts` on the first raster, NOT at module scope.
72
+ *
73
+ * Module scope would be a side effect in a package that declares
74
+ * `"sideEffects": false`, which is a licence for a bundler to drop it — and a
75
+ * check a bundler is free to delete is not a check. First-raster is also the
76
+ * honest trigger: it is the first moment this package has actually asked Skia
77
+ * for anything, which is exactly when skia's own `createContainer` check fires.
78
+ *
79
+ * Idempotent and cheap after the first call: one boolean read.
80
+ */
81
+ export declare const warnIfSkiaBelowFloor: (deps?: PeerCheckDeps) => void;
82
+ /** Test seam. Nothing in `src/` outside the suite calls this. */
83
+ export declare const resetPeerCheckForTests: () => void;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The runtime half of the skia peer-floor gate: a dev-only, once-per-process
3
+ * warning.
4
+ *
5
+ * ------------------------------------------------------------------------
6
+ * WHY THERE IS A RUNTIME LAYER AT ALL
7
+ * ------------------------------------------------------------------------
8
+ *
9
+ * It is the only layer that catches a drift no install ran. `peerDependencies`
10
+ * and the `postinstall` check both fire at install time; `yarn add
11
+ * @shopify/react-native-skia@2.6.2` afterwards, or an OTA update onto a build
12
+ * whose bundle was made elsewhere, is invisible to both.
13
+ *
14
+ * It is also the ecosystem idiom, not an invention. Skia does exactly this for
15
+ * ITS peer (`sksg/Container.native.ts`): `require(peer + '/package.json')
16
+ * .version`, compare, then throw in `__DEV__` and `console.error` once in
17
+ * production. Reanimated does the same via `assertWorkletsVersion`. Measured
18
+ * against BOLDR's 2020 installed packages, only six have any install hook at all
19
+ * and NONE of them checks a peer's version — the runtime check is where this
20
+ * genuinely lives in React Native.
21
+ *
22
+ * ------------------------------------------------------------------------
23
+ * WHY IT WARNS AND DOES NOT THROW, where skia and reanimated both throw
24
+ * ------------------------------------------------------------------------
25
+ *
26
+ * Because their check and ours are not the same kind of claim. Skia's reanimated
27
+ * mismatch means the feature does not work at all — animations silently freeze —
28
+ * so a dev throw converts a silent failure into a loud one. Our floor is a
29
+ * MEMORY POLICY claim about the WEB reconciler. Below it the package still
30
+ * renders correctly everywhere, and on native it is measurably fine: the full
31
+ * suite (181 tests, the offscreen CanvasKit pixel tests included) passes against
32
+ * skia 2.6.2, and BOLDR ships that combination on Android today.
33
+ *
34
+ * A throw here would therefore brick a configuration that works, from inside
35
+ * `imageFromPixels`, surfacing as a `backend-failure` against a raster key —
36
+ * i.e. reported as a backend bug. Warn once, name the fix, do no harm.
37
+ *
38
+ * ------------------------------------------------------------------------
39
+ * THE FLOOR IS NOT RESTATED HERE
40
+ * ------------------------------------------------------------------------
41
+ *
42
+ * It is read out of this package's own `peerDependencies`, the same single
43
+ * source of truth `scripts/skia-peer-check.mjs` reads. A floor bump is one line
44
+ * in package.json and both layers follow.
45
+ */
46
+ /**
47
+ * `require`, in a try, for the same reason `rasterizer/index.native.ts` uses one:
48
+ * a static import would make this module unloadable somewhere it must load.
49
+ * Here it is worse than that — a static import of a package's `package.json`
50
+ * needs a JSON-module assertion that neither Metro nor the tsc config here is
51
+ * set up for, and a failure to read a manifest must never be worse than not
52
+ * knowing.
53
+ *
54
+ * `@shopify/react-native-skia` declares no `exports` field at any version in the
55
+ * supported range (verified 2.6.2 through 2.11.0), so the `/package.json`
56
+ * subpath is reachable. If that ever changes this returns null and the check
57
+ * goes quiet, which is the correct failure direction.
58
+ *
59
+ * MEASURED LIMIT, and it is deliberate rather than repaired. A module-scoped
60
+ * `require` exists under Metro (native and web) and under vitest, which is every
61
+ * environment this check is aimed at. It does NOT exist in `dist/peer-check.js`
62
+ * loaded as real ESM by node: both reads return null there and the whole check
63
+ * goes silent. That is the safe direction, and the alternatives are worse — an
64
+ * `import.meta.url` + `createRequire` fallback buys a node-ESM consumer that has
65
+ * no Skia canvas to protect, at the price of `import.meta` in a file Metro
66
+ * transpiles to CJS. The INSTALL-time layer, which is where a real gate lives,
67
+ * is a standalone `.mjs` and has no such constraint.
68
+ *
69
+ * THE SPECIFIER IS INLINE, and must stay that way. Metro resolves `require` at
70
+ * BUILD time from a static string literal; handing it a variable is not a
71
+ * runtime failure that this try/catch would absorb, it is a bundler error that
72
+ * takes the whole app down:
73
+ *
74
+ * SyntaxError: dist/peer-check.js: Invalid call at line 71: require(specifier)
75
+ *
76
+ * That is not hypothetical — this module reaches a consuming app through
77
+ * `rasterizer/ingest.ts`, so the first Metro consumer hit it immediately. A
78
+ * parameterised helper reads as more general and is strictly less usable here;
79
+ * there has only ever been one specifier to read.
80
+ */
81
+ const readSkiaVersion = () => {
82
+ try {
83
+ const manifest = require('@shopify/react-native-skia/package.json');
84
+ return typeof manifest.version === 'string' ? manifest.version : null;
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ };
90
+ const readOwnFloor = () => {
91
+ try {
92
+ // From `dist/peer-check.js` and from `src/peer-check.ts` alike, `..` is the
93
+ // package root — `rootDir: src` flattens onto `dist/`, so the depth matches
94
+ // in both trees and the test exercises the same path production does.
95
+ const manifest = require('../package.json');
96
+ return manifest.peerDependencies?.['@shopify/react-native-skia'] ?? null;
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ };
102
+ /**
103
+ * `__DEV__` is a React Native global. It is undefined under node and in a plain
104
+ * web bundle, so it is read off `globalThis` rather than declared — and a
105
+ * missing `__DEV__` counts as NOT dev, so nothing in this file can ever emit in
106
+ * an environment it does not understand.
107
+ */
108
+ const isDevDefault = () => globalThis.__DEV__ === true;
109
+ export const DEFAULT_PEER_CHECK_DEPS = {
110
+ peerVersion: readSkiaVersion,
111
+ floorRange: readOwnFloor,
112
+ isDev: isDevDefault,
113
+ warn: message => console.warn(message),
114
+ };
115
+ const TUPLE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/;
116
+ const FLOOR = /^>=\s*(\d+)\.(\d+)\.(\d+)$/;
117
+ /**
118
+ * Below the floor, told apart from "cannot tell".
119
+ *
120
+ * `null` means no verdict — an unparseable version, an unparseable floor, or a
121
+ * manifest that could not be read. It is NOT folded into `false`, because the
122
+ * caller's only action on `true` is to shout, and shouting on a non-answer is
123
+ * how a version gate loses its credibility.
124
+ *
125
+ * Prerelease handling matches `scripts/skia-peer-check.mjs` exactly, including
126
+ * the rule that makes it more than a tuple compare: for an EQUAL tuple a
127
+ * prerelease is BELOW the floor, so `2.8.0-next.1` does not clear `>=2.8.0`
128
+ * while `2.12.0-next.1` does. `peer-check.test.ts` pins this function, that one
129
+ * and the real `semver` to the same table.
130
+ */
131
+ export const isBelowFloor = (version, floorRange) => {
132
+ if (version === null || floorRange === null)
133
+ return null;
134
+ const v = TUPLE.exec(version.trim());
135
+ const f = FLOOR.exec(floorRange.trim());
136
+ if (v === null || f === null)
137
+ return null;
138
+ for (let i = 1; i <= 3; i++) {
139
+ const a = Number(v[i]);
140
+ const b = Number(f[i]);
141
+ if (a !== b)
142
+ return a < b;
143
+ }
144
+ return v[4] !== undefined;
145
+ };
146
+ const message = (found, floor) => `[@reekon-tools/react-native-pdf-canvas] @shopify/react-native-skia ` +
147
+ `${found} is below the required ${floor}.\n` +
148
+ ` On web, below 2.8.0 the RN Web reconciler leaks one CanvasKit Paint per ` +
149
+ `drawn tile per frame into the Emscripten heap, which never returns pages ` +
150
+ `to the OS. This package tiles both the base and the detail layer, so that ` +
151
+ `leak scales with the number of pieces on screen and its memory policy ` +
152
+ `cannot hold.\n` +
153
+ ` Fix: yarn add @shopify/react-native-skia@^${floor.replace(/^>=\s*/, '')}` +
154
+ `\n (dev-only warning, shown once)`;
155
+ let warned = false;
156
+ /**
157
+ * Called from `ingest.ts` on the first raster, NOT at module scope.
158
+ *
159
+ * Module scope would be a side effect in a package that declares
160
+ * `"sideEffects": false`, which is a licence for a bundler to drop it — and a
161
+ * check a bundler is free to delete is not a check. First-raster is also the
162
+ * honest trigger: it is the first moment this package has actually asked Skia
163
+ * for anything, which is exactly when skia's own `createContainer` check fires.
164
+ *
165
+ * Idempotent and cheap after the first call: one boolean read.
166
+ */
167
+ export const warnIfSkiaBelowFloor = (deps = DEFAULT_PEER_CHECK_DEPS) => {
168
+ if (warned)
169
+ return;
170
+ warned = true;
171
+ if (!deps.isDev())
172
+ return;
173
+ const found = deps.peerVersion();
174
+ const floor = deps.floorRange();
175
+ if (isBelowFloor(found, floor) !== true)
176
+ return;
177
+ // Non-null by construction: isBelowFloor returns null unless both parsed.
178
+ deps.warn(message(found, floor));
179
+ };
180
+ /** Test seam. Nothing in `src/` outside the suite calls this. */
181
+ export const resetPeerCheckForTests = () => {
182
+ warned = false;
183
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The two pure decisions: WHAT to raster (`planRasters`) and WHEN
3
+ * (`shouldReraster`). No React, no Skia, no clock, no async — `nowMs` is
4
+ * passed in so the cadence is testable without fake timers.
5
+ */
6
+ import type { DocRect, EpochReason, EpochState, PageGeometry, RasterPlan, RasterPolicy, ViewportSnapshot } from './types.js';
7
+ export declare function planRasters(args: {
8
+ viewport: ViewportSnapshot;
9
+ pages: readonly PageGeometry[];
10
+ pageRects: readonly DocRect[];
11
+ visiblePages: readonly number[];
12
+ policy: RasterPolicy;
13
+ }): RasterPlan;
14
+ /**
15
+ * Evaluate the re-raster predicates. Called at settle (and by the quiet timer
16
+ * on the continuous path), never per frame.
17
+ *
18
+ * When `reraster` is false, `reason` names the predicate that WOULD have fired
19
+ * had `minEpochIntervalMs` not blocked it — so the caller can log a meaningful
20
+ * skip — and is `'stale'` as a neutral filler when nothing fired at all.
21
+ * Callers must branch on `reraster`, never on `reason` alone.
22
+ *
23
+ * `'layout'` is never returned: a layout change is not visible from a
24
+ * viewport snapshot, and the owner of the layout must force the epoch itself.
25
+ */
26
+ export declare function shouldReraster(args: {
27
+ next: ViewportSnapshot;
28
+ previous: ViewportSnapshot | null;
29
+ epoch: EpochState | null;
30
+ policy: RasterPolicy;
31
+ nowMs: number;
32
+ }): {
33
+ reraster: boolean;
34
+ reason: EpochReason;
35
+ };