@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.1

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 +77 -40
  4. package/README.md +218 -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 +559 -0
  61. package/scripts/pdfium-manifest.json +48 -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,277 +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
- import android.os.Build;
8
- import android.os.ParcelFileDescriptor;
9
-
10
- import java.io.File;
11
- import java.io.FileOutputStream;
12
- import java.nio.charset.Charset;
13
- import java.util.concurrent.atomic.AtomicBoolean;
14
-
15
- /**
16
- * Where this backend keeps what it has MEASURED about the platform it is running
17
- * on, as opposed to what the documentation promises.
18
- *
19
- * Two quirks live here, both found on the API 24 floor and both invisible on API
20
- * 31 and 33. They are kept together because they share a shape: the platform
21
- * does not report either of them, so the only way to know is to look.
22
- */
23
- public final class PdfPlatformQuirks {
24
-
25
- /**
26
- * The lowest API level MEASURED to survive a failed {@code PdfRenderer}
27
- * construction. 24 does not; 31 and 33 do.
28
- *
29
- * The bound is deliberately the lowest level measured GOOD rather than one
30
- * above the level measured bad: 25 through 30 were never tested (no image
31
- * available in this session), and treating untested territory as safe is how a
32
- * process-killing bug ships. The cost of being wrong in this direction is that
33
- * a device between 25 and 30 refuses further documents after it has already
34
- * met a corrupt one; the cost of being wrong in the other direction is a
35
- * SIGSEGV.
36
- */
37
- static final int LOWEST_SDK_MEASURED_SAFE_AFTER_FAILED_OPEN = 31;
38
-
39
- private static final AtomicBoolean pdfiumPoisoned = new AtomicBoolean(false);
40
-
41
- private PdfPlatformQuirks() {}
42
-
43
- /* ================================================================ *
44
- * Quirk 1 — a failed PdfRenderer construction poisons PDFium
45
- * ================================================================ */
46
-
47
- /**
48
- * True when a throwing {@code PdfRenderer} constructor is expected to leave
49
- * this process's PDFium unusable.
50
- *
51
- * See {@link PdfPreflight} for the mechanism and the measurement. Preflight
52
- * removes the two common triggers before the platform class ever sees the
53
- * file; this latch covers what is left — a file that looks like a PDF and is
54
- * structurally broken deeper in.
55
- */
56
- static boolean failedNativeOpenIsPoisonous() {
57
- return Build.VERSION.SDK_INT < LOWEST_SDK_MEASURED_SAFE_AFTER_FAILED_OPEN;
58
- }
59
-
60
- static void recordFailedNativeOpen() {
61
- if (failedNativeOpenIsPoisonous()) {
62
- pdfiumPoisoned.set(true);
63
- }
64
- }
65
-
66
- static boolean isPdfiumPoisoned() {
67
- return pdfiumPoisoned.get();
68
- }
69
-
70
- /**
71
- * Public form of {@link #isPdfiumPoisoned()}, for the React binding, which
72
- * lives in a different package and has no business with the rest of this
73
- * class's internals. A host can surface "PDF viewing needs an app restart"
74
- * instead of watching every subsequent open fail.
75
- */
76
- public static boolean isBackendPoisoned() {
77
- return pdfiumPoisoned.get();
78
- }
79
-
80
- /**
81
- * Refuses rather than risking the segfault.
82
- *
83
- * This is a session-wide capitulation and it is meant to be: once a
84
- * construction has thrown on an affected build, the reference count is already
85
- * wrong and there is no way to put it back. An error every consumer can show
86
- * is strictly better than a crash on whichever document happens to be opened
87
- * next, which will not be the one that caused it.
88
- */
89
- static void requireUsablePdfium() throws PdfRasterException {
90
- if (pdfiumPoisoned.get()) {
91
- throw new PdfRasterException(
92
- PdfErrorCode.BACKEND_FAILURE,
93
- "Refusing to open a document: a previous file failed to load and, on API "
94
- + Build.VERSION.SDK_INT + ", that leaves the platform's PDFium library "
95
- + "reference count corrupted — the next successful load would segfault. "
96
- + "Restarting the app clears it.");
97
- }
98
- }
99
-
100
- /** Test hook. Package-private, and the only way this flag is ever cleared. */
101
- static void setPdfiumPoisonedForTesting(boolean poisoned) {
102
- pdfiumPoisoned.set(poisoned);
103
- }
104
-
105
- /* ================================================================ *
106
- * Quirk 2 — /Rotate is not applied when rendering
107
- * ================================================================ */
108
-
109
- public enum RotationSupport {
110
- /** A page carrying /Rotate renders upright, as the contract requires. */
111
- SUPPORTED,
112
- /**
113
- * {@code getWidth()}/{@code getHeight()} swap for a /Rotate 90 page, but the
114
- * CONTENT is drawn unrotated, and the y-flip uses the post-rotation height —
115
- * so the page lands shifted and clipped. Measured on API 24:
116
- *
117
- * <pre>
118
- * rotate=0 geometry=612x792 drawn=[0,0..611,791] red=[0,0..111,99]
119
- * rotate=90 geometry=792x612 drawn=[0,0..611,611] red=ABSENT blue=[0,512..111,611]
120
- * rotate=180 geometry=612x792 drawn=[0,0..611,791] red=[0,0..111,99]
121
- * rotate=270 geometry=792x612 drawn=[0,0..611,611] red=ABSENT blue=[0,512..111,611]
122
- * </pre>
123
- *
124
- * /Rotate 180 producing output identical to /Rotate 0 is the tell: the
125
- * rotation is not applied at ANY angle. API 31 and 33 render all four
126
- * correctly.
127
- */
128
- UNSUPPORTED,
129
- /** The probe could not run. Treated as unsupported by callers that care. */
130
- UNKNOWN
131
- }
132
-
133
- private static volatile RotationSupport rotationSupport;
134
-
135
- /**
136
- * Renders a tiny purpose-built /Rotate 90 page and looks at where the ink
137
- * landed. Computed once per process and cached.
138
- *
139
- * Probed rather than keyed off {@code Build.VERSION.SDK_INT} for the same
140
- * reason the latch above is conservative: 25 to 30 are untested, and a version
141
- * check would have to guess about them. A probe answers for the device it is
142
- * actually running on. It costs one ~450-byte file and one 200x100 render.
143
- *
144
- * @param scratchDir somewhere writable; the probe file is deleted afterwards.
145
- */
146
- public static RotationSupport rotationSupport(File scratchDir) {
147
- RotationSupport cached = rotationSupport;
148
- if (cached != null) {
149
- return cached;
150
- }
151
- synchronized (PdfPlatformQuirks.class) {
152
- if (rotationSupport == null) {
153
- rotationSupport = probeRotation(scratchDir);
154
- }
155
- return rotationSupport;
156
- }
157
- }
158
-
159
- private static final Charset LATIN1 = Charset.forName("ISO-8859-1");
160
-
161
- /** 100x200pt, /Rotate 90, with a red 50x50 marker at the UNROTATED top-left. */
162
- private static byte[] rotationProbeDocument() {
163
- // PDF user space is y-up from the bottom-left, so the unrotated top-left
164
- // 50x50 of a 100x200 page is the rect (0, 150) to (50, 200).
165
- String content = "1 1 1 rg 0 0 100 200 re f\n1 0 0 rg 0 150 50 50 re f\n";
166
- StringBuilder body = new StringBuilder();
167
- int[] offsets = new int[4];
168
- StringBuilder pdf = new StringBuilder("%PDF-1.4\n");
169
-
170
- offsets[0] = pdf.length();
171
- pdf.append("1 0 obj\n<< /Length ").append(content.length()).append(" >>\nstream\n")
172
- .append(content).append("endstream\nendobj\n");
173
- offsets[1] = pdf.length();
174
- pdf.append("2 0 obj\n<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 200] /Rotate 90 "
175
- + "/Contents 1 0 R /Resources << >> >>\nendobj\n");
176
- offsets[2] = pdf.length();
177
- pdf.append("3 0 obj\n<< /Type /Pages /Kids [2 0 R] /Count 1 >>\nendobj\n");
178
- offsets[3] = pdf.length();
179
- pdf.append("4 0 obj\n<< /Type /Catalog /Pages 3 0 R >>\nendobj\n");
180
-
181
- int xref = pdf.length();
182
- pdf.append("xref\n0 5\n0000000000 65535 f \n");
183
- for (int offset : offsets) {
184
- pdf.append(String.format("%010d 00000 n \n", offset));
185
- }
186
- pdf.append("trailer\n<< /Size 5 /Root 4 0 R >>\nstartxref\n").append(xref).append("\n%%EOF\n");
187
- // Every byte written is ASCII, so String.length() above is the byte length.
188
- return pdf.toString().getBytes(LATIN1);
189
- }
190
-
191
- private static RotationSupport probeRotation(File scratchDir) {
192
- // The probe is a PdfRenderer construction like any other, so it is subject
193
- // to quirk 1 in both directions: it must not run on an already-poisoned
194
- // process (the constructor would be the segfault), and if it throws it must
195
- // arm the latch like every other construction site does. Neither was true
196
- // before — this was the one place that built a PdfRenderer inside a bare
197
- // `catch (Throwable) { return UNKNOWN; }`, so a throw here was swallowed and
198
- // the next open walked into it.
199
- if (pdfiumPoisoned.get()) {
200
- return RotationSupport.UNKNOWN;
201
- }
202
- File probe = new File(scratchDir, "pdfcanvas-rotation-probe.pdf");
203
- ParcelFileDescriptor pfd = null;
204
- PdfRenderer renderer = null;
205
- PdfRenderer.Page page = null;
206
- Bitmap bitmap = null;
207
- try {
208
- if (!scratchDir.exists() && !scratchDir.mkdirs()) {
209
- return RotationSupport.UNKNOWN;
210
- }
211
- try (FileOutputStream out = new FileOutputStream(probe)) {
212
- out.write(rotationProbeDocument());
213
- }
214
- pfd = ParcelFileDescriptor.open(probe, ParcelFileDescriptor.MODE_READ_ONLY);
215
- try {
216
- renderer = new PdfRenderer(pfd);
217
- } catch (Throwable t) {
218
- // The probe document is authored by this class and is valid, so this
219
- // should be unreachable — but "should be" is exactly the assumption that
220
- // costs a process. A constructor that threw has already left the
221
- // half-built finalizable object behind, so the latch is armed here for
222
- // the same reason PdfRendererPool arms it.
223
- recordFailedNativeOpen();
224
- throw t;
225
- }
226
- pfd = null; // ownership transferred
227
- page = renderer.openPage(0);
228
-
229
- // Past the constructor now. A throw from here on is not a poisoning event
230
- // — the document is open and PDFium's reference count is balanced — so
231
- // UNKNOWN is the honest answer and the catch below produces it.
232
- if (page.getWidth() != 200 || page.getHeight() != 100) {
233
- // Dimensions are not even swapped: /Rotate is ignored wholesale.
234
- return RotationSupport.UNSUPPORTED;
235
- }
236
-
237
- bitmap = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888);
238
- bitmap.eraseColor(Color.WHITE);
239
- page.render(bitmap, null, new Matrix(), PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
240
-
241
- // A clockwise quarter turn — which is what /Rotate means — puts the
242
- // unrotated top-left marker at the displayed top-RIGHT.
243
- int sample = bitmap.getPixel(185, 15);
244
- boolean red = Color.red(sample) > 200 && Color.green(sample) < 60 && Color.blue(sample) < 60;
245
- return red ? RotationSupport.SUPPORTED : RotationSupport.UNSUPPORTED;
246
- } catch (Throwable t) {
247
- return RotationSupport.UNKNOWN;
248
- } finally {
249
- if (bitmap != null) {
250
- bitmap.recycle();
251
- }
252
- if (page != null) {
253
- page.close();
254
- }
255
- if (renderer != null) {
256
- renderer.close();
257
- }
258
- if (pfd != null) {
259
- try {
260
- pfd.close();
261
- } catch (Exception ignored) {
262
- // Only reachable if the PdfRenderer constructor threw, and the
263
- // throwing path already carries the real cause.
264
- }
265
- }
266
- //noinspection ResultOfMethodCallIgnored
267
- probe.delete();
268
- }
269
- }
270
-
271
- /** Test hook: forget the cached verdict so the probe can be re-run. */
272
- static void resetRotationSupportForTesting() {
273
- synchronized (PdfPlatformQuirks.class) {
274
- rotationSupport = null;
275
- }
276
- }
277
- }
@@ -1,214 +0,0 @@
1
- package tools.reekon.pdfcanvas;
2
-
3
- import java.io.File;
4
- import java.io.IOException;
5
- import java.io.RandomAccessFile;
6
-
7
- /**
8
- * Cheap structural checks run BEFORE {@code new PdfRenderer(...)} is allowed
9
- * anywhere near the file.
10
- *
11
- * <h3>Why this exists, measured on device</h3>
12
- *
13
- * On API 24, a {@code PdfRenderer} CONSTRUCTOR THAT THROWS permanently poisons
14
- * PDFium for the whole process. The constructor assigns its
15
- * {@code ParcelFileDescriptor} field before calling {@code nativeCreate}, so a
16
- * throw leaves a half-built, unreachable, FINALIZABLE object; when the collector
17
- * runs it, {@code finalize -> doClose -> nativeClose(0)} decrements PDFium's
18
- * library reference count with no matching initialise. The count goes below
19
- * zero, {@code initializeLibraryIfNeeded} then never re-initialises, and the
20
- * NEXT document load dereferences a null {@code CPDF_ModuleMgr} and takes the
21
- * process down.
22
- *
23
- * Measured, not inferred — one failed construction plus a forced
24
- * {@code System.gc(); System.runFinalization()} is enough:
25
- *
26
- * <pre>
27
- * API 24: baseline good = OK(1)
28
- * failure #1 -> IOException; collecting
29
- * about to open good after 1 collected failures
30
- * F libc: Fatal signal 11 (SIGSEGV), fault addr 0x10
31
- * #00 libpdfium.so (CPDF_Document::CPDF_Document(CPDF_Parser*)+96)
32
- * API 31: SURVIVED 12
33
- * API 33: SURVIVED 12
34
- * </pre>
35
- *
36
- * So on the minSdk floor a single wrong file — a JPEG the user picked by
37
- * mistake, a password-protected drawing — is not an error, it is a delayed
38
- * process death. The two overwhelmingly common causes are cheap to detect from
39
- * the bytes, and detecting them here means the platform class never sees the
40
- * file at all.
41
- *
42
- * <h3>What this is NOT</h3>
43
- *
44
- * It is not a PDF validator and must not grow into one. It answers two
45
- * questions — "does this even claim to be a PDF" and "is it encrypted" — and
46
- * anything subtler (a broken xref, a truncated object stream) still reaches
47
- * PdfRenderer and is still caught by {@link PdfPlatformQuirks}'s latch. Being
48
- * incomplete is fine; being WRONG is not, which is why the encryption test
49
- * insists on the full indirect-reference shape rather than a bare substring.
50
- */
51
- final class PdfPreflight {
52
-
53
- /**
54
- * PDFium scans this far for the header rather than insisting on byte 0, and a
55
- * good many real-world PDFs have junk in front of {@code %PDF-}. Matching its
56
- * tolerance means preflight rejects only files PdfRenderer would also reject.
57
- */
58
- private static final int HEADER_SEARCH_BYTES = 1024;
59
-
60
- /** Enough to cover a linearised file's first-page xref and trailer. */
61
- private static final int HEAD_SCAN_BYTES = 32 * 1024;
62
- /** Enough to cover the trailer, the xref, and a few incremental updates. */
63
- private static final int TAIL_SCAN_BYTES = 256 * 1024;
64
-
65
- private static final byte[] PDF_HEADER = {'%', 'P', 'D', 'F', '-'};
66
- private static final byte[] ENCRYPT = {'/', 'E', 'n', 'c', 'r', 'y', 'p', 't'};
67
-
68
- private PdfPreflight() {}
69
-
70
- static void check(File file) throws PdfRasterException {
71
- if (!file.exists()) {
72
- throw new PdfRasterException(
73
- PdfErrorCode.NOT_FOUND, "No file at " + file.getAbsolutePath() + ".");
74
- }
75
- if (!file.isFile() || !file.canRead()) {
76
- throw new PdfRasterException(
77
- PdfErrorCode.NOT_FOUND, "Not a readable file: " + file.getAbsolutePath() + ".");
78
- }
79
- long length = file.length();
80
- if (length == 0L) {
81
- // Reported as corrupt rather than not-found: the path resolved, so the
82
- // actionable truth is that the download or copy never finished.
83
- throw new PdfRasterException(
84
- PdfErrorCode.CORRUPT, "File is empty: " + file.getAbsolutePath() + ".");
85
- }
86
-
87
- byte[] head;
88
- byte[] tail;
89
- try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
90
- head = read(raf, 0L, (int) Math.min(length, HEAD_SCAN_BYTES));
91
- long tailStart = Math.max(0L, length - TAIL_SCAN_BYTES);
92
- // The ONLY case where the tail can be skipped is a file the head already
93
- // covers END TO END. Comparing tailStart against head.length instead is
94
- // the bug this line used to have: for any length in
95
- // (HEAD_SCAN_BYTES, TAIL_SCAN_BYTES] the tail starts at 0, which is less
96
- // than head.length, so the tail was dropped while the head reached only
97
- // 32 KB — and every byte from there to EOF, TRAILER INCLUDED, went
98
- // unread. A 60 KB encrypted drawing is an entirely ordinary document, and
99
- // on API 24 it then reached `new PdfRenderer(...)` and poisoned PDFium
100
- // for the session: exactly the outcome this class exists to prevent.
101
- // preflightFindsTheEncryptTrailerBeyondTheHeadWindow pins it.
102
- tail = length <= head.length ? new byte[0] : read(raf, tailStart, TAIL_SCAN_BYTES);
103
- } catch (IOException e) {
104
- throw new PdfRasterException(
105
- PdfErrorCode.CORRUPT, "Could not read " + file.getName() + ".", e);
106
- }
107
-
108
- if (indexOf(head, PDF_HEADER, 0, Math.min(head.length, HEADER_SEARCH_BYTES)) < 0) {
109
- throw new PdfRasterException(
110
- PdfErrorCode.CORRUPT,
111
- "Not a PDF: no %PDF- header in the first " + HEADER_SEARCH_BYTES
112
- + " bytes of " + file.getName() + ".");
113
- }
114
-
115
- if (hasEncryptReference(head) || hasEncryptReference(tail)) {
116
- throw new PdfRasterException(
117
- PdfErrorCode.PASSWORD_REQUIRED,
118
- "The document is encrypted: " + file.getName() + ".");
119
- }
120
- }
121
-
122
- private static byte[] read(RandomAccessFile raf, long offset, int max) throws IOException {
123
- raf.seek(offset);
124
- byte[] buffer = new byte[max];
125
- int read = 0;
126
- while (read < max) {
127
- int n = raf.read(buffer, read, max - read);
128
- if (n < 0) {
129
- break;
130
- }
131
- read += n;
132
- }
133
- if (read == max) {
134
- return buffer;
135
- }
136
- byte[] exact = new byte[read];
137
- System.arraycopy(buffer, 0, exact, 0, read);
138
- return exact;
139
- }
140
-
141
- /**
142
- * True when the buffer contains {@code /Encrypt <num> <num> R}.
143
- *
144
- * The full shape is required, not just the name. A bare {@code /Encrypt}
145
- * substring can appear inside an uncompressed content stream or a piece of
146
- * metadata, and refusing such a document would be a false "this file needs a
147
- * password" — the single most confusing thing this class could say. The spec
148
- * requires the trailer's /Encrypt value to be an indirect reference, so
149
- * demanding the reference costs nothing in recall and removes almost all of
150
- * the false-positive surface.
151
- */
152
- private static boolean hasEncryptReference(byte[] buffer) {
153
- int from = 0;
154
- while (true) {
155
- int at = indexOf(buffer, ENCRYPT, from, buffer.length);
156
- if (at < 0) {
157
- return false;
158
- }
159
- if (isIndirectReferenceAt(buffer, at + ENCRYPT.length)) {
160
- return true;
161
- }
162
- from = at + 1;
163
- }
164
- }
165
-
166
- private static boolean isIndirectReferenceAt(byte[] buffer, int start) {
167
- int i = skipWhitespace(buffer, start);
168
- int digits = countDigits(buffer, i);
169
- if (digits == 0) {
170
- return false;
171
- }
172
- i = skipWhitespace(buffer, i + digits);
173
- digits = countDigits(buffer, i);
174
- if (digits == 0) {
175
- return false;
176
- }
177
- i = skipWhitespace(buffer, i + digits);
178
- return i < buffer.length && buffer[i] == 'R';
179
- }
180
-
181
- private static int skipWhitespace(byte[] buffer, int from) {
182
- int i = from;
183
- while (i < buffer.length) {
184
- byte b = buffer[i];
185
- if (b != ' ' && b != '\n' && b != '\r' && b != '\t' && b != 0 && b != 0x0c) {
186
- break;
187
- }
188
- i++;
189
- }
190
- return i;
191
- }
192
-
193
- private static int countDigits(byte[] buffer, int from) {
194
- int i = from;
195
- while (i < buffer.length && buffer[i] >= '0' && buffer[i] <= '9') {
196
- i++;
197
- }
198
- return i - from;
199
- }
200
-
201
- private static int indexOf(byte[] haystack, byte[] needle, int from, int limit) {
202
- int end = Math.min(limit, haystack.length) - needle.length;
203
- outer:
204
- for (int i = Math.max(0, from); i <= end; i++) {
205
- for (int j = 0; j < needle.length; j++) {
206
- if (haystack[i + j] != needle[j]) {
207
- continue outer;
208
- }
209
- }
210
- return i;
211
- }
212
- return -1;
213
- }
214
- }