@reekon-tools/react-native-pdf-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/PdfCanvas.podspec +80 -0
  2. package/README.md +1391 -0
  3. package/android/build.gradle +175 -0
  4. package/android/consumer-rules.pro +3 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +2 -0
  8. package/android/gradlew +248 -0
  9. package/android/gradlew.bat +98 -0
  10. package/android/settings.gradle +34 -0
  11. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
  12. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
  13. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
  14. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
  15. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
  16. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
  17. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
  18. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
  19. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
  20. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
  21. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
  22. package/android/src/jsi/cpp/CMakeLists.txt +33 -0
  23. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
  24. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
  25. package/android/src/main/AndroidManifest.xml +7 -0
  26. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
  27. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
  28. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
  29. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
  30. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
  31. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
  32. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
  33. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
  34. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
  35. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
  36. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
  37. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
  38. package/android/tools/compile-gate.sh +289 -0
  39. package/dist/cache.d.ts +161 -0
  40. package/dist/cache.js +389 -0
  41. package/dist/clock.d.ts +21 -0
  42. package/dist/clock.js +31 -0
  43. package/dist/controller.d.ts +131 -0
  44. package/dist/controller.js +1002 -0
  45. package/dist/geometry.d.ts +96 -0
  46. package/dist/geometry.js +248 -0
  47. package/dist/index.d.ts +52 -0
  48. package/dist/index.js +50 -0
  49. package/dist/index.native.d.ts +21 -0
  50. package/dist/index.native.js +23 -0
  51. package/dist/index.web.d.ts +24 -0
  52. package/dist/index.web.js +26 -0
  53. package/dist/layout.d.ts +78 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/peer-check.d.ts +83 -0
  56. package/dist/peer-check.js +183 -0
  57. package/dist/planner.d.ts +35 -0
  58. package/dist/planner.js +530 -0
  59. package/dist/policy.d.ts +14 -0
  60. package/dist/policy.js +168 -0
  61. package/dist/rasterizer/android.d.ts +121 -0
  62. package/dist/rasterizer/android.js +126 -0
  63. package/dist/rasterizer/buffer.d.ts +18 -0
  64. package/dist/rasterizer/buffer.js +18 -0
  65. package/dist/rasterizer/fake.d.ts +92 -0
  66. package/dist/rasterizer/fake.js +433 -0
  67. package/dist/rasterizer/index.d.ts +38 -0
  68. package/dist/rasterizer/index.js +69 -0
  69. package/dist/rasterizer/index.native.d.ts +19 -0
  70. package/dist/rasterizer/index.native.js +80 -0
  71. package/dist/rasterizer/index.web.d.ts +63 -0
  72. package/dist/rasterizer/index.web.js +75 -0
  73. package/dist/rasterizer/ingest.d.ts +180 -0
  74. package/dist/rasterizer/ingest.js +307 -0
  75. package/dist/rasterizer/ios.d.ts +84 -0
  76. package/dist/rasterizer/ios.js +155 -0
  77. package/dist/rasterizer/native-bridge.d.ts +261 -0
  78. package/dist/rasterizer/native-bridge.js +413 -0
  79. package/dist/rasterizer/web/client.d.ts +84 -0
  80. package/dist/rasterizer/web/client.js +359 -0
  81. package/dist/rasterizer/web/engine.d.ts +69 -0
  82. package/dist/rasterizer/web/engine.js +308 -0
  83. package/dist/rasterizer/web/index.d.ts +26 -0
  84. package/dist/rasterizer/web/index.js +21 -0
  85. package/dist/rasterizer/web/pdfium.d.ts +213 -0
  86. package/dist/rasterizer/web/pdfium.js +170 -0
  87. package/dist/rasterizer/web/protocol.d.ts +138 -0
  88. package/dist/rasterizer/web/protocol.js +37 -0
  89. package/dist/rasterizer/web/session.d.ts +106 -0
  90. package/dist/rasterizer/web/session.js +413 -0
  91. package/dist/rasterizer/web/worker.d.ts +44 -0
  92. package/dist/rasterizer/web/worker.js +76 -0
  93. package/dist/react/PdfContentView.d.ts +98 -0
  94. package/dist/react/PdfContentView.js +189 -0
  95. package/dist/react/index.d.ts +6 -0
  96. package/dist/react/index.js +3 -0
  97. package/dist/react/usePdfDocument.d.ts +134 -0
  98. package/dist/react/usePdfDocument.js +275 -0
  99. package/dist/react/usePdfLayer.d.ts +94 -0
  100. package/dist/react/usePdfLayer.js +388 -0
  101. package/dist/skia/index.d.ts +26 -0
  102. package/dist/skia/index.js +25 -0
  103. package/dist/skia/index.native.d.ts +14 -0
  104. package/dist/skia/index.native.js +16 -0
  105. package/dist/skia/index.web.d.ts +12 -0
  106. package/dist/skia/index.web.js +14 -0
  107. package/dist/skia/resolve.native.d.ts +18 -0
  108. package/dist/skia/resolve.native.js +22 -0
  109. package/dist/skia/resolve.web.d.ts +41 -0
  110. package/dist/skia/resolve.web.js +62 -0
  111. package/dist/skia/skia-api.d.ts +94 -0
  112. package/dist/skia/skia-api.js +113 -0
  113. package/dist/testing/index.d.ts +27 -0
  114. package/dist/testing/index.js +28 -0
  115. package/dist/testing/scenes.d.ts +100 -0
  116. package/dist/testing/scenes.js +267 -0
  117. package/dist/types.d.ts +611 -0
  118. package/dist/types.js +21 -0
  119. package/dist/web-init.d.ts +68 -0
  120. package/dist/web-init.js +64 -0
  121. package/ios/Package.swift +58 -0
  122. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
  123. package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
  124. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
  125. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
  126. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
  127. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
  128. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
  129. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
  130. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
  131. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
  132. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
  133. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
  134. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
  135. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
  136. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
  137. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
  138. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
  139. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
  140. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
  141. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
  142. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
  143. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
  144. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
  145. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
  146. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
  147. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
  148. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
  149. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
  150. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
  151. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
  152. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
  153. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
  154. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
  155. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
  156. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
  157. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
  158. package/package.json +144 -0
  159. package/react-native.config.js +40 -0
  160. package/scripts/check-skia-peer.mjs +86 -0
  161. package/scripts/skia-peer-check.mjs +363 -0
@@ -0,0 +1,289 @@
1
+ #!/bin/bash
2
+ #
3
+ # The Android compile gate: type- and syntax-check every Android source in this
4
+ # package WITHOUT gradle, an emulator, or a device. Runs in seconds.
5
+ #
6
+ # WHY THIS EXISTS. A cold gradle build of a consuming app is ~45 minutes here and
7
+ # does not reliably produce a runnable app, so the practical alternative to this
8
+ # script is "change JNI and find out on an EAS build". Two of the four things it
9
+ # checks cannot be caught any other way short of a device:
10
+ #
11
+ # * src/reactnative/PdfCanvasModule.java is not compiled by the standalone
12
+ # build at all. Its own header used to say "nothing in this file has ever
13
+ # been compiled or run"; this is what changed that.
14
+ # * A JNI signature that drifts from its Java `native` declaration compiles
15
+ # clean on BOTH sides and fails only as an UnsatisfiedLinkError at runtime.
16
+ # Section 5 makes javac derive the expected prototypes and has clang compare
17
+ # them against the definitions, in both directions.
18
+ #
19
+ # HOW FAR THE C++ HALF GOES. Not a syntax check — it compiles and LINKS the real
20
+ # shared library, in both configurations, with `-Wl,--no-undefined`, against the
21
+ # NDK's own libjnigraphics and the react-android AAR's own libjsi.so. So a missing
22
+ # `target_link_libraries` entry, a platform function that does not exist at the
23
+ # minSdk, or a jsi symbol the app's React Native version does not export all fail
24
+ # HERE. It then checks that the built .so exports every JNI symbol Java expects.
25
+ # What is skipped versus a real build is gradle and cmake orchestration only.
26
+ #
27
+ # WHAT IT IS NOT. It runs no Java, no C++ and no test. Correctness of the
28
+ # rasterizer and of the raster transport is proven by
29
+ # `cd android && ./gradlew connectedDebugAndroidTest` on a device, and nothing
30
+ # here substitutes for that.
31
+ #
32
+ # Usage: bash android/tools/compile-gate.sh
33
+ #
34
+ # Paths are discovered, with env overrides for a machine that keeps them
35
+ # elsewhere: ANDROID_SDK_ROOT, ANDROID_NDK_HOME, JAVA_HOME, RN_ROOT.
36
+
37
+ set -u
38
+ here=$(cd "$(dirname "$0")/.." && pwd) # <repo>/android
39
+ PKG=$here
40
+ fail=0
41
+ skipped=0
42
+
43
+ say_skip() { echo "SKIP $1"; echo " $2"; skipped=1; }
44
+
45
+ run() {
46
+ local name="$1"; shift
47
+ local out; out=$("$@" 2>&1); local rc=$?
48
+ out=$(echo "$out" | grep -v "^Note: ")
49
+ if [ $rc -eq 0 ]; then echo "PASS $name"
50
+ else echo "FAIL $name (exit $rc)"; echo "$out" | head -30; fail=1; fi
51
+ }
52
+
53
+ # ---------------------------------------------------------------- discovery
54
+ SDK=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/AppData/Local/Android/Sdk}}
55
+ [ -d "$SDK" ] || SDK=$HOME/Library/Android/sdk
56
+ [ -d "$SDK" ] || SDK=$HOME/Android/Sdk
57
+
58
+ # Newest installed NDK, unless one is pinned.
59
+ NDK=${ANDROID_NDK_HOME:-}
60
+ if [ -z "$NDK" ]; then
61
+ NDK=$(ls -d "$SDK"/ndk/* 2>/dev/null | sort -V | tail -1)
62
+ fi
63
+ CLANG=""
64
+ for host in windows-x86_64 darwin-x86_64 linux-x86_64; do
65
+ for ext in ".exe" ""; do
66
+ c="$NDK/toolchains/llvm/prebuilt/$host/bin/clang++$ext"
67
+ [ -x "$c" ] && CLANG="$c" && break 2
68
+ done
69
+ done
70
+
71
+ # Newest platform android.jar. -bootclasspath needs a Windows-style path on
72
+ # Windows because javac is a native binary; `cygpath -m` is a no-op elsewhere.
73
+ winpath() { if command -v cygpath >/dev/null 2>&1; then cygpath -m "$1"; else echo "$1"; fi; }
74
+ AJAR=$(ls -d "$SDK"/platforms/android-*/android.jar 2>/dev/null | sort -V | tail -1)
75
+ [ -n "$AJAR" ] && AJAR=$(winpath "$AJAR")
76
+
77
+ # javac 17: the module targets VERSION_17. A JDK 8 javac cannot compile it.
78
+ JAVAC=""
79
+ for cand in "${JAVA_HOME:-}/bin/javac" \
80
+ "/c/Program Files/Android/Android Studio/jbr/bin/javac.exe" \
81
+ "$(command -v javac 2>/dev/null)"; do
82
+ [ -n "$cand" ] && [ -x "$cand" ] || continue
83
+ v=$("$cand" -version 2>&1 | sed 's/javac //;s/\..*//')
84
+ [ "${v:-0}" -ge 11 ] 2>/dev/null && JAVAC="$cand" && break
85
+ done
86
+
87
+ # jsi/jsi.h and the react-android classes.jar come from a consuming app's
88
+ # node_modules. RN_ROOT can point straight at one.
89
+ RN=${RN_ROOT:-}
90
+ if [ -z "$RN" ]; then
91
+ for cand in "$here/../node_modules/react-native" \
92
+ "$here/../../BOLDR/node_modules/react-native" \
93
+ "$here/../example/node_modules/react-native"; do
94
+ [ -f "$cand/ReactCommon/jsi/jsi/jsi.h" ] && RN=$(cd "$cand" && pwd) && break
95
+ done
96
+ fi
97
+ JSI="$RN/ReactCommon/jsi"
98
+
99
+ OUT=${TMPDIR:-/tmp}/pdfcanvas-gate
100
+ rm -rf "$OUT"; mkdir -p "$OUT"/{standalone,autolinked,androidtest,h,classes,rn}
101
+
102
+ # react-android's classes.jar is inside an AAR in the gradle cache. Needed only
103
+ # for section 3.
104
+ # Extracted WHOLE rather than with a `jni/*` member filter: MSYS unzip extracts
105
+ # nothing for that pattern, which silently loses libjsi.so and turns the
106
+ # WITH_JSI link into a skip. The AAR is a couple of megabytes; just unpack it.
107
+ RNJAR=""
108
+ aar=$(ls -t "$HOME"/.gradle/caches/modules-2/files-2.1/com.facebook.react/react-android/*/*/*-release.aar 2>/dev/null | head -1)
109
+ [ -z "$aar" ] && aar=$(ls -t "$HOME"/.gradle/caches/modules-2/files-2.1/com.facebook.react/react-android/*/*/*-debug.aar 2>/dev/null | head -1)
110
+ if [ -n "$aar" ] && command -v unzip >/dev/null 2>&1; then
111
+ (cd "$OUT/rn" && unzip -o -q "$aar")
112
+ [ -f "$OUT/rn/classes.jar" ] && RNJAR=$(winpath "$OUT/rn/classes.jar")
113
+ fi
114
+
115
+ NM=""
116
+ for host in windows-x86_64 darwin-x86_64 linux-x86_64; do
117
+ for ext in ".exe" ""; do
118
+ c="$NDK/toolchains/llvm/prebuilt/$host/bin/llvm-nm$ext"
119
+ [ -x "$c" ] && NM="$c" && break 2
120
+ done
121
+ done
122
+
123
+ # libjsi.so ships inside the same AAR, one copy per ABI, already unpacked above.
124
+ JSILIB=""
125
+ [ -f "$OUT/rn/jni/arm64-v8a/libjsi.so" ] && JSILIB="$OUT/rn/jni"
126
+
127
+ echo "SDK $SDK"
128
+ echo "NDK ${NDK:-<none>}"
129
+ echo "javac ${JAVAC:-<none>}"
130
+ echo "RN ${RN:-<none>}"
131
+ echo
132
+
133
+ # ---------------------------------------------------------------- 1. C++
134
+ # Both #if configurations, both pointer widths. 32-bit is not redundant: it is
135
+ # what catches a size_t/jsize/uint64 width slip that arm64 hides.
136
+ abi_dir() { case "$1" in aarch64*) echo arm64-v8a;; armv7a*) echo armeabi-v7a;; esac; }
137
+
138
+ if [ -n "$CLANG" ]; then
139
+ for abi in aarch64-linux-android24 armv7a-linux-androideabi24; do
140
+ # --no-undefined is the point of linking rather than syntax-checking: it
141
+ # fails on any symbol the platform will not supply at runtime, which is
142
+ # exactly the class of mistake a -fsyntax-only pass waves through and a
143
+ # device then reports as a dlopen failure and a silent base64 fallback.
144
+ run "cpp+link standalone $abi" "$CLANG" --target=$abi -std=c++17 -O2 \
145
+ -Wall -Wextra -Werror -shared -fPIC -o "$OUT/lib-$abi.so" \
146
+ "$PKG/src/jsi/cpp/pdfcanvas-jsi.cpp" -ljnigraphics -Wl,--no-undefined
147
+
148
+ if [ -f "$JSI/jsi/jsi.h" ] && [ -n "$JSILIB" ] && [ -d "$JSILIB/$(abi_dir "$abi")" ]; then
149
+ run "cpp+link WITH_JSI $abi" "$CLANG" --target=$abi -std=c++17 -O2 \
150
+ -Wall -Wextra -Werror -frtti -fexceptions -shared -fPIC \
151
+ -DPDFCANVAS_WITH_JSI=1 -I"$JSI" -o "$OUT/lib-jsi-$abi.so" \
152
+ "$PKG/src/jsi/cpp/pdfcanvas-jsi.cpp" -ljnigraphics \
153
+ -L"$JSILIB/$(abi_dir "$abi")" -ljsi -Wl,--no-undefined
154
+ elif [ ! -f "$JSI/jsi/jsi.h" ]; then
155
+ say_skip "cpp+link WITH_JSI $abi" \
156
+ "no jsi/jsi.h; set RN_ROOT to a react-native install"
157
+ else
158
+ say_skip "cpp+link WITH_JSI $abi" \
159
+ "no libjsi.so for this ABI in the react-android AAR"
160
+ fi
161
+ done
162
+
163
+ # The gate links with an explicit -l list, so it would keep passing if the
164
+ # equivalent line were dropped from CMakeLists.txt — the real build would then
165
+ # fail, loudly, but only on a machine running gradle. Assert the two agree.
166
+ cml="$PKG/src/jsi/cpp/CMakeLists.txt"
167
+ missing_link=""
168
+ for lib in jnigraphics; do
169
+ grep -q "target_link_libraries.*[ (]$lib[ )]" "$cml" || missing_link="$missing_link $lib"
170
+ done
171
+ if [ -z "$missing_link" ]; then
172
+ echo "PASS CMakeLists links what this gate links"
173
+ else
174
+ echo "FAIL this gate links$missing_link but CMakeLists.txt does not"
175
+ fail=1
176
+ fi
177
+
178
+ # The built library must actually EXPORT what Java will look up. A visibility
179
+ # flag or a namespace slip hides a symbol without failing any compile.
180
+ if [ -n "$NM" ] && [ -f "$OUT/lib-aarch64-linux-android24.so" ]; then
181
+ exported=$("$NM" -D --defined-only "$OUT/lib-aarch64-linux-android24.so" 2>/dev/null \
182
+ | grep -c 'Java_tools_reekon_pdfcanvas')
183
+ if [ "${exported:-0}" -ge 7 ]; then
184
+ echo "PASS the linked .so exports $exported Java_tools_reekon_* symbols"
185
+ else
186
+ echo "FAIL the linked .so exports only ${exported:-0} JNI symbols (expected >= 7)"
187
+ fail=1
188
+ fi
189
+ else
190
+ say_skip "exported JNI symbols" "no llvm-nm, or nothing linked"
191
+ fi
192
+ else
193
+ say_skip "cpp (all)" "no NDK clang++ found; set ANDROID_NDK_HOME"
194
+ fi
195
+
196
+ # ---------------------------------------------------------------- 2/3/4. Java
197
+ if [ -n "$JAVAC" ] && [ -n "$AJAR" ]; then
198
+ # Standalone build: main + jsi, no React anywhere, against the strict Android
199
+ # API surface. -Werror here because this half is meant to stay clean.
200
+ run "javac standalone (main+jsi)" \
201
+ "$JAVAC" -proc:none -nowarn -Werror -d "$OUT/standalone" -bootclasspath "$AJAR" \
202
+ -source 8 -target 8 \
203
+ "$PKG"/src/main/java/tools/reekon/pdfcanvas/*.java \
204
+ "$PKG"/src/jsi/java/tools/reekon/pdfcanvas/jsi/*.java
205
+
206
+ # Autolinked build: adds src/reactnative. Its lambdas need a real java.base,
207
+ # so android.jar moves from -bootclasspath to -cp.
208
+ if [ -n "$RNJAR" ]; then
209
+ run "javac autolinked (+reactnative)" \
210
+ "$JAVAC" -proc:none -nowarn -d "$OUT/autolinked" -cp "$AJAR;$RNJAR" \
211
+ "$PKG"/src/main/java/tools/reekon/pdfcanvas/*.java \
212
+ "$PKG"/src/jsi/java/tools/reekon/pdfcanvas/jsi/*.java \
213
+ "$PKG"/src/reactnative/java/tools/reekon/pdfcanvas/rn/*.java
214
+ else
215
+ say_skip "javac autolinked (+reactnative)" \
216
+ "no react-android AAR in the gradle cache; build a consuming app once to populate it"
217
+ fi
218
+
219
+ # The instrumented suite, against the standalone classes it tests. Compiling
220
+ # it is how a change to the rasterizer's API is caught before a device run.
221
+ TESTCP=""
222
+ for a in androidx.test/core androidx.test/monitor androidx.test/runner androidx.test.ext/junit; do
223
+ aa=$(ls -t "$HOME"/.gradle/caches/modules-2/files-2.1/$a/*/*/*.aar 2>/dev/null | head -1)
224
+ [ -z "$aa" ] && continue
225
+ d="$OUT/$(basename "$a")"; mkdir -p "$d"
226
+ (cd "$d" && unzip -o -q "$aa" classes.jar) && TESTCP="$TESTCP;$(winpath "$d/classes.jar")"
227
+ done
228
+ for j in junit/junit org.hamcrest/hamcrest-core; do
229
+ jj=$(ls -t "$HOME"/.gradle/caches/modules-2/files-2.1/$j/*/*/*.jar 2>/dev/null | grep -v sources | head -1)
230
+ [ -n "$jj" ] && TESTCP="$TESTCP;$(winpath "$jj")"
231
+ done
232
+ if [ -n "$TESTCP" ]; then
233
+ run "javac androidTest suite" \
234
+ "$JAVAC" -proc:none -nowarn -d "$OUT/androidtest" \
235
+ -cp "$AJAR$TESTCP;$(winpath "$OUT/standalone")" \
236
+ "$PKG"/src/androidTest/java/tools/reekon/pdfcanvas/*.java
237
+ else
238
+ say_skip "javac androidTest suite" "no androidx.test jars in the gradle cache"
239
+ fi
240
+ else
241
+ say_skip "javac (all)" "need a JDK 11+ javac and an SDK platform android.jar"
242
+ fi
243
+
244
+ # ---------------------------------------------------------------- 5. JNI
245
+ # THE SECTION THAT EARNS THIS SCRIPT. javac -h derives the expected C prototypes
246
+ # from the Java `native` declarations; force-including that header makes clang
247
+ # compare every definition against its declaration, so a drifted parameter list
248
+ # or return type is a compile error instead of an UnsatisfiedLinkError on a
249
+ # device. The loop then checks the reverse: a method declared `native` in Java
250
+ # with no C++ definition compiles clean everywhere and fails only at runtime.
251
+ if [ -n "$JAVAC" ] && [ -n "$AJAR" ] && [ -n "$CLANG" ] && [ -f "$JSI/jsi/jsi.h" ]; then
252
+ if "$JAVAC" -proc:none -nowarn -d "$OUT/classes" -h "$OUT/h" -bootclasspath "$AJAR" \
253
+ -source 8 -target 8 \
254
+ "$PKG"/src/main/java/tools/reekon/pdfcanvas/*.java \
255
+ "$PKG"/src/jsi/java/tools/reekon/pdfcanvas/jsi/*.java >/dev/null 2>&1; then
256
+ for abi in aarch64-linux-android24 armv7a-linux-androideabi24; do
257
+ # -fsyntax-only here on purpose: this pass exists to compare declarations,
258
+ # and the link is already covered by section 1.
259
+ run "jni prototypes match $abi" "$CLANG" --target=$abi -std=c++17 -fsyntax-only \
260
+ -Wall -Wextra -Werror -frtti -fexceptions -DPDFCANVAS_WITH_JSI=1 -I"$JSI" \
261
+ -I"$OUT/h" -include tools_reekon_pdfcanvas_jsi_PdfCanvasPixelBridge.h \
262
+ "$PKG/src/jsi/cpp/pdfcanvas-jsi.cpp"
263
+ done
264
+ declared=$(grep -o 'Java_[A-Za-z0-9_]*' "$OUT"/h/*.h | sed 's/.*://' | sort -u)
265
+ n=0; missing=""
266
+ for m in $declared; do
267
+ n=$((n + 1))
268
+ grep -q "^${m}($" "$PKG/src/jsi/cpp/pdfcanvas-jsi.cpp" || missing="$missing $m"
269
+ done
270
+ # A floor, because an extraction that silently finds nothing would make this
271
+ # check pass forever while testing nothing.
272
+ if [ "$n" -lt 7 ]; then
273
+ echo "FAIL the JNI gate found only $n declared native methods (expected >= 7)"; fail=1
274
+ elif [ -n "$missing" ]; then
275
+ echo "FAIL declared native in Java, undefined in C++:$missing"; fail=1
276
+ else
277
+ echo "PASS all $n declared native methods are defined"
278
+ fi
279
+ else
280
+ echo "FAIL could not generate JNI headers"; fail=1
281
+ fi
282
+ else
283
+ say_skip "jni signature gate" "needs javac, android.jar, NDK clang++ and jsi/jsi.h together"
284
+ fi
285
+
286
+ echo "---"
287
+ if [ $fail -ne 0 ]; then echo "GATE: FAILURES"; exit 1; fi
288
+ if [ $skipped -ne 0 ]; then echo "GATE: PASS (with skips — see SKIP lines)"; exit 0; fi
289
+ echo "GATE: ALL PASS"
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Refcounted, byte-budgeted raster cache.
3
+ *
4
+ * Two things make this different from a plain LRU:
5
+ *
6
+ * 1. REFCOUNTS, NOT LIVENESS GUESSES. A raster is drawable exactly while some
7
+ * PdfContent slot references it. `acquire`/`release` count those slots, so a
8
+ * raster held by both the live detail set and the retiring set has refs 2 and
9
+ * survives until both are gone. Nothing referenced is ever evicted.
10
+ *
11
+ * 2. THE RETIRE QUEUE. A refcount reaching zero does NOT dispose. The entry goes
12
+ * to a queue that the controller flushes at a point that already causes a
13
+ * React commit, because freeing an SkImage that is still in a mounted Skia
14
+ * node draws a corpse. Retirement must never own a timer or a commit of its
15
+ * own — see `flushRetired`.
16
+ *
17
+ * 3. THE DISPOSAL FENCE, which is what makes point 2 actually true.
18
+ *
19
+ * MEASURED: releasing and flushing in the SAME turn — which is what the
20
+ * controller's commit did — is a delay of zero. The entry left `content` and
21
+ * was freed before React had even been notified, let alone re-rendered, so
22
+ * every Skia node still holding it was holding a deleted object. In a browser
23
+ * that is `BindingError: Cannot pass deleted object as a pointer of type
24
+ * Image const*`, thrown out of RN Skia's own `drawImage -> Image.width` while
25
+ * it RE-RECORDS the picture from the mounted tree: 16 occurrences over six
26
+ * high-zoom pans, 3-4 ms after an epoch commit, every time.
27
+ *
28
+ * So the queue is fenced, and the fence is a CLOCK the cache is told about
29
+ * rather than one it assumes:
30
+ *
31
+ * release(key, contentEpoch) tags the entry with the epoch of the content
32
+ * object that stopped referencing it.
33
+ * flushRetired(contentEpoch) frees only entries tagged STRICTLY OLDER than
34
+ * `contentEpoch` — i.e. one whole epoch of
35
+ * grace, which is the commit clock and is a
36
+ * heuristic: it assumes the renderer mounted
37
+ * epoch N before the controller published N+1.
38
+ * notePresented(epoch) the SOUND clock, IF a caller can honestly
39
+ * supply one: everything tagged `epoch` or older
40
+ * is treated as provably unreferenced and freed
41
+ * at once — sooner than the commit clock would.
42
+ *
43
+ * "PRESENTED" MEANS DRAWN, NOT MOUNTED, AND THE DIFFERENCE IS MEASURED. Wiring
44
+ * this to React — a `PdfContentView` prop reporting `content.epoch` from a
45
+ * PASSIVE effect, which is the latest signal a component can produce —
46
+ * REINTRODUCES the corpse it exists to prevent. Controlled A/B in rock-desktop's
47
+ * probe, 14 high-zoom pans per arm, twice each:
48
+ *
49
+ * onPresented wired 2, 2 `BindingError: ... Image const*`
50
+ * commit clock only 0, 0
51
+ *
52
+ * The reason is that the images are held by the RECORDED PICTURE, not by the
53
+ * React tree. RN Skia re-records on its own rAF, so after React has committed
54
+ * epoch N there is still a picture in hand that references epoch N-1's images,
55
+ * and `play()` -> `drawImage` -> `Image.width` reads one that has just been
56
+ * freed. Every stack in that A/B was exactly that. A commit is therefore NOT a
57
+ * presentation, and no React-reachable signal on web is late enough to be one.
58
+ *
59
+ * So this is NOT wired, deliberately, and must not be wired from an effect. It
60
+ * stays for a caller driving `createPdfController` against a renderer that has
61
+ * a genuine post-paint callback. Absent that, the commit clock is the correct
62
+ * answer and its only cost is holding one epoch's dropped rasters while the
63
+ * viewport is at rest.
64
+ *
65
+ * An UNTAGGED release (no epoch) means "nothing of mine is mounted any more",
66
+ * and so does a `flushRetired()` with no epoch: both are the teardown path,
67
+ * where immediate disposal is the correct answer and the original behaviour.
68
+ *
69
+ * The budget is a SOFT cap. Running out never throws and never blanks a page:
70
+ * the last resort is to refuse the newcomer and keep drawing the base we already
71
+ * have. Degrade, never crash.
72
+ */
73
+ import type { PdfRaster } from './types.js';
74
+ /** Mirrors the `reason` field of the `evict` diagnostic in types.ts. */
75
+ export type EvictionReason = 'budget' | 'page-window' | 'epoch';
76
+ export interface RasterCacheOptions {
77
+ /** Soft cap on cached bytes. A single raster larger than this is refused outright. */
78
+ budgetBytes: number;
79
+ /**
80
+ * Frees the backing SkImage. Called at most once per raster: on web,
81
+ * double-dispose THROWS where native tolerates it.
82
+ */
83
+ dispose: (raster: PdfRaster) => void;
84
+ /**
85
+ * How many pages either side of the live window keep their whole-page base.
86
+ * Bases inside this radius are the anti-blank mechanism and are evicted last;
87
+ * a base for page 300 while you read page 4 is just garbage.
88
+ */
89
+ baseRetentionRadius?: number;
90
+ }
91
+ export interface RasterCacheStats {
92
+ bytes: number;
93
+ entries: number;
94
+ /** Entries sitting in the retire queue, awaiting the next flush. */
95
+ retired: number;
96
+ /** Entries with at least one live reference. */
97
+ referenced: number;
98
+ }
99
+ export interface RasterCache {
100
+ /** Returns the cached raster and marks it recently used. */
101
+ get(key: string): PdfRaster | undefined;
102
+ has(key: string): boolean;
103
+ /**
104
+ * Inserts a raster, evicting as needed to stay under budget.
105
+ *
106
+ * TAKES OWNERSHIP. On `false` the raster has ALREADY been disposed and must
107
+ * not be drawn — the caller keeps drawing whatever it had.
108
+ *
109
+ * On `true` the caller must read the cache's copy back with `acquire`/`get`
110
+ * rather than reusing its own reference: an entry with this key may already
111
+ * have existed, in which case the incumbent stands and the newcomer is freed.
112
+ */
113
+ put(raster: PdfRaster): boolean;
114
+ /** Takes a reference. Un-retires the entry if it was queued for disposal. */
115
+ acquire(key: string): PdfRaster | undefined;
116
+ /**
117
+ * Drops a reference. At zero the entry joins the retire queue, NOT dispose.
118
+ *
119
+ * `contentEpoch` is the epoch of the content object that has just STOPPED
120
+ * referencing this raster — i.e. the epoch being published by the commit doing
121
+ * the releasing. It is what the disposal fence is measured against, so passing
122
+ * it is how a caller says "a renderer may still be holding this". Omit it only
123
+ * when nothing of yours is mounted at all (teardown), which is what the
124
+ * original unfenced behaviour was.
125
+ */
126
+ release(key: string, contentEpoch?: number): void;
127
+ refCount(key: string): number;
128
+ /**
129
+ * Disposes retired entries the renderer can no longer be holding.
130
+ *
131
+ * Pass the epoch of the content object you have just published: entries tagged
132
+ * with an OLDER epoch go, entries tagged with this one wait for the next commit
133
+ * (or for `notePresented`, which frees them as soon as it is provable). Call it
134
+ * only from a path that is already producing a React commit — retirement owns
135
+ * no timer and no commit of its own.
136
+ *
137
+ * With NO epoch it frees every retired entry outright. That is the teardown
138
+ * call: nothing of this cache's is mounted, so there is nothing to fence
139
+ * against.
140
+ */
141
+ flushRetired(contentEpoch?: number): void;
142
+ /**
143
+ * The renderer has mounted content epoch `epoch` — so every raster the content
144
+ * objects up to and including it stopped referencing is provably unreferenced,
145
+ * whatever the commit clock would have guessed, and is freed now.
146
+ *
147
+ * OPTIONAL, and the first call ARMS it: until a renderer reports, the fence
148
+ * falls back to the commit clock (see `flushRetired`). A host that reports once
149
+ * and then stops holds retired bytes until the budget evicts them or the cache
150
+ * is cleared, which is the correct failure — late is safe, early draws a corpse.
151
+ */
152
+ notePresented?(epoch: number): void;
153
+ /** The pages currently on screen. Drives both eviction categories below. */
154
+ setLiveWindow(pages: readonly number[]): void;
155
+ /** Disposes every entry, referenced or not. Teardown only. */
156
+ clear(): void;
157
+ stats(): RasterCacheStats;
158
+ /** Optional so a hand-written fake cache need not implement it. */
159
+ subscribeEvictions?(listener: (raster: PdfRaster, reason: EvictionReason) => void): () => void;
160
+ }
161
+ export declare function createRasterCache(options: RasterCacheOptions): RasterCache;