@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
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The React binding for the raster controller.
3
+ *
4
+ * THIS HOOK BELONGS ABOVE THE `<Canvas>`, NOT INSIDE IT. Two reasons, and both
5
+ * are load-bearing:
6
+ *
7
+ * 1. React context does not cross the Canvas boundary. RN Skia mounts its own
8
+ * reconciler for the Skia subtree, so a provider placed inside a `<Canvas>`
9
+ * is invisible to the host's tree — and a provider outside it is invisible to
10
+ * the Skia children. Anything shared has to be a prop, which is exactly what
11
+ * `content` is.
12
+ * 2. A magnifier loupe is a SECOND `<Canvas>` looking at the same document. If
13
+ * the cache were instantiated inside the canvas subtree there would be two of
14
+ * them, live at the same time, each with its own copy of every base raster —
15
+ * double the memory for pixels that are identical. Hoisting the hook above
16
+ * both canvases and passing the same `content` down is what makes the loupe
17
+ * free.
18
+ *
19
+ * The hook contributes ZERO Reanimated shared values to the Skia subtree. The
20
+ * host's transform does the panning and zooming; this hook only ever hears about
21
+ * the viewport through the `controller` methods the host calls.
22
+ */
23
+ import type { PdfContent, PdfController, PdfDiagnostic, PdfPageLayout, RasterPolicy } from '../types.js';
24
+ import type { PdfDocument } from './usePdfDocument.js';
25
+ /**
26
+ * Only the pages the viewport actually intersects may hold rasters.
27
+ *
28
+ * THE HONEST SPELLING OF THE DEFAULT, and the one to use. It carries no tuning
29
+ * number because there is no tuning to do: see `PdfPageWindow` for the knob this
30
+ * replaces and why that knob was a lie.
31
+ */
32
+ export interface PdfOnScreenPages {
33
+ readonly onScreen: true;
34
+ }
35
+ /**
36
+ * @deprecated THE RADIUS IS NOT IMPLEMENTED. `{window: 3}` behaves exactly like
37
+ * `{window: 0}` and always has — a window resolves to no allow-list at all (see
38
+ * `resolveAllowedPages`), so the number has nowhere to go. Use
39
+ * `{onScreen: true}`, which means what this does; or an explicit `number[]` if
40
+ * you want a genuine allow-list. Implementing the radius needs
41
+ * `PdfControllerOptions` to accept a per-viewport allow-list — an
42
+ * `(visiblePages: readonly number[]) => ReadonlySet<number>` in place of the set,
43
+ * or a plain `pageWindow` radius the planner applies — and until then this member
44
+ * exists only so an existing `{window: n}` call site keeps compiling.
45
+ */
46
+ export interface PdfPageWindow {
47
+ readonly window: number;
48
+ }
49
+ /**
50
+ * Which pages may hold rasters: an explicit allow-list, or every page the
51
+ * viewport is currently over.
52
+ */
53
+ export type PdfPageSelection = readonly number[] | PdfOnScreenPages | PdfPageWindow;
54
+ /**
55
+ * THE DEFAULT IS THE PAGES ON SCREEN, NEVER "ALL". A 400-page architectural spec
56
+ * book at ARCH-D holds roughly 10 MiB in a single base raster; "all" is therefore
57
+ * about 4 GiB of standing allocation before a single detail raster exists. The
58
+ * controller retains the bases of pages two either side of the plan's window, so
59
+ * a page-turn in a continuous layout still lands on an already-warm base.
60
+ */
61
+ export declare const DEFAULT_PAGE_SELECTION: PdfPageSelection;
62
+ export interface UsePdfLayerOptions {
63
+ /** From `usePdfDocument`. Null while loading; the hook returns empty content. */
64
+ document: PdfDocument | null;
65
+ /**
66
+ * @default {onScreen: true} — see DEFAULT_PAGE_SELECTION.
67
+ *
68
+ * A one-element list is what single-page (paged) mode is made of:
69
+ * `{layout: singlePage(), pages: [pageIndex]}`. Changing the list REBUILDS the
70
+ * controller and clears its raster cache, which for a stacked layout is the
71
+ * correct trade — see the note on the memo below.
72
+ */
73
+ pages?: PdfPageSelection;
74
+ /**
75
+ * Where pages sit in doc space. The package has no opinion; see `./layout`.
76
+ *
77
+ * @default continuousVertical()
78
+ */
79
+ layout?: PdfPageLayout;
80
+ /** Overrides merged over the default `RasterPolicy`. */
81
+ policy?: Partial<RasterPolicy>;
82
+ onDiagnostic?: (diagnostic: PdfDiagnostic) => void;
83
+ }
84
+ export interface PdfLayer {
85
+ /** Pass straight to `<PdfContentView content={...} />`. */
86
+ content: PdfContent;
87
+ /**
88
+ * Drive this from the host's gesture handlers. Identity is stable for the life
89
+ * of the component — including across document changes — so it can be captured
90
+ * in a gesture callback without a dependency array.
91
+ */
92
+ controller: PdfController;
93
+ }
94
+ export declare function usePdfLayer({ document, pages: pageSelection, layout, policy, onDiagnostic, }: UsePdfLayerOptions): PdfLayer;
@@ -0,0 +1,388 @@
1
+ /**
2
+ * The React binding for the raster controller.
3
+ *
4
+ * THIS HOOK BELONGS ABOVE THE `<Canvas>`, NOT INSIDE IT. Two reasons, and both
5
+ * are load-bearing:
6
+ *
7
+ * 1. React context does not cross the Canvas boundary. RN Skia mounts its own
8
+ * reconciler for the Skia subtree, so a provider placed inside a `<Canvas>`
9
+ * is invisible to the host's tree — and a provider outside it is invisible to
10
+ * the Skia children. Anything shared has to be a prop, which is exactly what
11
+ * `content` is.
12
+ * 2. A magnifier loupe is a SECOND `<Canvas>` looking at the same document. If
13
+ * the cache were instantiated inside the canvas subtree there would be two of
14
+ * them, live at the same time, each with its own copy of every base raster —
15
+ * double the memory for pixels that are identical. Hoisting the hook above
16
+ * both canvases and passing the same `content` down is what makes the loupe
17
+ * free.
18
+ *
19
+ * The hook contributes ZERO Reanimated shared values to the Skia subtree. The
20
+ * host's transform does the panning and zooming; this hook only ever hears about
21
+ * the viewport through the `controller` methods the host calls.
22
+ */
23
+ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore, } from 'react';
24
+ import { createRasterCache } from '../cache.js';
25
+ import { createPdfController } from '../controller.js';
26
+ import { continuousVertical, hasOverlappingPages } from '../layout.js';
27
+ import { resolvePolicy } from '../policy.js';
28
+ // Through the `./skia` barrel, not `../rasterizer/ingest.js` directly: that
29
+ // barrel is the documented home of the ingest seam, and routing every internal
30
+ // consumer through it is what keeps "which modules can touch Skia" a list of one
31
+ // directory rather than a grep. Nothing is paid for it — ingest itself no longer
32
+ // names `Skia` at module scope (see `../skia/skia-api.ts`), so this hook stays
33
+ // safe to evaluate before CanvasKit exists.
34
+ import { ingestRaster } from '../skia/index.js';
35
+ /** Same derivation as `PdfContentView`'s, restated rather than imported: that
36
+ * module reaches RN Skia's leaf components, and this hook must stay able to be
37
+ * evaluated before CanvasKit exists. */
38
+ const IS_DEV = typeof __DEV__ !== 'undefined'
39
+ ? __DEV__ === true
40
+ : typeof process !== 'undefined' &&
41
+ process.env?.['NODE_ENV'] !== 'production';
42
+ /**
43
+ * THE DEFAULT IS THE PAGES ON SCREEN, NEVER "ALL". A 400-page architectural spec
44
+ * book at ARCH-D holds roughly 10 MiB in a single base raster; "all" is therefore
45
+ * about 4 GiB of standing allocation before a single detail raster exists. The
46
+ * controller retains the bases of pages two either side of the plan's window, so
47
+ * a page-turn in a continuous layout still lands on an already-warm base.
48
+ */
49
+ export const DEFAULT_PAGE_SELECTION = Object.freeze({
50
+ onScreen: true,
51
+ });
52
+ const isPageList = (selection) => Array.isArray(selection);
53
+ /**
54
+ * An explicit list becomes a static allow-list, clamped to the pages the document
55
+ * actually has — an out-of-range index would otherwise plan a raster the backend
56
+ * can only reject.
57
+ *
58
+ * THE ON-SCREEN SELECTION RESOLVES TO `undefined`, i.e. no allow-list at all —
59
+ * the pages it names depend on the viewport, and no viewport exists when the
60
+ * controller is constructed. `undefined` is not "all": the controller plans only
61
+ * the pages intersecting the snapshot it is handed, and retains their bases two
62
+ * pages either side, so the standing cost is bounded by the screen.
63
+ *
64
+ * "Bounded by the screen" assumes page rects are DISJOINT. Under `singlePage()`
65
+ * every page rect is the same rect, every page is therefore on screen, and this
66
+ * returning `undefined` means the whole document is planned — N pages of bytes to
67
+ * draw one page, measured in `__tests__/single-page.test.ts`. That is what the
68
+ * `__DEV__` warning below is for, and why a stacked layout must be paired with a
69
+ * one-element list.
70
+ */
71
+ function resolveAllowedPages(selection, pageCount) {
72
+ if (!isPageList(selection)) {
73
+ return undefined;
74
+ }
75
+ const allowed = new Set();
76
+ for (const page of selection) {
77
+ if (Number.isInteger(page) && page >= 0 && page < pageCount) {
78
+ allowed.add(page);
79
+ }
80
+ }
81
+ return allowed;
82
+ }
83
+ /* ------------------------------------------------------------------ *
84
+ * Owned resources
85
+ * ------------------------------------------------------------------ */
86
+ /**
87
+ * `continuousVertical()` builds a fresh closure per call, and `layout` is a
88
+ * dependency of the controller. One module-level instance keeps the DEFAULT from
89
+ * looking like a layout change on every render and tearing the raster cache down
90
+ * with it.
91
+ *
92
+ * IT USED TO BE `singlePage()`, AND THAT WAS WRONG AS A DEFAULT. A stacked layout
93
+ * is correct only alongside a one-element `pages` list, and the default selection
94
+ * is the pages on screen — under `singlePage()` that is every page in the
95
+ * document, at one doc rect, drawn on top of each other. The two defaults
96
+ * contradicted each other, and nothing exercised the pair: both dev probes pass an
97
+ * explicit `continuousVertical(12)`. A continuous layout is safe with every
98
+ * selection, which is the only property a default needs to have.
99
+ */
100
+ const DEFAULT_LAYOUT = continuousVertical();
101
+ /**
102
+ * Idempotent by requirement, not by Skia's grace: on web a second
103
+ * `SkImage.dispose()` THROWS where native tolerates it. The cache guards its own
104
+ * paths, but this callback is the last thing holding the image and has to survive
105
+ * being reached twice — an eviction racing a teardown flush, or the same image
106
+ * somehow reaching two caches.
107
+ *
108
+ * Keyed on the SkImage rather than the PdfRaster, because the image is the thing
109
+ * that can be freed twice.
110
+ */
111
+ const disposedImages = new WeakSet();
112
+ function disposeRaster(raster) {
113
+ if (disposedImages.has(raster.image)) {
114
+ return;
115
+ }
116
+ disposedImages.add(raster.image);
117
+ raster.image.dispose();
118
+ }
119
+ /** Intrinsic geometry for every page, in page order. Cheap: loads no page. */
120
+ function allPageGeometry(document) {
121
+ const geometry = [];
122
+ for (let index = 0; index < document.pageCount; index += 1) {
123
+ geometry.push(document.pageGeometry(index));
124
+ }
125
+ return geometry;
126
+ }
127
+ /* ------------------------------------------------------------------ *
128
+ * Empty state
129
+ * ------------------------------------------------------------------ */
130
+ const EMPTY_PAGES = Object.freeze([]);
131
+ /**
132
+ * Returned before a document exists. A STABLE frozen reference: `getSnapshot`
133
+ * handing back a fresh `{pages: [], epoch: 0}` on every call is the classic
134
+ * useSyncExternalStore infinite-render loop.
135
+ */
136
+ const EMPTY_CONTENT = Object.freeze({
137
+ pages: EMPTY_PAGES,
138
+ epoch: 0,
139
+ settling: false,
140
+ });
141
+ const NOOP_UNSUBSCRIBE = () => { };
142
+ const NOOP_SUBSCRIBE = () => NOOP_UNSUBSCRIBE;
143
+ const getEmptyContent = () => EMPTY_CONTENT;
144
+ /* ------------------------------------------------------------------ *
145
+ * Hook
146
+ * ------------------------------------------------------------------ */
147
+ /**
148
+ * Stable structural key for a page selection, so an inline literal does not tear
149
+ * down and rebuild the controller (and its whole raster cache) every render.
150
+ *
151
+ * A list is SORTED and DEDUPED first, because the allow-list it resolves to is a
152
+ * Set and cannot tell `[3, 2]` from `[2, 3]`. Page navigation is exactly where a
153
+ * host builds these arrays programmatically, and rebuilding a controller over a
154
+ * reordering it cannot observe is a raster cache thrown away for nothing.
155
+ *
156
+ * Every window spelling collapses to the same key, since they all resolve to the
157
+ * same thing: no allow-list. `{window: 3}` and `{onScreen: true}` are the same
158
+ * controller.
159
+ */
160
+ function pageSelectionKey(pages) {
161
+ if (!isPageList(pages)) {
162
+ return 'on-screen';
163
+ }
164
+ const sorted = [...new Set(pages)].sort((a, b) => a - b);
165
+ return `l:${sorted.join(',')}`;
166
+ }
167
+ /** Same, for a policy override object. Every RasterPolicy field is a number or a
168
+ * boolean, so a sorted key/value join is a complete description. */
169
+ function policyKey(policy) {
170
+ if (policy === undefined) {
171
+ return '';
172
+ }
173
+ const keys = Object.keys(policy).sort();
174
+ let out = '';
175
+ for (const key of keys) {
176
+ out += `${key}=${String(policy[key])};`;
177
+ }
178
+ return out;
179
+ }
180
+ export function usePdfLayer({ document,
181
+ // The PUBLIC name stays `pages`; locally it is the SELECTION, and the
182
+ // controller's own `pages` is the per-page geometry. Two very different things
183
+ // one rename apart.
184
+ pages: pageSelection = DEFAULT_PAGE_SELECTION, layout, policy, onDiagnostic, }) {
185
+ // The diagnostic callback is almost always an inline arrow. Route it through a
186
+ // ref so a fresh identity on every render never counts as a reason to rebuild
187
+ // the controller.
188
+ const onDiagnosticRef = useRef(onDiagnostic);
189
+ useEffect(() => {
190
+ onDiagnosticRef.current = onDiagnostic;
191
+ }, [onDiagnostic]);
192
+ const emitDiagnostic = useCallback((diagnostic) => {
193
+ onDiagnosticRef.current?.(diagnostic);
194
+ }, []);
195
+ const pagesKey = pageSelectionKey(pageSelection);
196
+ const policySignature = policyKey(policy);
197
+ /**
198
+ * Split out of the controller memo so the `__DEV__` check below can see the
199
+ * rects without re-running the layout, and so a warning lives in an EFFECT
200
+ * rather than inside a memo React is free to invoke twice.
201
+ */
202
+ const pageRects = useMemo(() => document === null
203
+ ? null
204
+ : (layout ?? DEFAULT_LAYOUT)(document.pageSizes()), [document, layout]);
205
+ /**
206
+ * A stacked layout (`singlePage()`) is correct ONLY with a one-element `pages`
207
+ * list. Any other selection lets every page hold a base at the same doc rect,
208
+ * and `PdfContentView` draws them in page order — so the memory is N pages and
209
+ * the page on screen is the highest allowed index, not the one the host meant.
210
+ *
211
+ * A warning and not a correction: silently narrowing the selection would make a
212
+ * host's `pages` prop mean something it does not say, and silently swapping the
213
+ * layout would move doc space under the host's transform.
214
+ */
215
+ useEffect(() => {
216
+ if (!IS_DEV || pageRects === null || document === null) {
217
+ return;
218
+ }
219
+ if (!hasOverlappingPages(pageRects)) {
220
+ return;
221
+ }
222
+ const allowed = resolveAllowedPages(pageSelection, document.pageCount);
223
+ if (allowed !== undefined && allowed.size === 1) {
224
+ return;
225
+ }
226
+ console.warn('[react-native-pdf-canvas] usePdfLayer was given a layout that STACKS pages ' +
227
+ `(${pageRects.length} page rects overlap in doc space, e.g. singlePage()) ` +
228
+ `together with a page selection of ${allowed === undefined
229
+ ? 'every page on screen'
230
+ : `${allowed.size} pages`}. Every page whose rect is on screen will be rastered at that same rect ` +
231
+ 'and drawn on top of the others, so the visible page is the highest ' +
232
+ 'allowed index and the cost is one base per page. Pass a one-element list ' +
233
+ '— usePdfLayer({layout: singlePage(), pages: [pageIndex]}) — or use ' +
234
+ 'continuousVertical() / spread().');
235
+ }, [document, pageRects, pageSelection]);
236
+ const layer = useMemo(() => {
237
+ if (document === null || pageRects === null) {
238
+ return null;
239
+ }
240
+ const resolvedPolicy = resolvePolicy(policy);
241
+ const cache = createRasterCache({
242
+ budgetBytes: resolvedPolicy.cacheBudgetBytes,
243
+ dispose: disposeRaster,
244
+ });
245
+ const controller = createPdfController({
246
+ handle: document.handle,
247
+ pageRects,
248
+ pages: allPageGeometry(document),
249
+ policy: resolvedPolicy,
250
+ ingest: ingestRaster,
251
+ cache,
252
+ allowedPages: resolveAllowedPages(pageSelection, document.pageCount),
253
+ // The BACKEND's number, never a constant here. Absent (a backend written
254
+ // before the field existed) means 1, which is the sequential behaviour
255
+ // every backend was proven against.
256
+ maxConcurrentRenders: document.capabilities.maxConcurrentRenders,
257
+ onDiagnostic: emitDiagnostic,
258
+ backendId: document.rasterizerId,
259
+ });
260
+ return { controller, cache };
261
+ },
262
+ // `pageSelection` and `policy` are intentionally NOT deps: they are object
263
+ // literals at every realistic call site, and rebuilding the controller
264
+ // discards the entire raster cache. Their structural keys stand in for them,
265
+ // which is stronger than identity — a caller who memoises AND mutates gets
266
+ // the correct rebuild.
267
+ //
268
+ // A PAGE TURN THEREFORE REBUILDS EVERYTHING, AND THAT IS DELIBERATE. `pages`
269
+ // going from [2] to [3] changes `pagesKey`, so the controller is destroyed and
270
+ // the cache cleared: page 3 is rendered from scratch and turning back to 2
271
+ // re-renders it rather than hitting the cache. Keeping the controller alive
272
+ // across the change (a mutable allow-list the controller reads per plan) would
273
+ // buy that hit, and was rejected — the pages inside the controller's base
274
+ // retention radius stay MOUNTED, so in a stacked layout the page you left is
275
+ // still painted at the rect of the page you turned to, and even in a continuous
276
+ // layout the resident cost goes from one page to as many as five. The measured
277
+ // cost of the teardown is one page's rasters, which is the page you are turning
278
+ // to and would have paid for anyway. See `__tests__/single-page.test.ts`.
279
+ // eslint-disable-next-line react-hooks/exhaustive-deps
280
+ [document, pageRects, pagesKey, policySignature, emitDiagnostic]);
281
+ const instance = layer?.controller ?? null;
282
+ useEffect(() => {
283
+ if (layer === null) {
284
+ return;
285
+ }
286
+ return () => {
287
+ // Ordering: this cleanup runs after the render that replaced `layer` has
288
+ // already committed, so the Skia tree is no longer drawing anything the
289
+ // cache holds and freeing is safe.
290
+ layer.controller.destroy();
291
+ // destroy() flushes what it had referenced; clear() also takes the entries
292
+ // that were cached but never referenced (a put whose acquire lost a race),
293
+ // which nothing else will ever free. The hook owns this cache exclusively.
294
+ layer.cache.clear();
295
+ // The DOCUMENT is deliberately untouched. The controller only borrowed its
296
+ // handle, and the document may outlive it — a thumbnail strip on the same
297
+ // handle — so closing it here would pull the file descriptor out from
298
+ // under a live consumer. `usePdfDocument` owns that.
299
+ };
300
+ }, [layer]);
301
+ const subscribe = useMemo(() => {
302
+ if (instance === null) {
303
+ return NOOP_SUBSCRIBE;
304
+ }
305
+ // Wrapped rather than passed as `instance.subscribe`, so the controller is
306
+ // free to implement it as a class method without losing `this`. The wrapper
307
+ // identity is stable per instance, which is what useSyncExternalStore needs.
308
+ return (listener) => instance.subscribe(listener);
309
+ }, [instance]);
310
+ const getSnapshot = useMemo(() => {
311
+ if (instance === null) {
312
+ return getEmptyContent;
313
+ }
314
+ // MUST return a cached reference. React calls getSnapshot during render and
315
+ // again after every store notification, and compares with Object.is — a freshly
316
+ // built object here is an infinite render loop, not a stale-data bug. The
317
+ // controller guarantees one frozen PdfContent per epoch.
318
+ return () => instance.getContent();
319
+ }, [instance]);
320
+ const content = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
321
+ // A facade whose identity never changes, so hosts can wire it into gesture
322
+ // callbacks and animation completion handlers without dependency arrays. It
323
+ // reads the live instance through a ref rather than closing over one.
324
+ const instanceRef = useRef(instance);
325
+ instanceRef.current = instance;
326
+ /**
327
+ * The last viewport the host handed us, whichever entry point it used.
328
+ *
329
+ * A REF AND NOT STATE: it is written from a gesture callback at gesture rate and
330
+ * nothing renders from it.
331
+ */
332
+ const lastViewportRef = useRef(null);
333
+ const controller = useMemo(() => ({
334
+ hint(viewport) {
335
+ lastViewportRef.current = viewport;
336
+ instanceRef.current?.hint(viewport);
337
+ },
338
+ settle(viewport) {
339
+ lastViewportRef.current = viewport;
340
+ instanceRef.current?.settle(viewport);
341
+ },
342
+ suppress() {
343
+ instanceRef.current?.suppress();
344
+ },
345
+ resume() {
346
+ instanceRef.current?.resume();
347
+ },
348
+ }), []);
349
+ /**
350
+ * A FRESH CONTROLLER HAS NEVER SEEN A VIEWPORT, AND NOTHING ELSE WILL TELL IT.
351
+ *
352
+ * Every entry point is host-driven, and a host drives them from viewport CHANGES
353
+ * — a gesture, a wheel tick, a re-fit. Change `pages`, `layout` or `policy` while
354
+ * the viewport sits still and the memo above builds a new controller with an
355
+ * empty cache, no `latestViewport` and no epoch: it plans nothing, commits
356
+ * nothing, and the canvas goes BLANK until the user happens to touch the
357
+ * document. MEASURED in rock-desktop's probe before this replay existed: turning
358
+ * from page 0 to page 1 in single-page mode left `epoch 0`, `mounted: none`,
359
+ * `rasters` unchanged and an empty canvas, indefinitely — a page turn is exactly
360
+ * the case where the viewport must NOT move, so nothing ever arrived.
361
+ *
362
+ * The host cannot fix this itself. That the controller was rebuilt is an
363
+ * internal detail of this hook — the `controller` facade's identity is
364
+ * deliberately stable across the swap — so a host has nothing to react to.
365
+ *
366
+ * `settle` and not `hint`: this IS the discrete event (the layer changed under a
367
+ * stationary viewport), and a hint would only arm the quiet timer. It is skipped
368
+ * on the very first controller, where `lastViewportRef` is still null and the
369
+ * host's own first settle is what starts things.
370
+ *
371
+ * DELIBERATELY NOT GATED ON THE DOCUMENT BEING THE SAME ONE. A new document
372
+ * usually arrives with a re-frame, which supersedes this epoch and makes it a
373
+ * wasted raster — but a host that swaps a same-sized document and keeps its
374
+ * viewport gets exactly the right one, and for that host the alternative is the
375
+ * blank page above. One superseded epoch is the cheaper mistake of the two.
376
+ */
377
+ useEffect(() => {
378
+ if (instance === null) {
379
+ return;
380
+ }
381
+ const viewport = lastViewportRef.current;
382
+ if (viewport === null) {
383
+ return;
384
+ }
385
+ instance.settle(viewport);
386
+ }, [instance]);
387
+ return { content, controller };
388
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `@reekon-tools/react-native-pdf-canvas/skia` — THE SKIA-ONLY SUBPATH.
3
+ *
4
+ * Everything in this package that needs a live Skia runtime is reached from
5
+ * here, and nothing that is reachable from the ROOT barrel needs one at import
6
+ * time. That split is the point of the subpath, not tidiness:
7
+ *
8
+ * root (`.`) pure core, layouts, policy, controller, cache, planner, the
9
+ * React hooks, `PdfContentView`. Safe to name from a module
10
+ * that is evaluated in Node during SSR — no CanvasKit, no
11
+ * `Skia.web.js`, no decision taken at import.
12
+ * `./skia` `ingestRaster` / `imageFromPixels` / `rasterByteLength`, and
13
+ * the `skiaApi()` accessor they use.
14
+ *
15
+ * IMPORTING THIS SUBPATH IS STILL SAFE BEFORE CANVASKIT IS LOADED. The accessor
16
+ * defers to first use (see `./skia-api.ts`), so a route module may import it at
17
+ * the top and only pay for it when a raster is actually ingested. The subpath
18
+ * boundary is a second line of defence, not the only one — belt and braces,
19
+ * because the failure it guards is silent and permanent.
20
+ *
21
+ * On web, load CanvasKit first with `loadPdfCanvasSkiaWeb()` from the
22
+ * `./web-init` subpath.
23
+ */
24
+ export { ingestRaster, imageFromPixels, rasterByteLength, colorTypeFor, alphaTypeFor, BYTES_PER_PIXEL, } from '../rasterizer/ingest.js';
25
+ export { skiaApi, setSkiaApiResolver } from './skia-api.js';
26
+ export type { SkiaApi, SkiaApiResolver } from './skia-api.js';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@reekon-tools/react-native-pdf-canvas/skia` — THE SKIA-ONLY SUBPATH.
3
+ *
4
+ * Everything in this package that needs a live Skia runtime is reached from
5
+ * here, and nothing that is reachable from the ROOT barrel needs one at import
6
+ * time. That split is the point of the subpath, not tidiness:
7
+ *
8
+ * root (`.`) pure core, layouts, policy, controller, cache, planner, the
9
+ * React hooks, `PdfContentView`. Safe to name from a module
10
+ * that is evaluated in Node during SSR — no CanvasKit, no
11
+ * `Skia.web.js`, no decision taken at import.
12
+ * `./skia` `ingestRaster` / `imageFromPixels` / `rasterByteLength`, and
13
+ * the `skiaApi()` accessor they use.
14
+ *
15
+ * IMPORTING THIS SUBPATH IS STILL SAFE BEFORE CANVASKIT IS LOADED. The accessor
16
+ * defers to first use (see `./skia-api.ts`), so a route module may import it at
17
+ * the top and only pay for it when a raster is actually ingested. The subpath
18
+ * boundary is a second line of defence, not the only one — belt and braces,
19
+ * because the failure it guards is silent and permanent.
20
+ *
21
+ * On web, load CanvasKit first with `loadPdfCanvasSkiaWeb()` from the
22
+ * `./web-init` subpath.
23
+ */
24
+ export { ingestRaster, imageFromPixels, rasterByteLength, colorTypeFor, alphaTypeFor, BYTES_PER_PIXEL, } from '../rasterizer/ingest.js';
25
+ export { skiaApi, setSkiaApiResolver } from './skia-api.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Native entry point for `./skia`.
3
+ *
4
+ * Same mechanism as `rasterizer/index.native.ts`: an `exports` condition is
5
+ * matched LITERALLY, with no platform-extension expansion, so the `react-native`
6
+ * condition needs a real `dist/skia/index.native.js` to point at.
7
+ *
8
+ * The one thing it adds over the shared barrel is installing the native
9
+ * resolver, so a consumer who imports ONLY this subpath still gets a working
10
+ * `skiaApi()`. The call is a statement using an imported binding, not a bare
11
+ * `import './x.js'` — this package declares `sideEffects: false`, and a bare
12
+ * side-effect import is exactly what that flag licenses a bundler to delete.
13
+ */
14
+ export * from './index.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Native entry point for `./skia`.
3
+ *
4
+ * Same mechanism as `rasterizer/index.native.ts`: an `exports` condition is
5
+ * matched LITERALLY, with no platform-extension expansion, so the `react-native`
6
+ * condition needs a real `dist/skia/index.native.js` to point at.
7
+ *
8
+ * The one thing it adds over the shared barrel is installing the native
9
+ * resolver, so a consumer who imports ONLY this subpath still gets a working
10
+ * `skiaApi()`. The call is a statement using an imported binding, not a bare
11
+ * `import './x.js'` — this package declares `sideEffects: false`, and a bare
12
+ * side-effect import is exactly what that flag licenses a bundler to delete.
13
+ */
14
+ import { installNativeSkiaResolver } from './resolve.native.js';
15
+ export * from './index.js';
16
+ installNativeSkiaResolver();
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Web/default entry point for `./skia`.
3
+ *
4
+ * Installs the web resolver so a consumer who imports only this subpath still
5
+ * gets a working `skiaApi()` — provided CanvasKit has been loaded by then. The
6
+ * install itself decides nothing: it stores a closure, and
7
+ * `globalThis.CanvasKit` is not read until the first raster.
8
+ *
9
+ * See `index.native.ts` for why this is a statement and not a bare
10
+ * `import './resolve.web.js'`.
11
+ */
12
+ export * from './index.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Web/default entry point for `./skia`.
3
+ *
4
+ * Installs the web resolver so a consumer who imports only this subpath still
5
+ * gets a working `skiaApi()` — provided CanvasKit has been loaded by then. The
6
+ * install itself decides nothing: it stores a closure, and
7
+ * `globalThis.CanvasKit` is not read until the first raster.
8
+ *
9
+ * See `index.native.ts` for why this is a statement and not a bare
10
+ * `import './resolve.web.js'`.
11
+ */
12
+ import { installWebSkiaResolver } from './resolve.web.js';
13
+ export * from './index.js';
14
+ installWebSkiaResolver();
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The NATIVE resolver: RN Skia's own `Skia`, imported normally.
3
+ *
4
+ * NATIVE ONLY, and the bare-specifier import below is deliberate. On native
5
+ * `lib/module/skia/Skia.js` is `export const Skia = SkiaApi;` — a read of the
6
+ * JSI global that the package's `NativeSetup` side effect installs during that
7
+ * same import — so there is no asynchronous initialisation and therefore no
8
+ * ordering hazard. Importing the barrel here is what GUARANTEES `NativeSetup`
9
+ * has run, which is stronger than reading `globalThis.SkiaApi` and hoping
10
+ * somebody else imported it.
11
+ *
12
+ * The web build never resolves this file: every `exports` entry that reaches it
13
+ * does so through a `react-native` condition. That is the same mechanism
14
+ * `rasterizer/index.native.ts` uses to keep `require('react-native')` out of a
15
+ * browser bundle.
16
+ */
17
+ /** Install the native resolver. Idempotent. */
18
+ export declare function installNativeSkiaResolver(): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The NATIVE resolver: RN Skia's own `Skia`, imported normally.
3
+ *
4
+ * NATIVE ONLY, and the bare-specifier import below is deliberate. On native
5
+ * `lib/module/skia/Skia.js` is `export const Skia = SkiaApi;` — a read of the
6
+ * JSI global that the package's `NativeSetup` side effect installs during that
7
+ * same import — so there is no asynchronous initialisation and therefore no
8
+ * ordering hazard. Importing the barrel here is what GUARANTEES `NativeSetup`
9
+ * has run, which is stronger than reading `globalThis.SkiaApi` and hoping
10
+ * somebody else imported it.
11
+ *
12
+ * The web build never resolves this file: every `exports` entry that reaches it
13
+ * does so through a `react-native` condition. That is the same mechanism
14
+ * `rasterizer/index.native.ts` uses to keep `require('react-native')` out of a
15
+ * browser bundle.
16
+ */
17
+ import { Skia } from '@shopify/react-native-skia';
18
+ import { setSkiaApiResolver } from './skia-api.js';
19
+ /** Install the native resolver. Idempotent. */
20
+ export function installNativeSkiaResolver() {
21
+ setSkiaApiResolver(() => Skia);
22
+ }