@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.
- package/PdfCanvas.podspec +80 -0
- package/README.md +1391 -0
- package/android/build.gradle +175 -0
- package/android/consumer-rules.pro +3 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +2 -0
- package/android/gradlew +248 -0
- package/android/gradlew.bat +98 -0
- package/android/settings.gradle +34 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
- package/android/src/jsi/cpp/CMakeLists.txt +33 -0
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
- package/android/tools/compile-gate.sh +289 -0
- package/dist/cache.d.ts +161 -0
- package/dist/cache.js +389 -0
- package/dist/clock.d.ts +21 -0
- package/dist/clock.js +31 -0
- package/dist/controller.d.ts +131 -0
- package/dist/controller.js +1002 -0
- package/dist/geometry.d.ts +96 -0
- package/dist/geometry.js +248 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +50 -0
- package/dist/index.native.d.ts +21 -0
- package/dist/index.native.js +23 -0
- package/dist/index.web.d.ts +24 -0
- package/dist/index.web.js +26 -0
- package/dist/layout.d.ts +78 -0
- package/dist/layout.js +152 -0
- package/dist/peer-check.d.ts +83 -0
- package/dist/peer-check.js +183 -0
- package/dist/planner.d.ts +35 -0
- package/dist/planner.js +530 -0
- package/dist/policy.d.ts +14 -0
- package/dist/policy.js +168 -0
- package/dist/rasterizer/android.d.ts +121 -0
- package/dist/rasterizer/android.js +126 -0
- package/dist/rasterizer/buffer.d.ts +18 -0
- package/dist/rasterizer/buffer.js +18 -0
- package/dist/rasterizer/fake.d.ts +92 -0
- package/dist/rasterizer/fake.js +433 -0
- package/dist/rasterizer/index.d.ts +38 -0
- package/dist/rasterizer/index.js +69 -0
- package/dist/rasterizer/index.native.d.ts +19 -0
- package/dist/rasterizer/index.native.js +80 -0
- package/dist/rasterizer/index.web.d.ts +63 -0
- package/dist/rasterizer/index.web.js +75 -0
- package/dist/rasterizer/ingest.d.ts +180 -0
- package/dist/rasterizer/ingest.js +307 -0
- package/dist/rasterizer/ios.d.ts +84 -0
- package/dist/rasterizer/ios.js +155 -0
- package/dist/rasterizer/native-bridge.d.ts +261 -0
- package/dist/rasterizer/native-bridge.js +413 -0
- package/dist/rasterizer/web/client.d.ts +84 -0
- package/dist/rasterizer/web/client.js +359 -0
- package/dist/rasterizer/web/engine.d.ts +69 -0
- package/dist/rasterizer/web/engine.js +308 -0
- package/dist/rasterizer/web/index.d.ts +26 -0
- package/dist/rasterizer/web/index.js +21 -0
- package/dist/rasterizer/web/pdfium.d.ts +213 -0
- package/dist/rasterizer/web/pdfium.js +170 -0
- package/dist/rasterizer/web/protocol.d.ts +138 -0
- package/dist/rasterizer/web/protocol.js +37 -0
- package/dist/rasterizer/web/session.d.ts +106 -0
- package/dist/rasterizer/web/session.js +413 -0
- package/dist/rasterizer/web/worker.d.ts +44 -0
- package/dist/rasterizer/web/worker.js +76 -0
- package/dist/react/PdfContentView.d.ts +98 -0
- package/dist/react/PdfContentView.js +189 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +3 -0
- package/dist/react/usePdfDocument.d.ts +134 -0
- package/dist/react/usePdfDocument.js +275 -0
- package/dist/react/usePdfLayer.d.ts +94 -0
- package/dist/react/usePdfLayer.js +388 -0
- package/dist/skia/index.d.ts +26 -0
- package/dist/skia/index.js +25 -0
- package/dist/skia/index.native.d.ts +14 -0
- package/dist/skia/index.native.js +16 -0
- package/dist/skia/index.web.d.ts +12 -0
- package/dist/skia/index.web.js +14 -0
- package/dist/skia/resolve.native.d.ts +18 -0
- package/dist/skia/resolve.native.js +22 -0
- package/dist/skia/resolve.web.d.ts +41 -0
- package/dist/skia/resolve.web.js +62 -0
- package/dist/skia/skia-api.d.ts +94 -0
- package/dist/skia/skia-api.js +113 -0
- package/dist/testing/index.d.ts +27 -0
- package/dist/testing/index.js +28 -0
- package/dist/testing/scenes.d.ts +100 -0
- package/dist/testing/scenes.js +267 -0
- package/dist/types.d.ts +611 -0
- package/dist/types.js +21 -0
- package/dist/web-init.d.ts +68 -0
- package/dist/web-init.js +64 -0
- package/ios/Package.swift +58 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
- package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
- package/package.json +144 -0
- package/react-native.config.js +40 -0
- package/scripts/check-skia-peer.mjs +86 -0
- 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()
|