@reekon-tools/react-native-pdf-canvas 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/PdfCanvas.podspec +68 -39
  4. package/README.md +213 -1316
  5. package/android/build.gradle +67 -54
  6. package/android/consumer-rules.pro +7 -0
  7. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
  8. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
  9. package/android/src/main/cpp/CMakeLists.txt +54 -0
  10. package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
  11. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
  12. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
  13. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
  14. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
  15. package/android/tools/compile-gate.sh +81 -101
  16. package/dist/controller.d.ts +14 -0
  17. package/dist/controller.js +4 -3
  18. package/dist/index.native.d.ts +5 -0
  19. package/dist/index.native.js +5 -0
  20. package/dist/index.web.d.ts +10 -0
  21. package/dist/index.web.js +10 -0
  22. package/dist/rasterizer/index.native.d.ts +8 -12
  23. package/dist/rasterizer/index.native.js +15 -21
  24. package/dist/rasterizer/native-bridge.d.ts +51 -75
  25. package/dist/rasterizer/native-bridge.js +49 -33
  26. package/dist/rasterizer/native.d.ts +45 -0
  27. package/dist/rasterizer/native.js +123 -0
  28. package/dist/react/usePdfDocument.d.ts +5 -5
  29. package/dist/react/usePdfLayer.d.ts +10 -1
  30. package/dist/react/usePdfLayer.js +10 -2
  31. package/dist/skia/index.native.d.ts +7 -3
  32. package/dist/skia/index.native.js +7 -3
  33. package/dist/skia/index.web.d.ts +4 -2
  34. package/dist/skia/index.web.js +4 -2
  35. package/dist/testing/scenes.d.ts +3 -3
  36. package/dist/testing/scenes.js +4 -4
  37. package/dist/types.d.ts +36 -42
  38. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  39. package/native/.clangd +5 -0
  40. package/native/CMakeLists.txt +89 -0
  41. package/native/core/include/pdfcanvas/document.h +90 -0
  42. package/native/core/include/pdfcanvas/error.h +47 -0
  43. package/native/core/include/pdfcanvas/jsi.h +18 -0
  44. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  45. package/native/core/include/pdfcanvas/library.h +28 -0
  46. package/native/core/include/pdfcanvas/pixels.h +92 -0
  47. package/native/core/include/pdfcanvas/service.h +87 -0
  48. package/native/core/include/pdfcanvas/slots.h +113 -0
  49. package/native/core/include/pdfcanvas/types.h +110 -0
  50. package/native/core/pdfcanvas-core.cmake +22 -0
  51. package/native/core/src/document.cpp +516 -0
  52. package/native/core/src/error.cpp +29 -0
  53. package/native/core/src/jsi.cpp +66 -0
  54. package/native/core/src/library.cpp +52 -0
  55. package/native/core/src/pixels.cpp +45 -0
  56. package/native/core/src/service.cpp +156 -0
  57. package/native/core/src/slots.cpp +139 -0
  58. package/native/tests/fixtures.cpp +451 -0
  59. package/native/tests/fixtures.h +96 -0
  60. package/native/tests/harness.h +122 -0
  61. package/native/tests/main.cpp +64 -0
  62. package/native/tests/pixels_util.h +126 -0
  63. package/native/tests/test_document.cpp +485 -0
  64. package/native/tests/test_service.cpp +130 -0
  65. package/native/tests/test_slots.cpp +250 -0
  66. package/package.json +38 -8
  67. package/react-native.config.js +5 -2
  68. package/scripts/fetch-pdfium.mjs +487 -0
  69. package/scripts/pdfium-manifest.json +46 -0
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  72. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  73. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  74. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  75. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  76. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  77. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  78. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  79. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  80. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  81. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  82. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  83. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  84. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  85. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  86. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  87. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  88. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  89. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  90. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  91. package/dist/rasterizer/android.d.ts +0 -121
  92. package/dist/rasterizer/android.js +0 -126
  93. package/dist/rasterizer/ios.d.ts +0 -84
  94. package/dist/rasterizer/ios.js +0 -155
  95. package/ios/Package.swift +0 -58
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  98. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  99. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  100. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  101. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  102. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  103. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  104. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  105. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  109. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  110. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  111. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  112. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  113. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  114. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  115. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  116. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  117. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  118. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  119. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  120. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  121. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  122. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  123. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  124. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  125. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  126. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  127. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  128. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  129. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -1,45 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- /**
4
- * A page's intrinsic size in PDF points, mirroring {@code PageGeometry} in
5
- * {@code src/types.ts}.
6
- *
7
- * POST-ROTATION, per that contract. {@code PdfRenderer.Page.getWidth()} already
8
- * is: it forwards to PDFium's {@code FPDF_GetPageWidth}, and
9
- * {@code CPDF_Page::UpdateDimensions} swaps the media box's width and height
10
- * when {@code /Rotate} is 90 or 270. So a 612x792 page carrying {@code /Rotate 90}
11
- * reports 792x612, and the render matrix operates in that same upright space —
12
- * which is why the tiling arithmetic never has to know about rotation at all.
13
- * {@code pageGeometryIsPostRotation} measures this rather than trusting it.
14
- *
15
- * {@link #rotationDegrees} is therefore reported as 0, NOT as the page's
16
- * {@code /Rotate} value: the platform pre-applies the rotation to both the
17
- * dimensions and the transform, so from this backend's vantage point there is no
18
- * residual rotation left to apply. There is also no way to read {@code /Rotate}
19
- * through {@code PdfRenderer} below API 35, so a non-zero value here could only
20
- * be invented. Reporting the truth of what the caller must do — nothing — beats
21
- * reporting a number the caller would then wrongly compensate for.
22
- */
23
- public final class PageGeometry {
24
-
25
- public final int index;
26
- /** Width in PDF points, after /Rotate. */
27
- public final int widthPoints;
28
- /** Height in PDF points, after /Rotate. */
29
- public final int heightPoints;
30
- /** Residual rotation the caller must still apply. Always 0 here; see above. */
31
- public final int rotationDegrees;
32
-
33
- PageGeometry(int index, int widthPoints, int heightPoints, int rotationDegrees) {
34
- this.index = index;
35
- this.widthPoints = widthPoints;
36
- this.heightPoints = heightPoints;
37
- this.rotationDegrees = rotationDegrees;
38
- }
39
-
40
- @Override
41
- public String toString() {
42
- return "PageGeometry{" + index + " " + widthPoints + "x" + heightPoints
43
- + "pt rot=" + rotationDegrees + "}";
44
- }
45
- }
@@ -1,564 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import android.graphics.Bitmap;
4
- import android.graphics.Color;
5
- import android.graphics.Matrix;
6
- import android.graphics.pdf.PdfRenderer;
7
-
8
- import java.io.Closeable;
9
- import java.io.File;
10
- import java.io.FileOutputStream;
11
- import java.io.IOException;
12
- import java.nio.ByteBuffer;
13
-
14
- import tools.reekon.pdfcanvas.jsi.PdfCanvasPixelBridge;
15
-
16
- /**
17
- * The Android page rasterizer. Plain Android — {@code PdfRenderer},
18
- * {@code Bitmap}, {@code ParcelFileDescriptor} — and NOTHING from React Native
19
- * or Nitro, so it can be proven on a bare emulator with
20
- * {@code ./gradlew connectedDebugAndroidTest} and no app around it.
21
- *
22
- * Shaped to serve {@code RasterizerHandle} in {@code src/types.ts}:
23
- * {@code pageCount}, {@code pageGeometry(index)}, {@code render(request, signal)},
24
- * {@code close()}. The binding in {@code src/reactnative} is a marshalling layer
25
- * over this and adds no behaviour.
26
- *
27
- * <h3>How a tile is drawn</h3>
28
- *
29
- * {@code setScale(s, s); postTranslate(-x*s, -y*s)} — spike S1, finding 1,
30
- * MEASURED on API 24, 31 and 33: a tile rendered with that matrix is
31
- * pixel-identical to the same region cropped out of a full-page render
32
- * (0/262144 mismatching pixels, max channel delta 1) on all three. A translate
33
- * that puts the page origin OUTSIDE the destination bitmap is honoured exactly,
34
- * which is the only reason tiling on the platform renderer is possible at all;
35
- * had it failed, Android would have gone straight to vendored PDFium.
36
- *
37
- * S1 finding 2, also measured: cost tracks DESTINATION BITMAP AREA, not page
38
- * area or scale. An ARCH-D tile at 8x costs the same as at 1x, so the engine
39
- * clips rather than rasterizing the page and discarding most of it. That is what
40
- * makes a viewport-sized detail raster constant in zoom.
41
- */
42
- public final class PdfCanvasRasterizer implements Closeable {
43
-
44
- /** Surfaced as {@code PageRasterizer.id}, so a bug report names the backend. */
45
- public static final String ID = "android-pdfrenderer";
46
-
47
- /**
48
- * Refuse absurd rasters instead of dying on {@code OutOfMemoryError}.
49
- *
50
- * 64 Mpx is 256 MB at 4 bytes per pixel — already past what a mid-tier device
51
- * will hand out in one allocation, so anything above it is a planner bug, and
52
- * a named error against a raster key is a far better diagnostic than a process
53
- * death with no key attached. The TS policy has its own, much lower, ceilings
54
- * ({@code maxRasterPixels}); this is the backstop for a request that reaches
55
- * here anyway.
56
- */
57
- public static final int MAX_RASTER_PIXELS = 64_000_000;
58
-
59
- private final PdfRendererPool pool;
60
- private final PageGeometry[] pages;
61
- private final File spooled;
62
- private volatile boolean closed;
63
-
64
- private PdfCanvasRasterizer(PdfRendererPool pool, PageGeometry[] pages, File spooled) {
65
- this.pool = pool;
66
- this.pages = pages;
67
- this.spooled = spooled;
68
- }
69
-
70
- /* ---------------------------------------------------------------- *
71
- * Opening
72
- * ---------------------------------------------------------------- */
73
-
74
- /**
75
- * @param password must be null. Kept in the signature because the TS
76
- * {@code PdfSource} carries one and dropping it here would make the
77
- * backend silently ignore it; {@link PdfRendererPool#open} rejects a
78
- * non-null value with {@code unsupported} and says why.
79
- * @param poolSize number of concurrent renders to allow, clamped to
80
- * [1, {@link PdfRendererPool#MAX_POOL_SIZE}].
81
- */
82
- public static PdfCanvasRasterizer openFile(File file, String password, int poolSize)
83
- throws PdfRasterException {
84
- return open(file, password, poolSize, null);
85
- }
86
-
87
- /**
88
- * Spools bytes to a file first, because {@code PdfRenderer} requires a
89
- * SEEKABLE descriptor — it reads through a seek-and-read callback, so a pipe
90
- * or a socket is not an option and neither is anything streamed. The spooled
91
- * file is deleted by {@link #close()}.
92
- */
93
- public static PdfCanvasRasterizer openBytes(
94
- File spoolDir, byte[] data, String password, int poolSize) throws PdfRasterException {
95
- if (data == null || data.length == 0) {
96
- throw new PdfRasterException(PdfErrorCode.CORRUPT, "Document data is empty.");
97
- }
98
- File spool;
99
- try {
100
- if (!spoolDir.exists() && !spoolDir.mkdirs()) {
101
- throw new IOException("Could not create " + spoolDir.getAbsolutePath());
102
- }
103
- spool = File.createTempFile("pdfcanvas-", ".pdf", spoolDir);
104
- try (FileOutputStream out = new FileOutputStream(spool)) {
105
- out.write(data);
106
- }
107
- } catch (IOException e) {
108
- throw new PdfRasterException(
109
- PdfErrorCode.BACKEND_FAILURE, "Could not spool document data to disk.", e);
110
- }
111
- try {
112
- return open(spool, password, poolSize, spool);
113
- } catch (PdfRasterException e) {
114
- // The spool is ours; nothing else will ever delete it if open() fails.
115
- //noinspection ResultOfMethodCallIgnored
116
- spool.delete();
117
- throw e;
118
- }
119
- }
120
-
121
- private static PdfCanvasRasterizer open(
122
- File file, String password, int poolSize, File spooled) throws PdfRasterException {
123
- PdfRendererPool pool = PdfRendererPool.open(file, password, poolSize);
124
- try {
125
- // Every page's geometry is probed ONCE, here. The contract says
126
- // pageGeometry() is cheap and "must not load a page", and the only way to
127
- // learn a page's size through PdfRenderer is to open it — so the loading
128
- // happens at open() and pageGeometry() becomes an array read.
129
- // pageGeometryIsPrecomputedAtOpenNotLoadedOnDemand pins that by asking for
130
- // geometry AFTER close(), when every PdfRenderer is shut: a lazy
131
- // implementation would throw or crash there, and it reads a page it never
132
- // asked about before the close so a lazily-cached page 0 cannot pass by
133
- // accident. (This comment previously named a pageGeometryDoesNotOpenAPage
134
- // test, which does not exist, and described a leased-out-pool setup that
135
- // is not what the real test does.)
136
- PageGeometry[] pages = probeGeometry(pool);
137
- return new PdfCanvasRasterizer(pool, pages, spooled);
138
- } catch (PdfRasterException | RuntimeException e) {
139
- pool.close();
140
- throw e;
141
- }
142
- }
143
-
144
- private static PageGeometry[] probeGeometry(PdfRendererPool pool)
145
- throws PdfRasterException {
146
- int count = pool.pageCount();
147
- PageGeometry[] pages = new PageGeometry[count];
148
- PdfRenderer renderer = pool.lease();
149
- try {
150
- for (int i = 0; i < count; i++) {
151
- PdfRenderer.Page page = null;
152
- try {
153
- page = renderer.openPage(i);
154
- // getWidth()/getHeight() are already post-/Rotate; see PageGeometry.
155
- pages[i] = new PageGeometry(i, page.getWidth(), page.getHeight(), 0);
156
- } catch (RuntimeException e) {
157
- throw new PdfRasterException(
158
- PdfErrorCode.CORRUPT, "Could not read page " + i + ".", e);
159
- } finally {
160
- if (page != null) {
161
- page.close();
162
- }
163
- }
164
- }
165
- } finally {
166
- pool.release(renderer);
167
- }
168
- return pages;
169
- }
170
-
171
- /* ---------------------------------------------------------------- *
172
- * Cheap accessors
173
- * ---------------------------------------------------------------- */
174
-
175
- public int pageCount() {
176
- return pages.length;
177
- }
178
-
179
- /** An array read. See the note in {@link #open}. */
180
- public PageGeometry pageGeometry(int index) throws PdfRasterException {
181
- if (index < 0 || index >= pages.length) {
182
- throw new PdfRasterException(
183
- PdfErrorCode.NOT_FOUND,
184
- "Document has " + pages.length + " pages; asked for " + index + ".");
185
- }
186
- return pages[index];
187
- }
188
-
189
- /** How many renders can genuinely be in flight at once. */
190
- public int poolSize() {
191
- return pool.size();
192
- }
193
-
194
- /**
195
- * Whether this device draws a page carrying {@code /Rotate} upright.
196
- *
197
- * NOT a formality: on API 24 it does not — the dimensions swap but the content
198
- * is drawn unrotated and clipped, so a rotated page comes back wrong with no
199
- * error anywhere. There is nothing this backend can do about it, because
200
- * {@code PdfRenderer} exposes no way to read a page's {@code /Rotate} value
201
- * below API 35 and so no way to compensate. What it CAN do is say so, which is
202
- * what this is for: a host can fall back to another backend, or warn, instead
203
- * of silently showing a sheared drawing. See {@link PdfPlatformQuirks}.
204
- */
205
- public static PdfPlatformQuirks.RotationSupport rotationSupport(File scratchDir) {
206
- return PdfPlatformQuirks.rotationSupport(scratchDir);
207
- }
208
-
209
- /* ---------------------------------------------------------------- *
210
- * Rendering
211
- * ---------------------------------------------------------------- */
212
-
213
- /**
214
- * The heap form: the pixels come back in {@code RasterPixels.bytes}.
215
- *
216
- * This is what every caller that wants to READ pixels uses, and it is the only
217
- * form the instrumented suite exercises for correctness.
218
- */
219
- public RasterPixels render(RasterRequest request, RasterCancellation signal)
220
- throws PdfRasterException {
221
- return render(request, signal, RasterPixels.NO_SLOT);
222
- }
223
-
224
- /**
225
- * The one-copy form: the finished raster is written straight into native
226
- * memory under {@code slot} and never touches the Java heap.
227
- *
228
- * DELETES, per tile, one {@code new byte[byteCount]} on ART's large-object
229
- * space and one full-raster memcpy out of it. What is left is the single copy
230
- * out of the {@code Bitmap}'s own pixels, which the heap form pays as well.
231
- *
232
- * <h3>The result may be heap-backed anyway, and callers must handle that</h3>
233
- *
234
- * {@code PdfCanvasPixelBridge.nativeStoreFromBitmap} is allowed to REFUSE — no
235
- * native memory to be had, a stride the NDK reports differently from the
236
- * Bitmap, a slot id already occupied — and every refusal falls through to the
237
- * heap path, because a slower correct tile beats a lost one. So check
238
- * {@link RasterPixels#isInNativeSlot()} on the way out rather than assuming.
239
- * When it is false, {@code slot} holds nothing and there is nothing to release.
240
- *
241
- * <h3>What is released, and where</h3>
242
- *
243
- * Nothing, on the happy path: the slot is taken exactly once by JS. This
244
- * method DOES release it on the one abandon path it owns — a cancellation
245
- * observed after the draw finished, which is the post-render check in
246
- * {@link #renderLeased}. Every abandon path AFTER this method returns belongs
247
- * to the caller, and {@code PdfCanvasModule.render} closes them with a
248
- * {@code finally} that releases unless the slot was handed to the promise.
249
- *
250
- * @param slot a slot id from a per-process monotonic counter. Must not be
251
- * {@link RasterPixels#NO_SLOT}, and must not be an id already in use — the
252
- * native side refuses a collision rather than overwriting, since an
253
- * overwrite hands one raster's memory to another raster's take.
254
- */
255
- public RasterPixels renderToNativeSlot(
256
- RasterRequest request, RasterCancellation signal, long slot)
257
- throws PdfRasterException {
258
- if (slot == RasterPixels.NO_SLOT) {
259
- throw new PdfRasterException(
260
- PdfErrorCode.BACKEND_FAILURE,
261
- "renderToNativeSlot needs a real slot id; " + RasterPixels.NO_SLOT
262
- + " is reserved for heap-backed rasters.");
263
- }
264
- return render(request, signal, slot);
265
- }
266
-
267
- private RasterPixels render(RasterRequest request, RasterCancellation signal, long slot)
268
- throws PdfRasterException {
269
- RasterCancellation.throwIfCancelled(signal, "before start");
270
- requireOpen();
271
-
272
- // Validates the page index before anything expensive is allocated or leased.
273
- pageGeometry(request.page);
274
-
275
- if (!(request.scale > 0f) || Float.isInfinite(request.scale)) {
276
- throw new PdfRasterException(
277
- PdfErrorCode.BACKEND_FAILURE, "Scale must be finite and positive: " + request.scale);
278
- }
279
- if (!(request.docWidth > 0f) || !(request.docHeight > 0f)) {
280
- throw new PdfRasterException(
281
- PdfErrorCode.BACKEND_FAILURE,
282
- "Doc rect has no area: " + request.docWidth + "x" + request.docHeight);
283
- }
284
-
285
- int pxW = request.pixelWidth();
286
- int pxH = request.pixelHeight();
287
- long pixels = (long) pxW * (long) pxH;
288
- if (pixels > MAX_RASTER_PIXELS) {
289
- throw new PdfRasterException(
290
- PdfErrorCode.OUT_OF_MEMORY,
291
- "Refused a " + pxW + "x" + pxH + " raster (" + pixels + " pixels > "
292
- + MAX_RASTER_PIXELS + ").");
293
- }
294
-
295
- PdfRenderer renderer = pool.lease();
296
- // The lease can block for the length of another render, so this is the check
297
- // that actually saves work when a raster is superseded mid-queue.
298
- try {
299
- RasterCancellation.throwIfCancelled(signal, "after pool wait");
300
- requireOpen();
301
- return renderLeased(renderer, request, pxW, pxH, signal, slot);
302
- } finally {
303
- pool.release(renderer);
304
- }
305
- }
306
-
307
- private RasterPixels renderLeased(
308
- PdfRenderer renderer,
309
- RasterRequest request,
310
- int pxW,
311
- int pxH,
312
- RasterCancellation signal,
313
- long slot)
314
- throws PdfRasterException {
315
- PdfRenderer.Page page = null;
316
- Bitmap bitmap = null;
317
- try {
318
- page = renderer.openPage(request.page);
319
- bitmap = createBitmap(pxW, pxH);
320
-
321
- // S1 finding 3, MEASURED: render() does NOT clear the destination. A
322
- // bitmap arrives from createBitmap already zeroed, so 'transparent' would
323
- // survive without this call — but writing it explicitly is what makes the
324
- // two backgrounds one decision instead of one decision and one accident,
325
- // and it is required for the reuse path the moment bitmaps get pooled.
326
- bitmap.eraseColor(
327
- request.background == RasterRequest.BACKGROUND_TRANSPARENT
328
- ? Color.TRANSPARENT
329
- : Color.WHITE);
330
-
331
- Matrix matrix = tileMatrix(request);
332
-
333
- RasterCancellation.throwIfCancelled(signal, "before render");
334
- // Uninterruptible from here to the next statement. See RasterCancellation.
335
- page.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
336
-
337
- RasterPixels out = extract(bitmap, pxW, pxH, slot);
338
-
339
- // Post-check. This saves no drawing — the pixels above are already
340
- // finished — it only stops a superseded raster being handed back and
341
- // ingested into Skia, which is the expensive half on the JS side.
342
- //
343
- // THE RELEASE IS NOT OPTIONAL. `out` is dropped on the floor when this
344
- // throws, and a heap-backed raster is simply collected — but a
345
- // slot-backed one has already been published to the native map, where
346
- // nothing else will ever look for it. A superseded epoch produces a
347
- // stream of these, so leaking here is megabytes per gesture, freed only
348
- // by the next reload. The check stays AFTER extract rather than moving
349
- // in front of it so that the cancel window it covers does not shrink.
350
- try {
351
- RasterCancellation.throwIfCancelled(signal, "after render");
352
- } catch (PdfRasterException e) {
353
- out.release();
354
- throw e;
355
- }
356
- return out;
357
- } catch (IllegalStateException e) {
358
- // openPage's documented failure when a page is already open on this
359
- // renderer. Reaching it means the lease discipline broke, so say that
360
- // rather than reporting a generic backend failure.
361
- throw new PdfRasterException(
362
- PdfErrorCode.BACKEND_FAILURE,
363
- "PdfRenderer refused page " + request.page
364
- + " — a page was already open on this renderer, which means the "
365
- + "pool leased it twice.",
366
- e);
367
- } catch (IllegalArgumentException e) {
368
- throw new PdfRasterException(
369
- PdfErrorCode.NOT_FOUND, "No page " + request.page + " in this document.", e);
370
- } finally {
371
- if (bitmap != null) {
372
- // The pixels have been copied out; holding the Bitmap alive until the GC
373
- // notices costs pxW*pxH*4 of native heap per raster in flight.
374
- bitmap.recycle();
375
- }
376
- if (page != null) {
377
- page.close();
378
- }
379
- }
380
- }
381
-
382
- /**
383
- * The S1 matrix. {@code scale} is used EXACTLY as requested rather than being
384
- * re-derived from the rounded pixel size ({@code pxW / docWidth}).
385
- *
386
- * That choice matters: it is what makes two rasters requested at the same
387
- * scale agree pixel-for-pixel wherever they overlap, which is the property the
388
- * cache and the overlapping detail pieces depend on. Re-deriving would instead
389
- * make each raster cover its doc rect to the last sub-pixel, at the cost of
390
- * every pair of neighbours being drawn at imperceptibly different scales. At
391
- * most half a pixel of the far edge goes undrawn, and it is left showing the
392
- * background fill.
393
- *
394
- * PINNED BY {@code rastersAtTheSameScaleAgreeWhereTheyOverlap}, which asks for
395
- * two rects at 4x whose pixel widths round in OPPOSITE directions (512.5 up to
396
- * 513, 512.4 down to 512) and requires them to agree over the overlap. NOT by
397
- * the S1 equivalence test, which this comment used to name: its rects are 400
398
- * and 128 points at 4x, both landing on exact pixel counts, so a re-derived
399
- * scale comes out to exactly 4 for both and that test cannot tell the two
400
- * implementations apart.
401
- */
402
- private static Matrix tileMatrix(RasterRequest request) {
403
- Matrix matrix = new Matrix();
404
- matrix.setScale(request.scale, request.scale);
405
- matrix.postTranslate(-request.docX * request.scale, -request.docY * request.scale);
406
- return matrix;
407
- }
408
-
409
- private static Bitmap createBitmap(int pxW, int pxH) throws PdfRasterException {
410
- try {
411
- // MUTABLE ARGB_8888 is not a preference, it is the platform's rule:
412
- // PdfRenderer.render throws IllegalArgumentException on an immutable
413
- // bitmap and on any config other than ARGB_8888. createBitmap returns a
414
- // mutable bitmap, so there is nothing further to assert here.
415
- return Bitmap.createBitmap(pxW, pxH, Bitmap.Config.ARGB_8888);
416
- } catch (OutOfMemoryError e) {
417
- // Caught, not propagated. An OOM on one oversized raster is recoverable —
418
- // the cache drops it and the planner asks for less — whereas letting the
419
- // Error escape takes the whole JS thread's promise chain with it.
420
- throw new PdfRasterException(
421
- PdfErrorCode.OUT_OF_MEMORY,
422
- "Could not allocate a " + pxW + "x" + pxH + " ARGB_8888 bitmap ("
423
- + ((long) pxW * pxH * 4L) + " bytes).",
424
- e);
425
- }
426
- }
427
-
428
- /**
429
- * Bitmap -> pixels, with the stride the platform actually used.
430
- *
431
- * TWO DESTINATIONS. With {@code slot == RasterPixels.NO_SLOT} this is the
432
- * original: a {@code byte[]} on the Java heap, filled by
433
- * {@code copyPixelsToBuffer}. With a real slot it tries native memory first
434
- * and falls back to that same heap path on any refusal, so the heap path is
435
- * never dead code and never diverges — it is what the base64 transport, every
436
- * pixel-reading caller and the whole instrumented suite still use.
437
- *
438
- * Everything below about stride, premultiplication and byte order is true of
439
- * BOTH, because both copy the identical bytes out of the identical bitmap: the
440
- * native path's memcpy from {@code AndroidBitmap_lockPixels} and Skia's memcpy
441
- * inside {@code nativeCopyPixelsToBuffer} read the same
442
- * {@code bitmap.getPixels()} address for the same {@code getByteCount()}
443
- * length. (Byte ORDER never enters it — {@code copyPixelsToBuffer} into a
444
- * {@code ByteBuffer} is a raw memcpy and ignores the buffer's
445
- * {@code order()}. That would NOT be true of an {@code IntBuffer}.)
446
- *
447
- * {@code copyPixelsToBuffer} is a raw copy of the bitmap's pixel memory:
448
- * {@code getByteCount()} is {@code getRowBytes() * getHeight()}, so any row
449
- * padding is copied through and {@code getRowBytes()} is the honest
450
- * {@code rowBytes}. Assuming {@code width * 4} here would not fail — it would
451
- * SHEAR the image by one pixel per row on any padded allocation, which reads
452
- * as a plausible skew rather than an error.
453
- *
454
- * On the three levels measured that shear is unreachable, and the comment used
455
- * to imply otherwise: {@code Bitmap.createBitmap} allocates at
456
- * {@code SkImageInfo::minRowBytes()}, i.e. exactly {@code width * 4}, so the
457
- * assumption and the reading agree at every width tested.
458
- * {@code rowBytesIsReportedNotAssumed} pins the reported value against the
459
- * platform's own stride and says so in its own comment; the padded path is
460
- * exercised in vitest against the fake backend. Reading the value is still the
461
- * right call — it costs nothing and it is correct on a device that pads.
462
- *
463
- * The bytes are PREMULTIPLIED. Bitmaps are premultiplied by default and this
464
- * code never calls {@code setPremultiplied(false)} — doing so would make a
465
- * {@code background: 'transparent'} raster composite wrongly rather than
466
- * visibly fail.
467
- *
468
- * The TS side DECLARES that rather than assuming it: {@code ANDROID_ALPHA} in
469
- * {@code src/rasterizer/android.ts} sets {@code RasterPixels.alpha} to
470
- * {@code 'premultiplied'} and {@code ingest.ts} maps it to
471
- * {@code AlphaType.Premul}. It is a constant there, not a field of the wire
472
- * result, because it is a property of {@code Bitmap} and cannot vary per
473
- * render. It is NOT a global default — PDFium and pdf.js both emit STRAIGHT
474
- * alpha, so the other backends declare the opposite.
475
- */
476
- private static RasterPixels extract(Bitmap bitmap, int pxW, int pxH, long slot)
477
- throws PdfRasterException {
478
- int rowBytes = bitmap.getRowBytes();
479
- int byteCount = bitmap.getByteCount();
480
- // FIRST, and before either destination is allocated. Both paths copy exactly
481
- // byteCount bytes and both would shear on a bad stride, so this cannot move
482
- // inside one of the branches.
483
- if (rowBytes < pxW * 4 || byteCount < rowBytes * pxH) {
484
- throw new PdfRasterException(
485
- PdfErrorCode.BACKEND_FAILURE,
486
- "Bitmap reported an impossible layout: " + pxW + "x" + pxH
487
- + " rowBytes=" + rowBytes + " byteCount=" + byteCount + ".");
488
- }
489
-
490
- // THE ONE-COPY PATH. Tried first when a slot was asked for, and every
491
- // failure mode of it is a `false` that falls through to the heap path below
492
- // — including running out of native memory, which is why the OUT_OF_MEMORY
493
- // mapping still comes from the Java allocation and not from here.
494
- //
495
- // `isLoaded()` is checked rather than assumed: every method on the bridge is
496
- // `native`, so on a build with no libpdfcanvasjsi.so this call is an
497
- // UnsatisfiedLinkError, not a false.
498
- if (slot != RasterPixels.NO_SLOT && PdfCanvasPixelBridge.isLoaded()) {
499
- boolean stored;
500
- try {
501
- stored =
502
- PdfCanvasPixelBridge.nativeStoreFromBitmap(slot, bitmap, pxW, pxH, byteCount);
503
- } catch (Throwable t) {
504
- // The JNI method reports every failure of its OWN as a false. What can
505
- // still arrive as a throwable is a Java exception raised by the platform
506
- // calls it makes on the way — {@code AndroidBitmap_getInfo} and
507
- // {@code AndroidBitmap_lockPixels} may leave one pending, and a JNI
508
- // method that returns while an exception is pending throws it at the
509
- // caller regardless of the value it returned. Either way the answer is
510
- // the same and it is the point of this catch: fall through to the heap.
511
- // Letting it escape would abandon a raster that IS already drawn in
512
- // exchange for nothing.
513
- stored = false;
514
- }
515
- if (stored) {
516
- return RasterPixels.inNativeSlot(
517
- slot, pxW, pxH, rowBytes, RasterPixels.FORMAT_RGBA_8888);
518
- }
519
- }
520
-
521
- byte[] out;
522
- try {
523
- out = new byte[byteCount];
524
- } catch (OutOfMemoryError e) {
525
- throw new PdfRasterException(
526
- PdfErrorCode.OUT_OF_MEMORY,
527
- "Could not allocate " + byteCount + " bytes for raster pixels.", e);
528
- }
529
- // ByteBuffer.wrap starts at position 0, and copyPixelsToBuffer ADVANCES the
530
- // position by byteCount as it copies. Nothing downstream reads the buffer —
531
- // `out` is indexed from 0 — and a fresh wrapper is made every call, so no
532
- // rewind is needed and none is implied. A REUSED buffer would need one.
533
- bitmap.copyPixelsToBuffer(ByteBuffer.wrap(out));
534
- return new RasterPixels(out, pxW, pxH, rowBytes, RasterPixels.FORMAT_RGBA_8888);
535
- }
536
-
537
- /* ---------------------------------------------------------------- *
538
- * Teardown
539
- * ---------------------------------------------------------------- */
540
-
541
- private void requireOpen() throws PdfRasterException {
542
- if (closed || pool.isClosed()) {
543
- throw new PdfRasterException(
544
- PdfErrorCode.BACKEND_FAILURE, "Document handle is closed.");
545
- }
546
- }
547
-
548
- @Override
549
- public void close() {
550
- if (closed) {
551
- return;
552
- }
553
- closed = true;
554
- pool.close();
555
- if (spooled != null) {
556
- //noinspection ResultOfMethodCallIgnored
557
- spooled.delete();
558
- }
559
- }
560
-
561
- public boolean isClosed() {
562
- return closed;
563
- }
564
- }