@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,234 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import android.graphics.pdf.PdfRenderer;
4
- import android.os.ParcelFileDescriptor;
5
-
6
- import java.io.File;
7
- import java.io.IOException;
8
- import java.util.ArrayList;
9
- import java.util.List;
10
- import java.util.concurrent.ArrayBlockingQueue;
11
- import java.util.concurrent.BlockingQueue;
12
-
13
- /**
14
- * A fixed set of {@link PdfRenderer} instances over the same file, leased one at
15
- * a time.
16
- *
17
- * WHY A POOL EXISTS AT ALL. {@code PdfRenderer} permits exactly ONE open page
18
- * per instance: {@code openPage} throws {@code IllegalStateException}
19
- * ("Current page not closed") if another is already open. That is a per-instance
20
- * rule, not a per-process one, so the thing that does not work is TWO THREADS ON
21
- * ONE RENDERER — not two renderers. Renderer-per-concurrent-render is the only
22
- * way to have more than one raster in flight, and it is what this class hands
23
- * out. Nothing in here makes a single renderer thread-safe, and nothing should
24
- * try: the lease is the mutual exclusion.
25
- *
26
- * WHY NOT {@code ParcelFileDescriptor.dup()}. It is the obvious way to give each
27
- * renderer its own fd, and it is wrong here. {@code dup()} is {@code dup(2)},
28
- * which produces a new descriptor onto the SAME open file description — and the
29
- * file offset lives in the description, so two dup'd fds share one seek
30
- * position. AOSP's PdfRenderer reads through an {@code FPDF_FILEACCESS} callback
31
- * that seeks then reads, so two renderers reading concurrently through dup'd fds
32
- * are racing on that offset. Today AOSP happens to serialise every PdfRenderer
33
- * native call behind a process-wide PDFium lock, which hides it; that is a
34
- * property of one implementation, not of the API. Opening the file afresh per
35
- * renderer costs one extra {@code open(2)} and gives each renderer an
36
- * independent file description, which is the invariant the code actually needs.
37
- */
38
- final class PdfRendererPool {
39
-
40
- /**
41
- * Ceiling on renderers. Each one holds an fd and PDFium's parsed document
42
- * structure for the same file, so the memory cost is per-instance and real on
43
- * a large sheet. Two in flight is already enough to keep a detail raster from
44
- * queueing behind a base raster, which is the only concurrency the cadence
45
- * ever asks for.
46
- */
47
- static final int MAX_POOL_SIZE = 4;
48
-
49
- private final File file;
50
- private final List<PdfRenderer> all;
51
- private final BlockingQueue<PdfRenderer> free;
52
- private final int pageCount;
53
- private volatile boolean closed;
54
-
55
- private PdfRendererPool(File file, List<PdfRenderer> renderers, int pageCount) {
56
- this.file = file;
57
- this.all = renderers;
58
- this.free = new ArrayBlockingQueue<>(renderers.size());
59
- this.free.addAll(renderers);
60
- this.pageCount = pageCount;
61
- }
62
-
63
- static PdfRendererPool open(File file, String password, int requestedSize)
64
- throws PdfRasterException {
65
- // ORDER IS LOAD-BEARING. Preflight runs before anything can construct a
66
- // PdfRenderer, because on API 24 a throwing constructor poisons PDFium for
67
- // the whole process — see PdfPreflight for the measurement. The latch then
68
- // covers files preflight cannot judge.
69
- PdfPlatformQuirks.requireUsablePdfium();
70
- PdfPreflight.check(file);
71
- if (password != null) {
72
- // compileSdk 34 has no `android.graphics.pdf.LoadParams`, which is where
73
- // a password became expressible (API 35). Below that the platform class
74
- // offers no way to supply one at all, so accepting the argument and
75
- // silently ignoring it would turn "wrong password" into "corrupt file".
76
- throw new PdfRasterException(
77
- PdfErrorCode.UNSUPPORTED,
78
- "A password was supplied, but android.graphics.pdf.PdfRenderer cannot "
79
- + "accept one below API 35 (LoadParams). Use a PDFium backend for "
80
- + "encrypted documents.");
81
- }
82
-
83
- int size = Math.max(1, Math.min(MAX_POOL_SIZE, requestedSize));
84
- List<PdfRenderer> renderers = new ArrayList<>(size);
85
- int pages = 0;
86
- try {
87
- for (int i = 0; i < size; i++) {
88
- renderers.add(openRenderer(file));
89
- }
90
- pages = renderers.get(0).getPageCount();
91
- } catch (PdfRasterException | RuntimeException e) {
92
- closeAll(renderers);
93
- throw e;
94
- }
95
-
96
- if (pages <= 0) {
97
- closeAll(renderers);
98
- throw new PdfRasterException(
99
- PdfErrorCode.CORRUPT, "Document reports " + pages + " pages.");
100
- }
101
- return new PdfRendererPool(file, renderers, pages);
102
- }
103
-
104
- private static PdfRenderer openRenderer(File file) throws PdfRasterException {
105
- ParcelFileDescriptor pfd = null;
106
- try {
107
- pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
108
- // PdfRenderer takes ownership of the descriptor and closes it in close().
109
- return new PdfRenderer(pfd);
110
- } catch (SecurityException e) {
111
- // Reaching here means preflight missed an encryption scheme it does not
112
- // recognise. The damage on an affected build is already done — the
113
- // half-built PdfRenderer is unreachable and finalizable — so all that is
114
- // left is to stop the next open walking into the segfault.
115
- PdfPlatformQuirks.recordFailedNativeOpen();
116
- closeQuietly(pfd);
117
- // The platform's documented signal for "this file requires a password or
118
- // uses a security scheme we do not support". No password could have been
119
- // supplied (open() rejects that above), so this is always "required".
120
- throw new PdfRasterException(
121
- PdfErrorCode.PASSWORD_REQUIRED,
122
- "The document is encrypted: " + file.getName() + ".",
123
- e);
124
- } catch (IOException e) {
125
- PdfPlatformQuirks.recordFailedNativeOpen();
126
- closeQuietly(pfd);
127
- throw new PdfRasterException(
128
- PdfErrorCode.CORRUPT,
129
- "Not a readable PDF: " + file.getName() + " (" + e.getMessage() + ").",
130
- e);
131
- } catch (RuntimeException e) {
132
- // IllegalArgumentException ("file descriptor not seekable") and anything
133
- // else the constructor can throw land here. Same reasoning as above: the
134
- // constructor threw, so the process is on borrowed time.
135
- PdfPlatformQuirks.recordFailedNativeOpen();
136
- closeQuietly(pfd);
137
- throw new PdfRasterException(
138
- PdfErrorCode.BACKEND_FAILURE,
139
- "PdfRenderer refused " + file.getName() + ": " + e.getMessage(),
140
- e);
141
- }
142
- }
143
-
144
- int pageCount() {
145
- return pageCount;
146
- }
147
-
148
- File file() {
149
- return file;
150
- }
151
-
152
- int size() {
153
- return all.size();
154
- }
155
-
156
- /**
157
- * Blocks until a renderer is free. The wait is where a cancel most often lands
158
- * — a superseded detail raster typically queues behind a base raster — so the
159
- * caller re-checks its signal on the far side of this call.
160
- */
161
- PdfRenderer lease() throws PdfRasterException {
162
- if (closed) {
163
- throw new PdfRasterException(
164
- PdfErrorCode.BACKEND_FAILURE, "Document handle is closed.");
165
- }
166
- try {
167
- PdfRenderer renderer = free.take();
168
- if (closed) {
169
- // close() raced us. Do not hand out a renderer that close() has already
170
- // closed, and do not put it back either — closeAll owns it now.
171
- throw new PdfRasterException(
172
- PdfErrorCode.BACKEND_FAILURE, "Document handle was closed while waiting.");
173
- }
174
- return renderer;
175
- } catch (InterruptedException e) {
176
- Thread.currentThread().interrupt();
177
- throw new PdfRasterException(
178
- PdfErrorCode.CANCELLED, "Interrupted waiting for a PdfRenderer.", e);
179
- }
180
- }
181
-
182
- /** Always call from a finally: a leaked lease deadlocks every later render. */
183
- void release(PdfRenderer renderer) {
184
- if (renderer != null) {
185
- free.offer(renderer);
186
- }
187
- }
188
-
189
- /**
190
- * Closes every renderer, including ones currently leased.
191
- *
192
- * The leased case is deliberate and it is the only honest option: close() is
193
- * called from JS when a document unmounts, and blocking the caller until an
194
- * in-flight ARCH-D render finishes is worse than the alternative. The renderer
195
- * whose {@code render()} is mid-flight is not closed out from under it —
196
- * {@code closed} is set first, {@code PdfCanvasRasterizer} re-checks it, and
197
- * the renderers are closed after. The remaining window is the same one every
198
- * native handle has.
199
- */
200
- void close() {
201
- if (closed) {
202
- return;
203
- }
204
- closed = true;
205
- free.clear();
206
- closeAll(all);
207
- }
208
-
209
- boolean isClosed() {
210
- return closed;
211
- }
212
-
213
- private static void closeAll(List<PdfRenderer> renderers) {
214
- for (PdfRenderer renderer : renderers) {
215
- try {
216
- renderer.close();
217
- } catch (RuntimeException ignored) {
218
- // close() throws IllegalStateException if a page is still open. There is
219
- // nothing useful to do about it during teardown, and letting it escape
220
- // would skip closing the remaining renderers and leak their fds.
221
- }
222
- }
223
- }
224
-
225
- private static void closeQuietly(ParcelFileDescriptor pfd) {
226
- if (pfd != null) {
227
- try {
228
- pfd.close();
229
- } catch (IOException ignored) {
230
- // Nothing to salvage; the throwing path already has the real cause.
231
- }
232
- }
233
- }
234
- }
@@ -1,47 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import java.util.concurrent.atomic.AtomicBoolean;
4
-
5
- /**
6
- * Best-effort cancellation, and the emphasis is on BEST-EFFORT.
7
- *
8
- * {@code PdfRenderer.Page.render} is synchronous and uninterruptible once
9
- * entered. There is no cancel token, no progress callback, and no safe way to
10
- * kill the thread — PDFium is mid-way through writing into a Bitmap this process
11
- * owns. So this class cannot stop a render; it can only stop the NEXT one
12
- * starting, which is the same shape the TS contract already assumes for PDFKit
13
- * ({@code RasterPolicy.minEpochIntervalMs} exists because of it).
14
- *
15
- * Concretely, a cancel is observed at four points and no others:
16
- * on entry, after the pool lease (the wait can be long when every renderer is
17
- * busy), immediately before {@code render()}, and immediately after it. The last
18
- * one does not save any work — the pixels are already drawn — it only stops a
19
- * superseded raster being handed back and ingested into Skia. The
20
- * {@code cancelDuringAnInFlightRenderIsObservedOnlyAfterItFinishes} test asserts
21
- * that shape rather than pretending otherwise. (It was named here as
22
- * {@code cancellationCannotInterruptAnInFlightRender}, which does not exist.)
23
- */
24
- public final class RasterCancellation {
25
-
26
- private final AtomicBoolean cancelled = new AtomicBoolean(false);
27
-
28
- public void cancel() {
29
- cancelled.set(true);
30
- }
31
-
32
- public boolean isCancelled() {
33
- return cancelled.get();
34
- }
35
-
36
- /**
37
- * @param stage named in the message so a cancellation reported from the
38
- * post-render check is distinguishable from one that saved real work.
39
- */
40
- static void throwIfCancelled(RasterCancellation signal, String stage)
41
- throws PdfRasterException {
42
- if (signal != null && signal.isCancelled()) {
43
- throw new PdfRasterException(
44
- PdfErrorCode.CANCELLED, "Render cancelled (" + stage + ").");
45
- }
46
- }
47
- }
@@ -1,143 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import tools.reekon.pdfcanvas.jsi.PdfCanvasPixelBridge;
4
-
5
- /**
6
- * A finished raster, mirroring {@code RasterPixels} in {@code src/types.ts}.
7
- *
8
- * {@link #rowBytes} is STATED, never assumed to be {@code width * 4}. It is read
9
- * back from {@code Bitmap.getRowBytes()}, because a wrong stride does not fail —
10
- * it shears the image, one pixel further per row, which reads as a plausible
11
- * skew rather than an error.
12
- *
13
- * HOW FAR THAT IS PROVEN, precisely. {@code rowBytesIsReportedNotAssumed} checks
14
- * the value against the stride the platform itself chose for an identically
15
- * sized {@code ARGB_8888} bitmap, at seven widths including odd and prime ones.
16
- * What it CANNOT show is the difference between reading the stride and assuming
17
- * {@code width * 4}: {@code Bitmap.createBitmap} allocates through
18
- * {@code SkImageInfo::minRowBytes()}, which is exactly {@code width * 4} with no
19
- * alignment, so on API 24, 31 and 33 every measured stride is tight and the two
20
- * implementations produce identical output. The padded case is exercised on the
21
- * JS side instead, where the fake backend pads on demand. Reading the value is
22
- * therefore the correct thing to do and is what happens here — but treat "we
23
- * read it" as a design decision that the device suite agrees with, not as
24
- * something the device suite could have falsified.
25
- *
26
- * <h3>Where the pixels actually are</h3>
27
- *
28
- * ONE OF TWO PLACES, and {@link #bytes} being null is how you tell:
29
- *
30
- * <ul>
31
- * <li>ON THE JAVA HEAP — {@code bytes} is the raster and {@link #slot} is
32
- * {@link #NO_SLOT}. Every caller that reads pixels wants this, and it is
33
- * what {@link PdfCanvasRasterizer#render} returns.
34
- * <li>IN A NATIVE SLOT — {@code bytes} is null and {@code slot} names a buffer
35
- * in {@code pdfcanvas-jsi.cpp} waiting to become a JS {@code ArrayBuffer}.
36
- * Only {@link PdfCanvasRasterizer#renderToNativeSlot} produces this, only
37
- * the React binding asks for it, and NOTHING can read the pixels back from
38
- * Java — the whole point is that they never touch the Java heap.
39
- * </ul>
40
- *
41
- * That is a deliberately awkward shape rather than two classes, because the one
42
- * caller that asks for a slot has to handle both anyway: the native store is
43
- * allowed to refuse (see
44
- * {@link PdfCanvasPixelBridge#nativeStoreFromBitmap}) and the heap is the
45
- * fallback, so a slot request can legitimately come back heap-backed.
46
- * {@link #channel} and {@link #offsetOf} therefore fail LOUDLY and by name on a
47
- * slot-backed raster instead of throwing a bare NPE from somewhere further in.
48
- *
49
- * <h3>Who frees the slot</h3>
50
- *
51
- * Normally nobody here: the buffer is taken exactly once by
52
- * {@code __pdfCanvasTakePixels} and freed when the JS {@code ArrayBuffer} is
53
- * collected. {@link #release()} exists for the paths where the raster is
54
- * ABANDONED after it was filled — a cancellation observed after the draw, or a
55
- * throw between the render finishing and the promise resolving. Without it those
56
- * megabytes would sit in the slot map until the next
57
- * {@code nativeReleaseAll()}, which only happens on a reload.
58
- */
59
- public final class RasterPixels {
60
-
61
- /**
62
- * The only format this backend emits.
63
- *
64
- * {@code Bitmap.Config.ARGB_8888} is named for the packed 32-bit int that
65
- * {@code getPixel} returns, but the IN-MEMORY byte order is R, G, B, A —
66
- * which is what {@code copyPixelsToBuffer} produces and what Skia calls
67
- * {@code kRGBA_8888}. The name and the layout disagree, which is precisely the
68
- * kind of thing that gets assumed instead of checked, so it is measured.
69
- */
70
- public static final String FORMAT_RGBA_8888 = "rgba8888";
71
-
72
- /** {@link #slot} when the pixels are on the Java heap. Never a real slot id. */
73
- public static final long NO_SLOT = 0L;
74
-
75
- /** The raster, or null when it is in a native slot. See the class note. */
76
- public final byte[] bytes;
77
- public final int width;
78
- public final int height;
79
- public final int rowBytes;
80
- public final String format;
81
- /** {@link #NO_SLOT}, or the native slot holding the raster. */
82
- public final long slot;
83
-
84
- RasterPixels(byte[] bytes, int width, int height, int rowBytes, String format) {
85
- this(bytes, width, height, rowBytes, format, NO_SLOT);
86
- }
87
-
88
- private RasterPixels(
89
- byte[] bytes, int width, int height, int rowBytes, String format, long slot) {
90
- this.bytes = bytes;
91
- this.width = width;
92
- this.height = height;
93
- this.rowBytes = rowBytes;
94
- this.format = format;
95
- this.slot = slot;
96
- }
97
-
98
- static RasterPixels inNativeSlot(
99
- long slot, int width, int height, int rowBytes, String format) {
100
- return new RasterPixels(null, width, height, rowBytes, format, slot);
101
- }
102
-
103
- /** Whether the pixels are in native memory and unreadable from Java. */
104
- public boolean isInNativeSlot() {
105
- return slot != NO_SLOT;
106
- }
107
-
108
- /**
109
- * Frees the native slot if this raster is in one. Idempotent, and a no-op for
110
- * a heap-backed raster.
111
- *
112
- * Call this on any path that DROPS the raster after it was produced. A taken
113
- * slot is already gone from the map, so releasing after a successful take is
114
- * harmless — {@code nativeRelease} is a lookup that finds nothing.
115
- */
116
- public void release() {
117
- if (isInNativeSlot() && PdfCanvasPixelBridge.isLoaded()) {
118
- PdfCanvasPixelBridge.nativeRelease(slot);
119
- }
120
- }
121
-
122
- /** Byte offset of pixel (x, y), honouring the real stride. */
123
- public int offsetOf(int x, int y) {
124
- requireHeapBacked();
125
- return y * rowBytes + x * 4;
126
- }
127
-
128
- /** One channel of one pixel, as an unsigned 0..255 value. */
129
- public int channel(int x, int y, int channel) {
130
- requireHeapBacked();
131
- return bytes[offsetOf(x, y) + channel] & 0xff;
132
- }
133
-
134
- private void requireHeapBacked() {
135
- if (bytes == null) {
136
- throw new IllegalStateException(
137
- "This raster's pixels are in native slot " + slot + ", not on the Java "
138
- + "heap, so they cannot be read from Java. Ask "
139
- + "PdfCanvasRasterizer.render() for a heap-backed raster instead of "
140
- + "renderToNativeSlot().");
141
- }
142
- }
143
- }
@@ -1,95 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- /**
4
- * One raster to produce, mirroring {@code RasterRequest} in {@code src/types.ts}.
5
- *
6
- * The doc rect is in PAGE-LOCAL points — y-down, origin at the page's top-left,
7
- * already post-rotation (see {@link PageGeometry}). It is NOT in whatever
8
- * document-wide space the JS layout function laid the pages out in; the TS
9
- * adapter subtracts the page origin before it gets here. A backend that had to
10
- * know the page layout would be a backend the layout function could break.
11
- */
12
- public final class RasterRequest {
13
-
14
- /** Fill the bitmap opaque white before drawing. */
15
- public static final int BACKGROUND_WHITE = 0;
16
- /**
17
- * Leave the bitmap cleared to transparent before drawing.
18
- *
19
- * This means the CALLER accepts undrawn area coming back transparent. It is
20
- * emphatically NOT a claim that {@code PdfRenderer.render} clears anything —
21
- * spike S1 measured that it does not (finding 3): untouched pixels keep
22
- * whatever the caller left in the bitmap. The {@code eraseColor} call happens
23
- * on both paths; only the colour differs.
24
- */
25
- public static final int BACKGROUND_TRANSPARENT = 1;
26
-
27
- public final int page;
28
- public final float docX;
29
- public final float docY;
30
- public final float docWidth;
31
- public final float docHeight;
32
- /** Device pixels per PDF point. */
33
- public final float scale;
34
- /**
35
- * Whether PDF-embedded annotations should be drawn.
36
- *
37
- * RECORDED, NOT HONOURED — and it cannot be. {@code PdfRenderer.Page.render}
38
- * takes a render MODE ({@code FOR_DISPLAY} / {@code FOR_PRINT}) and no
39
- * annotation switch; PDFium's {@code FPDF_ANNOT} flag is not reachable through
40
- * the platform class at any API level. Annotations are always composited.
41
- * {@code FOR_PRINT} is not a suppression flag either — it selects an
42
- * annotation's /Print appearance stream, which is still an appearance.
43
- *
44
- * The consequence for the package is real and belongs in the open: an app that
45
- * draws its OWN copy of a PDF-embedded annotation on the Skia layer will see it
46
- * twice on Android. The fix is a PDFium backend, not a different mode here.
47
- */
48
- public final boolean annotations;
49
- public final int background;
50
-
51
- public RasterRequest(
52
- int page,
53
- float docX,
54
- float docY,
55
- float docWidth,
56
- float docHeight,
57
- float scale,
58
- boolean annotations,
59
- int background) {
60
- this.page = page;
61
- this.docX = docX;
62
- this.docY = docY;
63
- this.docWidth = docWidth;
64
- this.docHeight = docHeight;
65
- this.scale = scale;
66
- this.annotations = annotations;
67
- this.background = background;
68
- }
69
-
70
- /**
71
- * Destination width in device pixels.
72
- *
73
- * ROUND, not ceil, and at least 1 — the same rule the deterministic fake
74
- * backend documents. The half-pixel this can shave off the far edge is
75
- * absorbed by the fact that a raster is drawn to fill its doc rect; going the
76
- * other way (ceil) would make two rasters requested at the same scale
77
- * disagree on size for rects of equal width, which the cache keys off.
78
- */
79
- public int pixelWidth() {
80
- return Math.max(1, Math.round(docWidth * scale));
81
- }
82
-
83
- /** Destination height in device pixels. See {@link #pixelWidth()}. */
84
- public int pixelHeight() {
85
- return Math.max(1, Math.round(docHeight * scale));
86
- }
87
-
88
- @Override
89
- public String toString() {
90
- return "RasterRequest{p=" + page + " rect=" + docX + "," + docY + " "
91
- + docWidth + "x" + docHeight + " @" + scale + " -> "
92
- + pixelWidth() + "x" + pixelHeight()
93
- + " bg=" + (background == BACKGROUND_WHITE ? "white" : "transparent") + "}";
94
- }
95
- }
@@ -1,121 +0,0 @@
1
- /**
2
- * The TypeScript half of the Android backend.
3
- *
4
- * THIN, AND IT USED TO BE THE WHOLE THING. Everything that is not
5
- * Android-specific — error mapping, page-geometry marshalling, the transport
6
- * negotiation, the base64 decode, the cancellation wiring, the phase timings —
7
- * moved to `./native-bridge.ts` when the iOS backend landed, because the two
8
- * platforms speak the identical wire format and the spec's claim for the iOS step
9
- * is that the seam is a seam. What is left here is the four things that are
10
- * genuinely Android's: the id, the capabilities, the alpha encoding, and what
11
- * `PdfSource` it can accept.
12
- *
13
- * The names exported from `./native-bridge.ts` are re-exported below under their
14
- * original `Android*` spellings, so nothing that imported them from here breaks.
15
- *
16
- * PURE ON PURPOSE. Nothing here imports `react-native`, which is what lets the
17
- * whole adapter be tested in vitest against a fake native module.
18
- * `./index.native.ts` is the only file that resolves the real one, and it does
19
- * nothing except hand it to `createAndroidRasterizer`.
20
- *
21
- * The native side is `android/src/main/java/tools/reekon/pdfcanvas/`, a
22
- * React-free Android library that is proven on device by
23
- * `./gradlew connectedDebugAndroidTest`. That split is the reason a bug here is
24
- * a bug in marshalling and never a bug in rasterizing.
25
- */
26
- import type { NativeRasterizerHandle, NativeRotationSupport, NativeOpenResult, NativePdfPage, NativeRenderRequest, NativeRenderResult, NativeSource, NativeTransport, PdfCanvasNativeModule } from './native-bridge.js';
27
- import type { PageRasterizer } from '../types.js';
28
- export { TRANSPORT_PROBE_BYTES, bytesFromResult, decodeBase64, lookupTakePixels, negotiateTransport, toPageGeometry, } from './native-bridge.js';
29
- export type { NativeRasterizerHandle, NativeRotationSupport, NativeOpenResult, NativePdfPage, NativeRenderRequest, NativeRenderResult, NativeSource, NativeTransport, PdfCanvasNativeModule, TakePixels, } from './native-bridge.js';
30
- /**
31
- * ALIASES, NOT COPIES. These were the shipped names before the wire format was
32
- * shared with iOS, and a consumer that imported one should not have to care that
33
- * a second backend now speaks it too.
34
- */
35
- export type AndroidRotationSupport = NativeRotationSupport;
36
- export type AndroidNativePage = NativePdfPage;
37
- export type AndroidNativeOpenResult = NativeOpenResult;
38
- export type AndroidNativeRenderResult = NativeRenderResult;
39
- export type AndroidNativeRenderRequest = NativeRenderRequest;
40
- export type AndroidNativeSource = NativeSource;
41
- export type AndroidPdfNativeModule = PdfCanvasNativeModule;
42
- export type AndroidTransport = NativeTransport;
43
- export type AndroidRasterizerHandle = NativeRasterizerHandle;
44
- export interface AndroidRasterizerOptions {
45
- /**
46
- * Concurrent renders. Clamped to [1, 4] natively — each slot is one more
47
- * `PdfRenderer` holding a file descriptor and a parsed document.
48
- *
49
- * ONE. It is also what the rasterizer reports as
50
- * `RasterizerCapabilities.maxConcurrentRenders`, so raising it does two things
51
- * at once: it opens more native renderers AND it widens the controller's lane
52
- * bound. Both of those are measured below and both come out negative.
53
- *
54
- * THE POOL DOES NOT PARALLELISE PDFium. AOSP serialises `PdfRenderer` behind a
55
- * process-wide PDFium lock, and the lock does not merely serialise, it CONVOYS.
56
- * On an LG V60 (Snapdragon 865, 8 cores, API 33) at the field report's scale of
57
- * 131.25 device px/pt with 1024 px tiles, over a real field-measure drawing,
58
- * four threads inflate the per-tile PDFium phases `openPage` 16.2x,
59
- * `PdfRenderer.render` 10.9x and `recycle+close` 33.6x, for a whole-tile
60
- * inflation of 5.84x, against 1.1x-5.3x for every non-PDFium phase. On a dense
61
- * electrical schematic it is worse still: `render` 30.9x, `openPage` 11.2x.
62
- * A pure-Java control on the same threads scales 2.8x. So the native calls do
63
- * not overlap, and contending for them makes each one dramatically slower.
64
- *
65
- * NOTHING ABOUT THIS CARRIES OVER TO iOS, and the iOS option says so on its own
66
- * doc comment: the lock being measured here is AOSP's, in
67
- * `android_graphics_pdf_PdfRenderer.cpp`, and CoreGraphics has no counterpart
68
- * to it that anyone has measured.
69
- *
70
- * WHY THIS WAS 2, AND WHY THAT REASON IS GONE. The second slot never existed to
71
- * rasterize in parallel; it existed to PIPELINE, hiding the JS thread's
72
- * `decodeBase64` (~1005 ms per tile, 90.6% of the tile) behind the next tile's
73
- * native work. The JSI transport deleted that cost outright — the JS thread's
74
- * whole share is now a pointer wrap — so there is nothing left to hide. Worse,
75
- * deleting the base64 ENCODE from the Java side deleted the only phase that was
76
- * genuinely parallel: on the real drawing a tile was 72.7% parallelisable work
77
- * (19.3 ms of encode inside 30.6 ms) and is now 35.6% (4.6 ms of bitmap fill,
78
- * pixel copy and JNI store inside 13.0 ms), with the PDFium-serialised part
79
- * risen from 25.5% to 60.2%. Amdahl caps that at 1.67x before the convoy is
80
- * counted, and the convoy is what turns the remaining headroom negative.
81
- *
82
- * MEASURED, on the JSI transport, by `laneScheduleOnTheJsiTransport` in
83
- * `PdfCanvasPhaseTimingTest` — N lanes over one shared cursor of 6 plan items,
84
- * which is the schedule the controller actually runs, 21 interleaved rounds per
85
- * lane count. Mean epoch wall clock relative to one lane, higher is better:
86
- *
87
- * - real field-measure drawing, 4 independent passes:
88
- * 2 lanes 0.82x / 0.81x / 0.95x / 0.76x,
89
- * 3 lanes 0.54x / 0.61x / 0.85x / 0.58x,
90
- * 4 lanes 0.59x / 0.58x / 0.92x / 0.55x. One lane is never beaten.
91
- * - dense electrical schematic: 2 lanes 0.48x, 3 lanes 0.41x, 4 lanes 0.38x.
92
- * - THE TAIL IS WHERE IT HURTS MOST, and the tail is what a user sees as a
93
- * stutter. Worst-of-21 on the drawing: 126-172 ms at one lane against
94
- * 177-238 ms at two. On the schematic: 134 ms against 321 ms.
95
- *
96
- * ONE IS ALSO THE CHEAPER OBJECT: one file descriptor and one parsed document
97
- * instead of two, which on a large drawing is the larger of the two costs.
98
- *
99
- * DO NOT READ THE SYNTHETIC FIXTURE'S NUMBERS AS AGREEING OR DISAGREEING. With
100
- * no `customer.pdf` pushed, `PdfCanvasPhaseTimingTest` falls back to a
101
- * vector-rects-and-lines page whose `PdfRenderer.render` is 0.8 ms rather than
102
- * 6.5 ms; that shrinks the serialised fraction, and two lanes duly measure
103
- * 1.06x-1.25x there. The fixture warns that it is an optimistic bound, and this
104
- * is exactly the decision it would have inverted. Push a real drawing before
105
- * re-running this.
106
- */
107
- poolSize?: number;
108
- id?: string;
109
- /**
110
- * How to find the installed JSI host function. Injected only by tests; the
111
- * default reads `globalThis`.
112
- */
113
- lookupTakePixels?: () => import('./native-bridge.js').TakePixels | null;
114
- }
115
- /**
116
- * Wraps a native module in a `PageRasterizer`.
117
- *
118
- * The native module is INJECTED rather than imported so this file stays pure.
119
- * `getDefaultRasterizer()` in `./index.native.ts` supplies the real one.
120
- */
121
- export declare function createAndroidRasterizer(native: AndroidPdfNativeModule, options?: AndroidRasterizerOptions): PageRasterizer;