@reekon-tools/react-native-pdf-canvas 0.1.1 → 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 (121) 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/rasterizer/index.native.d.ts +8 -12
  19. package/dist/rasterizer/index.native.js +15 -21
  20. package/dist/rasterizer/native-bridge.d.ts +51 -75
  21. package/dist/rasterizer/native-bridge.js +49 -33
  22. package/dist/rasterizer/native.d.ts +45 -0
  23. package/dist/rasterizer/native.js +123 -0
  24. package/dist/react/usePdfDocument.d.ts +5 -5
  25. package/dist/react/usePdfLayer.d.ts +10 -1
  26. package/dist/react/usePdfLayer.js +10 -2
  27. package/dist/testing/scenes.d.ts +3 -3
  28. package/dist/testing/scenes.js +4 -4
  29. package/dist/types.d.ts +36 -42
  30. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  31. package/native/.clangd +5 -0
  32. package/native/CMakeLists.txt +89 -0
  33. package/native/core/include/pdfcanvas/document.h +90 -0
  34. package/native/core/include/pdfcanvas/error.h +47 -0
  35. package/native/core/include/pdfcanvas/jsi.h +18 -0
  36. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  37. package/native/core/include/pdfcanvas/library.h +28 -0
  38. package/native/core/include/pdfcanvas/pixels.h +92 -0
  39. package/native/core/include/pdfcanvas/service.h +87 -0
  40. package/native/core/include/pdfcanvas/slots.h +113 -0
  41. package/native/core/include/pdfcanvas/types.h +110 -0
  42. package/native/core/pdfcanvas-core.cmake +22 -0
  43. package/native/core/src/document.cpp +516 -0
  44. package/native/core/src/error.cpp +29 -0
  45. package/native/core/src/jsi.cpp +66 -0
  46. package/native/core/src/library.cpp +52 -0
  47. package/native/core/src/pixels.cpp +45 -0
  48. package/native/core/src/service.cpp +156 -0
  49. package/native/core/src/slots.cpp +139 -0
  50. package/native/tests/fixtures.cpp +451 -0
  51. package/native/tests/fixtures.h +96 -0
  52. package/native/tests/harness.h +122 -0
  53. package/native/tests/main.cpp +64 -0
  54. package/native/tests/pixels_util.h +126 -0
  55. package/native/tests/test_document.cpp +485 -0
  56. package/native/tests/test_service.cpp +130 -0
  57. package/native/tests/test_slots.cpp +250 -0
  58. package/package.json +27 -7
  59. package/react-native.config.js +5 -2
  60. package/scripts/fetch-pdfium.mjs +487 -0
  61. package/scripts/pdfium-manifest.json +46 -0
  62. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  63. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  64. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  65. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  66. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  67. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  68. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  69. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  72. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  73. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  74. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  75. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  76. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  77. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  78. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  79. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  80. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  81. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  82. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  83. package/dist/rasterizer/android.d.ts +0 -121
  84. package/dist/rasterizer/android.js +0 -126
  85. package/dist/rasterizer/ios.d.ts +0 -84
  86. package/dist/rasterizer/ios.js +0 -155
  87. package/ios/Package.swift +0 -58
  88. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  89. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  90. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  91. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  92. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  93. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  94. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  95. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  98. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  99. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  100. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  101. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  102. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  103. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  104. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  105. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  109. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  110. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  111. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  112. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  113. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  114. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  115. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  116. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  117. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  118. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  119. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  120. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  121. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -1,226 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import static org.junit.Assert.assertEquals;
4
- import static org.junit.Assert.assertNotNull;
5
- import static org.junit.Assert.assertNull;
6
- import static org.junit.Assert.assertTrue;
7
- import static org.junit.Assert.fail;
8
-
9
- import android.content.Context;
10
- import android.util.Log;
11
-
12
- import androidx.test.ext.junit.runners.AndroidJUnit4;
13
- import androidx.test.platform.app.InstrumentationRegistry;
14
-
15
- import org.junit.Test;
16
- import org.junit.runner.RunWith;
17
-
18
- import java.io.File;
19
- import java.util.concurrent.CountDownLatch;
20
- import java.util.concurrent.TimeUnit;
21
- import java.util.concurrent.atomic.AtomicReference;
22
-
23
- /**
24
- * Cancellation, and specifically the HONESTY of it.
25
- *
26
- * {@code PdfRenderer.Page.render} is synchronous and uninterruptible once
27
- * entered. The claim this backend makes is therefore narrow: a cancel prevents
28
- * the NEXT piece of work starting and discards a result that is no longer
29
- * wanted, and it does not — cannot — stop a render that is already drawing.
30
- *
31
- * A test that only asserted "cancelling makes it throw" would pass equally for a
32
- * backend that genuinely interrupted and for one that pretends to. The
33
- * distinguishing evidence is WHICH STAGE reported the cancellation, which is why
34
- * {@link RasterCancellation} puts the stage in the message.
35
- */
36
- @RunWith(AndroidJUnit4.class)
37
- public class PdfCanvasCancellationTest {
38
-
39
- private static final String TAG = "PDFCANVAS";
40
-
41
- private static Context ctx() {
42
- return InstrumentationRegistry.getInstrumentation().getTargetContext();
43
- }
44
-
45
- /**
46
- * CATCHES: a signal that is only consulted after the work is done — i.e. an
47
- * implementation that renders first and checks later. The bar here is not just
48
- * the error code but that no bitmap was allocated and no page opened, which
49
- * shows up as this call returning far faster than a real render.
50
- */
51
- @Test
52
- public void cancelBeforeStartRefusesWithoutRendering() throws Exception {
53
- File pdf = TestPdfs.pattern(ctx(), "cancel-archd.pdf", TestPdfs.ARCH_D_W, TestPdfs.ARCH_D_H);
54
- PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
55
- try {
56
- RasterRequest heavy =
57
- new RasterRequest(
58
- 0, 200f, 300f, 1200f, 1200f, 1f, false, RasterRequest.BACKGROUND_WHITE);
59
-
60
- long t0 = System.nanoTime();
61
- r.render(heavy, null);
62
- long uncancelledMs = (System.nanoTime() - t0) / 1_000_000;
63
-
64
- RasterCancellation signal = new RasterCancellation();
65
- signal.cancel();
66
- long t1 = System.nanoTime();
67
- try {
68
- r.render(heavy, signal);
69
- fail("A pre-cancelled render must not return pixels");
70
- } catch (PdfRasterException e) {
71
- long cancelledMs = (System.nanoTime() - t1) / 1_000_000;
72
- assertEquals(PdfErrorCode.CANCELLED, e.code());
73
- assertTrue(
74
- "should have reported the entry check, not a later stage: " + e.getMessage(),
75
- e.getMessage().contains("before start"));
76
- Log.i(TAG, "cancel-before-start: uncancelled=" + uncancelledMs + "ms refused in "
77
- + cancelledMs + "ms");
78
- assertTrue(
79
- "refusing should be far cheaper than rendering (uncancelled=" + uncancelledMs
80
- + "ms, refused=" + cancelledMs + "ms)",
81
- cancelledMs * 4 < uncancelledMs || cancelledMs <= 2);
82
- }
83
- } finally {
84
- r.close();
85
- }
86
- }
87
-
88
- /**
89
- * CATCHES: a backend that CLAIMS to interrupt a render.
90
- *
91
- * A cancel raised while {@code PdfRenderer.render} is executing cannot take
92
- * effect until it returns, so the only stage that can report it is the
93
- * post-render check — the one that saves no drawing and merely stops a
94
- * superseded raster being handed back. If some future change chunked the
95
- * render and bailed early, this assertion would fail, which is the point:
96
- * the claim in {@link RasterCancellation}'s comment and the code would have
97
- * diverged.
98
- *
99
- * The retry loop is not flakiness padding — it is how the test lands the
100
- * cancel INSIDE the render window on machines whose render time it does not
101
- * know. A cancel that arrives too early is reported by the "before render"
102
- * stage and a cancel that arrives too late does not fire at all; both are
103
- * detected by name and retried with an adjusted delay, and neither is accepted
104
- * as a pass.
105
- */
106
- @Test
107
- public void cancelDuringAnInFlightRenderIsObservedOnlyAfterItFinishes() throws Exception {
108
- File pdf = TestPdfs.pattern(ctx(), "cancel2-archd.pdf", TestPdfs.ARCH_D_W, TestPdfs.ARCH_D_H);
109
- PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
110
- try {
111
- // Big enough that the render window is measurable. S1 finding 2: cost
112
- // tracks DESTINATION BITMAP AREA, so 1600x1600 is the lever, not the
113
- // scale.
114
- RasterRequest heavy =
115
- new RasterRequest(
116
- 0, 200f, 300f, 800f, 800f, 2f, false, RasterRequest.BACKGROUND_WHITE);
117
-
118
- long baselineMs = Long.MAX_VALUE;
119
- for (int i = 0; i < 3; i++) {
120
- long t = System.nanoTime();
121
- r.render(heavy, null);
122
- baselineMs = Math.min(baselineMs, (System.nanoTime() - t) / 1_000_000);
123
- }
124
- Log.i(TAG, "cancel-in-flight: baseline render = " + baselineMs + "ms");
125
-
126
- long delayMs = Math.max(5, baselineMs / 2);
127
- String stage = null;
128
- long elapsedMs = -1;
129
-
130
- for (int attempt = 0; attempt < 6 && stage == null; attempt++) {
131
- CountDownLatch started = new CountDownLatch(1);
132
- AtomicReference<PdfRasterException> thrown = new AtomicReference<>();
133
- AtomicReference<RasterPixels> returned = new AtomicReference<>();
134
- RasterCancellation signal = new RasterCancellation();
135
- long[] elapsed = new long[1];
136
-
137
- Thread worker =
138
- new Thread(
139
- () -> {
140
- started.countDown();
141
- long t = System.nanoTime();
142
- try {
143
- returned.set(r.render(heavy, signal));
144
- } catch (PdfRasterException e) {
145
- thrown.set(e);
146
- }
147
- elapsed[0] = (System.nanoTime() - t) / 1_000_000;
148
- });
149
- worker.start();
150
- assertTrue(started.await(10, TimeUnit.SECONDS));
151
- Thread.sleep(delayMs);
152
- signal.cancel();
153
- worker.join(120_000);
154
-
155
- PdfRasterException e = thrown.get();
156
- if (e == null) {
157
- // Cancel landed after the render had already returned. Aim earlier.
158
- assertNotNull("neither a result nor an exception", returned.get());
159
- delayMs = Math.max(2, delayMs / 2);
160
- Log.i(TAG, "cancel-in-flight: attempt " + attempt + " landed too late, retrying at "
161
- + delayMs + "ms");
162
- continue;
163
- }
164
- assertEquals(PdfErrorCode.CANCELLED, e.code());
165
- if (e.getMessage().contains("after render")) {
166
- stage = "after render";
167
- elapsedMs = elapsed[0];
168
- } else {
169
- // Landed before render() was entered. Aim later.
170
- delayMs = Math.max(delayMs * 2, delayMs + 5);
171
- Log.i(TAG, "cancel-in-flight: attempt " + attempt + " landed at '" + e.getMessage()
172
- + "', retrying at " + delayMs + "ms");
173
- }
174
- }
175
-
176
- assertNotNull(
177
- "never managed to cancel inside the render window; baseline=" + baselineMs + "ms",
178
- stage);
179
- Log.i(TAG, "cancel-in-flight: observed at '" + stage + "' after " + elapsedMs
180
- + "ms (baseline " + baselineMs + "ms)");
181
- // The render ran to completion despite the cancel. If it had been
182
- // interrupted, this would be a fraction of the baseline.
183
- assertTrue(
184
- "a cancelled in-flight render should still have taken about a full render "
185
- + "(elapsed=" + elapsedMs + "ms, baseline=" + baselineMs + "ms)",
186
- elapsedMs * 2 >= baselineMs);
187
- } finally {
188
- r.close();
189
- }
190
- }
191
-
192
- /**
193
- * CATCHES: a cancel that leaks across requests — a shared or static signal, or
194
- * a pool slot left in a cancelled state. One superseded tile must not poison
195
- * the next one, and with a pool of one the two renders provably share a
196
- * renderer.
197
- */
198
- @Test
199
- public void aCancelledRenderDoesNotAffectTheNextOne() throws Exception {
200
- File pdf = TestPdfs.quadrants(ctx(), "cancel-quads.pdf", 200, 300);
201
- PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
202
- try {
203
- RasterRequest request =
204
- new RasterRequest(0, 0f, 0f, 200f, 300f, 1f, false, RasterRequest.BACKGROUND_WHITE);
205
-
206
- RasterCancellation cancelled = new RasterCancellation();
207
- cancelled.cancel();
208
- try {
209
- r.render(request, cancelled);
210
- fail("expected the cancelled render to throw");
211
- } catch (PdfRasterException expected) {
212
- assertEquals(PdfErrorCode.CANCELLED, expected.code());
213
- }
214
-
215
- RasterPixels px = r.render(request, null);
216
- assertTrue(
217
- "the render after a cancelled one must be correct",
218
- PdfCanvasRasterizerTest.isRed(PdfCanvasRasterizerTest.rgba(px, 50, 75)));
219
-
220
- RasterPixels withFreshSignal = r.render(request, new RasterCancellation());
221
- assertNotNull("a fresh, uncancelled signal must not refuse", withFreshSignal);
222
- } finally {
223
- r.close();
224
- }
225
- }
226
- }
@@ -1,239 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import static org.junit.Assert.assertEquals;
4
- import static org.junit.Assert.assertNull;
5
- import static org.junit.Assert.assertTrue;
6
-
7
- import android.content.Context;
8
- import android.util.Log;
9
-
10
- import androidx.test.ext.junit.runners.AndroidJUnit4;
11
- import androidx.test.platform.app.InstrumentationRegistry;
12
-
13
- import org.junit.Test;
14
- import org.junit.runner.RunWith;
15
-
16
- import java.io.File;
17
- import java.util.ArrayList;
18
- import java.util.List;
19
- import java.util.concurrent.CountDownLatch;
20
- import java.util.concurrent.ExecutorService;
21
- import java.util.concurrent.Executors;
22
- import java.util.concurrent.TimeUnit;
23
- import java.util.concurrent.atomic.AtomicReference;
24
-
25
- /**
26
- * The pool, under concurrency.
27
- *
28
- * The rule being defended is narrow and absolute: {@code PdfRenderer} allows ONE
29
- * open page per instance, so two threads on ONE renderer is what does not work.
30
- * Two renderers is fine. Every test here exists because the failure mode of
31
- * getting that wrong is either an {@code IllegalStateException} from a thread
32
- * nobody is watching, or — worse — two renders interleaving into each other's
33
- * bitmaps and producing two plausible, wrong images.
34
- */
35
- @RunWith(AndroidJUnit4.class)
36
- public class PdfCanvasConcurrencyTest {
37
-
38
- private static final String TAG = "PDFCANVAS";
39
-
40
- private static Context ctx() {
41
- return InstrumentationRegistry.getInstrumentation().getTargetContext();
42
- }
43
-
44
- /** Eight distinct interior tiles of an ARCH-D sheet, each visually different. */
45
- private static List<RasterRequest> tileSet() {
46
- List<RasterRequest> out = new ArrayList<>();
47
- for (int i = 0; i < 8; i++) {
48
- float x = 100f + i * 137f;
49
- float y = 200f + i * 211f;
50
- out.add(new RasterRequest(0, x, y, 96f, 96f, 3f, false, RasterRequest.BACKGROUND_WHITE));
51
- }
52
- return out;
53
- }
54
-
55
- /**
56
- * CATCHES: any interleaving that corrupts a raster once several renders are
57
- * genuinely in flight through a pool of four.
58
- *
59
- * References are computed SERIALLY first, on one thread, so they are known
60
- * good. Every concurrent result is then compared byte-for-byte against its
61
- * reference — because the dangerous failure is not the one that throws, it is
62
- * two renders mixing pixels and both coming back plausible.
63
- *
64
- * WHAT THIS TEST DOES NOT RELIABLY CATCH, corrected after measuring rather
65
- * than assumed. It used to claim it catches "a pool that hands the same
66
- * PdfRenderer to two threads". It does not, or not dependably: with
67
- * {@code lease()} mutated to hand every thread renderer #0, this test failed
68
- * on API 24 and 33 in one run and on API 24 and 31 in the next — the threads
69
- * simply do not always collide inside the {@code openPage}..{@code close}
70
- * window, which for these 288x288 tiles is a fraction of a millisecond. A
71
- * test that catches a bug in two runs out of three is a coin toss reported as
72
- * evidence.
73
- *
74
- * The double-lease property is pinned DETERMINISTICALLY by
75
- * {@link #poolOfOneSerialisesRatherThanDoubleLeasing}, which failed on all
76
- * three levels in both runs of that same mutation. That is the test to look at
77
- * if this one ever starts passing while the pool is broken.
78
- */
79
- @Test
80
- public void concurrentRendersThroughThePoolAreAllCorrect() throws Exception {
81
- File pdf = TestPdfs.pattern(ctx(), "conc-archd.pdf", TestPdfs.ARCH_D_W, TestPdfs.ARCH_D_H);
82
- List<RasterRequest> requests = tileSet();
83
-
84
- PdfCanvasRasterizer serial = PdfCanvasRasterizer.openFile(pdf, null, 1);
85
- List<RasterPixels> reference = new ArrayList<>();
86
- long serialStart = System.nanoTime();
87
- try {
88
- for (RasterRequest request : requests) {
89
- reference.add(serial.render(request, null));
90
- }
91
- } finally {
92
- serial.close();
93
- }
94
- long serialMs = (System.nanoTime() - serialStart) / 1_000_000;
95
-
96
- PdfCanvasRasterizer parallel = PdfCanvasRasterizer.openFile(pdf, null, 4);
97
- assertEquals("pool should have been created at the requested size", 4, parallel.poolSize());
98
-
99
- int threads = 4;
100
- int rounds = 3;
101
- ExecutorService pool = Executors.newFixedThreadPool(threads);
102
- CountDownLatch go = new CountDownLatch(1);
103
- CountDownLatch done = new CountDownLatch(threads);
104
- AtomicReference<Throwable> failure = new AtomicReference<>();
105
-
106
- long parallelStart;
107
- try {
108
- for (int t = 0; t < threads; t++) {
109
- final int threadIndex = t;
110
- pool.execute(
111
- () -> {
112
- try {
113
- go.await();
114
- for (int round = 0; round < rounds; round++) {
115
- for (int i = threadIndex; i < requests.size(); i += threads) {
116
- RasterPixels got = parallel.render(requests.get(i), null);
117
- RasterPixels want = reference.get(i);
118
- assertEquals("width for tile " + i, want.width, got.width);
119
- PdfCanvasRasterizerTest.Diff diff =
120
- PdfCanvasRasterizerTest.compareRegion(got, want, 0, 0);
121
- if (diff.mismatches != 0) {
122
- throw new AssertionError("tile " + i + " differs from its serial "
123
- + "reference: " + diff);
124
- }
125
- }
126
- }
127
- } catch (Throwable e) {
128
- failure.compareAndSet(null, e);
129
- } finally {
130
- done.countDown();
131
- }
132
- });
133
- }
134
- parallelStart = System.nanoTime();
135
- go.countDown();
136
- assertTrue("threads did not finish in 120s", done.await(120, TimeUnit.SECONDS));
137
- } finally {
138
- pool.shutdownNow();
139
- parallel.close();
140
- }
141
- long parallelMs = (System.nanoTime() - parallelStart) / 1_000_000;
142
-
143
- assertNull(
144
- failure.get() == null ? null : String.valueOf(failure.get()), failure.get());
145
-
146
- // Reported, not asserted. AOSP serialises every PdfRenderer native call
147
- // behind a process-wide PDFium lock, so the pool buys CORRECTNESS (no shared
148
- // open page) rather than throughput. If these two numbers are close, that is
149
- // the lock; if the parallel run is meaningfully faster, this platform build
150
- // does not have it. Either way the pool is still required.
151
- Log.i(TAG, "concurrency: " + requests.size() + " tiles serial=" + serialMs + "ms | "
152
- + (threads * rounds * requests.size() / threads) + " tiles across " + threads
153
- + " threads, pool=4, =" + parallelMs + "ms");
154
- }
155
-
156
- /**
157
- * CATCHES: a pool that leases the same renderer twice under contention. This
158
- * is THE test for that property, and the only one that catches it every time.
159
- *
160
- * With a pool of ONE and four threads pushing, every request must queue. A
161
- * correct pool serialises them and every result is right; a broken lease hands
162
- * renderer #0 to two threads at once, and {@code openPage} throws
163
- * "Current page not closed" — which this backend reports as a
164
- * {@code backend-failure} naming the double lease, so the failure message
165
- * points straight at the cause.
166
- *
167
- * Why this one is deterministic where
168
- * {@link #concurrentRendersThroughThePoolAreAllCorrect} is not: a pool of one
169
- * means EVERY pair of threads contends for the same renderer, so a lease that
170
- * does not remove the renderer from the free queue collides on essentially the
171
- * first overlap rather than on whichever pair of the four happens to line up.
172
- * Measured: mutating {@code lease()} to hand out renderer #0 without taking it
173
- * failed here on API 24, 31 and 33 in both runs, while the four-renderer test
174
- * caught it on only two of the three each time.
175
- */
176
- @Test
177
- public void poolOfOneSerialisesRatherThanDoubleLeasing() throws Exception {
178
- File pdf = TestPdfs.pattern(ctx(), "conc1-letter.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
179
- RasterRequest request =
180
- new RasterRequest(0, 100f, 200f, 200f, 200f, 2f, false, RasterRequest.BACKGROUND_WHITE);
181
-
182
- PdfCanvasRasterizer r = PdfCanvasRasterizer.openFile(pdf, null, 1);
183
- assertEquals(1, r.poolSize());
184
- RasterPixels reference = r.render(request, null);
185
-
186
- int threads = 4;
187
- ExecutorService pool = Executors.newFixedThreadPool(threads);
188
- CountDownLatch go = new CountDownLatch(1);
189
- CountDownLatch done = new CountDownLatch(threads);
190
- AtomicReference<Throwable> failure = new AtomicReference<>();
191
- try {
192
- for (int t = 0; t < threads; t++) {
193
- pool.execute(
194
- () -> {
195
- try {
196
- go.await();
197
- for (int i = 0; i < 6; i++) {
198
- RasterPixels got = r.render(request, null);
199
- PdfCanvasRasterizerTest.Diff diff =
200
- PdfCanvasRasterizerTest.compareRegion(got, reference, 0, 0);
201
- if (diff.mismatches != 0) {
202
- throw new AssertionError("serialised render differs: " + diff);
203
- }
204
- }
205
- } catch (Throwable e) {
206
- failure.compareAndSet(null, e);
207
- } finally {
208
- done.countDown();
209
- }
210
- });
211
- }
212
- go.countDown();
213
- assertTrue("threads did not finish in 120s", done.await(120, TimeUnit.SECONDS));
214
- } finally {
215
- pool.shutdownNow();
216
- r.close();
217
- }
218
- assertNull(String.valueOf(failure.get()), failure.get());
219
- }
220
-
221
- /**
222
- * CATCHES: a pool that clamps its size wrongly, or accepts a nonsense size and
223
- * then allocates that many renderers. Each renderer holds an fd and a parsed
224
- * document, so an unclamped 64 would be 64 of both.
225
- */
226
- @Test
227
- public void poolSizeIsClamped() throws Exception {
228
- File pdf = TestPdfs.pattern(ctx(), "clamp.pdf", TestPdfs.LETTER_W, TestPdfs.LETTER_H);
229
- PdfCanvasRasterizer zero = PdfCanvasRasterizer.openFile(pdf, null, 0);
230
- PdfCanvasRasterizer huge = PdfCanvasRasterizer.openFile(pdf, null, 64);
231
- try {
232
- assertEquals(1, zero.poolSize());
233
- assertEquals(PdfRendererPool.MAX_POOL_SIZE, huge.poolSize());
234
- } finally {
235
- zero.close();
236
- huge.close();
237
- }
238
- }
239
- }