@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +77 -40
- package/README.md +218 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +559 -0
- package/scripts/pdfium-manifest.json +48 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
package/android/build.gradle
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* The Android half of @reekon-tools/react-native-pdf-canvas.
|
|
3
3
|
*
|
|
4
|
+
* WHAT IS BUILT. One shared library, `libpdfcanvas.so`, from the C++ core in
|
|
5
|
+
* `native/core` plus the JNI surface in `src/main/cpp`, linked against the
|
|
6
|
+
* prebuilt PDFium this package pins (`scripts/pdfium-manifest.json`). Java is a
|
|
7
|
+
* thin `PdfCanvasNative` over it, and `PdfCanvasModule` is marshalling over
|
|
8
|
+
* that. There is no Java rasterizer any more: `android.graphics.pdf.PdfRenderer`
|
|
9
|
+
* was PDFium behind a wrapper that hid annotations, passwords and cancel, and
|
|
10
|
+
* the core now talks to PDFium directly — the same release iOS and the web
|
|
11
|
+
* backend use.
|
|
12
|
+
*
|
|
4
13
|
* TWO BUILDS, ONE FILE.
|
|
5
14
|
*
|
|
6
15
|
* standalone — `cd android && ./gradlew connectedDebugAndroidTest`. Compiles
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
16
|
+
* the core, the JNI and `src/androidTest` with no React Native
|
|
17
|
+
* on the classpath. What it proves is the JNI seam and the
|
|
18
|
+
* transport's byte fidelity on a device; rendering itself is
|
|
19
|
+
* proven on a laptop by `native/tests`.
|
|
10
20
|
*
|
|
11
21
|
* autolinked — a React Native app pulls this directory in. `src/reactnative`
|
|
12
|
-
* joins the source set
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* and the only way to run the rasterizer is to build a whole app around it.
|
|
22
|
+
* joins the source set, the React dependency is added, and the
|
|
23
|
+
* native build gets `-DPDFCANVAS_WITH_JSI=ON` so the JSI raster
|
|
24
|
+
* transport is compiled in and linked against ReactAndroid's
|
|
25
|
+
* prefab `jsi`.
|
|
17
26
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `-DPDFCANVAS_WITH_JSI=ON` and links ReactAndroid's prefab `jsi` target, while
|
|
23
|
-
* the standalone build compiles the same file with the JSI half `#ifdef`'d out.
|
|
24
|
-
* That is deliberate rather than a convenience: it is what lets the instrumented
|
|
25
|
-
* suite prove ON DEVICE that the bytes entering the transport are byte-identical
|
|
26
|
-
* to the ones the base64 path produced, with no app anywhere in the picture.
|
|
27
|
+
* PDFIUM IS FETCHED, NOT VENDORED. `fetchPdfium` runs `scripts/fetch-pdfium.mjs`
|
|
28
|
+
* before anything native is configured; it downloads exactly the ABIs the app
|
|
29
|
+
* asked for, verifies each against a pinned SHA-256, and is a stat when done.
|
|
30
|
+
* See the script for the offline/mirror knobs.
|
|
27
31
|
*/
|
|
28
32
|
|
|
29
33
|
buildscript {
|
|
@@ -48,16 +52,12 @@ apply plugin: 'com.android.library'
|
|
|
48
52
|
def standalone = gradle.ext.has('pdfCanvasStandalone')
|
|
49
53
|
|
|
50
54
|
/**
|
|
51
|
-
* Which ABIs
|
|
52
|
-
*
|
|
53
|
-
* Autolinked: whatever the app asked for, so the library never ships an ABI the
|
|
54
|
-
* app does not — a missing .so is a silent fall back to base64 on that device,
|
|
55
|
-
* which is the one failure this list can cause.
|
|
55
|
+
* Which ABIs are built — and therefore which PDFium binaries are fetched.
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* Autolinked: whatever the app asked for, so the AAR never carries an ABI the
|
|
58
|
+
* app does not. Standalone: the two the instrumented suite can actually run on.
|
|
59
59
|
*/
|
|
60
|
-
def
|
|
60
|
+
def targetAbis() {
|
|
61
61
|
if (!gradle.ext.has('pdfCanvasStandalone')) {
|
|
62
62
|
def requested = rootProject.properties.get('reactNativeArchitectures')
|
|
63
63
|
if (requested) {
|
|
@@ -68,6 +68,24 @@ def transportAbis() {
|
|
|
68
68
|
return ['arm64-v8a', 'x86_64']
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
def packageRoot = file("${projectDir}/..")
|
|
72
|
+
def pdfiumRoot = new File(packageRoot, 'native/.pdfium/android')
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Downloads + verifies the pinned PDFium build for `targetAbis()`.
|
|
76
|
+
*
|
|
77
|
+
* Runs every build, deliberately: the script checks a stamp and exits in
|
|
78
|
+
* milliseconds when the right release is already there, and a Gradle
|
|
79
|
+
* up-to-date check would have to re-derive exactly that logic to be correct.
|
|
80
|
+
*/
|
|
81
|
+
tasks.register('fetchPdfium', Exec) {
|
|
82
|
+
description = 'Fetches the pinned PDFium binaries for the ABIs being built (scripts/fetch-pdfium.mjs).'
|
|
83
|
+
workingDir packageRoot
|
|
84
|
+
commandLine(System.getenv('PDFCANVAS_NODE') ?: 'node',
|
|
85
|
+
'scripts/fetch-pdfium.mjs', 'android', '--abis', targetAbis().join(','))
|
|
86
|
+
outputs.upToDateWhen { false }
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
android {
|
|
72
90
|
namespace 'tools.reekon.pdfcanvas'
|
|
73
91
|
compileSdk 34
|
|
@@ -75,8 +93,6 @@ android {
|
|
|
75
93
|
if (!standalone && rootProject.hasProperty('ndkVersion')) {
|
|
76
94
|
// Mirrors what every other autolinked native module does: take the app's
|
|
77
95
|
// pinned NDK when it has one, and let AGP pick its default when it does not.
|
|
78
|
-
// Pinning a version here that the machine has not installed is a build
|
|
79
|
-
// failure in somebody else's project.
|
|
80
96
|
ndkVersion rootProject.ext.ndkVersion
|
|
81
97
|
}
|
|
82
98
|
|
|
@@ -88,28 +104,22 @@ android {
|
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
defaultConfig {
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// -origin transform is honoured identically there.
|
|
107
|
+
// PDFium's own floor is far lower; 24 is this package's declared minimum
|
|
108
|
+
// and what the retired core measured against.
|
|
94
109
|
minSdk 24
|
|
95
110
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
|
96
|
-
// One test deliberately triggers the API 24 PDFium poisoning (see
|
|
97
|
-
// PdfCanvasPoisonGuardTest). Once it has, every test scheduled after it in
|
|
98
|
-
// the same process dies — so it is excluded here and run on its own. The
|
|
99
|
-
// exclusion is by annotation rather than by class name so that moving the
|
|
100
|
-
// test cannot silently re-admit it.
|
|
101
|
-
testInstrumentationRunnerArguments notAnnotation: 'tools.reekon.pdfcanvas.PoisonsTheProcess'
|
|
102
111
|
consumerProguardFiles 'consumer-rules.pro'
|
|
103
112
|
|
|
104
113
|
externalNativeBuild {
|
|
105
114
|
cmake {
|
|
106
|
-
// -frtti and -fexceptions are not optional:
|
|
107
|
-
// host function reports failure by throwing
|
|
115
|
+
// -frtti and -fexceptions are not optional: the core throws PdfError,
|
|
116
|
+
// jsi.h uses both, and a host function reports failure by throwing.
|
|
108
117
|
cppFlags '-O2', '-frtti', '-fexceptions', '-Wall', '-std=c++17'
|
|
109
118
|
arguments "-DANDROID_STL=c++_shared",
|
|
110
119
|
"-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
|
|
120
|
+
"-DPDFCANVAS_PDFIUM_ROOT=${pdfiumRoot.absolutePath.replace('\\', '/')}".toString(),
|
|
111
121
|
"-DPDFCANVAS_WITH_JSI=${standalone ? 'OFF' : 'ON'}".toString()
|
|
112
|
-
abiFilters(*
|
|
122
|
+
abiFilters(*targetAbis())
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
125
|
}
|
|
@@ -121,19 +131,14 @@ android {
|
|
|
121
131
|
|
|
122
132
|
externalNativeBuild {
|
|
123
133
|
cmake {
|
|
124
|
-
path 'src/
|
|
134
|
+
path 'src/main/cpp/CMakeLists.txt'
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
sourceSets {
|
|
129
139
|
main {
|
|
130
|
-
// The transport is React-free and joins BOTH builds — the standalone one
|
|
131
|
-
// compiles it with the JSI half switched off and exercises the slot store
|
|
132
|
-
// directly from androidTest.
|
|
133
|
-
java.srcDirs += 'src/jsi/java'
|
|
134
140
|
// The React binding is additive. On the standalone path it is not
|
|
135
|
-
// compiled at all, which is what keeps `src/main` provably React-free
|
|
136
|
-
// if a core file ever imports com.facebook.*, this build stops compiling.
|
|
141
|
+
// compiled at all, which is what keeps `src/main` provably React-free.
|
|
137
142
|
if (!standalone) {
|
|
138
143
|
java.srcDirs += 'src/reactnative/java'
|
|
139
144
|
}
|
|
@@ -143,15 +148,13 @@ android {
|
|
|
143
148
|
if (!standalone) {
|
|
144
149
|
packagingOptions {
|
|
145
150
|
// Everything below is already in the app, shipped by React Native itself.
|
|
146
|
-
//
|
|
147
|
-
//
|
|
151
|
+
// libjsi.so arrives because AGP packages every IMPORTED shared library a
|
|
152
|
+
// target links — which is also how libpdfium.so gets in, and that one
|
|
153
|
+
// must NOT be excluded.
|
|
148
154
|
//
|
|
149
|
-
// ONLY on the autolinked path
|
|
150
|
-
//
|
|
151
|
-
// standalone test APK has no React Native to supply it
|
|
152
|
-
// there makes `System.loadLibrary` fail, `isLoaded()` return false, and
|
|
153
|
-
// the whole transport silently degrade. Which is precisely the failure
|
|
154
|
-
// `nativeTransportLibraryIsPresent` exists to catch, and did.
|
|
155
|
+
// ONLY on the autolinked path: `-DANDROID_STL=c++_shared` gives
|
|
156
|
+
// libpdfcanvas.so a hard dependency on libc++_shared.so, and the
|
|
157
|
+
// standalone test APK has no React Native to supply it.
|
|
155
158
|
excludes += ['**/libjsi.so', '**/libc++_shared.so', '**/libreactnative.so']
|
|
156
159
|
}
|
|
157
160
|
}
|
|
@@ -161,6 +164,16 @@ android {
|
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
afterEvaluate {
|
|
168
|
+
// PDFium must exist before CMake CONFIGURES (the IMPORTED_LOCATION is checked
|
|
169
|
+
// then), not merely before it builds. AGP's task names vary by version, so
|
|
170
|
+
// every native-build task is matched by prefix, and preBuild is wired too.
|
|
171
|
+
tasks.named('preBuild') { dependsOn(tasks.named('fetchPdfium')) }
|
|
172
|
+
tasks.matching { task ->
|
|
173
|
+
['configureCMake', 'generateJsonModel', 'buildCMake', 'externalNativeBuild'].any { task.name.startsWith(it) }
|
|
174
|
+
}.configureEach { dependsOn(tasks.named('fetchPdfium')) }
|
|
175
|
+
}
|
|
176
|
+
|
|
164
177
|
dependencies {
|
|
165
178
|
if (!standalone) {
|
|
166
179
|
// Supplied by the consuming app's React Native version. Never pinned here:
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
# The React binding is reached reflectively by React Native's module registry,
|
|
2
2
|
# so R8 cannot see the call site and would strip it from a release build.
|
|
3
3
|
-keep class tools.reekon.pdfcanvas.rn.** { *; }
|
|
4
|
+
|
|
5
|
+
# JNI resolves `native` methods by their mangled Java name, and the C++ side
|
|
6
|
+
# raises `PdfRasterException.fromWire` by signature. Neither call site is
|
|
7
|
+
# visible to R8, so both names have to survive minification.
|
|
8
|
+
-keepclasseswithmembers class tools.reekon.pdfcanvas.PdfCanvasNative { native <methods>; }
|
|
9
|
+
-keep class tools.reekon.pdfcanvas.PdfRasterException { *; }
|
|
10
|
+
-keep class tools.reekon.pdfcanvas.PdfErrorCode { *; }
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
package tools.reekon.pdfcanvas;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertArrayEquals;
|
|
4
|
+
import static org.junit.Assert.assertEquals;
|
|
5
|
+
import static org.junit.Assert.assertFalse;
|
|
6
|
+
import static org.junit.Assert.assertNotNull;
|
|
7
|
+
import static org.junit.Assert.assertNull;
|
|
8
|
+
import static org.junit.Assert.assertTrue;
|
|
9
|
+
import static org.junit.Assert.fail;
|
|
10
|
+
|
|
11
|
+
import android.content.Context;
|
|
12
|
+
|
|
13
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
14
|
+
import androidx.test.platform.app.InstrumentationRegistry;
|
|
15
|
+
|
|
16
|
+
import org.junit.After;
|
|
17
|
+
import org.junit.Before;
|
|
18
|
+
import org.junit.Test;
|
|
19
|
+
import org.junit.runner.RunWith;
|
|
20
|
+
|
|
21
|
+
import java.io.File;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The JNI seam, on device.
|
|
25
|
+
*
|
|
26
|
+
* <h3>What this proves, and what it leaves to the laptop</h3>
|
|
27
|
+
*
|
|
28
|
+
* Rendering is proven by {@code native/tests} — 50-odd cases against the same
|
|
29
|
+
* PDFium build, on a laptop, in under a second. This suite exists for the two
|
|
30
|
+
* things a laptop cannot show: that {@code libpdfcanvas.so} and the
|
|
31
|
+
* {@code libpdfium.so} it links load on a real Android runtime for this ABI,
|
|
32
|
+
* and that bytes cross JNI intact in both directions — a document path in, a
|
|
33
|
+
* raster out on both transports, an error code out with its type. Every
|
|
34
|
+
* failure below is either an {@code UnsatisfiedLinkError} or a plausible wrong
|
|
35
|
+
* image, and neither shows up anywhere but a device.
|
|
36
|
+
*
|
|
37
|
+
* <p>This is the STANDALONE build: no React Native, no Hermes, no libjsi.so, so
|
|
38
|
+
* {@link PdfCanvasNative#installJsi} honestly reports false and the slot store
|
|
39
|
+
* is exercised through {@link PdfCanvasNative#takeSlotForTest} — the identical
|
|
40
|
+
* storage the ArrayBuffer would have wrapped.
|
|
41
|
+
*/
|
|
42
|
+
@RunWith(AndroidJUnit4.class)
|
|
43
|
+
public class PdfCanvasNativeTest {
|
|
44
|
+
|
|
45
|
+
private long service;
|
|
46
|
+
|
|
47
|
+
private static Context ctx() {
|
|
48
|
+
return InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Before
|
|
52
|
+
public void setUp() {
|
|
53
|
+
assertTrue("libpdfcanvas.so (and the libpdfium.so it links) must load", PdfCanvasNative.isLoaded());
|
|
54
|
+
PdfCanvasNative.releaseAllSlots();
|
|
55
|
+
service = PdfCanvasNative.createService();
|
|
56
|
+
assertTrue(service != 0L);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@After
|
|
60
|
+
public void tearDown() {
|
|
61
|
+
PdfCanvasNative.invalidateService(service);
|
|
62
|
+
PdfCanvasNative.releaseAllSlots();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private static int channel(byte[] bytes, int rowBytes, int x, int y, int c) {
|
|
66
|
+
return bytes[y * rowBytes + x * 4 + c] & 0xff;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static void assertPixel(byte[] bytes, int rowBytes, int x, int y, int r, int g, int b) {
|
|
70
|
+
String actual = "[" + channel(bytes, rowBytes, x, y, 0) + "," + channel(bytes, rowBytes, x, y, 1)
|
|
71
|
+
+ "," + channel(bytes, rowBytes, x, y, 2) + "," + channel(bytes, rowBytes, x, y, 3) + "]";
|
|
72
|
+
assertTrue("(" + x + "," + y + ") is " + actual + ", expected [" + r + "," + g + "," + b + ",255]",
|
|
73
|
+
Math.abs(channel(bytes, rowBytes, x, y, 0) - r) <= 4
|
|
74
|
+
&& Math.abs(channel(bytes, rowBytes, x, y, 1) - g) <= 4
|
|
75
|
+
&& Math.abs(channel(bytes, rowBytes, x, y, 2) - b) <= 4
|
|
76
|
+
&& channel(bytes, rowBytes, x, y, 3) == 255);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private int open(File file) throws PdfRasterException {
|
|
80
|
+
double[] opened = PdfCanvasNative.open(service, file.getAbsolutePath(), null, null);
|
|
81
|
+
assertNotNull(opened);
|
|
82
|
+
return (int) opened[0];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ================================================================ *
|
|
86
|
+
* 1. Bytes in, bytes out
|
|
87
|
+
* ================================================================ */
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* CATCHES: a channel swap (BGRA arriving where RGBA is declared), a stride
|
|
91
|
+
* shear, a y-flip, or a page geometry that did not survive the double[] hop.
|
|
92
|
+
* The quadrants are saturated so each of those moves whole pixels.
|
|
93
|
+
*/
|
|
94
|
+
@Test
|
|
95
|
+
public void quadrantsCrossJniInRgbaOrder() throws Exception {
|
|
96
|
+
File pdf = TestPdfs.quadrants(ctx(), "jni-quadrants.pdf", 200, 100);
|
|
97
|
+
double[] opened = PdfCanvasNative.open(service, pdf.getAbsolutePath(), null, null);
|
|
98
|
+
assertEquals(1, (int) opened[1]);
|
|
99
|
+
assertEquals(200.0, opened[2], 0.01);
|
|
100
|
+
assertEquals(100.0, opened[3], 0.01);
|
|
101
|
+
int handle = (int) opened[0];
|
|
102
|
+
|
|
103
|
+
long[] shape = new long[4];
|
|
104
|
+
byte[] bytes = PdfCanvasNative.render(
|
|
105
|
+
service, handle, 0, 0, 0, 200, 100, 2, false, false, 1, false, shape);
|
|
106
|
+
assertNotNull("a heap render returns bytes", bytes);
|
|
107
|
+
assertEquals(0L, shape[0]);
|
|
108
|
+
assertEquals(400L, shape[1]);
|
|
109
|
+
assertEquals(200L, shape[2]);
|
|
110
|
+
assertEquals(1600L, shape[3]);
|
|
111
|
+
assertEquals(1600 * 200, bytes.length);
|
|
112
|
+
int rowBytes = (int) shape[3];
|
|
113
|
+
assertPixel(bytes, rowBytes, 20, 20, 0, 0, 255); // blue: doc top-left
|
|
114
|
+
assertPixel(bytes, rowBytes, 380, 20, 0, 0, 0); // black: doc top-right
|
|
115
|
+
assertPixel(bytes, rowBytes, 20, 180, 255, 0, 0); // red: doc bottom-left
|
|
116
|
+
assertPixel(bytes, rowBytes, 380, 180, 0, 255, 0); // green: doc bottom-right
|
|
117
|
+
PdfCanvasNative.close(service, handle);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** CATCHES: the annotations flag not surviving the JNI boolean hop. */
|
|
121
|
+
@Test
|
|
122
|
+
public void annotationsFlagReachesTheCore() throws Exception {
|
|
123
|
+
int handle = open(TestPdfs.annotated(ctx(), "jni-annotated.pdf"));
|
|
124
|
+
long[] shape = new long[4];
|
|
125
|
+
byte[] with = PdfCanvasNative.render(service, handle, 0, 0, 0, 200, 200, 1, true, false, 1, false, shape);
|
|
126
|
+
byte[] without = PdfCanvasNative.render(service, handle, 0, 0, 0, 200, 200, 1, false, false, 2, false, shape);
|
|
127
|
+
int rowBytes = (int) shape[3];
|
|
128
|
+
assertPixel(with, rowBytes, 100, 100, 0, 255, 0);
|
|
129
|
+
assertPixel(without, rowBytes, 100, 100, 255, 255, 255);
|
|
130
|
+
PdfCanvasNative.close(service, handle);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ================================================================ *
|
|
134
|
+
* 2. The transport
|
|
135
|
+
* ================================================================ */
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* THE HEADLINE: bytes taken out of a slot are byte-identical to the same
|
|
139
|
+
* raster rendered to the heap. PDFium wrote the tile straight into the slot
|
|
140
|
+
* buffer, so a wrong buffer, stride or publish would be a plausible-looking
|
|
141
|
+
* wrong image rather than an error.
|
|
142
|
+
*/
|
|
143
|
+
@Test
|
|
144
|
+
public void slotBytesEqualHeapBytesForARealRaster() throws Exception {
|
|
145
|
+
int handle = open(TestPdfs.quadrants(ctx(), "jni-slot.pdf", 300, 200));
|
|
146
|
+
long[] heapShape = new long[4];
|
|
147
|
+
byte[] heap = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, false, false, 1, false, heapShape);
|
|
148
|
+
assertNotNull(heap);
|
|
149
|
+
|
|
150
|
+
long[] slotShape = new long[4];
|
|
151
|
+
byte[] none = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, false, false, 2, true, slotShape);
|
|
152
|
+
assertNull("a slot render returns no bytes", none);
|
|
153
|
+
assertTrue("a slot render names a slot", slotShape[0] > 0);
|
|
154
|
+
assertEquals(heapShape[1], slotShape[1]);
|
|
155
|
+
assertEquals(heapShape[2], slotShape[2]);
|
|
156
|
+
assertEquals(heapShape[3], slotShape[3]);
|
|
157
|
+
assertEquals(1, PdfCanvasNative.slotCount());
|
|
158
|
+
|
|
159
|
+
byte[] taken = PdfCanvasNative.takeSlotForTest(slotShape[0]);
|
|
160
|
+
assertNotNull(taken);
|
|
161
|
+
assertArrayEquals(heap, taken);
|
|
162
|
+
assertEquals(0, PdfCanvasNative.slotCount());
|
|
163
|
+
assertNull("a slot is taken exactly once", PdfCanvasNative.takeSlotForTest(slotShape[0]));
|
|
164
|
+
PdfCanvasNative.close(service, handle);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The probe the JS side negotiates with round-trips through the real store. */
|
|
168
|
+
@Test
|
|
169
|
+
public void probeBytesRoundTrip() {
|
|
170
|
+
long slot = PdfCanvasNative.storeProbe();
|
|
171
|
+
assertTrue(slot > 0);
|
|
172
|
+
byte[] taken = PdfCanvasNative.takeSlotForTest(slot);
|
|
173
|
+
assertArrayEquals(PdfCanvasNative.PROBE_BYTES, taken);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
public void releaseDropsASlotAndReleaseAllDrainsEverything() {
|
|
178
|
+
long a = PdfCanvasNative.storeProbe();
|
|
179
|
+
long b = PdfCanvasNative.storeProbe();
|
|
180
|
+
assertEquals(2, PdfCanvasNative.slotCount());
|
|
181
|
+
PdfCanvasNative.releaseSlot(a);
|
|
182
|
+
PdfCanvasNative.releaseSlot(a);
|
|
183
|
+
assertEquals(1, PdfCanvasNative.slotCount());
|
|
184
|
+
assertNotNull(PdfCanvasNative.takeSlotForTest(b));
|
|
185
|
+
for (int i = 0; i < 8; i++) {
|
|
186
|
+
PdfCanvasNative.storeProbe();
|
|
187
|
+
}
|
|
188
|
+
PdfCanvasNative.releaseAllSlots();
|
|
189
|
+
assertEquals(0, PdfCanvasNative.slotCount());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** In the standalone build there is no JSI, and the install must say so rather than dereference. */
|
|
193
|
+
@Test
|
|
194
|
+
public void installJsiReportsFalseWithoutAJsiRuntime() {
|
|
195
|
+
assertFalse(PdfCanvasNative.installJsi(0L));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ================================================================ *
|
|
199
|
+
* 3. Errors and cancellation cross with their codes
|
|
200
|
+
* ================================================================ */
|
|
201
|
+
|
|
202
|
+
@Test
|
|
203
|
+
public void errorsArriveAsTypedExceptions() throws Exception {
|
|
204
|
+
try {
|
|
205
|
+
PdfCanvasNative.open(service, TestPdfs.missing(ctx(), "jni-missing.pdf").getAbsolutePath(), null, null);
|
|
206
|
+
fail("a missing file must throw");
|
|
207
|
+
} catch (PdfRasterException e) {
|
|
208
|
+
assertEquals(PdfErrorCode.NOT_FOUND, e.code());
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
PdfCanvasNative.open(service, TestPdfs.notAPdf(ctx(), "jni-not-a-pdf.pdf").getAbsolutePath(), null, null);
|
|
212
|
+
fail("a non-PDF must throw");
|
|
213
|
+
} catch (PdfRasterException e) {
|
|
214
|
+
assertEquals(PdfErrorCode.CORRUPT, e.code());
|
|
215
|
+
}
|
|
216
|
+
int handle = open(TestPdfs.quadrants(ctx(), "jni-range.pdf", 40, 40));
|
|
217
|
+
try {
|
|
218
|
+
PdfCanvasNative.render(service, handle, 7, 0, 0, 10, 10, 1, false, false, 1, false, new long[4]);
|
|
219
|
+
fail("an out-of-range page must throw");
|
|
220
|
+
} catch (PdfRasterException e) {
|
|
221
|
+
assertEquals(PdfErrorCode.NOT_FOUND, e.code());
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@Test
|
|
226
|
+
public void aRegisteredCancellationIsHonoured() throws Exception {
|
|
227
|
+
int handle = open(TestPdfs.quadrants(ctx(), "jni-cancel.pdf", 40, 40));
|
|
228
|
+
PdfCanvasNative.registerCancellation(service, handle, 5);
|
|
229
|
+
PdfCanvasNative.cancel(service, handle, 5);
|
|
230
|
+
try {
|
|
231
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 5, false, new long[4]);
|
|
232
|
+
fail("a cancelled render must throw");
|
|
233
|
+
} catch (PdfRasterException e) {
|
|
234
|
+
assertEquals(PdfErrorCode.CANCELLED, e.code());
|
|
235
|
+
}
|
|
236
|
+
// The signal was consumed: the same token renders fine afterwards.
|
|
237
|
+
assertNotNull(PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 5, false, new long[4]));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@Test
|
|
241
|
+
public void invalidateClosesEverythingAndRefusesAfterwards() throws Exception {
|
|
242
|
+
int handle = open(TestPdfs.quadrants(ctx(), "jni-invalidate.pdf", 40, 40));
|
|
243
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 1, true, new long[4]);
|
|
244
|
+
assertEquals(1, PdfCanvasNative.slotCount());
|
|
245
|
+
PdfCanvasNative.invalidateService(service);
|
|
246
|
+
assertEquals("invalidate drains parked slots", 0, PdfCanvasNative.slotCount());
|
|
247
|
+
try {
|
|
248
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 2, false, new long[4]);
|
|
249
|
+
fail("a render after invalidate must throw");
|
|
250
|
+
} catch (PdfRasterException e) {
|
|
251
|
+
assertEquals(PdfErrorCode.BACKEND_FAILURE, e.code());
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|