@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,96 @@
1
+ /**
2
+ * Doc-space geometry: PDF points, y-down, origin at the page's top-left.
3
+ *
4
+ * Every function here is pure and total — no throws, no NaN propagation that
5
+ * survives a guard. These are the numbers that end up as a texture allocation
6
+ * and a draw rect, so a silent NaN here is a blank page or an OOM later.
7
+ */
8
+ import type { DocRect, DocSize, PixelSize } from './types.js';
9
+ /** The result of aligning a doc rect to the device-pixel grid. */
10
+ export interface SnappedRect {
11
+ /** The rect actually covered by the bytes. NOT the rect you asked for. */
12
+ docRect: DocRect;
13
+ pixels: PixelSize;
14
+ }
15
+ export declare function rectArea(r: DocRect): number;
16
+ /**
17
+ * The negation is deliberate: `!(w > 0)` treats NaN as empty, where
18
+ * `w <= 0` would call a NaN rect non-empty and let it reach a texture alloc.
19
+ */
20
+ export declare function isEmptyRect(r: DocRect): boolean;
21
+ export declare function rectIntersects(a: DocRect, b: DocRect): boolean;
22
+ /**
23
+ * True when `inner` lies wholly within `outer`. Touching edges count as
24
+ * contained; `epsilon` widens `outer` to absorb float noise at high zoom,
25
+ * where a rect is reconstructed from a transform every frame.
26
+ */
27
+ export declare function rectContains(outer: DocRect, inner: DocRect, epsilon?: number): boolean;
28
+ /**
29
+ * True when `covers` TOGETHER contain every point of `target`.
30
+ *
31
+ * WHY THIS IS NOT `covers.some(c => rectContains(c, target))`. Once a layer is
32
+ * tiled, the thing replacing an old raster is a GRID, and no single grid cell
33
+ * contains an old cell that straddles a seam. A per-rect containment test
34
+ * therefore reports "not covered" for a grid that re-covers the area completely
35
+ * — which is how the controller ended up retaining an entire previous grid on
36
+ * every epoch, holding two full grids through every transition.
37
+ *
38
+ * WHY NOT THE UNION RECT EITHER. `rectUnion` of an L-shaped set spans the
39
+ * missing corner, so a union test claims coverage nobody paints. This is an
40
+ * EXACT rectangle-union coverage test instead, and it is exact for both shapes:
41
+ * cut the target into vertical slabs at every candidate edge, and inside each
42
+ * slab merge the y-intervals of the candidates that span it. A slab that cannot
43
+ * be spanned from `target`'s top edge to its bottom edge without a gap is a
44
+ * hole, wherever it came from.
45
+ *
46
+ * `epsilon` widens each candidate — both rects are the product of float
47
+ * arithmetic and a 1e-9 overhang is not a coverage failure. O(n^2) over an n in
48
+ * the low tens (a grid is at most a few dozen pieces), evaluated once per commit.
49
+ */
50
+ export declare function rectsCover(covers: readonly DocRect[], target: DocRect, epsilon?: number): boolean;
51
+ /** Null rather than an empty rect, so "no overlap" cannot be drawn by accident. */
52
+ export declare function rectIntersection(a: DocRect, b: DocRect): DocRect | null;
53
+ /**
54
+ * An empty operand is skipped rather than unioned: a zero-size rect still has
55
+ * an origin, and folding one in would drag the union to (0,0) and make the
56
+ * coverage predicate think the whole document was covered.
57
+ */
58
+ export declare function rectUnion(a: DocRect, b: DocRect): DocRect;
59
+ /**
60
+ * Grow by a FRACTION of the rect's own extents on each side — `fx = 0.25`
61
+ * adds 25% of the width to the left AND to the right, so the result is 1.5x
62
+ * wide. Fractions, not absolute points, because the only caller is
63
+ * `detailOverflow`, which is defined that way and must stay scale-invariant.
64
+ */
65
+ export declare function inflateRect(r: DocRect, fx: number, fy: number): DocRect;
66
+ /**
67
+ * Intersection that always returns a rect. A disjoint input collapses to a
68
+ * zero-size rect on the nearest edge of `bounds` rather than to null, so
69
+ * callers that only need "clip this to the page" need no null branch.
70
+ */
71
+ export declare function clampRectTo(r: DocRect, bounds: DocRect): DocRect;
72
+ export declare function rectsEqual(a: DocRect, b: DocRect, epsilon?: number): boolean;
73
+ /**
74
+ * Device pixels per point at which `content` fits inside `viewport`.
75
+ *
76
+ * `padding` is in DEVICE PIXELS (it is inset from a pixel-space box) and is
77
+ * removed from both sides of each axis. Returns 0 when nothing can fit, which
78
+ * callers must treat as "do not plan a raster" rather than scaling by zero.
79
+ */
80
+ export declare function fitScale(content: DocSize, viewport: PixelSize, padding?: number): number;
81
+ export declare function scaleToPixels(rect: DocRect, scale: number): PixelSize;
82
+ /**
83
+ * Expand `rect` outward until its device-pixel bounds are whole numbers.
84
+ *
85
+ * LOAD-BEARING: the returned `docRect` is what the caller must draw at, not
86
+ * the rect it asked for. A backend renders `round(width * scale)` pixels; if
87
+ * the request straddled the pixel grid, those bytes cover a slightly larger
88
+ * area than the requested rect, and drawing them into the requested rect
89
+ * rescales the bitmap by a sub-pixel fraction. That is not a rounding error
90
+ * you can ignore — it lands as blur, and as misregistration against the
91
+ * annotation layer drawn above at exact doc coordinates.
92
+ *
93
+ * Always expands (floor the near edge, ceil the far one) so the covered area
94
+ * is a superset of the request; adjacent pieces therefore overlap, never gap.
95
+ */
96
+ export declare function snapRectToDevicePixels(rect: DocRect, scale: number): SnappedRect;
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Doc-space geometry: PDF points, y-down, origin at the page's top-left.
3
+ *
4
+ * Every function here is pure and total — no throws, no NaN propagation that
5
+ * survives a guard. These are the numbers that end up as a texture allocation
6
+ * and a draw rect, so a silent NaN here is a blank page or an OOM later.
7
+ */
8
+ /** Comparison tolerance in points. Sub-micron on any real sheet. */
9
+ const DEFAULT_EPSILON = 1e-6;
10
+ export function rectArea(r) {
11
+ return Math.max(0, r.width) * Math.max(0, r.height);
12
+ }
13
+ /**
14
+ * The negation is deliberate: `!(w > 0)` treats NaN as empty, where
15
+ * `w <= 0` would call a NaN rect non-empty and let it reach a texture alloc.
16
+ */
17
+ export function isEmptyRect(r) {
18
+ return !(r.width > 0) || !(r.height > 0);
19
+ }
20
+ export function rectIntersects(a, b) {
21
+ return (a.x < b.x + b.width &&
22
+ b.x < a.x + a.width &&
23
+ a.y < b.y + b.height &&
24
+ b.y < a.y + a.height);
25
+ }
26
+ /**
27
+ * True when `inner` lies wholly within `outer`. Touching edges count as
28
+ * contained; `epsilon` widens `outer` to absorb float noise at high zoom,
29
+ * where a rect is reconstructed from a transform every frame.
30
+ */
31
+ export function rectContains(outer, inner, epsilon = 0) {
32
+ return (inner.x >= outer.x - epsilon &&
33
+ inner.y >= outer.y - epsilon &&
34
+ inner.x + inner.width <= outer.x + outer.width + epsilon &&
35
+ inner.y + inner.height <= outer.y + outer.height + epsilon);
36
+ }
37
+ /**
38
+ * True when `covers` TOGETHER contain every point of `target`.
39
+ *
40
+ * WHY THIS IS NOT `covers.some(c => rectContains(c, target))`. Once a layer is
41
+ * tiled, the thing replacing an old raster is a GRID, and no single grid cell
42
+ * contains an old cell that straddles a seam. A per-rect containment test
43
+ * therefore reports "not covered" for a grid that re-covers the area completely
44
+ * — which is how the controller ended up retaining an entire previous grid on
45
+ * every epoch, holding two full grids through every transition.
46
+ *
47
+ * WHY NOT THE UNION RECT EITHER. `rectUnion` of an L-shaped set spans the
48
+ * missing corner, so a union test claims coverage nobody paints. This is an
49
+ * EXACT rectangle-union coverage test instead, and it is exact for both shapes:
50
+ * cut the target into vertical slabs at every candidate edge, and inside each
51
+ * slab merge the y-intervals of the candidates that span it. A slab that cannot
52
+ * be spanned from `target`'s top edge to its bottom edge without a gap is a
53
+ * hole, wherever it came from.
54
+ *
55
+ * `epsilon` widens each candidate — both rects are the product of float
56
+ * arithmetic and a 1e-9 overhang is not a coverage failure. O(n^2) over an n in
57
+ * the low tens (a grid is at most a few dozen pieces), evaluated once per commit.
58
+ */
59
+ export function rectsCover(covers, target, epsilon = 0) {
60
+ // Nothing to cover. Vacuously true, and the caller's "may I release the old
61
+ // set" question is answered yes, which is correct for an empty old set.
62
+ if (isEmptyRect(target))
63
+ return true;
64
+ const tx0 = target.x;
65
+ const tx1 = target.x + target.width;
66
+ const ty0 = target.y;
67
+ const ty1 = target.y + target.height;
68
+ /** Candidates clipped to the target, in edge coordinates. */
69
+ const clipped = [];
70
+ for (const c of covers) {
71
+ if (isEmptyRect(c))
72
+ continue;
73
+ const x0 = Math.max(c.x - epsilon, tx0);
74
+ const x1 = Math.min(c.x + c.width + epsilon, tx1);
75
+ const y0 = Math.max(c.y - epsilon, ty0);
76
+ const y1 = Math.min(c.y + c.height + epsilon, ty1);
77
+ if (x1 - x0 > DEFAULT_EPSILON && y1 - y0 > DEFAULT_EPSILON) {
78
+ clipped.push({ x0, x1, y0, y1 });
79
+ }
80
+ }
81
+ if (clipped.length === 0)
82
+ return false;
83
+ const xs = [tx0, tx1];
84
+ for (const c of clipped) {
85
+ if (c.x0 > tx0 && c.x0 < tx1)
86
+ xs.push(c.x0);
87
+ if (c.x1 > tx0 && c.x1 < tx1)
88
+ xs.push(c.x1);
89
+ }
90
+ xs.sort((a, b) => a - b);
91
+ for (let i = 0; i < xs.length - 1; i++) {
92
+ const from = xs[i] ?? tx0;
93
+ const to = xs[i + 1] ?? tx1;
94
+ // Duplicate edges collapse to a zero-width slab. Sub-micron on any real
95
+ // sheet, and a slab with no width has no area to leave uncovered.
96
+ if (to - from <= DEFAULT_EPSILON)
97
+ continue;
98
+ const spans = [];
99
+ for (const c of clipped) {
100
+ if (c.x0 <= from + DEFAULT_EPSILON && c.x1 >= to - DEFAULT_EPSILON) {
101
+ spans.push({ y0: c.y0, y1: c.y1 });
102
+ }
103
+ }
104
+ if (spans.length === 0)
105
+ return false;
106
+ spans.sort((a, b) => a.y0 - b.y0);
107
+ let reach = ty0;
108
+ for (const span of spans) {
109
+ // A gap between the last covered y and this span's top is a hole, and
110
+ // sorting by y0 means no later span can fill it.
111
+ if (span.y0 > reach + DEFAULT_EPSILON)
112
+ break;
113
+ if (span.y1 > reach)
114
+ reach = span.y1;
115
+ if (reach >= ty1 - DEFAULT_EPSILON)
116
+ break;
117
+ }
118
+ if (reach < ty1 - DEFAULT_EPSILON)
119
+ return false;
120
+ }
121
+ return true;
122
+ }
123
+ /** Null rather than an empty rect, so "no overlap" cannot be drawn by accident. */
124
+ export function rectIntersection(a, b) {
125
+ const x = Math.max(a.x, b.x);
126
+ const y = Math.max(a.y, b.y);
127
+ const right = Math.min(a.x + a.width, b.x + b.width);
128
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
129
+ if (!(right > x) || !(bottom > y))
130
+ return null;
131
+ return { x, y, width: right - x, height: bottom - y };
132
+ }
133
+ /**
134
+ * An empty operand is skipped rather than unioned: a zero-size rect still has
135
+ * an origin, and folding one in would drag the union to (0,0) and make the
136
+ * coverage predicate think the whole document was covered.
137
+ */
138
+ export function rectUnion(a, b) {
139
+ if (isEmptyRect(a))
140
+ return { ...b };
141
+ if (isEmptyRect(b))
142
+ return { ...a };
143
+ const x = Math.min(a.x, b.x);
144
+ const y = Math.min(a.y, b.y);
145
+ const right = Math.max(a.x + a.width, b.x + b.width);
146
+ const bottom = Math.max(a.y + a.height, b.y + b.height);
147
+ return { x, y, width: right - x, height: bottom - y };
148
+ }
149
+ /**
150
+ * Grow by a FRACTION of the rect's own extents on each side — `fx = 0.25`
151
+ * adds 25% of the width to the left AND to the right, so the result is 1.5x
152
+ * wide. Fractions, not absolute points, because the only caller is
153
+ * `detailOverflow`, which is defined that way and must stay scale-invariant.
154
+ */
155
+ export function inflateRect(r, fx, fy) {
156
+ const dx = r.width * fx;
157
+ const dy = r.height * fy;
158
+ return {
159
+ x: r.x - dx,
160
+ y: r.y - dy,
161
+ width: r.width + dx * 2,
162
+ height: r.height + dy * 2,
163
+ };
164
+ }
165
+ /**
166
+ * Intersection that always returns a rect. A disjoint input collapses to a
167
+ * zero-size rect on the nearest edge of `bounds` rather than to null, so
168
+ * callers that only need "clip this to the page" need no null branch.
169
+ */
170
+ export function clampRectTo(r, bounds) {
171
+ const bRight = bounds.x + bounds.width;
172
+ const bBottom = bounds.y + bounds.height;
173
+ const x = Math.min(Math.max(r.x, bounds.x), bRight);
174
+ const y = Math.min(Math.max(r.y, bounds.y), bBottom);
175
+ const right = Math.min(Math.max(r.x + r.width, bounds.x), bRight);
176
+ const bottom = Math.min(Math.max(r.y + r.height, bounds.y), bBottom);
177
+ return {
178
+ x,
179
+ y,
180
+ width: Math.max(0, right - x),
181
+ height: Math.max(0, bottom - y),
182
+ };
183
+ }
184
+ export function rectsEqual(a, b, epsilon = DEFAULT_EPSILON) {
185
+ return (Math.abs(a.x - b.x) <= epsilon &&
186
+ Math.abs(a.y - b.y) <= epsilon &&
187
+ Math.abs(a.width - b.width) <= epsilon &&
188
+ Math.abs(a.height - b.height) <= epsilon);
189
+ }
190
+ /**
191
+ * Device pixels per point at which `content` fits inside `viewport`.
192
+ *
193
+ * `padding` is in DEVICE PIXELS (it is inset from a pixel-space box) and is
194
+ * removed from both sides of each axis. Returns 0 when nothing can fit, which
195
+ * callers must treat as "do not plan a raster" rather than scaling by zero.
196
+ */
197
+ export function fitScale(content, viewport, padding = 0) {
198
+ if (!(content.width > 0) || !(content.height > 0))
199
+ return 0;
200
+ const available = {
201
+ width: viewport.width - padding * 2,
202
+ height: viewport.height - padding * 2,
203
+ };
204
+ if (!(available.width > 0) || !(available.height > 0))
205
+ return 0;
206
+ return Math.min(available.width / content.width, available.height / content.height);
207
+ }
208
+ export function scaleToPixels(rect, scale) {
209
+ if (!(scale > 0))
210
+ return { width: 0, height: 0 };
211
+ return {
212
+ width: Math.max(0, Math.round(rect.width * scale)),
213
+ height: Math.max(0, Math.round(rect.height * scale)),
214
+ };
215
+ }
216
+ /**
217
+ * Expand `rect` outward until its device-pixel bounds are whole numbers.
218
+ *
219
+ * LOAD-BEARING: the returned `docRect` is what the caller must draw at, not
220
+ * the rect it asked for. A backend renders `round(width * scale)` pixels; if
221
+ * the request straddled the pixel grid, those bytes cover a slightly larger
222
+ * area than the requested rect, and drawing them into the requested rect
223
+ * rescales the bitmap by a sub-pixel fraction. That is not a rounding error
224
+ * you can ignore — it lands as blur, and as misregistration against the
225
+ * annotation layer drawn above at exact doc coordinates.
226
+ *
227
+ * Always expands (floor the near edge, ceil the far one) so the covered area
228
+ * is a superset of the request; adjacent pieces therefore overlap, never gap.
229
+ */
230
+ export function snapRectToDevicePixels(rect, scale) {
231
+ if (!(scale > 0))
232
+ return { docRect: { ...rect }, pixels: { width: 0, height: 0 } };
233
+ const left = Math.floor(rect.x * scale);
234
+ const top = Math.floor(rect.y * scale);
235
+ const right = Math.ceil((rect.x + rect.width) * scale);
236
+ const bottom = Math.ceil((rect.y + rect.height) * scale);
237
+ const width = Math.max(0, right - left);
238
+ const height = Math.max(0, bottom - top);
239
+ return {
240
+ docRect: {
241
+ x: left / scale,
242
+ y: top / scale,
243
+ width: width / scale,
244
+ height: height / scale,
245
+ },
246
+ pixels: { width, height },
247
+ };
248
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The shared public surface of @reekon-tools/react-native-pdf-canvas.
3
+ *
4
+ * `./testing` is deliberately NOT re-exported here — the deterministic fake
5
+ * rasterizer has its own subpath export so it never ends up in a production
6
+ * bundle just because someone imported a type from the root.
7
+ *
8
+ * Nor is `./rasterizer/index.{native,web}.js`: the platform default backend is
9
+ * resolved through `setDefaultRasterizer` / `getDefaultRasterizer` on the React
10
+ * side, and star-exporting a platform file from this platform-agnostic barrel is
11
+ * how the two entry points start disagreeing about what `getDefaultRasterizer`
12
+ * means.
13
+ *
14
+ * ------------------------------------------------------------------------
15
+ * AND NEITHER IS THE INGEST SEAM. THIS BARREL MUST STAY SKIA-FREE.
16
+ * ------------------------------------------------------------------------
17
+ *
18
+ * `ingestRaster` / `imageFromPixels` / `rasterByteLength` used to be re-exported
19
+ * from here, which meant that merely NAMING this package pulled
20
+ * `@shopify/react-native-skia`'s barrel — and with it `Skia.web.js`, whose
21
+ * module body is `JsiSkApi(global.CanvasKit)`. On web that call is taken once
22
+ * and never retaken, so evaluating it before `LoadSkiaWeb` resolves leaves a
23
+ * permanently broken `Skia` that re-importing cannot repair. rock-desktop is
24
+ * `{ssr: true}` with `flatRoutes()` and no prerender, so every route module is
25
+ * evaluated in Node on every request: one root import is enough.
26
+ *
27
+ * They now live on the `./skia` subpath, and everything reachable from here is
28
+ * free of module-scope Skia. That is not a style rule — it is asserted by
29
+ * `__tests__/module-ordering.test.ts`, which BUNDLES this file and greps the
30
+ * output for `JsiSkApi(global.CanvasKit)`.
31
+ */
32
+ /** The frozen type contract, plus the `PdfError` class every seam throws. */
33
+ export * from './types.js';
34
+ /** The React surface: both hooks and the Skia node. */
35
+ export * from './react/index.js';
36
+ /** Page layouts. `layout.ts` is singular — there is no `layouts.js`. */
37
+ export * from './layout.js';
38
+ /** Cadence constants and the ignore-undefined merge over them. */
39
+ export * from './policy.js';
40
+ /**
41
+ * The cadence engine, for a host that drives it outside React — an export or a
42
+ * print path, where there is no component to hang a hook on. Its companion types
43
+ * come with it: an exported factory whose options type is unreachable is a dead
44
+ * export.
45
+ */
46
+ export { createPdfController } from './controller.js';
47
+ export type { PdfControllerInstance, PdfControllerOptions, RasterIngestMeta, ScheduledHandle, } from './controller.js';
48
+ /** The cache `createPdfController` requires. */
49
+ export { createRasterCache } from './cache.js';
50
+ export type { EvictionReason, RasterCache, RasterCacheOptions, RasterCacheStats, } from './cache.js';
51
+ /** The two pure decisions, WHAT to raster and WHEN, usable on their own. */
52
+ export { planRasters, shouldReraster } from './planner.js';
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The shared public surface of @reekon-tools/react-native-pdf-canvas.
3
+ *
4
+ * `./testing` is deliberately NOT re-exported here — the deterministic fake
5
+ * rasterizer has its own subpath export so it never ends up in a production
6
+ * bundle just because someone imported a type from the root.
7
+ *
8
+ * Nor is `./rasterizer/index.{native,web}.js`: the platform default backend is
9
+ * resolved through `setDefaultRasterizer` / `getDefaultRasterizer` on the React
10
+ * side, and star-exporting a platform file from this platform-agnostic barrel is
11
+ * how the two entry points start disagreeing about what `getDefaultRasterizer`
12
+ * means.
13
+ *
14
+ * ------------------------------------------------------------------------
15
+ * AND NEITHER IS THE INGEST SEAM. THIS BARREL MUST STAY SKIA-FREE.
16
+ * ------------------------------------------------------------------------
17
+ *
18
+ * `ingestRaster` / `imageFromPixels` / `rasterByteLength` used to be re-exported
19
+ * from here, which meant that merely NAMING this package pulled
20
+ * `@shopify/react-native-skia`'s barrel — and with it `Skia.web.js`, whose
21
+ * module body is `JsiSkApi(global.CanvasKit)`. On web that call is taken once
22
+ * and never retaken, so evaluating it before `LoadSkiaWeb` resolves leaves a
23
+ * permanently broken `Skia` that re-importing cannot repair. rock-desktop is
24
+ * `{ssr: true}` with `flatRoutes()` and no prerender, so every route module is
25
+ * evaluated in Node on every request: one root import is enough.
26
+ *
27
+ * They now live on the `./skia` subpath, and everything reachable from here is
28
+ * free of module-scope Skia. That is not a style rule — it is asserted by
29
+ * `__tests__/module-ordering.test.ts`, which BUNDLES this file and greps the
30
+ * output for `JsiSkApi(global.CanvasKit)`.
31
+ */
32
+ /** The frozen type contract, plus the `PdfError` class every seam throws. */
33
+ export * from './types.js';
34
+ /** The React surface: both hooks and the Skia node. */
35
+ export * from './react/index.js';
36
+ /** Page layouts. `layout.ts` is singular — there is no `layouts.js`. */
37
+ export * from './layout.js';
38
+ /** Cadence constants and the ignore-undefined merge over them. */
39
+ export * from './policy.js';
40
+ /**
41
+ * The cadence engine, for a host that drives it outside React — an export or a
42
+ * print path, where there is no component to hang a hook on. Its companion types
43
+ * come with it: an exported factory whose options type is unreachable is a dead
44
+ * export.
45
+ */
46
+ export { createPdfController } from './controller.js';
47
+ /** The cache `createPdfController` requires. */
48
+ export { createRasterCache } from './cache.js';
49
+ /** The two pure decisions, WHAT to raster and WHEN, usable on their own. */
50
+ export { planRasters, shouldReraster } from './planner.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Platform entry point — the shared surface, plus the native Skia resolver.
3
+ *
4
+ * This file exists ONLY because Metro applies NO platform-extension expansion to
5
+ * the targets of a package's `exports` map. Metro will happily resolve
6
+ * `./foo` to `foo.native.ts` for a normal relative import, but an `exports`
7
+ * condition is matched literally: `"react-native": "./dist/index.js"` would load
8
+ * `dist/index.js` and never look for `dist/index.native.js`. So the map points at
9
+ * the literal `dist/index.native.js` path, and this file is what produces it.
10
+ *
11
+ * Keep the EXPORTS a pure re-export. The moment platform-specific wiring is
12
+ * exported from here, the two entry points can drift and the `types` condition
13
+ * (which resolves to `index.d.ts`) starts lying about one of them.
14
+ *
15
+ * The resolver install below adds no exports and is the one thing that
16
+ * legitimately differs: on native `Skia` is `global.SkiaApi`, installed
17
+ * synchronously by RN Skia's own import-time side effect, so importing the
18
+ * barrel here is both correct and free of the web ordering hazard. `index.web.ts`
19
+ * installs the CanvasKit-backed resolver instead. See `./skia/skia-api.ts`.
20
+ */
21
+ export * from './index.js';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Platform entry point — the shared surface, plus the native Skia resolver.
3
+ *
4
+ * This file exists ONLY because Metro applies NO platform-extension expansion to
5
+ * the targets of a package's `exports` map. Metro will happily resolve
6
+ * `./foo` to `foo.native.ts` for a normal relative import, but an `exports`
7
+ * condition is matched literally: `"react-native": "./dist/index.js"` would load
8
+ * `dist/index.js` and never look for `dist/index.native.js`. So the map points at
9
+ * the literal `dist/index.native.js` path, and this file is what produces it.
10
+ *
11
+ * Keep the EXPORTS a pure re-export. The moment platform-specific wiring is
12
+ * exported from here, the two entry points can drift and the `types` condition
13
+ * (which resolves to `index.d.ts`) starts lying about one of them.
14
+ *
15
+ * The resolver install below adds no exports and is the one thing that
16
+ * legitimately differs: on native `Skia` is `global.SkiaApi`, installed
17
+ * synchronously by RN Skia's own import-time side effect, so importing the
18
+ * barrel here is both correct and free of the web ordering hazard. `index.web.ts`
19
+ * installs the CanvasKit-backed resolver instead. See `./skia/skia-api.ts`.
20
+ */
21
+ import { installNativeSkiaResolver } from './skia/resolve.native.js';
22
+ export * from './index.js';
23
+ installNativeSkiaResolver();
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Web/default entry point — the shared surface, plus the web Skia resolver.
3
+ *
4
+ * Same reason as index.native.ts for existing at all: `exports` targets are
5
+ * matched literally, with no platform-extension expansion, so the `browser` and
6
+ * `default` conditions need a real `dist/index.web.js` file to point at.
7
+ *
8
+ * ------------------------------------------------------------------------
9
+ * THE INSTALL BELOW DECIDES NOTHING AT IMPORT TIME.
10
+ * ------------------------------------------------------------------------
11
+ *
12
+ * It stores a closure. `globalThis.CanvasKit` is not read, `JsiSkApi` is not
13
+ * called, and no module that calls it is evaluated — so this file stays safe to
14
+ * evaluate in Node during rock-desktop's SSR pass, before CanvasKit exists
15
+ * anywhere. Asserted by `__tests__/module-ordering.test.ts`, which bundles this
16
+ * exact file and greps for `JsiSkApi(global.CanvasKit)`.
17
+ *
18
+ * It exists so that a host which loads CanvasKit its own way (`WithSkiaWeb`
19
+ * around the canvas, RN Skia's bare `LoadSkiaWeb`, a `<script>` that sets
20
+ * `globalThis.CanvasKit`) still gets a working `skiaApi()` without knowing this
21
+ * package has a `./web-init` subpath. `loadPdfCanvasSkiaWeb()` remains the
22
+ * explicit, awaitable route and is what the README points at.
23
+ */
24
+ export * from './index.js';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Web/default entry point — the shared surface, plus the web Skia resolver.
3
+ *
4
+ * Same reason as index.native.ts for existing at all: `exports` targets are
5
+ * matched literally, with no platform-extension expansion, so the `browser` and
6
+ * `default` conditions need a real `dist/index.web.js` file to point at.
7
+ *
8
+ * ------------------------------------------------------------------------
9
+ * THE INSTALL BELOW DECIDES NOTHING AT IMPORT TIME.
10
+ * ------------------------------------------------------------------------
11
+ *
12
+ * It stores a closure. `globalThis.CanvasKit` is not read, `JsiSkApi` is not
13
+ * called, and no module that calls it is evaluated — so this file stays safe to
14
+ * evaluate in Node during rock-desktop's SSR pass, before CanvasKit exists
15
+ * anywhere. Asserted by `__tests__/module-ordering.test.ts`, which bundles this
16
+ * exact file and greps for `JsiSkApi(global.CanvasKit)`.
17
+ *
18
+ * It exists so that a host which loads CanvasKit its own way (`WithSkiaWeb`
19
+ * around the canvas, RN Skia's bare `LoadSkiaWeb`, a `<script>` that sets
20
+ * `globalThis.CanvasKit`) still gets a working `skiaApi()` without knowing this
21
+ * package has a `./web-init` subpath. `loadPdfCanvasSkiaWeb()` remains the
22
+ * explicit, awaitable route and is what the README points at.
23
+ */
24
+ import { installWebSkiaResolver } from './skia/resolve.web.js';
25
+ export * from './index.js';
26
+ installWebSkiaResolver();
@@ -0,0 +1,78 @@
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 type { DocRect, PdfPageLayout } from './types.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 declare function singlePage(): PdfPageLayout;
45
+ /**
46
+ * Does this layout put two pages on top of each other?
47
+ *
48
+ * Answered about the RECTS a layout produced for a specific document, never
49
+ * about the layout function, because it is a property of both: `singlePage()`
50
+ * over a one-page document stacks nothing, and a bespoke host layout can stack
51
+ * without being `singlePage()`.
52
+ *
53
+ * Overlap is STRICT — `rectIntersects` uses `<`, so a zero-gap continuous layout,
54
+ * where consecutive pages share an edge exactly, is not a stack. Empty rects are
55
+ * skipped: a layout that produced two zero-size rects at the same origin has a
56
+ * different problem than stacking, and reporting this one would send the reader
57
+ * to the wrong place.
58
+ *
59
+ * O(n^2) with an early exit, evaluated once per layout/document change and only
60
+ * behind a `__DEV__` guard at its one call site. Do not put it on a per-frame
61
+ * path.
62
+ */
63
+ export declare function hasOverlappingPages(rects: readonly DocRect[]): boolean;
64
+ /**
65
+ * Pages stacked top to bottom, horizontally centred on the widest page.
66
+ * Centring (rather than left-aligning) matters in mixed-size documents — a
67
+ * letter page bound into a set of plans should not hug the left edge.
68
+ */
69
+ export declare function continuousVertical(gap?: number): PdfPageLayout;
70
+ /**
71
+ * Two-up: pages (0,1) form the first row, (2,3) the second, and so on. `gap`
72
+ * separates both the two pages of a row and consecutive rows.
73
+ *
74
+ * No cover-page special case. A viewer that wants page 0 alone can pad its own
75
+ * page list; baking that convention in here would make the layout lie about
76
+ * which page sits where for every document that does not follow it.
77
+ */
78
+ export declare function spread(gap?: number): PdfPageLayout;