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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/PdfCanvas.podspec +77 -40
  4. package/README.md +218 -1316
  5. package/android/build.gradle +67 -54
  6. package/android/consumer-rules.pro +7 -0
  7. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
  8. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
  9. package/android/src/main/cpp/CMakeLists.txt +54 -0
  10. package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
  11. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
  12. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
  13. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
  14. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
  15. package/android/tools/compile-gate.sh +81 -101
  16. package/dist/controller.d.ts +14 -0
  17. package/dist/controller.js +4 -3
  18. package/dist/rasterizer/index.native.d.ts +8 -12
  19. package/dist/rasterizer/index.native.js +15 -21
  20. package/dist/rasterizer/native-bridge.d.ts +51 -75
  21. package/dist/rasterizer/native-bridge.js +49 -33
  22. package/dist/rasterizer/native.d.ts +45 -0
  23. package/dist/rasterizer/native.js +123 -0
  24. package/dist/react/usePdfDocument.d.ts +5 -5
  25. package/dist/react/usePdfLayer.d.ts +10 -1
  26. package/dist/react/usePdfLayer.js +10 -2
  27. package/dist/testing/scenes.d.ts +3 -3
  28. package/dist/testing/scenes.js +4 -4
  29. package/dist/types.d.ts +36 -42
  30. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  31. package/native/.clangd +5 -0
  32. package/native/CMakeLists.txt +89 -0
  33. package/native/core/include/pdfcanvas/document.h +90 -0
  34. package/native/core/include/pdfcanvas/error.h +47 -0
  35. package/native/core/include/pdfcanvas/jsi.h +18 -0
  36. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  37. package/native/core/include/pdfcanvas/library.h +28 -0
  38. package/native/core/include/pdfcanvas/pixels.h +92 -0
  39. package/native/core/include/pdfcanvas/service.h +87 -0
  40. package/native/core/include/pdfcanvas/slots.h +113 -0
  41. package/native/core/include/pdfcanvas/types.h +110 -0
  42. package/native/core/pdfcanvas-core.cmake +22 -0
  43. package/native/core/src/document.cpp +516 -0
  44. package/native/core/src/error.cpp +29 -0
  45. package/native/core/src/jsi.cpp +66 -0
  46. package/native/core/src/library.cpp +52 -0
  47. package/native/core/src/pixels.cpp +45 -0
  48. package/native/core/src/service.cpp +156 -0
  49. package/native/core/src/slots.cpp +139 -0
  50. package/native/tests/fixtures.cpp +451 -0
  51. package/native/tests/fixtures.h +96 -0
  52. package/native/tests/harness.h +122 -0
  53. package/native/tests/main.cpp +64 -0
  54. package/native/tests/pixels_util.h +126 -0
  55. package/native/tests/test_document.cpp +485 -0
  56. package/native/tests/test_service.cpp +130 -0
  57. package/native/tests/test_slots.cpp +250 -0
  58. package/package.json +27 -7
  59. package/react-native.config.js +5 -2
  60. package/scripts/fetch-pdfium.mjs +559 -0
  61. package/scripts/pdfium-manifest.json +48 -0
  62. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  63. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  64. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  65. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  66. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  67. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  68. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  69. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  72. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  73. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  74. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  75. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  76. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  77. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  78. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  79. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  80. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  81. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  82. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  83. package/dist/rasterizer/android.d.ts +0 -121
  84. package/dist/rasterizer/android.js +0 -126
  85. package/dist/rasterizer/ios.d.ts +0 -84
  86. package/dist/rasterizer/ios.js +0 -155
  87. package/ios/Package.swift +0 -58
  88. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  89. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  90. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  91. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  92. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  93. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  94. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  95. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  98. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  99. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  100. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  101. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  102. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  103. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  104. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  105. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  109. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  110. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  111. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  112. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  113. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  114. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  115. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  116. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  117. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  118. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  119. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  120. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  121. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -1,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
- * `src/main` (the rasterizer core) and `src/androidTest`. There
8
- * is no React Native on the classpath and nothing here asks for
9
- * one. This is the build that PROVES the backend.
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 and the React dependency is added.
13
- *
14
- * The split is not cosmetic. `src/main` is the half that can be tested today on
15
- * a bare emulator; the moment a React import lands in it, that stops being true
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
- * `src/jsi` is a THIRD source set and it joins BOTH builds. It holds the raster
19
- * transport — a JNI slot store whose contents become a JS ArrayBuffer with no
20
- * base64 in between. It is React-free like `src/main`, but native, so it is
21
- * compiled twice with different capabilities: the autolinked build passes
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 the transport's .so is built for.
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
- * Standalone: the two the instrumented suite can actually run on. Building four
58
- * to run one is a minute of NDK time per test cycle for nothing.
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 transportAbis() {
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
- // 24 is the floor spike S1 measured against. PdfRenderer itself is API 21+,
92
- // but 24 is the package's declared minimum and S1 confirmed the off-page
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: jsi.h uses both, and a
107
- // host function reports failure by throwing jsi::JSError.
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(*transportAbis())
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/jsi/cpp/CMakeLists.txt'
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
- // Packaging a second copy out of this AAR is at best duplicate-file noise
147
- // and at worst two libjsi.so with different ABIs in one process.
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. `-DANDROID_STL=c++_shared` means
150
- // libpdfcanvasjsi.so has a hard dependency on libc++_shared.so, and the
151
- // standalone test APK has no React Native to supply it — excluding 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
+ }