@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
package/native/.clangd ADDED
@@ -0,0 +1,5 @@
1
+ # Points clangd at the host build's compile_commands.json so the editor sees the
2
+ # same include paths and -std the real build uses. Generated by
3
+ # `cmake -S native -B native/build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON`.
4
+ CompileFlags:
5
+ CompilationDatabase: build
@@ -0,0 +1,89 @@
1
+ # THE LAPTOP PROOF OF THE C++ CORE.
2
+ #
3
+ # Builds `native/core` against the pinned PDFium release for whatever machine
4
+ # this is — Windows, macOS or Linux — and runs `native/tests` against it. No
5
+ # device, no emulator, no simulator, no React Native, no Xcode, no Gradle: the
6
+ # same sources the Android library and the CocoaPods build compile, proven here
7
+ # with real PDFium in a few seconds. It replaces the Swift suite (`swift test`)
8
+ # and the instrumented Android suite as the place rendering is proven, because
9
+ # both platforms now share this one engine.
10
+ #
11
+ # yarn pdfium:fetch host # once; downloads + verifies PDFium
12
+ # cmake -S native -B native/build -G Ninja
13
+ # cmake --build native/build
14
+ # ctest --test-dir native/build --output-on-failure
15
+ #
16
+ # (`yarn native:test` does all four.) On Windows use the MinGW g++ or an LLVM
17
+ # clang that targets the GNU ABI — PDFium's DLL exports a plain C surface, so
18
+ # any toolchain that can link a DLL works.
19
+
20
+ cmake_minimum_required(VERSION 3.20)
21
+ project(pdfcanvas_native CXX)
22
+
23
+ set(CMAKE_CXX_STANDARD 17)
24
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
25
+ set(CMAKE_CXX_EXTENSIONS OFF)
26
+ if(NOT CMAKE_BUILD_TYPE)
27
+ set(CMAKE_BUILD_TYPE Release)
28
+ endif()
29
+
30
+ # Where `scripts/fetch-pdfium.mjs host` put the release. Overridable so a
31
+ # vendored copy can be pointed at.
32
+ set(PDFCANVAS_PDFIUM_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.pdfium/host"
33
+ CACHE PATH "Extracted pdfium-<host>.tgz (include/, lib/, bin/)")
34
+ if(NOT EXISTS "${PDFCANVAS_PDFIUM_DIR}/include/fpdfview.h")
35
+ message(FATAL_ERROR
36
+ "No PDFium at ${PDFCANVAS_PDFIUM_DIR}. Run `node scripts/fetch-pdfium.mjs host` "
37
+ "from the package root first (or set PDFCANVAS_PDFIUM_DIR).")
38
+ endif()
39
+
40
+ # ---------------------------------------------------------------- PDFium
41
+ add_library(pdfium SHARED IMPORTED)
42
+ if(WIN32)
43
+ set_target_properties(pdfium PROPERTIES
44
+ IMPORTED_LOCATION "${PDFCANVAS_PDFIUM_DIR}/bin/pdfium.dll"
45
+ IMPORTED_IMPLIB "${PDFCANVAS_PDFIUM_DIR}/lib/pdfium.dll.lib")
46
+ elseif(APPLE)
47
+ set_target_properties(pdfium PROPERTIES
48
+ IMPORTED_LOCATION "${PDFCANVAS_PDFIUM_DIR}/lib/libpdfium.dylib")
49
+ else()
50
+ set_target_properties(pdfium PROPERTIES
51
+ IMPORTED_LOCATION "${PDFCANVAS_PDFIUM_DIR}/lib/libpdfium.so")
52
+ endif()
53
+ target_include_directories(pdfium INTERFACE "${PDFCANVAS_PDFIUM_DIR}/include")
54
+
55
+ # ---------------------------------------------------------------- core
56
+ include(core/pdfcanvas-core.cmake)
57
+ add_library(pdfcanvas_core STATIC ${PDFCANVAS_CORE_SOURCES})
58
+ target_include_directories(pdfcanvas_core PUBLIC "${PDFCANVAS_CORE_INCLUDE_DIR}")
59
+ target_link_libraries(pdfcanvas_core PUBLIC pdfium)
60
+ target_compile_options(pdfcanvas_core PRIVATE
61
+ $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall -Wextra -Werror>)
62
+
63
+ # ---------------------------------------------------------------- tests
64
+ file(GLOB PDFCANVAS_TEST_SOURCES CONFIGURE_DEPENDS "tests/*.cpp")
65
+ add_executable(pdfcanvas_tests ${PDFCANVAS_TEST_SOURCES})
66
+ target_link_libraries(pdfcanvas_tests PRIVATE pdfcanvas_core)
67
+ target_compile_options(pdfcanvas_tests PRIVATE
68
+ $<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wall -Wextra -Werror>)
69
+ if(WIN32)
70
+ # The DLL has to sit next to the executable; there is no rpath on Windows.
71
+ add_custom_command(TARGET pdfcanvas_tests POST_BUILD
72
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
73
+ "${PDFCANVAS_PDFIUM_DIR}/bin/pdfium.dll" "$<TARGET_FILE_DIR:pdfcanvas_tests>")
74
+ if(MINGW)
75
+ # Static C++ runtime, so the test binary runs from ctest without the MinGW
76
+ # bin directory on PATH (a missing libstdc++-6.dll exits silently with
77
+ # 0xC0000135 and no output — which reads as "the suite printed nothing").
78
+ target_link_options(pdfcanvas_tests PRIVATE -static-libstdc++ -static-libgcc -static)
79
+ endif()
80
+ elseif(APPLE)
81
+ set_target_properties(pdfcanvas_tests PROPERTIES
82
+ BUILD_RPATH "${PDFCANVAS_PDFIUM_DIR}/lib")
83
+ else()
84
+ set_target_properties(pdfcanvas_tests PROPERTIES
85
+ BUILD_RPATH "${PDFCANVAS_PDFIUM_DIR}/lib")
86
+ endif()
87
+
88
+ enable_testing()
89
+ add_test(NAME pdfcanvas_tests COMMAND pdfcanvas_tests)
@@ -0,0 +1,90 @@
1
+ // One open PDF, rendered by PDFium.
2
+ //
3
+ // THE SAME ENGINE ON EVERY PLATFORM. Android's `PdfRenderer` was PDFium behind
4
+ // a Java wrapper that exposed no annotation switch, no cancel, no password and
5
+ // a process-wide lock that convoyed; iOS was CoreGraphics, which antialiases a
6
+ // 0.25pt CAD line at fit zoom into 10% grey where PDFium clamps every stroke to
7
+ // a device pixel. This class replaces both with the engine the web backend
8
+ // already used, so a tile is byte-identical across all three platforms rather
9
+ // than merely similar, and `annotations` means the same thing everywhere.
10
+ //
11
+ // THREADING. PDFium is not thread-safe — not across documents, not across
12
+ // threads, not at all — so every entry point below takes the process-wide
13
+ // library lock. That is not a regression on either platform: Android declared
14
+ // one render lane because AOSP's lock convoyed, and iOS declared one because a
15
+ // second was never measured to help. Cancellation is what makes one lane
16
+ // cheap: a superseded tile stops MID-RENDER (see `Cancellation`), so a stale
17
+ // render never holds the lock for longer than one progressive pause.
18
+ #pragma once
19
+
20
+ #include <memory>
21
+ #include <vector>
22
+
23
+ #include "pdfcanvas/error.h"
24
+ #include "pdfcanvas/pixels.h"
25
+ #include "pdfcanvas/types.h"
26
+
27
+ namespace pdfcanvas {
28
+
29
+ class Document {
30
+ public:
31
+ /// Refuse absurd rasters instead of dying on an allocation failure. 64 Mpx
32
+ /// is 256 MB at 4 bytes per pixel — past what a device hands out in one
33
+ /// allocation — so anything above it is a planner bug, and a named error
34
+ /// against a raster key beats a jetsam kill with no key attached. The TS
35
+ /// policy has its own, much lower, ceilings (`maxRasterPixels`).
36
+ static constexpr long long kMaxRasterPixels = 64'000'000;
37
+
38
+ /// How many parsed pages one document keeps. An epoch is several tiles of
39
+ /// the same page, and a continuous layout keeps two or three pages live, so
40
+ /// alternating between them must not re-parse a dense drawing every tile.
41
+ static constexpr int kPageCacheCapacity = 3;
42
+
43
+ /// Opens a document. Throws `PdfError` with `not-found` (no such file, or
44
+ /// unreadable), `corrupt` (empty, not a PDF, unparseable page tree),
45
+ /// `password-required` / `password-incorrect` (decided by whether the caller
46
+ /// supplied one — PDFium reports the same code for both), `unsupported` (an
47
+ /// encryption scheme this build cannot handle) or `backend-failure`.
48
+ static std::unique_ptr<Document> open(const Source& source);
49
+
50
+ ~Document();
51
+ Document(const Document&) = delete;
52
+ Document& operator=(const Document&) = delete;
53
+
54
+ int pageCount() const noexcept { return static_cast<int>(pages_.size()); }
55
+
56
+ /// Every page's geometry, measured once at open from the page dictionary —
57
+ /// no content stream is parsed, so this is proportional to the page tree and
58
+ /// not to what is drawn on it, which is what keeps `pageGeometry` cheap.
59
+ const std::vector<PageGeometry>& pages() const noexcept { return pages_; }
60
+
61
+ /// Throws `not-found` for an index the document does not have.
62
+ const PageGeometry& geometry(int index) const;
63
+
64
+ /// Renders one raster.
65
+ ///
66
+ /// `signal` may be null. It is observed before the lock, after it, before
67
+ /// the draw, DURING the draw (PDFium's progressive API pauses and asks), and
68
+ /// after it — the last one only stops a superseded raster being handed back.
69
+ ///
70
+ /// `sink` may be null, which means the heap. A non-null sink may REFUSE its
71
+ /// reservation, in which case the render falls through to the heap and the
72
+ /// result is heap-backed — check `RasterPixels::inSlot()` rather than
73
+ /// assuming. A cancellation observed after the draw abandons the sink itself.
74
+ RasterPixels render(const RasterRequest& request, Cancellation* signal,
75
+ PixelSink* sink);
76
+
77
+ /// Releases every PDFium object. Blocks until an in-flight render on this
78
+ /// document finishes (it holds the library lock), and is therefore free of
79
+ /// the use-after-free window the Android pool's close had. Idempotent.
80
+ void close();
81
+ bool isClosed() const noexcept;
82
+
83
+ private:
84
+ struct Impl;
85
+ explicit Document(std::unique_ptr<Impl> impl);
86
+ std::unique_ptr<Impl> impl_;
87
+ std::vector<PageGeometry> pages_;
88
+ };
89
+
90
+ } // namespace pdfcanvas
@@ -0,0 +1,47 @@
1
+ // The eight failure kinds, mirroring `PdfErrorCode` in `src/types.ts`.
2
+ //
3
+ // THE WIRE STRINGS ARE THE TYPESCRIPT UNION MEMBERS VERBATIM. Both bindings
4
+ // reject a promise with `wireName(code)` and `native-bridge.ts` reconstructs a
5
+ // `PdfError` from it with no translation table in between. There used to be
6
+ // three copies of this list (Java, Objective-C, TypeScript); there are now two,
7
+ // and this is the native one.
8
+ #pragma once
9
+
10
+ #include <exception>
11
+ #include <string>
12
+
13
+ namespace pdfcanvas {
14
+
15
+ enum class ErrorCode {
16
+ NotFound,
17
+ PasswordRequired,
18
+ PasswordIncorrect,
19
+ Corrupt,
20
+ Unsupported,
21
+ Cancelled,
22
+ OutOfMemory,
23
+ BackendFailure,
24
+ };
25
+
26
+ /// The TypeScript union member for a code. Never null.
27
+ const char* wireName(ErrorCode code);
28
+
29
+ /// The one exception type the core throws. Bindings catch it at the JNI /
30
+ /// Objective-C boundary and turn it into a promise rejection; nothing else may
31
+ /// escape the core, because a C++ exception unwinding into ART or ARC is a
32
+ /// process abort rather than an error anyone can read.
33
+ class PdfError : public std::exception {
34
+ public:
35
+ PdfError(ErrorCode code, std::string message)
36
+ : code_(code), message_(std::move(message)) {}
37
+
38
+ ErrorCode code() const noexcept { return code_; }
39
+ const char* what() const noexcept override { return message_.c_str(); }
40
+ const std::string& message() const noexcept { return message_; }
41
+
42
+ private:
43
+ ErrorCode code_;
44
+ std::string message_;
45
+ };
46
+
47
+ } // namespace pdfcanvas
@@ -0,0 +1,18 @@
1
+ // The one JSI touch point: installing `globalThis.__pdfCanvasTakePixels`.
2
+ #pragma once
3
+
4
+ namespace pdfcanvas {
5
+
6
+ /// Installs the host function into a `facebook::jsi::Runtime`.
7
+ ///
8
+ /// MUST be called on the JS thread with a live runtime pointer. Returns false —
9
+ /// never throws — when the pointer is null, when this build has no JSI
10
+ /// (`PDFCANVAS_HAS_JSI == 0`), or when the runtime refuses the host function.
11
+ /// The JS side then falls back to base64: rendering correctly is worth more
12
+ /// than rendering fast.
13
+ ///
14
+ /// Takes `void*` so the declaration is includable from plain C++, Objective-C++
15
+ /// and JNI code alike without dragging `<jsi/jsi.h>` into every consumer.
16
+ bool installTakePixels(void* runtimePointer);
17
+
18
+ } // namespace pdfcanvas
@@ -0,0 +1,30 @@
1
+ // Whether this translation unit is being built where `<jsi/jsi.h>` exists.
2
+ //
3
+ // TWO BUILDS, ONE CORE, decided here and nowhere else:
4
+ //
5
+ // PDFCANVAS_HAS_JSI == 1 the autolinked Android library (ReactAndroid's
6
+ // prefab publishes jsi/jsi.h and libjsi.so) and the
7
+ // CocoaPods build (React-jsi is a dependency). The
8
+ // slot buffer IS a `jsi::MutableBuffer` and
9
+ // `installTakePixels` puts the host function on the
10
+ // JS global.
11
+ // PDFCANVAS_HAS_JSI == 0 the host test suite and the standalone Android
12
+ // library. No React Native anywhere; the storage
13
+ // compiles against a restated interface and
14
+ // `installTakePixels` honestly reports false.
15
+ //
16
+ // Every TU in one target must agree, which they do: the include path is a
17
+ // property of the target, and the explicit define is passed target-wide.
18
+ #pragma once
19
+
20
+ #if defined(PDFCANVAS_WITH_JSI)
21
+ #define PDFCANVAS_HAS_JSI 1
22
+ #elif defined(__has_include)
23
+ #if __has_include(<jsi/jsi.h>)
24
+ #define PDFCANVAS_HAS_JSI 1
25
+ #else
26
+ #define PDFCANVAS_HAS_JSI 0
27
+ #endif
28
+ #else
29
+ #define PDFCANVAS_HAS_JSI 0
30
+ #endif
@@ -0,0 +1,28 @@
1
+ // PDFium's process-wide state: one initialisation, one lock.
2
+ #pragma once
3
+
4
+ #include <mutex>
5
+
6
+ namespace pdfcanvas {
7
+
8
+ class Library {
9
+ public:
10
+ /// The lock every PDFium call is made under. PDFium keeps global state
11
+ /// (`CPDF_ModuleMgr`, the font cache, the AGG rasterizer's scratch buffers)
12
+ /// and is documented not thread-safe across documents or threads, so the
13
+ /// only correct shape is one mutex around the whole library. Held for the
14
+ /// duration of a render — which is why the render polls for cancellation
15
+ /// while it holds it.
16
+ static std::mutex& mutex();
17
+
18
+ /// Initialises PDFium exactly once. Callers hold `mutex()` when they call
19
+ /// this; it is cheap after the first time.
20
+ static void ensureInitialized();
21
+
22
+ /// Font directories handed to `FPDF_InitLibraryWithConfig`. Platform-specific
23
+ /// and null-terminated; may be ignored by platforms whose font mapper does
24
+ /// its own scanning (Android's does — `/system/fonts` is built in).
25
+ static const char** userFontPaths();
26
+ };
27
+
28
+ } // namespace pdfcanvas
@@ -0,0 +1,92 @@
1
+ // Where a raster's bytes go, and what comes back.
2
+ //
3
+ // THE SINK IS WHY THE TRANSPORT COSTS ZERO COPIES ON BOTH PLATFORMS NOW.
4
+ // `FPDFBitmap_CreateEx` takes the destination pointer as an argument, so PDFium
5
+ // writes the tile straight into the buffer that becomes the JS `ArrayBuffer`.
6
+ // The iOS CoreGraphics core had this property; the Android `PdfRenderer` core
7
+ // could not (a `Bitmap` owns its own pixels and had to be copied out), and that
8
+ // asymmetry is gone.
9
+ //
10
+ // RESERVE, FILL, PUBLISH — in that order, and the order is the safety property.
11
+ // A buffer reachable from the process-wide slot map BEFORE it is filled can be
12
+ // freed by `slots::releaseAll()` — which runs on another thread on every Fast
13
+ // Refresh — while PDFium is still writing into it. So a reserved buffer is
14
+ // owned by the sink object alone, on the rendering thread's stack, and becomes
15
+ // reachable only at `publish()`.
16
+ #pragma once
17
+
18
+ #include <cstddef>
19
+ #include <cstdint>
20
+ #include <vector>
21
+
22
+ namespace pdfcanvas {
23
+
24
+ class PixelSink {
25
+ public:
26
+ virtual ~PixelSink() = default;
27
+
28
+ /// Reserve `length` bytes, ZEROED, and return them — or nullptr to refuse.
29
+ /// Called at most once per sink, before anything is drawn. Zeroed is part of
30
+ /// the contract: it is what makes `Background::Transparent` a fill of zeros
31
+ /// over zeros, and it is the invariant that stops a published buffer ever
32
+ /// handing out memory nobody wrote.
33
+ virtual uint8_t* reserve(size_t length) = 0;
34
+
35
+ /// Publish the reserved buffer and return the id a caller redeems it with,
36
+ /// or 0 for a sink with no id (the heap). Must not fail: every reason a
37
+ /// publish could be refused is decided at `reserve()`, before the raster
38
+ /// exists.
39
+ virtual int64_t publish() = 0;
40
+
41
+ /// Drop the buffer, published or not. Idempotent. Called on every path that
42
+ /// abandons a raster after reserving for it.
43
+ virtual void abandon() = 0;
44
+ };
45
+
46
+ /// The default destination: an owned, zero-initialised vector. What every
47
+ /// pixel-reading caller — the host test suite, the base64 fallback — uses.
48
+ class HeapSink final : public PixelSink {
49
+ public:
50
+ uint8_t* reserve(size_t length) override;
51
+ int64_t publish() override { return 0; }
52
+ void abandon() override { bytes_.clear(); bytes_.shrink_to_fit(); }
53
+
54
+ /// Moves the bytes out. Valid once per sink, after `publish()`.
55
+ std::vector<uint8_t> takeBytes() { return std::move(bytes_); }
56
+
57
+ private:
58
+ std::vector<uint8_t> bytes_;
59
+ bool reserved_ = false;
60
+ };
61
+
62
+ /// A finished raster, mirroring `RasterPixels` in `src/types.ts`.
63
+ ///
64
+ /// The pixels are in ONE OF TWO PLACES, and `slot` tells you which:
65
+ /// - `slot == 0`: on the heap, in `bytes`.
66
+ /// - `slot != 0`: in the process-wide slot store, waiting to become a JS
67
+ /// `ArrayBuffer` through `__pdfCanvasTakePixels`; `bytes` is empty.
68
+ ///
69
+ /// Format is always RGBA8888 with STRAIGHT (un-premultiplied) alpha —
70
+ /// `FPDF_REVERSE_BYTE_ORDER` on a `FPDFBitmap_BGRA` bitmap — which the TS
71
+ /// adapter declares as `format: 'rgba8888', alpha: 'straight'`. Measured in the
72
+ /// web backend: an antialiased opaque-white edge over a transparent page reads
73
+ /// `[255, 255, 255, a]`, not `[a, a, a, a]`.
74
+ struct RasterPixels {
75
+ int width = 0;
76
+ int height = 0;
77
+ /// Always `width * 4` here — this core chooses the stride it hands PDFium —
78
+ /// and still carried explicitly, because a consumer that assumed it would
79
+ /// shear the image rather than fail the day it changed.
80
+ int rowBytes = 0;
81
+ int64_t slot = 0;
82
+ std::vector<uint8_t> bytes;
83
+
84
+ bool inSlot() const noexcept { return slot != 0; }
85
+
86
+ /// One channel of one pixel as 0..255. Heap-backed rasters only.
87
+ int channel(int x, int y, int c) const {
88
+ return bytes[static_cast<size_t>(y) * rowBytes + static_cast<size_t>(x) * 4 + c];
89
+ }
90
+ };
91
+
92
+ } // namespace pdfcanvas
@@ -0,0 +1,87 @@
1
+ // Everything a React Native binding needs that is not marshalling.
2
+ //
3
+ // Both bindings used to carry their own handle map, their own (handle, token)
4
+ // cancellation registry, their own "read the transport flag once" dance and
5
+ // their own invalidate ordering, in two languages, with the same long comments
6
+ // twice. This class is that logic once, in the language both bindings can call.
7
+ // `PdfCanvasModule.java` and `PdfCanvasModule.mm` are now what their headers
8
+ // always claimed: type conversion, URI resolution, a worker queue, and promises.
9
+ //
10
+ // THREAD-SAFE. Bindings call `open`/`render` from a worker and `cancel`/`close`
11
+ // /`invalidate` from the bridge or JS thread. Rendering itself is serialised by
12
+ // the library lock inside `Document`; this class only guards its own maps, and
13
+ // never holds its mutex across a render.
14
+ #pragma once
15
+
16
+ #include <cstdint>
17
+ #include <memory>
18
+ #include <mutex>
19
+ #include <unordered_map>
20
+ #include <vector>
21
+
22
+ #include "pdfcanvas/document.h"
23
+ #include "pdfcanvas/pixels.h"
24
+ #include "pdfcanvas/types.h"
25
+
26
+ namespace pdfcanvas {
27
+
28
+ class Service {
29
+ public:
30
+ struct Opened {
31
+ int handle = 0;
32
+ std::vector<PageGeometry> pages;
33
+ };
34
+
35
+ Service() = default;
36
+ Service(const Service&) = delete;
37
+ Service& operator=(const Service&) = delete;
38
+
39
+ /// Opens a document and returns its handle plus every page's geometry.
40
+ /// Throws `PdfError`; after `invalidate()` it throws `backend-failure`.
41
+ Opened open(const Source& source);
42
+
43
+ /// Registers a cancellation signal for (handle, token) BEFORE the render is
44
+ /// dispatched to a worker. That ordering is what lets `cancel()` be a pure
45
+ /// lookup: a cancel cannot arrive for a token that was never issued, so a
46
+ /// missing entry means the render already finished and was cleaned up.
47
+ void registerCancellation(int handle, int64_t token);
48
+
49
+ /// Renders one raster on the calling thread. The (handle, token) signal is
50
+ /// consumed and unregistered on every path out, success or throw.
51
+ ///
52
+ /// `useSlot` asks for the zero-copy transport; the result may still be
53
+ /// heap-backed if the store refused, so bindings branch on
54
+ /// `RasterPixels::inSlot()` and not on what they asked for.
55
+ RasterPixels render(int handle, const RasterRequest& request, int64_t token,
56
+ bool useSlot);
57
+
58
+ /// Best-effort, and genuinely mid-render: sets the signal a render polls.
59
+ void cancel(int handle, int64_t token);
60
+
61
+ /// Closes one document. Blocks until its in-flight render finishes.
62
+ void close(int handle);
63
+
64
+ /// Closes every document and drains every parked slot. Called from a
65
+ /// binding's invalidate hook — a reload drops the JS side without ever
66
+ /// calling `close`, so this is what stops file descriptors and parsed pages
67
+ /// leaking across dozens of Fast Refreshes a day. Further calls fail with
68
+ /// `backend-failure` rather than reopening anything.
69
+ void invalidate();
70
+
71
+ bool isInvalidated() const;
72
+
73
+ /// Open documents, for diagnostics and tests.
74
+ size_t openCount() const;
75
+
76
+ private:
77
+ static int64_t signalKey(int handle, int64_t token) noexcept;
78
+ std::shared_ptr<Document> documentFor(int handle);
79
+
80
+ mutable std::mutex mutex_;
81
+ std::unordered_map<int, std::shared_ptr<Document>> documents_;
82
+ std::unordered_map<int64_t, std::shared_ptr<Cancellation>> signals_;
83
+ int nextHandle_ = 1;
84
+ bool invalidated_ = false;
85
+ };
86
+
87
+ } // namespace pdfcanvas
@@ -0,0 +1,113 @@
1
+ // The raster transport's storage: PDFium writes a tile, and JS gets an
2
+ // `ArrayBuffer` over those exact bytes with no base64 and no copy anywhere.
3
+ //
4
+ // ONE STORE FOR BOTH PLATFORMS. `android/src/jsi/cpp/pdfcanvas-jsi.cpp` and
5
+ // `ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm` used to be two near-identical
6
+ // files with two sets of notes about the same hazards; this is the one that
7
+ // replaced them. The JSI half — installing `__pdfCanvasTakePixels` into a
8
+ // runtime — lives in `jsi.h` and is compiled only where `<jsi/jsi.h>` exists.
9
+ //
10
+ // WHY THE FALLBACK IS BASE64. Neither the legacy bridge nor new-architecture
11
+ // codegen has a binary type (measured: RN's `convertObjCObjectToJSIValue`
12
+ // handles NSString, NSNumber, NSDictionary, NSArray and nothing else, and a
13
+ // `WritableMap` has no bytes either). So a raster crosses as a slot NUMBER that
14
+ // JS redeems once, synchronously, for an `ArrayBuffer` wrapping this memory —
15
+ // or, when the transport could not be negotiated, as a base64 string, which on
16
+ // an LG V60 cost 1005 ms of Hermes decode per 1024x1024 tile.
17
+ //
18
+ // WHO FREES A SLOT. The map drops its reference the instant a take succeeds, so
19
+ // from that moment the only owner is the JS `ArrayBuffer` and the buffer dies
20
+ // when Hermes collects it. A slot that is stored and never taken — a teardown
21
+ // between the promise resolving and its continuation running — is freed by
22
+ // `releaseAll()`, which both bindings call from their invalidate hook.
23
+ #pragma once
24
+
25
+ #include <array>
26
+ #include <cstddef>
27
+ #include <cstdint>
28
+ #include <memory>
29
+ #include <vector>
30
+
31
+ #include "pdfcanvas/jsi_config.h"
32
+ #include "pdfcanvas/pixels.h"
33
+
34
+ #if PDFCANVAS_HAS_JSI
35
+ #include <jsi/jsi.h>
36
+ #endif
37
+
38
+ namespace pdfcanvas {
39
+
40
+ #if PDFCANVAS_HAS_JSI
41
+ using SlotBufferBase = facebook::jsi::MutableBuffer;
42
+ #else
43
+ /// The two methods `jsi::MutableBuffer` declares, restated so a build with no
44
+ /// JSI header (the host test suite, the standalone Android library) compiles
45
+ /// the identical storage code. If these ever drift from jsi.h the JSI builds
46
+ /// stop compiling, which is the failure mode worth having.
47
+ struct SlotBufferBase {
48
+ virtual ~SlotBufferBase() = default;
49
+ virtual size_t size() const = 0;
50
+ virtual uint8_t* data() = 0;
51
+ };
52
+ #endif
53
+
54
+ /// One raster's bytes, owned outright, shaped so Hermes can wrap it.
55
+ ///
56
+ /// `std::vector<uint8_t> bytes_(size)` VALUE-initialises, so constructing this
57
+ /// zeroes the raster. That is the sink contract (`PixelSink::reserve`) and not
58
+ /// an accident of the implementation: it is what makes `Background::Transparent`
59
+ /// free and what stops a published buffer ever handing out somebody else's heap.
60
+ class SlotBuffer final : public SlotBufferBase {
61
+ public:
62
+ explicit SlotBuffer(size_t size) : bytes_(size) {}
63
+ size_t size() const override { return bytes_.size(); }
64
+ uint8_t* data() override { return bytes_.data(); }
65
+
66
+ private:
67
+ std::vector<uint8_t> bytes_;
68
+ };
69
+
70
+ namespace slots {
71
+
72
+ /// The self-test payload, mirroring `TRANSPORT_PROBE_BYTES` in
73
+ /// `src/rasterizer/native-bridge.ts`. Restated there rather than derived: the
74
+ /// two lists are the only place the transport's byte fidelity is asserted, and
75
+ /// a mismatch has to make the probe FAIL and the platform fall back to base64.
76
+ const std::array<uint8_t, 8>& probeBytes();
77
+
78
+ /// Parks the probe bytes in a fresh slot and returns its id.
79
+ int64_t storeProbe();
80
+
81
+ /// Takes a slot out of the store. nullptr if it is not there. Exactly-once.
82
+ std::shared_ptr<SlotBuffer> take(int64_t slot);
83
+
84
+ /// Drops one slot if it is still there. Returns whether it was. Idempotent.
85
+ bool release(int64_t slot);
86
+
87
+ /// Drops every slot. Called from the bindings' invalidate hooks.
88
+ void releaseAll();
89
+
90
+ /// Slots currently waiting to be taken. For assertions and diagnostics.
91
+ size_t count();
92
+
93
+ } // namespace slots
94
+
95
+ /// A `PixelSink` that reserves store-owned memory and publishes it under a
96
+ /// fresh, process-wide, monotonic id — so a collision at publish is
97
+ /// unrepresentable and `publish()` cannot fail. (The retired Android store had
98
+ /// to REFUSE colliding ids because Java allocated them; allocating next to the
99
+ /// map removes that whole failure class.)
100
+ class SlotSink final : public PixelSink {
101
+ public:
102
+ uint8_t* reserve(size_t length) override;
103
+ int64_t publish() override;
104
+ void abandon() override;
105
+
106
+ private:
107
+ std::shared_ptr<SlotBuffer> buffer_;
108
+ int64_t slot_ = 0;
109
+ bool reserved_ = false;
110
+ bool published_ = false;
111
+ };
112
+
113
+ } // namespace pdfcanvas