@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,211 +1,40 @@
1
1
  package tools.reekon.pdfcanvas;
2
2
 
3
3
  import android.content.Context;
4
- import android.graphics.Canvas;
5
- import android.graphics.Color;
6
- import android.graphics.Paint;
7
- import android.graphics.pdf.PdfDocument;
8
4
 
9
5
  import java.io.ByteArrayOutputStream;
10
6
  import java.io.File;
11
7
  import java.io.FileOutputStream;
12
8
  import java.io.IOException;
13
9
  import java.nio.charset.Charset;
14
- import java.security.MessageDigest;
15
10
  import java.util.ArrayList;
16
11
  import java.util.List;
17
12
 
18
13
  /**
19
- * PDFs built ON DEVICE, so a test has no asset to ship and the content is known
20
- * exactly rather than eyeballed.
14
+ * Hand-written PDFs for the JNI seam test.
21
15
  *
22
- * Three kinds, because three different things need proving:
23
- *
24
- * pattern() — via android.graphics.pdf.PdfDocument. Detail at every spatial
25
- * frequency, so a scale error, a translation error and an x/y
26
- * transposition each move a large share of the pixels. A flat or
27
- * symmetric page would let all three through, which is the whole
28
- * reason the spike used this pattern and not a gradient.
29
- *
30
- * rotated() — hand-written, because PdfDocument.PageInfo has no way to set
31
- * /Rotate. Post-rotation geometry cannot be tested without it.
32
- *
33
- * encrypted() — hand-written, because nothing in the platform writes an
34
- * encrypted PDF. Without it "password-protected produces a
35
- * distinguishable error" is an assertion about code that has
36
- * never met an encrypted file.
16
+ * DELIBERATELY SMALL. Rendering itself — geometry, registration, backgrounds,
17
+ * alpha, annotations, forms, rotation, encryption, cancellation — is proven on a
18
+ * laptop by {@code native/tests} against the same PDFium build, with a much
19
+ * richer fixture set. What is left to prove ON DEVICE is that the bytes cross
20
+ * JNI intact and the transport's slot store hands back what PDFium wrote, and
21
+ * two fixtures are enough for that: one whose channels are all saturated (a
22
+ * channel swap or a stride shear cannot hide) and one whose green exists only
23
+ * inside an annotation.
37
24
  */
38
25
  final class TestPdfs {
39
26
 
40
- static final int LETTER_W = 612;
41
- static final int LETTER_H = 792;
42
- /** ARCH-D, the sheet size this package exists for. */
43
- static final int ARCH_D_W = 1728;
44
- static final int ARCH_D_H = 2592;
45
-
46
27
  private static final Charset LATIN1 = Charset.forName("ISO-8859-1");
47
28
 
48
29
  private TestPdfs() {}
49
30
 
50
- /* ---------------------------------------------------------------- *
51
- * PdfDocument-authored pages
52
- * ---------------------------------------------------------------- */
53
-
54
- /**
55
- * Detail at every frequency: an 8pt grid catches a scale error, 64pt-pitch
56
- * colour blocks catch a translation error, and the two diagonals catch an x/y
57
- * transposition.
58
- */
59
- private static void drawPattern(Canvas canvas, int wPt, int hPt) {
60
- canvas.drawColor(Color.WHITE);
61
-
62
- Paint grid = new Paint();
63
- grid.setColor(Color.BLACK);
64
- grid.setStrokeWidth(0.5f);
65
- grid.setAntiAlias(true);
66
- for (int x = 0; x <= wPt; x += 8) canvas.drawLine(x, 0, x, hPt, grid);
67
- for (int y = 0; y <= hPt; y += 8) canvas.drawLine(0, y, wPt, y, grid);
68
-
69
- Paint block = new Paint();
70
- block.setAntiAlias(true);
71
- int i = 0;
72
- for (int y = 0; y < hPt; y += 64) {
73
- for (int x = 0; x < wPt; x += 64) {
74
- block.setColor(Color.rgb((x * 7 + i) % 256, (y * 5 + i) % 256, (i * 13) % 256));
75
- canvas.drawRect(x + 4, y + 4, x + 40, y + 40, block);
76
- i++;
77
- }
78
- }
79
-
80
- Paint diag = new Paint();
81
- diag.setColor(Color.RED);
82
- diag.setStrokeWidth(2f);
83
- diag.setAntiAlias(true);
84
- canvas.drawLine(0, 0, wPt, hPt, diag);
85
- canvas.drawLine(wPt, 0, 0, hPt, diag);
86
- }
87
-
88
- static File pattern(Context ctx, String name, int wPt, int hPt) throws IOException {
89
- return pattern(ctx, name, wPt, hPt, 1);
90
- }
91
-
92
- static File pattern(Context ctx, String name, int wPt, int hPt, int pageCount)
93
- throws IOException {
94
- PdfDocument doc = new PdfDocument();
95
- for (int p = 0; p < pageCount; p++) {
96
- PdfDocument.PageInfo info =
97
- new PdfDocument.PageInfo.Builder(wPt, hPt, p).create();
98
- PdfDocument.Page page = doc.startPage(info);
99
- drawPattern(page.getCanvas(), wPt, hPt);
100
- doc.finishPage(page);
101
- }
102
- File out = new File(ctx.getCacheDir(), name);
103
- try (FileOutputStream fos = new FileOutputStream(out)) {
104
- doc.writeTo(fos);
105
- }
106
- doc.close();
107
- return out;
108
- }
109
-
110
- /**
111
- * Pages of DIFFERENT sizes in one document, so a geometry test can catch an
112
- * implementation that probes page 0 and reports its size for every page.
113
- */
114
- static File mixedSizes(Context ctx, String name) throws IOException {
115
- int[][] sizes = {{LETTER_W, LETTER_H}, {ARCH_D_W, ARCH_D_H}, {200, 900}};
116
- PdfDocument doc = new PdfDocument();
117
- for (int p = 0; p < sizes.length; p++) {
118
- PdfDocument.PageInfo info =
119
- new PdfDocument.PageInfo.Builder(sizes[p][0], sizes[p][1], p).create();
120
- PdfDocument.Page page = doc.startPage(info);
121
- page.getCanvas().drawColor(Color.WHITE);
122
- doc.finishPage(page);
123
- }
124
- File out = new File(ctx.getCacheDir(), name);
125
- try (FileOutputStream fos = new FileOutputStream(out)) {
126
- doc.writeTo(fos);
127
- }
128
- doc.close();
129
- return out;
130
- }
131
-
132
- static int[] mixedSizeExpectation(int page) {
133
- int[][] sizes = {{LETTER_W, LETTER_H}, {ARCH_D_W, ARCH_D_H}, {200, 900}};
134
- return sizes[page];
135
- }
136
-
137
- /**
138
- * One flat colour per page. Catches a backend that ignores
139
- * {@code RasterRequest.page} and renders page 0 for every request — which a
140
- * single-page fixture cannot catch at all, and a multi-page fixture with
141
- * identical pages catches only by luck.
142
- */
143
- static File coloredPages(Context ctx, String name, int[] colors, int wPt, int hPt)
144
- throws IOException {
145
- PdfDocument doc = new PdfDocument();
146
- for (int p = 0; p < colors.length; p++) {
147
- PdfDocument.PageInfo info = new PdfDocument.PageInfo.Builder(wPt, hPt, p).create();
148
- PdfDocument.Page page = doc.startPage(info);
149
- page.getCanvas().drawColor(colors[p]);
150
- doc.finishPage(page);
151
- }
152
- File out = new File(ctx.getCacheDir(), name);
153
- try (FileOutputStream fos = new FileOutputStream(out)) {
154
- doc.writeTo(fos);
155
- }
156
- doc.close();
157
- return out;
158
- }
159
-
160
- /**
161
- * Four saturated quadrants — red, green, blue, black — which is what makes a
162
- * channel-order test possible at all. A grey or white page reads the same in
163
- * RGBA and BGRA, so the two orderings are indistinguishable on it; pure red
164
- * differs in the first byte.
165
- *
166
- * Quadrants also make a STRIDE error visible: a raster read back with the
167
- * wrong row stride walks diagonally across the quadrant boundaries, so the
168
- * sampled colours come out wrong rather than merely shifted.
169
- */
170
- static File quadrants(Context ctx, String name, int wPt, int hPt) throws IOException {
171
- PdfDocument doc = new PdfDocument();
172
- PdfDocument.PageInfo info = new PdfDocument.PageInfo.Builder(wPt, hPt, 0).create();
173
- PdfDocument.Page page = doc.startPage(info);
174
- Canvas canvas = page.getCanvas();
175
- Paint paint = new Paint();
176
- paint.setAntiAlias(false);
177
- int halfW = wPt / 2;
178
- int halfH = hPt / 2;
179
- paint.setColor(Color.RED);
180
- canvas.drawRect(0, 0, halfW, halfH, paint);
181
- paint.setColor(Color.GREEN);
182
- canvas.drawRect(halfW, 0, wPt, halfH, paint);
183
- paint.setColor(Color.BLUE);
184
- canvas.drawRect(0, halfH, halfW, hPt, paint);
185
- paint.setColor(Color.BLACK);
186
- canvas.drawRect(halfW, halfH, wPt, hPt, paint);
187
- doc.finishPage(page);
188
-
189
- File out = new File(ctx.getCacheDir(), name);
190
- try (FileOutputStream fos = new FileOutputStream(out)) {
191
- doc.writeTo(fos);
192
- }
193
- doc.close();
194
- return out;
195
- }
196
-
197
- /* ---------------------------------------------------------------- *
198
- * Hand-written PDFs
199
- * ---------------------------------------------------------------- */
200
-
201
- /** Assembles objects and writes a correct xref table, which PDFium prefers. */
31
+ /** Assembles objects and writes a correct xref table. */
202
32
  private static final class Writer {
203
33
  private final ByteArrayOutputStream out = new ByteArrayOutputStream();
204
34
  private final List<Integer> offsets = new ArrayList<>();
205
35
 
206
36
  Writer() {
207
37
  write("%PDF-1.4\n");
208
- // A binary comment line marks the file as binary, per the spec's advice.
209
38
  out.write(0x25);
210
39
  out.write(0xE2);
211
40
  out.write(0xE3);
@@ -219,16 +48,11 @@ final class TestPdfs {
219
48
  out.write(b, 0, b.length);
220
49
  }
221
50
 
222
- void write(byte[] b) {
223
- out.write(b, 0, b.length);
51
+ int count() {
52
+ return offsets.size();
224
53
  }
225
54
 
226
- /** @return the object number, 1-based. */
227
55
  int add(String body) {
228
- return add(body.getBytes(LATIN1));
229
- }
230
-
231
- int add(byte[] body) {
232
56
  int number = offsets.size() + 1;
233
57
  offsets.add(out.size());
234
58
  write(number + " 0 obj\n");
@@ -237,26 +61,7 @@ final class TestPdfs {
237
61
  return number;
238
62
  }
239
63
 
240
- /**
241
- * Writes at least {@code atLeast} bytes of PDF COMMENT lines.
242
- *
243
- * A `%` line is legal anywhere between objects and is never drawn, so this
244
- * changes the file's LENGTH and nothing else — which is the only lever a
245
- * preflight scan-window test needs. Call it after the last {@code add()} and
246
- * before {@code finish()}, so the trailer is pushed past the head window
247
- * while every recorded xref offset stays correct.
248
- */
249
- void pad(int atLeast) {
250
- StringBuilder sb = new StringBuilder(atLeast + 64);
251
- int line = 0;
252
- while (sb.length() < atLeast) {
253
- sb.append("% padding line ").append(line++)
254
- .append(" - legal PDF comment, never drawn\n");
255
- }
256
- write(sb.toString());
257
- }
258
-
259
- byte[] finish(int rootObject, String extraTrailerEntries) {
64
+ byte[] finish(int rootObject) {
260
65
  int xref = out.size();
261
66
  int size = offsets.size() + 1;
262
67
  write("xref\n0 " + size + "\n");
@@ -264,271 +69,62 @@ final class TestPdfs {
264
69
  for (int offset : offsets) {
265
70
  write(String.format("%010d 00000 n \n", offset));
266
71
  }
267
- write("trailer\n<< /Size " + size + " /Root " + rootObject + " 0 R "
268
- + extraTrailerEntries + " >>\nstartxref\n" + xref + "\n%%EOF\n");
72
+ write("trailer\n<< /Size " + size + " /Root " + rootObject + " 0 R >>\nstartxref\n"
73
+ + xref + "\n%%EOF\n");
269
74
  return out.toByteArray();
270
75
  }
271
76
  }
272
77
 
273
- /**
274
- * A 612x792 page carrying {@code /Rotate 90}, with a red marker filling the
275
- * top-left 112x100 points of the UNROTATED page.
276
- *
277
- * /Rotate is "degrees the page shall be rotated CLOCKWISE when displayed", so
278
- * a correct renderer reports the page as 792x612 and puts that marker in the
279
- * displayed TOP-RIGHT. Both halves matter: swapped dimensions alone would also
280
- * be produced by an implementation that swapped width and height and drew the
281
- * content unrotated.
282
- *
283
- * PDF user space is y-UP from the bottom-left, so the top-left 112x100 of the
284
- * page is the rect (0, 692) to (112, 792).
285
- */
286
- static File rotated90(Context ctx, String name) throws IOException {
287
- String content =
288
- "1 1 1 rg 0 0 " + LETTER_W + " " + LETTER_H + " re f\n"
289
- + "1 0 0 rg 0 692 112 100 re f\n"
290
- // A blue marker at the unrotated BOTTOM-left, so an implementation
291
- // that rotated the wrong way round is caught too: 180 degrees out
292
- // would put red where blue is expected.
293
- + "0 0 1 rg 0 0 112 100 re f\n";
294
- byte[] stream = content.getBytes(LATIN1);
295
-
296
- Writer w = new Writer();
297
- int contents = w.add("<< /Length " + stream.length + " >>\nstream\n"
298
- + content + "endstream");
299
- int page = w.add("<< /Type /Page /Parent 3 0 R /MediaBox [0 0 " + LETTER_W + " "
300
- + LETTER_H + "] /Rotate 90 /Contents " + contents + " 0 R "
301
- + "/Resources << >> >>");
302
- int pages = w.add("<< /Type /Pages /Kids [" + page + " 0 R] /Count 1 >>");
303
- int catalog = w.add("<< /Type /Catalog /Pages " + pages + " 0 R >>");
304
- // /Parent was written as "3 0 R" above, which is the number `pages` gets;
305
- // assert rather than hope, because a silent mismatch makes PDFium rebuild
306
- // the tree and the test would still pass for the wrong reason.
307
- if (pages != 3) {
308
- throw new IllegalStateException("Pages object is " + pages + ", not 3");
309
- }
310
- return writeFile(ctx, name, w.finish(catalog, ""));
311
- }
312
-
313
- /**
314
- * A perfectly ordinary PDF whose content stream contains the literal text
315
- * {@code /Encrypt}.
316
- *
317
- * The decoy for {@link PdfPreflight}: a naive substring scan for the name
318
- * would refuse this document with "needs a password", which is the most
319
- * confusing thing a preflight can say. The name appears WITHOUT a following
320
- * indirect reference, which is exactly the shape the real check requires — so
321
- * this passes only if the check looks at the whole reference.
322
- */
323
- static File withLiteralEncryptInContent(Context ctx, String name) throws IOException {
324
- // A comment inside a content stream is legal and is never drawn, so the
325
- // bytes are present in the file without changing what the page looks like.
326
- String content = "% /Encrypt and /Encrypt again, as decoys\n1 0 0 rg 0 0 200 200 re f\n";
327
-
328
- Writer w = new Writer();
329
- int contents = w.add("<< /Length " + content.length() + " >>\nstream\n"
330
- + content + "endstream");
331
- int page = w.add("<< /Type /Page /Parent 3 0 R /MediaBox [0 0 200 200] /Contents "
332
- + contents + " 0 R /Resources << >> >>");
333
- int pages = w.add("<< /Type /Pages /Kids [" + page + " 0 R] /Count 1 >>");
334
- int catalog = w.add("<< /Type /Catalog /Pages " + pages + " 0 R >>");
335
- if (pages != 3) {
336
- throw new IllegalStateException("Pages object is " + pages + ", not 3");
337
- }
338
- return writeFile(ctx, name, w.finish(catalog, ""));
78
+ private static String stream(String content) {
79
+ return "<< /Length " + content.getBytes(LATIN1).length + " >>\nstream\n" + content + "endstream";
339
80
  }
340
81
 
341
82
  /**
342
- * A file with a valid {@code %PDF-} header and nothing else that parses.
343
- *
344
- * Preflight cannot reject it — it claims to be a PDF and is not encrypted — so
345
- * it is the shape that still reaches {@code new PdfRenderer(...)} and, on API
346
- * 24, still poisons the process. Used only by the excluded poison-guard test.
83
+ * Four saturated quadrants in PDF user space (y-up): red bottom-left, green
84
+ * bottom-right, blue top-left, black top-right.
347
85
  */
348
- static File headerOnlyGarbage(Context ctx, String name) throws IOException {
349
- StringBuilder sb = new StringBuilder("%PDF-1.7\n");
350
- for (int i = 0; i < 200; i++) {
351
- sb.append("garbage line ").append(i).append('\n');
352
- }
353
- return writeFile(ctx, name, sb.toString().getBytes(LATIN1));
354
- }
355
-
356
- /* ---------------------------------------------------------------- *
357
- * Encryption (standard security handler, R=2, V=1, RC4-40)
358
- * ---------------------------------------------------------------- */
359
-
360
- private static final byte[] PAD = {
361
- (byte) 0x28, (byte) 0xBF, (byte) 0x4E, (byte) 0x5E, (byte) 0x4E, (byte) 0x75,
362
- (byte) 0x8A, (byte) 0x41, (byte) 0x64, (byte) 0x00, (byte) 0x4E, (byte) 0x56,
363
- (byte) 0xFF, (byte) 0xFA, (byte) 0x01, (byte) 0x08, (byte) 0x2E, (byte) 0x2E,
364
- (byte) 0x00, (byte) 0xB6, (byte) 0xD0, (byte) 0x68, (byte) 0x3E, (byte) 0x80,
365
- (byte) 0x2F, (byte) 0x0C, (byte) 0xA9, (byte) 0xFE, (byte) 0x64, (byte) 0x53,
366
- (byte) 0x69, (byte) 0x7A
367
- };
368
-
369
- static final String USER_PASSWORD = "secret";
370
-
371
- private static byte[] pad(String password) {
372
- byte[] pw = password.getBytes(LATIN1);
373
- byte[] out = new byte[32];
374
- int n = Math.min(pw.length, 32);
375
- System.arraycopy(pw, 0, out, 0, n);
376
- System.arraycopy(PAD, 0, out, n, 32 - n);
377
- return out;
378
- }
379
-
380
- private static byte[] md5(byte[]... parts) {
381
- try {
382
- MessageDigest md = MessageDigest.getInstance("MD5");
383
- for (byte[] part : parts) {
384
- md.update(part);
385
- }
386
- return md.digest();
387
- } catch (Exception e) {
388
- throw new IllegalStateException("MD5 is mandatory on every JRE", e);
389
- }
390
- }
391
-
392
- /** RC4, 25 lines, because javax.crypto's ARCFOUR is not guaranteed present. */
393
- private static byte[] rc4(byte[] key, byte[] data) {
394
- int[] s = new int[256];
395
- for (int i = 0; i < 256; i++) {
396
- s[i] = i;
397
- }
398
- for (int i = 0, j = 0; i < 256; i++) {
399
- j = (j + s[i] + (key[i % key.length] & 0xff)) & 0xff;
400
- int t = s[i];
401
- s[i] = s[j];
402
- s[j] = t;
403
- }
404
- byte[] out = new byte[data.length];
405
- for (int n = 0, i = 0, j = 0; n < data.length; n++) {
406
- i = (i + 1) & 0xff;
407
- j = (j + s[i]) & 0xff;
408
- int t = s[i];
409
- s[i] = s[j];
410
- s[j] = t;
411
- out[n] = (byte) (data[n] ^ s[(s[i] + s[j]) & 0xff]);
412
- }
413
- return out;
414
- }
415
-
416
- private static byte[] leInt(int value) {
417
- return new byte[] {
418
- (byte) value, (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24)
419
- };
420
- }
421
-
422
- private static String hex(byte[] b) {
423
- StringBuilder sb = new StringBuilder(b.length * 2);
424
- for (byte x : b) {
425
- sb.append(String.format("%02X", x));
426
- }
427
- return sb.toString();
428
- }
429
-
430
- /**
431
- * A PDF encrypted with the standard security handler and a NON-EMPTY user
432
- * password, so opening it with no password must fail.
433
- *
434
- * The empty-user-password case would open cleanly on every viewer and prove
435
- * nothing. The O and U values are computed properly (Algorithms 2, 3 and 4 of
436
- * the spec) rather than filled with plausible-looking bytes, because a viewer
437
- * that validates them and one that merely notices /Encrypt would then give
438
- * different answers and the test would only be pinning the weaker of the two.
439
- */
440
- static File encrypted(Context ctx, String name) throws IOException {
441
- return encrypted(ctx, name, 0);
442
- }
443
-
444
- /**
445
- * @param paddingBytes comment bytes inserted between the last object and the
446
- * xref table, so the file — and therefore its TRAILER, which is where
447
- * {@code /Encrypt} lives — can be pushed to any chosen length.
448
- * {@link PdfPreflight} reads a 32 KB head and a 256 KB tail, so a value
449
- * around 60 KB lands the trailer in the band that only the tail scan
450
- * reaches. Nothing else in the document changes.
451
- */
452
- static File encrypted(Context ctx, String name, int paddingBytes) throws IOException {
453
- byte[] id = new byte[16];
454
- for (int i = 0; i < id.length; i++) {
455
- id[i] = (byte) (i * 17 + 3);
456
- }
457
- int permissions = -1;
458
-
459
- // Algorithm 3: the owner-password entry.
460
- byte[] ownerKey = new byte[5];
461
- System.arraycopy(md5(pad("owner")), 0, ownerKey, 0, 5);
462
- byte[] o = rc4(ownerKey, pad(USER_PASSWORD));
463
-
464
- // Algorithm 2: the file encryption key.
465
- byte[] fileKey = new byte[5];
466
- System.arraycopy(md5(pad(USER_PASSWORD), o, leInt(permissions), id), 0, fileKey, 0, 5);
467
-
468
- // Algorithm 4 (R=2): the user-password entry.
469
- byte[] u = rc4(fileKey, PAD);
470
-
86
+ static File quadrants(Context ctx, String name, int w, int h) throws IOException {
87
+ int hw = w / 2;
88
+ int hh = h / 2;
471
89
  String content =
472
- "1 1 1 rg 0 0 " + LETTER_W + " " + LETTER_H + " re f\n"
473
- + "0 0 0 rg 100 100 200 200 re f\n";
474
- byte[] plain = content.getBytes(LATIN1);
475
-
476
- Writer w = new Writer();
477
-
478
- // Algorithm 1: per-object key = MD5(fileKey + objNum LE24 + gen LE16)[0..9].
479
- int contentsNumber = 1;
480
- byte[] objKey = new byte[10];
481
- System.arraycopy(
482
- md5(
483
- fileKey,
484
- new byte[] {
485
- (byte) contentsNumber, (byte) (contentsNumber >> 8),
486
- (byte) (contentsNumber >> 16), 0, 0
487
- }),
488
- 0,
489
- objKey,
490
- 0,
491
- 10);
492
- byte[] cipher = rc4(objKey, plain);
493
-
494
- ByteArrayOutputStream body = new ByteArrayOutputStream();
495
- byte[] head = ("<< /Length " + cipher.length + " >>\nstream\n").getBytes(LATIN1);
496
- body.write(head, 0, head.length);
497
- body.write(cipher, 0, cipher.length);
498
- byte[] tail = "\nendstream".getBytes(LATIN1);
499
- body.write(tail, 0, tail.length);
500
- int contents = w.add(body.toByteArray());
501
- if (contents != contentsNumber) {
502
- throw new IllegalStateException("Contents object is " + contents);
503
- }
504
-
505
- int page = w.add("<< /Type /Page /Parent 3 0 R /MediaBox [0 0 " + LETTER_W + " "
506
- + LETTER_H + "] /Contents " + contents + " 0 R /Resources << >> >>");
507
- int pages = w.add("<< /Type /Pages /Kids [" + page + " 0 R] /Count 1 >>");
508
- int catalog = w.add("<< /Type /Catalog /Pages " + pages + " 0 R >>");
509
- int encrypt =
510
- w.add("<< /Filter /Standard /V 1 /R 2 /Length 40 /P " + permissions
511
- + " /O <" + hex(o) + "> /U <" + hex(u) + "> >>");
512
- if (pages != 3) {
513
- throw new IllegalStateException("Pages object is " + pages + ", not 3");
514
- }
515
- if (paddingBytes > 0) {
516
- w.pad(paddingBytes);
517
- }
518
-
519
- String trailer = "/Encrypt " + encrypt + " 0 R /ID [<" + hex(id) + "> <" + hex(id) + ">]";
520
- return writeFile(ctx, name, w.finish(catalog, trailer));
521
- }
522
-
523
- /* ---------------------------------------------------------------- *
524
- * Non-PDFs
525
- * ---------------------------------------------------------------- */
526
-
527
- /**
528
- * Deliberately has no %PDF header AND no recoverable objects. PDFium rebuilds
529
- * a broken xref rather than giving up, so a file that merely has a bad trailer
530
- * would open fine and the "corrupt" branch would never be reached.
531
- */
90
+ "1 0 0 rg 0 0 " + hw + " " + hh + " re f\n"
91
+ + "0 1 0 rg " + hw + " 0 " + hw + " " + hh + " re f\n"
92
+ + "0 0 1 rg 0 " + hh + " " + hw + " " + hh + " re f\n"
93
+ + "0 0 0 rg " + hw + " " + hh + " " + hw + " " + hh + " re f\n";
94
+ Writer wr = new Writer();
95
+ int contents = wr.add(stream(content));
96
+ int pagesNumber = wr.count() + 2;
97
+ int page = wr.add("<< /Type /Page /Parent " + pagesNumber + " 0 R /MediaBox [0 0 " + w + " " + h
98
+ + "] /Contents " + contents + " 0 R /Resources << >> >>");
99
+ int pages = wr.add("<< /Type /Pages /Kids [" + page + " 0 R] /Count 1 >>");
100
+ int catalog = wr.add("<< /Type /Catalog /Pages " + pages + " 0 R >>");
101
+ if (pages != pagesNumber) {
102
+ throw new IllegalStateException("Pages object is " + pages + ", not " + pagesNumber);
103
+ }
104
+ return writeFile(ctx, name, wr.finish(catalog));
105
+ }
106
+
107
+ /** A 200x200 white page whose only GREEN is a /Square annotation's appearance stream. */
108
+ static File annotated(Context ctx, String name) throws IOException {
109
+ Writer wr = new Writer();
110
+ int contents = wr.add(stream("1 1 1 rg 0 0 200 200 re f\n1 0 0 rg 10 10 30 30 re f\n"));
111
+ String appearance = "0 1 0 rg 0 0 100 100 re f\n";
112
+ int ap = wr.add("<< /Type /XObject /Subtype /Form /BBox [0 0 100 100] /Length "
113
+ + appearance.getBytes(LATIN1).length + " >>\nstream\n" + appearance + "endstream");
114
+ int annot = wr.add("<< /Type /Annot /Subtype /Square /Rect [50 50 150 150] /F 4 /AP << /N "
115
+ + ap + " 0 R >> >>");
116
+ int pagesNumber = wr.count() + 2;
117
+ int page = wr.add("<< /Type /Page /Parent " + pagesNumber + " 0 R /MediaBox [0 0 200 200] /Annots ["
118
+ + annot + " 0 R] /Contents " + contents + " 0 R /Resources << >> >>");
119
+ int pages = wr.add("<< /Type /Pages /Kids [" + page + " 0 R] /Count 1 >>");
120
+ int catalog = wr.add("<< /Type /Catalog /Pages " + pages + " 0 R >>");
121
+ if (pages != pagesNumber) {
122
+ throw new IllegalStateException("Pages object is " + pages + ", not " + pagesNumber);
123
+ }
124
+ return writeFile(ctx, name, wr.finish(catalog));
125
+ }
126
+
127
+ /** No %PDF header and no recoverable objects. */
532
128
  static File notAPdf(Context ctx, String name) throws IOException {
533
129
  StringBuilder sb = new StringBuilder();
534
130
  for (int i = 0; i < 200; i++) {
@@ -537,10 +133,6 @@ final class TestPdfs {
537
133
  return writeFile(ctx, name, sb.toString().getBytes(LATIN1));
538
134
  }
539
135
 
540
- static File empty(Context ctx, String name) throws IOException {
541
- return writeFile(ctx, name, new byte[0]);
542
- }
543
-
544
136
  static File missing(Context ctx, String name) {
545
137
  File f = new File(ctx.getCacheDir(), name);
546
138
  //noinspection ResultOfMethodCallIgnored
@@ -0,0 +1,54 @@
1
+ # libpdfcanvas.so: the C++ core from `native/core`, the JNI surface, and — in
2
+ # the autolinked build — the JSI transport install. Links the prebuilt PDFium
3
+ # for this ABI, which AGP then packages alongside it (an IMPORTED SHARED library
4
+ # a target links is a runtime file in AGP's model, which is also why
5
+ # build.gradle has to EXCLUDE the prefab-imported libjsi.so).
6
+ #
7
+ # PDFCANVAS_PDFIUM_ROOT is passed by build.gradle and is what
8
+ # `scripts/fetch-pdfium.mjs android` produced: `include/` plus
9
+ # `jniLibs/<abi>/libpdfium.so` for every ABI the app asked for.
10
+ #
11
+ # PDFCANVAS_WITH_JSI is passed ON only by the autolinked build, where
12
+ # ReactAndroid's prefab publishes jsi/jsi.h and libjsi.so. The standalone build
13
+ # (`cd android && ./gradlew connectedDebugAndroidTest`) passes nothing, has no
14
+ # React Native anywhere on its classpath, and still compiles the identical core
15
+ # — the slot storage and its JNI surface are React-free.
16
+
17
+ cmake_minimum_required(VERSION 3.13)
18
+ project(pdfcanvas CXX)
19
+
20
+ set(CMAKE_CXX_STANDARD 17)
21
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
22
+
23
+ if(NOT PDFCANVAS_PDFIUM_ROOT)
24
+ message(FATAL_ERROR "PDFCANVAS_PDFIUM_ROOT was not passed; build.gradle's fetchPdfium task sets it.")
25
+ endif()
26
+ set(PDFIUM_SO "${PDFCANVAS_PDFIUM_ROOT}/jniLibs/${ANDROID_ABI}/libpdfium.so")
27
+ if(NOT EXISTS "${PDFIUM_SO}")
28
+ message(FATAL_ERROR
29
+ "No PDFium for ${ANDROID_ABI} at ${PDFIUM_SO}. Run "
30
+ "`node scripts/fetch-pdfium.mjs android --abis ${ANDROID_ABI}` from the package root.")
31
+ endif()
32
+
33
+ add_library(pdfium SHARED IMPORTED)
34
+ set_target_properties(pdfium PROPERTIES
35
+ IMPORTED_LOCATION "${PDFIUM_SO}"
36
+ INTERFACE_INCLUDE_DIRECTORIES "${PDFCANVAS_PDFIUM_ROOT}/include")
37
+
38
+ include("${CMAKE_CURRENT_SOURCE_DIR}/../../../../native/core/pdfcanvas-core.cmake")
39
+
40
+ add_library(pdfcanvas SHARED
41
+ ${PDFCANVAS_CORE_SOURCES}
42
+ pdfcanvas-jni.cpp)
43
+ target_include_directories(pdfcanvas PRIVATE "${PDFCANVAS_CORE_INCLUDE_DIR}")
44
+ # `log` for __android_log_print should anyone add diagnostics; `pdfium` for the
45
+ # engine. No jnigraphics any more: nothing here touches a Bitmap.
46
+ target_link_libraries(pdfcanvas pdfium log)
47
+
48
+ if(PDFCANVAS_WITH_JSI)
49
+ target_compile_definitions(pdfcanvas PRIVATE PDFCANVAS_WITH_JSI=1)
50
+ # CONFIG mode against the react-android AAR's prefab package. `jsi` is the
51
+ # only target needed: nothing here touches TurboModules, fbjni or folly.
52
+ find_package(ReactAndroid REQUIRED CONFIG)
53
+ target_link_libraries(pdfcanvas ReactAndroid::jsi)
54
+ endif()