@reekon-tools/react-native-pdf-canvas 0.1.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 (161) hide show
  1. package/PdfCanvas.podspec +80 -0
  2. package/README.md +1391 -0
  3. package/android/build.gradle +175 -0
  4. package/android/consumer-rules.pro +3 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +2 -0
  8. package/android/gradlew +248 -0
  9. package/android/gradlew.bat +98 -0
  10. package/android/settings.gradle +34 -0
  11. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
  12. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
  13. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
  14. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
  15. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
  16. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
  17. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
  18. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
  19. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
  20. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
  21. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
  22. package/android/src/jsi/cpp/CMakeLists.txt +33 -0
  23. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
  24. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
  25. package/android/src/main/AndroidManifest.xml +7 -0
  26. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
  27. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
  28. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
  29. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
  30. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
  31. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
  32. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
  33. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
  34. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
  35. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
  36. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
  37. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
  38. package/android/tools/compile-gate.sh +289 -0
  39. package/dist/cache.d.ts +161 -0
  40. package/dist/cache.js +389 -0
  41. package/dist/clock.d.ts +21 -0
  42. package/dist/clock.js +31 -0
  43. package/dist/controller.d.ts +131 -0
  44. package/dist/controller.js +1002 -0
  45. package/dist/geometry.d.ts +96 -0
  46. package/dist/geometry.js +248 -0
  47. package/dist/index.d.ts +52 -0
  48. package/dist/index.js +50 -0
  49. package/dist/index.native.d.ts +21 -0
  50. package/dist/index.native.js +23 -0
  51. package/dist/index.web.d.ts +24 -0
  52. package/dist/index.web.js +26 -0
  53. package/dist/layout.d.ts +78 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/peer-check.d.ts +83 -0
  56. package/dist/peer-check.js +183 -0
  57. package/dist/planner.d.ts +35 -0
  58. package/dist/planner.js +530 -0
  59. package/dist/policy.d.ts +14 -0
  60. package/dist/policy.js +168 -0
  61. package/dist/rasterizer/android.d.ts +121 -0
  62. package/dist/rasterizer/android.js +126 -0
  63. package/dist/rasterizer/buffer.d.ts +18 -0
  64. package/dist/rasterizer/buffer.js +18 -0
  65. package/dist/rasterizer/fake.d.ts +92 -0
  66. package/dist/rasterizer/fake.js +433 -0
  67. package/dist/rasterizer/index.d.ts +38 -0
  68. package/dist/rasterizer/index.js +69 -0
  69. package/dist/rasterizer/index.native.d.ts +19 -0
  70. package/dist/rasterizer/index.native.js +80 -0
  71. package/dist/rasterizer/index.web.d.ts +63 -0
  72. package/dist/rasterizer/index.web.js +75 -0
  73. package/dist/rasterizer/ingest.d.ts +180 -0
  74. package/dist/rasterizer/ingest.js +307 -0
  75. package/dist/rasterizer/ios.d.ts +84 -0
  76. package/dist/rasterizer/ios.js +155 -0
  77. package/dist/rasterizer/native-bridge.d.ts +261 -0
  78. package/dist/rasterizer/native-bridge.js +413 -0
  79. package/dist/rasterizer/web/client.d.ts +84 -0
  80. package/dist/rasterizer/web/client.js +359 -0
  81. package/dist/rasterizer/web/engine.d.ts +69 -0
  82. package/dist/rasterizer/web/engine.js +308 -0
  83. package/dist/rasterizer/web/index.d.ts +26 -0
  84. package/dist/rasterizer/web/index.js +21 -0
  85. package/dist/rasterizer/web/pdfium.d.ts +213 -0
  86. package/dist/rasterizer/web/pdfium.js +170 -0
  87. package/dist/rasterizer/web/protocol.d.ts +138 -0
  88. package/dist/rasterizer/web/protocol.js +37 -0
  89. package/dist/rasterizer/web/session.d.ts +106 -0
  90. package/dist/rasterizer/web/session.js +413 -0
  91. package/dist/rasterizer/web/worker.d.ts +44 -0
  92. package/dist/rasterizer/web/worker.js +76 -0
  93. package/dist/react/PdfContentView.d.ts +98 -0
  94. package/dist/react/PdfContentView.js +189 -0
  95. package/dist/react/index.d.ts +6 -0
  96. package/dist/react/index.js +3 -0
  97. package/dist/react/usePdfDocument.d.ts +134 -0
  98. package/dist/react/usePdfDocument.js +275 -0
  99. package/dist/react/usePdfLayer.d.ts +94 -0
  100. package/dist/react/usePdfLayer.js +388 -0
  101. package/dist/skia/index.d.ts +26 -0
  102. package/dist/skia/index.js +25 -0
  103. package/dist/skia/index.native.d.ts +14 -0
  104. package/dist/skia/index.native.js +16 -0
  105. package/dist/skia/index.web.d.ts +12 -0
  106. package/dist/skia/index.web.js +14 -0
  107. package/dist/skia/resolve.native.d.ts +18 -0
  108. package/dist/skia/resolve.native.js +22 -0
  109. package/dist/skia/resolve.web.d.ts +41 -0
  110. package/dist/skia/resolve.web.js +62 -0
  111. package/dist/skia/skia-api.d.ts +94 -0
  112. package/dist/skia/skia-api.js +113 -0
  113. package/dist/testing/index.d.ts +27 -0
  114. package/dist/testing/index.js +28 -0
  115. package/dist/testing/scenes.d.ts +100 -0
  116. package/dist/testing/scenes.js +267 -0
  117. package/dist/types.d.ts +611 -0
  118. package/dist/types.js +21 -0
  119. package/dist/web-init.d.ts +68 -0
  120. package/dist/web-init.js +64 -0
  121. package/ios/Package.swift +58 -0
  122. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
  123. package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
  124. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
  125. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
  126. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
  127. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
  128. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
  129. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
  130. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
  131. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
  132. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
  133. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
  134. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
  135. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
  136. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
  137. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
  138. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
  139. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
  140. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
  141. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
  142. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
  143. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
  144. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
  145. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
  146. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
  147. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
  148. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
  149. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
  150. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
  151. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
  152. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
  153. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
  154. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
  155. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
  156. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
  157. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
  158. package/package.json +144 -0
  159. package/react-native.config.js +40 -0
  160. package/scripts/check-skia-peer.mjs +86 -0
  161. package/scripts/skia-peer-check.mjs +363 -0
@@ -0,0 +1,20 @@
1
+ package tools.reekon.pdfcanvas;
2
+
3
+ import java.lang.annotation.ElementType;
4
+ import java.lang.annotation.Retention;
5
+ import java.lang.annotation.RetentionPolicy;
6
+ import java.lang.annotation.Target;
7
+
8
+ /**
9
+ * Marks a test that deliberately triggers the API 24 PDFium poisoning and can
10
+ * therefore only be the LAST thing its process does.
11
+ *
12
+ * `android/build.gradle` passes `notAnnotation` for this type, so
13
+ * `connectedDebugAndroidTest` never runs it by accident. It is run on its own —
14
+ * the command is in {@code PdfCanvasPoisonGuardTest}'s javadoc — which is the
15
+ * only way to observe the guard doing its job, because observing it requires the
16
+ * platform to actually be broken.
17
+ */
18
+ @Retention(RetentionPolicy.RUNTIME)
19
+ @Target({ElementType.METHOD, ElementType.TYPE})
20
+ public @interface PoisonsTheProcess {}
@@ -0,0 +1,558 @@
1
+ package tools.reekon.pdfcanvas;
2
+
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
+
9
+ import java.io.ByteArrayOutputStream;
10
+ import java.io.File;
11
+ import java.io.FileOutputStream;
12
+ import java.io.IOException;
13
+ import java.nio.charset.Charset;
14
+ import java.security.MessageDigest;
15
+ import java.util.ArrayList;
16
+ import java.util.List;
17
+
18
+ /**
19
+ * PDFs built ON DEVICE, so a test has no asset to ship and the content is known
20
+ * exactly rather than eyeballed.
21
+ *
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.
37
+ */
38
+ final class TestPdfs {
39
+
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
+ private static final Charset LATIN1 = Charset.forName("ISO-8859-1");
47
+
48
+ private TestPdfs() {}
49
+
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. */
202
+ private static final class Writer {
203
+ private final ByteArrayOutputStream out = new ByteArrayOutputStream();
204
+ private final List<Integer> offsets = new ArrayList<>();
205
+
206
+ Writer() {
207
+ write("%PDF-1.4\n");
208
+ // A binary comment line marks the file as binary, per the spec's advice.
209
+ out.write(0x25);
210
+ out.write(0xE2);
211
+ out.write(0xE3);
212
+ out.write(0xCF);
213
+ out.write(0xD3);
214
+ out.write('\n');
215
+ }
216
+
217
+ void write(String s) {
218
+ byte[] b = s.getBytes(LATIN1);
219
+ out.write(b, 0, b.length);
220
+ }
221
+
222
+ void write(byte[] b) {
223
+ out.write(b, 0, b.length);
224
+ }
225
+
226
+ /** @return the object number, 1-based. */
227
+ int add(String body) {
228
+ return add(body.getBytes(LATIN1));
229
+ }
230
+
231
+ int add(byte[] body) {
232
+ int number = offsets.size() + 1;
233
+ offsets.add(out.size());
234
+ write(number + " 0 obj\n");
235
+ write(body);
236
+ write("\nendobj\n");
237
+ return number;
238
+ }
239
+
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) {
260
+ int xref = out.size();
261
+ int size = offsets.size() + 1;
262
+ write("xref\n0 " + size + "\n");
263
+ write("0000000000 65535 f \n");
264
+ for (int offset : offsets) {
265
+ write(String.format("%010d 00000 n \n", offset));
266
+ }
267
+ write("trailer\n<< /Size " + size + " /Root " + rootObject + " 0 R "
268
+ + extraTrailerEntries + " >>\nstartxref\n" + xref + "\n%%EOF\n");
269
+ return out.toByteArray();
270
+ }
271
+ }
272
+
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, ""));
339
+ }
340
+
341
+ /**
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.
347
+ */
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
+
471
+ 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
+ */
532
+ static File notAPdf(Context ctx, String name) throws IOException {
533
+ StringBuilder sb = new StringBuilder();
534
+ for (int i = 0; i < 200; i++) {
535
+ sb.append("this is not a pdf, line ").append(i).append('\n');
536
+ }
537
+ return writeFile(ctx, name, sb.toString().getBytes(LATIN1));
538
+ }
539
+
540
+ static File empty(Context ctx, String name) throws IOException {
541
+ return writeFile(ctx, name, new byte[0]);
542
+ }
543
+
544
+ static File missing(Context ctx, String name) {
545
+ File f = new File(ctx.getCacheDir(), name);
546
+ //noinspection ResultOfMethodCallIgnored
547
+ f.delete();
548
+ return f;
549
+ }
550
+
551
+ private static File writeFile(Context ctx, String name, byte[] bytes) throws IOException {
552
+ File out = new File(ctx.getCacheDir(), name);
553
+ try (FileOutputStream fos = new FileOutputStream(out)) {
554
+ fos.write(bytes);
555
+ }
556
+ return out;
557
+ }
558
+ }
@@ -0,0 +1,33 @@
1
+ # The raster transport's native half. See pdfcanvas-jsi.cpp for what it does.
2
+ #
3
+ # PDFCANVAS_WITH_JSI is passed ON only by the autolinked build, where
4
+ # ReactAndroid's prefab publishes jsi/jsi.h and libjsi.so. The standalone build
5
+ # (`cd android && ./gradlew connectedDebugAndroidTest`) passes nothing, has no
6
+ # React Native anywhere on its classpath, and still compiles this library — the
7
+ # slot storage and its JNI surface are React-free, which is what lets the device
8
+ # suite prove the transport's byte fidelity with no app in the picture.
9
+
10
+ cmake_minimum_required(VERSION 3.13)
11
+ project(pdfcanvasjsi)
12
+
13
+ set(CMAKE_CXX_STANDARD 17)
14
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
15
+
16
+ add_library(pdfcanvasjsi SHARED pdfcanvas-jsi.cpp)
17
+
18
+ # jnigraphics is the NDK's Bitmap accessor (AndroidBitmap_getInfo /
19
+ # AndroidBitmap_lockPixels), which is how the one-copy path reads a raster
20
+ # straight out of the Bitmap's own pixel memory. Part of the platform since
21
+ # API 8 and present on every device this package supports, so it is linked in
22
+ # BOTH configurations — the standalone build needs it too, and that is what lets
23
+ # the instrumented suite prove the fill's byte fidelity with no app in the
24
+ # picture.
25
+ target_link_libraries(pdfcanvasjsi jnigraphics)
26
+
27
+ if(PDFCANVAS_WITH_JSI)
28
+ target_compile_definitions(pdfcanvasjsi PRIVATE PDFCANVAS_WITH_JSI=1)
29
+ # CONFIG mode against the react-android AAR's prefab package. `jsi` is the
30
+ # only target needed: nothing here touches TurboModules, fbjni or folly.
31
+ find_package(ReactAndroid REQUIRED CONFIG)
32
+ target_link_libraries(pdfcanvasjsi ReactAndroid::jsi)
33
+ endif()