@reekon-tools/react-native-pdf-canvas 0.1.0 → 0.2.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 (129) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/PdfCanvas.podspec +68 -39
  4. package/README.md +213 -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/index.native.d.ts +5 -0
  19. package/dist/index.native.js +5 -0
  20. package/dist/index.web.d.ts +10 -0
  21. package/dist/index.web.js +10 -0
  22. package/dist/rasterizer/index.native.d.ts +8 -12
  23. package/dist/rasterizer/index.native.js +15 -21
  24. package/dist/rasterizer/native-bridge.d.ts +51 -75
  25. package/dist/rasterizer/native-bridge.js +49 -33
  26. package/dist/rasterizer/native.d.ts +45 -0
  27. package/dist/rasterizer/native.js +123 -0
  28. package/dist/react/usePdfDocument.d.ts +5 -5
  29. package/dist/react/usePdfLayer.d.ts +10 -1
  30. package/dist/react/usePdfLayer.js +10 -2
  31. package/dist/skia/index.native.d.ts +7 -3
  32. package/dist/skia/index.native.js +7 -3
  33. package/dist/skia/index.web.d.ts +4 -2
  34. package/dist/skia/index.web.js +4 -2
  35. package/dist/testing/scenes.d.ts +3 -3
  36. package/dist/testing/scenes.js +4 -4
  37. package/dist/types.d.ts +36 -42
  38. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  39. package/native/.clangd +5 -0
  40. package/native/CMakeLists.txt +89 -0
  41. package/native/core/include/pdfcanvas/document.h +90 -0
  42. package/native/core/include/pdfcanvas/error.h +47 -0
  43. package/native/core/include/pdfcanvas/jsi.h +18 -0
  44. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  45. package/native/core/include/pdfcanvas/library.h +28 -0
  46. package/native/core/include/pdfcanvas/pixels.h +92 -0
  47. package/native/core/include/pdfcanvas/service.h +87 -0
  48. package/native/core/include/pdfcanvas/slots.h +113 -0
  49. package/native/core/include/pdfcanvas/types.h +110 -0
  50. package/native/core/pdfcanvas-core.cmake +22 -0
  51. package/native/core/src/document.cpp +516 -0
  52. package/native/core/src/error.cpp +29 -0
  53. package/native/core/src/jsi.cpp +66 -0
  54. package/native/core/src/library.cpp +52 -0
  55. package/native/core/src/pixels.cpp +45 -0
  56. package/native/core/src/service.cpp +156 -0
  57. package/native/core/src/slots.cpp +139 -0
  58. package/native/tests/fixtures.cpp +451 -0
  59. package/native/tests/fixtures.h +96 -0
  60. package/native/tests/harness.h +122 -0
  61. package/native/tests/main.cpp +64 -0
  62. package/native/tests/pixels_util.h +126 -0
  63. package/native/tests/test_document.cpp +485 -0
  64. package/native/tests/test_service.cpp +130 -0
  65. package/native/tests/test_slots.cpp +250 -0
  66. package/package.json +38 -8
  67. package/react-native.config.js +5 -2
  68. package/scripts/fetch-pdfium.mjs +487 -0
  69. package/scripts/pdfium-manifest.json +46 -0
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  72. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  73. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  74. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  75. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  76. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  77. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  78. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  79. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  80. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  81. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  82. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  83. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  84. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  85. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  86. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  87. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  88. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  89. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  90. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  91. package/dist/rasterizer/android.d.ts +0 -121
  92. package/dist/rasterizer/android.js +0 -126
  93. package/dist/rasterizer/ios.d.ts +0 -84
  94. package/dist/rasterizer/ios.js +0 -155
  95. package/ios/Package.swift +0 -58
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  98. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  99. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  100. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  101. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  102. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  103. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  104. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  105. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  109. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  110. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  111. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  112. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  113. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  114. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  115. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  116. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  117. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  118. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  119. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  120. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  121. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  122. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  123. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  124. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  125. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  126. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  127. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  128. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  129. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -1,111 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
-
3
- #import "PdfCanvasPixelSink.h"
4
-
5
- NS_ASSUME_NONNULL_BEGIN
6
-
7
- /**
8
- * The only format this backend emits: 8 bits per channel, R G B A in memory.
9
- *
10
- * It is `kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big`, which is
11
- * what Skia calls `kRGBA_8888` and what `PixelFormat` in `src/types.ts` calls
12
- * `'rgba8888'`. Matching Android's byte order rather than reaching for the
13
- * Apple-native BGRA is deliberate: cost is flat in scale on this engine
14
- * (measured: a 512² tile costs 0.49 / 0.39 / 0.43 / 0.39 ms at scale 1 / 2 / 4 /
15
- * 8), so rasterizing dominates and the byte order is not where the time goes —
16
- * whereas one byte order across both native backends is one fewer thing for a
17
- * cross-platform pixel test to have to account for.
18
- */
19
- FOUNDATION_EXPORT NSString *const PdfCanvasFormatRGBA8888;
20
-
21
- /**
22
- * A finished raster, mirroring `RasterPixels` in `src/types.ts`.
23
- *
24
- * `rowBytes` is STATED, never assumed to be `width * 4` by whoever reads it — a
25
- * wrong stride does not fail, it SHEARS the image, one pixel further per row,
26
- * which reads as a plausible skew rather than an error. On this backend it is
27
- * tight by construction, because this library chooses the stride it hands
28
- * `CGBitmapContextCreate`; the value still travels because
29
- * `imageFromPixels` needs it and because "tight today" is a fact about today.
30
- *
31
- * MEASURED, and it constrains what a future alignment change may do:
32
- * `CGBitmapContextCreate` REFUSES a `bytesPerRow` that is not a multiple of 4 at
33
- * 32 bits per pixel (`width * 4 + 1`, `+ 2`, `+ 3` and `+ 15` all return NULL;
34
- * `+ 0`, `+ 4`, `+ 8`, `+ 12`, `+ 16`, `+ 64` are accepted). So row padding is
35
- * expressible if it ever pays for itself, but only in whole pixels.
36
- *
37
- * <h3>Where the pixels actually are</h3>
38
- *
39
- * ONE OF TWO PLACES, and `bytes` being nil is how you tell:
40
- *
41
- * - ON THE HEAP — `bytes` is the raster and `slot` is 0. Every caller that reads
42
- * pixels wants this, and it is what a nil sink produces.
43
- * - IN A NATIVE SLOT — `bytes` is nil and `slot` names a buffer in
44
- * `PdfCanvasSlots.mm` waiting to become a JS `ArrayBuffer`. Nothing can read
45
- * the pixels back from Objective-C; the whole point is that they were written
46
- * once, by CoreGraphics, and are never copied again.
47
- *
48
- * That is a deliberately awkward shape rather than two classes, because the one
49
- * caller that asks for a slot has to handle both anyway: a sink is allowed to
50
- * refuse its reservation, and the heap is the fallback. `-channelAtX:y:channel:`
51
- * therefore fails LOUDLY and by name on a slot-backed raster instead of throwing
52
- * a bare exception from somewhere further in.
53
- *
54
- * <h3>Who frees the slot</h3>
55
- *
56
- * Normally nobody: the buffer is taken exactly once by `__pdfCanvasTakePixels`
57
- * and freed when the JS `ArrayBuffer` is collected. `-releaseSlot` exists for the
58
- * paths where the raster is ABANDONED after it was drawn — a cancellation
59
- * observed after the draw, or a throw between the render finishing and the
60
- * promise resolving. Without it those megabytes sit in the slot map until the
61
- * next teardown, which on a dev build is the next Fast Refresh and in a release
62
- * build is never.
63
- *
64
- * NOT NAMED `-release`. ARC forbids declaring that selector, and a category
65
- * hiding a retain-count method behind a domain meaning would be a trap even if it
66
- * were allowed.
67
- */
68
- @interface PdfCanvasRasterPixels : NSObject
69
-
70
- /** The raster, or nil when it is in a native slot. See the class note. */
71
- @property(nonatomic, readonly, nullable) NSData *bytes;
72
- @property(nonatomic, readonly) NSInteger width;
73
- @property(nonatomic, readonly) NSInteger height;
74
- @property(nonatomic, readonly) NSInteger rowBytes;
75
- @property(nonatomic, readonly, copy) NSString *format;
76
- /** 0, or the native slot holding the raster. */
77
- @property(nonatomic, readonly) int64_t slot;
78
-
79
- - (instancetype)initWithSink:(id<PdfCanvasPixelSink>)sink
80
- slot:(int64_t)slot
81
- bytes:(nullable NSData *)bytes
82
- width:(NSInteger)width
83
- height:(NSInteger)height
84
- rowBytes:(NSInteger)rowBytes
85
- format:(NSString *)format NS_DESIGNATED_INITIALIZER;
86
- - (instancetype)init NS_UNAVAILABLE;
87
-
88
- /** Whether the pixels are in native memory and unreadable from here. */
89
- @property(nonatomic, readonly) BOOL isInNativeSlot;
90
-
91
- /**
92
- * Frees the native slot if this raster is in one. Idempotent, and a no-op for a
93
- * heap-backed raster.
94
- *
95
- * Call this on any path that DROPS the raster after it was produced. A taken slot
96
- * is already gone from the map, so releasing after a successful take is harmless.
97
- */
98
- - (void)releaseSlot;
99
-
100
- /** Byte offset of pixel (x, y), honouring the real stride. */
101
- - (NSInteger)offsetOfX:(NSInteger)x y:(NSInteger)y NS_SWIFT_NAME(offset(x:y:));
102
-
103
- /** One channel of one pixel, as an unsigned 0..255 value. */
104
- - (NSInteger)channelAtX:(NSInteger)x
105
- y:(NSInteger)y
106
- channel:(NSInteger)channel
107
- NS_SWIFT_NAME(channelAt(x:y:channel:));
108
-
109
- @end
110
-
111
- NS_ASSUME_NONNULL_END
@@ -1,85 +0,0 @@
1
- #import <CoreGraphics/CoreGraphics.h>
2
- #import <Foundation/Foundation.h>
3
-
4
- NS_ASSUME_NONNULL_BEGIN
5
-
6
- typedef NS_ENUM(NSInteger, PdfCanvasBackground) {
7
- /** Fill the buffer opaque white before drawing. */
8
- PdfCanvasBackgroundWhite = 0,
9
- /**
10
- * Leave the buffer cleared to transparent before drawing.
11
- *
12
- * This means the CALLER accepts undrawn area coming back transparent. It is not
13
- * a claim that CoreGraphics clears anything: a `CGBitmapContext` draws into
14
- * whatever memory it was handed, so both backgrounds are an explicit fill and
15
- * only the value differs. The transparent case happens to be free, because both
16
- * destinations this library allocates arrive zeroed.
17
- */
18
- PdfCanvasBackgroundTransparent = 1,
19
- };
20
-
21
- /**
22
- * One raster to produce, mirroring `RasterRequest` in `src/types.ts` and
23
- * `RasterRequest.java` on Android.
24
- *
25
- * The doc rect is in PAGE-LOCAL points — y-down, origin at the page's top-left,
26
- * already post-rotation (see `PdfCanvasPageGeometry`). It is NOT in whatever
27
- * document-wide space the JS layout function laid the pages out in; the TS
28
- * adapter subtracts the page origin before it gets here. A backend that had to
29
- * know the page layout would be a backend the layout function could break.
30
- *
31
- * `double` rather than Android's `float`, because the values arrive from JS as
32
- * doubles and narrowing them buys nothing. It matters in one place: at a large
33
- * doc offset and a high scale, `docX * scale` in float has a quantum well above a
34
- * pixel, and that offset is exactly what the detail layer is made of.
35
- */
36
- @interface PdfCanvasRasterRequest : NSObject
37
-
38
- @property(nonatomic, readonly) NSInteger page;
39
- @property(nonatomic, readonly) double docX;
40
- @property(nonatomic, readonly) double docY;
41
- @property(nonatomic, readonly) double docWidth;
42
- @property(nonatomic, readonly) double docHeight;
43
- /** Device pixels per PDF point. */
44
- @property(nonatomic, readonly) double scale;
45
- /**
46
- * Whether PDF-embedded annotations are drawn.
47
- *
48
- * HONOURED, unlike on Android. YES routes the render through PDFKit
49
- * (`-[PDFPage drawWithBox:toContext:]`, which composites annotation appearance
50
- * streams); NO routes it through `CGContextDrawPDFPage`, which draws the content
51
- * stream and nothing else. That makes iOS the only backend where the tile path
52
- * — which passes NO for every tile — genuinely does not double-draw an
53
- * annotation the app is also rendering on its Skia layer.
54
- */
55
- @property(nonatomic, readonly) BOOL annotations;
56
- @property(nonatomic, readonly) PdfCanvasBackground background;
57
-
58
- - (instancetype)initWithPage:(NSInteger)page
59
- docX:(double)docX
60
- docY:(double)docY
61
- docWidth:(double)docWidth
62
- docHeight:(double)docHeight
63
- scale:(double)scale
64
- annotations:(BOOL)annotations
65
- background:(PdfCanvasBackground)background
66
- NS_DESIGNATED_INITIALIZER;
67
- - (instancetype)init NS_UNAVAILABLE;
68
-
69
- /**
70
- * Destination width in device pixels.
71
- *
72
- * ROUND, not ceil, and at least 1 — the same rule `planner.ts` applies
73
- * (`Math.max(1, Math.round(pageRect.width * scale))`), the deterministic fake
74
- * backend documents, and `RasterRequest.java` implements. The three have to agree
75
- * exactly: the planner declares the pixel size in the `RasterPlanItem` the cache
76
- * is keyed on, and a backend that rounded the other way would return a raster of
77
- * a different size than the plan promised.
78
- */
79
- @property(nonatomic, readonly) NSInteger pixelWidth;
80
- /** Destination height in device pixels. See `pixelWidth`. */
81
- @property(nonatomic, readonly) NSInteger pixelHeight;
82
-
83
- @end
84
-
85
- NS_ASSUME_NONNULL_END
@@ -1,163 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
-
3
- #import "PdfCanvasCancellation.h"
4
- #import "PdfCanvasErrorCode.h"
5
- #import "PdfCanvasPageGeometry.h"
6
- #import "PdfCanvasPixelSink.h"
7
- #import "PdfCanvasRasterPixels.h"
8
- #import "PdfCanvasRasterRequest.h"
9
-
10
- NS_ASSUME_NONNULL_BEGIN
11
-
12
- /**
13
- * The iOS page rasterizer. CoreGraphics and PDFKit, and NOTHING from React
14
- * Native, JSI or Nitro — so it builds and is proven by `swift test` from
15
- * `ios/Package.swift` on a Mac, with no device, no simulator and no app around
16
- * it. That is the same split `android/src/main` draws against
17
- * `android/src/reactnative`, and it exists for the same reason: a bug in the
18
- * binding must never be able to hide as a bug in rasterizing.
19
- *
20
- * Shaped to serve `RasterizerHandle` in `src/types.ts`: `pageCount`,
21
- * `pageGeometry(index)`, `render(request, signal)`, `close()`.
22
- * `ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm` is a marshalling layer over
23
- * this and adds no behaviour.
24
- *
25
- * ------------------------------------------------------------------------
26
- * HOW A TILE IS DRAWN
27
- * ------------------------------------------------------------------------
28
- *
29
- * `PdfCanvasDocToRasterTransform` composed with `PdfCanvasPdfToDocTransform`, into
30
- * a `CGBitmapContext` of exactly `pixelWidth` x `pixelHeight`. MEASURED: a tile
31
- * rendered that way is pixel-identical to the same region cropped out of a
32
- * full-page render — 0 of 41,984 pixels differing, max channel delta 0, for a
33
- * 64x41pt tile at doc (37, 23) at scale 4 against an 800x400 whole-page raster.
34
- * That is the same property spike S1 measured for Android's `PdfRenderer` and S3
35
- * for PDFium on web, so the off-page-origin transform the whole detail layer
36
- * depends on is now exact on all three engines.
37
- *
38
- * ALSO MEASURED: cost is FLAT IN SCALE. A 512² tile of a dense 540-line ARCH-D
39
- * page costs 0.49 / 0.39 / 0.43 / 0.39 ms at scale 1 / 2 / 4 / 8 (best of five, on
40
- * an Apple-silicon Mac). CoreGraphics clips rather than rasterizing the page and
41
- * discarding most of it — which is what makes a viewport-sized detail raster
42
- * constant in zoom, the premise the entire render model rests on, and it now holds
43
- * on every engine this package has.
44
- *
45
- * ------------------------------------------------------------------------
46
- * TWO ENGINES, SELECTED BY `annotations`
47
- * ------------------------------------------------------------------------
48
- *
49
- * `annotations: NO` — every tile — draws with `CGContextDrawPDFPage`, which
50
- * composites the page's content stream and not its annotation appearance streams.
51
- * `annotations: YES` draws with `-[PDFPage drawWithBox:toContext:]`, which
52
- * composites both. On a page with no annotations the two agree byte-for-byte
53
- * (0 of 1,280,000 bytes differing, maxDelta 0, at scale 4), which is what makes
54
- * choosing per render safe: the geometry does not move, only what is composited.
55
- *
56
- * The two need DIFFERENT transforms, because PDFKit applies `/Rotate` and the
57
- * crop-box origin itself and CoreGraphics applies neither. See
58
- * `PdfCanvasPageTransform.h`, which has the measurements and the trap.
59
- */
60
- @interface PdfCanvasRasterizer : NSObject
61
-
62
- /**
63
- * Refuse absurd rasters instead of dying on an allocation failure.
64
- *
65
- * 64 Mpx is 256 MB at 4 bytes per pixel — already past what a device will hand
66
- * out in one allocation, so anything above it is a planner bug, and a named error
67
- * against a raster key is a far better diagnostic than a jetsam kill with no key
68
- * attached. The TS policy has its own, much lower, ceilings (`maxRasterPixels`);
69
- * this is the backstop for a request that reaches here anyway. Same number as
70
- * `PdfCanvasRasterizer.MAX_RASTER_PIXELS` on Android.
71
- */
72
- @property(class, nonatomic, readonly) NSInteger maxRasterPixels;
73
-
74
- /**
75
- * @param password nil unless the document needs one. Unlike Android — which
76
- * rejects any password with `unsupported`, because `LoadParams` landed at API
77
- * 35 — encrypted documents genuinely work here.
78
- * @param poolSize concurrent renders, clamped to [1, `kPdfCanvasMaxPoolSize`].
79
- */
80
- + (nullable instancetype)openFileURL:(NSURL *)url
81
- password:(nullable NSString *)password
82
- poolSize:(NSInteger)poolSize
83
- error:(NSError *_Nullable *_Nullable)error
84
- NS_SWIFT_NAME(open(fileURL:password:poolSize:));
85
-
86
- /**
87
- * Opens from bytes, with NO spool file.
88
- *
89
- * Android's equivalent must write a temp file first — `PdfRenderer` needs a
90
- * seekable descriptor and reads through a seek-and-read callback — and deletes it
91
- * in `close()`. `CGPDFDocumentCreateWithProvider` needs no file at all.
92
- *
93
- * Reachable only from a caller that already has the bytes in memory. The JS
94
- * binding deliberately does NOT expose it: getting bytes across the bridge means
95
- * base64-encoding the whole document, which the spec's measured rules forbid.
96
- */
97
- + (nullable instancetype)openData:(NSData *)data
98
- password:(nullable NSString *)password
99
- poolSize:(NSInteger)poolSize
100
- error:(NSError *_Nullable *_Nullable)error
101
- NS_SWIFT_NAME(open(data:password:poolSize:));
102
-
103
- @property(nonatomic, readonly) NSInteger pageCount;
104
- /** How many renders can genuinely be in flight at once. */
105
- @property(nonatomic, readonly) NSInteger poolSize;
106
- @property(nonatomic, readonly, getter=isClosed) BOOL closed;
107
-
108
- /**
109
- * An array read.
110
- *
111
- * Every page's geometry is probed ONCE, at open. The `RasterizerHandle` contract
112
- * says `pageGeometry` is cheap and "must not load a page", and reading a page's
113
- * crop box and rotation means asking CoreGraphics for a `CGPDFPageRef` — so the
114
- * asking happens at open and this becomes a lookup. Note that a `CGPDFPageRef` is
115
- * built from the page DICTIONARY and parses no content stream, so the open cost is
116
- * proportional to the page tree rather than to what is drawn.
117
- */
118
- - (nullable PdfCanvasPageGeometry *)pageGeometryAtIndex:(NSInteger)index
119
- error:
120
- (NSError *_Nullable
121
- *_Nullable)error
122
- NS_SWIFT_NAME(pageGeometry(at:));
123
-
124
- /**
125
- * Renders one raster.
126
- *
127
- * @param sink where the bytes go. Nil means the heap, which is what every
128
- * pixel-reading caller wants and what the whole Swift suite uses. A non-nil
129
- * sink is the zero-copy transport: CoreGraphics writes straight into the
130
- * buffer that becomes a JS `ArrayBuffer`. A sink is allowed to REFUSE its
131
- * reservation, in which case this falls through to the heap and the result is
132
- * heap-backed — so check `isInNativeSlot` on the way out rather than assuming.
133
- *
134
- * @return nil on failure, with `error` carrying a `PdfCanvasErrorCode`. A
135
- * cancellation observed AFTER the draw finished releases the sink itself
136
- * before returning; every abandon path after this method returns belongs to
137
- * the caller.
138
- */
139
- - (nullable PdfCanvasRasterPixels *)render:(PdfCanvasRasterRequest *)request
140
- cancellation:
141
- (nullable PdfCanvasCancellation *)signal
142
- sink:(nullable id<PdfCanvasPixelSink>)sink
143
- error:(NSError *_Nullable *_Nullable)error;
144
-
145
- - (void)close;
146
-
147
- - (instancetype)init NS_UNAVAILABLE;
148
-
149
- /**
150
- * Whether this platform draws a page carrying `/Rotate` upright.
151
- *
152
- * Always `@"supported"`, and it is a constant rather than a probe because there is
153
- * no device variable: `PdfCanvasPdfToDocTransform` applies the rotation and
154
- * PDFKit applies its own, on every OS version this library builds for. It exists
155
- * only because it is part of the shared wire format — Android has to probe it,
156
- * since API 24 reports rotated DIMENSIONS while drawing the content unrotated and
157
- * clipped, and a host should not have to know which platform it is on to ask.
158
- */
159
- @property(class, nonatomic, readonly) NSString *rotationSupport;
160
-
161
- @end
162
-
163
- NS_ASSUME_NONNULL_END
@@ -1,297 +0,0 @@
1
- #import "PdfCanvasSlots.h"
2
-
3
- /*
4
- * TWO BUILDS, ONE FILE — the same split `android/src/jsi/cpp/pdfcanvas-jsi.cpp`
5
- * draws, and for the same reason.
6
- *
7
- * jsi.h present — the podspec's build. The host function is installed onto the JS
8
- * global and a taken slot becomes a `jsi::ArrayBuffer` over the
9
- * very bytes CoreGraphics wrote.
10
- * jsi.h absent — the SwiftPM build (`ios/Package.swift`), which has no React
11
- * Native and no `jsi.h` to include. `PdfCanvasSlotsInstall`
12
- * reports NO and the slot storage compiles on its own.
13
- *
14
- * That second configuration is not a stub for its own sake: it is what lets
15
- * `SlotsTests.swift` prove ON A LAPTOP that the bytes which become the ArrayBuffer
16
- * are the bytes CoreGraphics wrote, that a slot is taken exactly once, and that every
17
- * abandon path frees. Those are the failure modes that are a leak or a
18
- * use-after-free rather than a wrong pixel, and a device session does not show them.
19
- * The storage under test is the production storage — the only thing the two builds do
20
- * not share is the JSI call itself.
21
- */
22
- #if __has_include(<jsi/jsi.h>)
23
- #define PDFCANVAS_WITH_JSI 1
24
- #import <jsi/jsi.h>
25
- #endif
26
-
27
- #import <atomic>
28
- #import <cstdint>
29
- #import <cstring>
30
- #import <memory>
31
- #import <mutex>
32
- #import <string>
33
- #import <unordered_map>
34
- #import <vector>
35
-
36
- namespace {
37
-
38
- #ifdef PDFCANVAS_WITH_JSI
39
- using SlotBufferBase = facebook::jsi::MutableBuffer;
40
- #else
41
- /**
42
- * The two methods `jsi::MutableBuffer` declares, restated so the SwiftPM build
43
- * compiles the identical storage code with no JSI header in sight.
44
- *
45
- * If these ever drift from `jsi.h` the podspec build stops compiling, which is the
46
- * failure mode worth having — a silent divergence would mean the tested storage and
47
- * the shipped storage were different types.
48
- */
49
- struct SlotBufferBase {
50
- virtual ~SlotBufferBase() = default;
51
- virtual size_t size() const = 0;
52
- virtual uint8_t *data() = 0;
53
- };
54
- #endif
55
-
56
- /**
57
- * One raster's bytes, owned outright, and shaped so Hermes can wrap it.
58
- *
59
- * `std::vector<uint8_t> bytes_(size)` VALUE-initialises, so constructing this
60
- * zeroes the whole raster. On Android the equivalent line carries a long note
61
- * about that being a second full-raster pass whose only justification is an
62
- * invariant; here it is not optional and not merely an invariant — the
63
- * `PdfCanvasPixelSink` contract promises zeroed memory, and that promise is what
64
- * makes `background: 'transparent'` free. Removing it would break a documented
65
- * behaviour rather than shaving a pass.
66
- */
67
- class SlotBuffer final : public SlotBufferBase {
68
- public:
69
- explicit SlotBuffer(size_t size) : bytes_(size) {}
70
-
71
- size_t size() const override { return bytes_.size(); }
72
- uint8_t *data() override { return bytes_.data(); }
73
-
74
- private:
75
- std::vector<uint8_t> bytes_;
76
- };
77
-
78
- /**
79
- * Slots are published on a render worker and taken on the JS thread, so the map is
80
- * guarded. The lock is held only around the map operation itself — never across an
81
- * allocation, never across a fill, and never across a JSI call.
82
- */
83
- std::mutex g_slotsMutex;
84
- std::unordered_map<int64_t, std::shared_ptr<SlotBuffer>> g_slots;
85
-
86
- /**
87
- * Slot ids, from one counter per PROCESS.
88
- *
89
- * Starting at 1 because 0 is `PdfCanvasRasterPixels`' "heap-backed" sentinel. It is
90
- * a process-wide `std::atomic` rather than per-module state for the reason
91
- * Android's equivalent is `static`: React Native builds a fresh set of native
92
- * modules on every reload while the outgoing one may still have rasters parked, and
93
- * a per-module counter starting at 1 would hand the SAME id to two modules. The
94
- * second store would silently replace the first module's buffer and that module's
95
- * take would return ANOTHER tile's pixels — a wrong image, not an error.
96
- */
97
- std::atomic<int64_t> g_nextSlot{1};
98
-
99
- std::shared_ptr<SlotBuffer> takeSlot(int64_t slot) {
100
- std::lock_guard<std::mutex> lock(g_slotsMutex);
101
- auto found = g_slots.find(slot);
102
- if (found == g_slots.end()) {
103
- return nullptr;
104
- }
105
- std::shared_ptr<SlotBuffer> buffer = std::move(found->second);
106
- g_slots.erase(found);
107
- return buffer;
108
- }
109
-
110
- } // namespace
111
-
112
- /* ------------------------------------------------------------------ *
113
- * The sink
114
- * ------------------------------------------------------------------ */
115
-
116
- @implementation PdfCanvasSlotSink {
117
- /**
118
- * Held HERE and nowhere else until `pdfCanvasPublish`. That is the whole safety
119
- * property: until the buffer is in `g_slots` it is unreachable from
120
- * `PdfCanvasSlotsReleaseAll`, which runs on another thread on every Fast
121
- * Refresh, so a reload landing mid-draw cannot free memory CoreGraphics is
122
- * writing into.
123
- */
124
- std::shared_ptr<SlotBuffer> _buffer;
125
- int64_t _slot;
126
- BOOL _published;
127
- BOOL _reserved;
128
- }
129
-
130
- - (nullable void *)pdfCanvasReserveBytes:(NSUInteger)length {
131
- if (_reserved || length == 0) {
132
- return NULL;
133
- }
134
- _reserved = YES;
135
- try {
136
- _buffer = std::make_shared<SlotBuffer>(static_cast<size_t>(length));
137
- } catch (...) {
138
- // `std::bad_alloc` for a multi-megabyte raster under memory pressure. Reported
139
- // as a refusal rather than propagated: the rasterizer falls straight through to
140
- // a heap sink and the tile still goes out, which is the point of refusing
141
- // BEFORE anything is drawn. A C++ exception escaping into ARC-managed
142
- // Objective-C would also be a very bad way to say "no memory".
143
- _buffer = nullptr;
144
- return NULL;
145
- }
146
- // Allocated next to the map it goes into, from a process-wide counter, so a
147
- // collision at publish is unrepresentable. See the class note in the header.
148
- _slot = g_nextSlot.fetch_add(1, std::memory_order_relaxed);
149
- return _buffer->data();
150
- }
151
-
152
- - (int64_t)pdfCanvasPublish {
153
- if (_buffer == nullptr || _published) {
154
- return 0;
155
- }
156
- {
157
- std::lock_guard<std::mutex> lock(g_slotsMutex);
158
- // ASSIGNMENT, not `emplace`, and it cannot collide: the id came from the
159
- // process-wide counter a moment ago. Android's equivalent has to refuse a
160
- // collision because its ids are handed in from Java.
161
- g_slots[_slot] = _buffer;
162
- }
163
- _published = YES;
164
- // The map holds the only reference this object needs to care about from here on.
165
- // Dropping the local one means a successful take is the sole owner, so the buffer
166
- // dies with the JS ArrayBuffer rather than with this sink.
167
- _buffer = nullptr;
168
- return _slot;
169
- }
170
-
171
- - (void)pdfCanvasAbandon {
172
- if (_published) {
173
- // Already in the map. Taking it back out is what frees it — and finding
174
- // nothing is the normal case when JS has already redeemed it, which is why
175
- // this is a lookup rather than an assertion.
176
- takeSlot(_slot);
177
- _published = NO;
178
- _slot = 0;
179
- return;
180
- }
181
- // Never published: the buffer is still ours and the shared_ptr frees it.
182
- _buffer = nullptr;
183
- }
184
-
185
- @end
186
-
187
- /* ------------------------------------------------------------------ *
188
- * The probe
189
- * ------------------------------------------------------------------ */
190
-
191
- NSData *PdfCanvasSlotsProbeBytes(void) {
192
- // Chosen, not arbitrary. 0x00 and 0xFF pin both ends of the range, and
193
- // 0xFD/0xFE/0xFF sit above 0x7F where a signed-byte slip would show up. The
194
- // sign-extension hazard does not exist on this platform — the whole path is
195
- // `uint8_t` — but the list is shared with Android and with the TS side, and
196
- // three copies that agree are worth more than one that is locally optimal.
197
- static const uint8_t bytes[] = {0x00, 0x01, 0x02, 0xFD, 0xFE, 0xFF, 0x2A, 0x07};
198
- return [NSData dataWithBytes:bytes length:sizeof(bytes)];
199
- }
200
-
201
- int64_t PdfCanvasSlotsStoreProbe(void) {
202
- NSData *probe = PdfCanvasSlotsProbeBytes();
203
- PdfCanvasSlotSink *sink = [[PdfCanvasSlotSink alloc] init];
204
- void *bytes = [sink pdfCanvasReserveBytes:probe.length];
205
- if (bytes == NULL) {
206
- return -1;
207
- }
208
- std::memcpy(bytes, probe.bytes, probe.length);
209
- return [sink pdfCanvasPublish];
210
- }
211
-
212
- /* ------------------------------------------------------------------ *
213
- * Install
214
- * ------------------------------------------------------------------ */
215
-
216
- BOOL PdfCanvasSlotsInstall(void *runtimePointer) {
217
- #ifdef PDFCANVAS_WITH_JSI
218
- if (runtimePointer == nullptr) {
219
- return NO;
220
- }
221
- auto &runtime = *reinterpret_cast<facebook::jsi::Runtime *>(runtimePointer);
222
- try {
223
- auto name =
224
- facebook::jsi::PropNameID::forAscii(runtime, "__pdfCanvasTakePixels");
225
- auto take = facebook::jsi::Function::createFromHostFunction(
226
- runtime, name, 1,
227
- [](facebook::jsi::Runtime &rt, const facebook::jsi::Value &,
228
- const facebook::jsi::Value *args,
229
- size_t count) -> facebook::jsi::Value {
230
- if (count < 1 || !args[0].isNumber()) {
231
- throw facebook::jsi::JSError(
232
- rt, "__pdfCanvasTakePixels(slot): slot must be a number.");
233
- }
234
- const auto slot = static_cast<int64_t>(args[0].asNumber());
235
- std::shared_ptr<SlotBuffer> buffer = takeSlot(slot);
236
- if (buffer == nullptr) {
237
- // Every slot is taken exactly once, so this means the render that
238
- // filled it was torn down (a reload drains every slot) or the same
239
- // slot was taken twice. Named rather than silently empty: an empty
240
- // ArrayBuffer would reach Skia and draw a black tile.
241
- throw facebook::jsi::JSError(
242
- rt,
243
- "__pdfCanvasTakePixels: no raster is waiting in slot " +
244
- std::to_string(slot) +
245
- ". It was already taken, or the module was invalidated.");
246
- }
247
- // The ArrayBuffer BORROWS this buffer and the shared_ptr keeps it alive;
248
- // the slot map has already dropped its reference, so JS is now the sole
249
- // owner and the GC is what frees it.
250
- return facebook::jsi::Value(
251
- rt, facebook::jsi::ArrayBuffer(rt, std::move(buffer)));
252
- });
253
- runtime.global().setProperty(runtime, name, take);
254
- return YES;
255
- } catch (...) {
256
- // A runtime that refuses to install a host function is a runtime this package
257
- // will not use. The JS side falls back to base64.
258
- return NO;
259
- }
260
- #else
261
- // The SwiftPM build. There is no runtime to install into and nothing that could
262
- // call the host function, so this is the honest answer rather than a stub.
263
- (void)runtimePointer;
264
- return NO;
265
- #endif
266
- }
267
-
268
- /* ------------------------------------------------------------------ *
269
- * Teardown and diagnostics
270
- * ------------------------------------------------------------------ */
271
-
272
- void PdfCanvasSlotsRelease(int64_t slot) {
273
- takeSlot(slot);
274
- }
275
-
276
- void PdfCanvasSlotsReleaseAll(void) {
277
- std::unordered_map<int64_t, std::shared_ptr<SlotBuffer>> drained;
278
- {
279
- std::lock_guard<std::mutex> lock(g_slotsMutex);
280
- drained.swap(g_slots);
281
- }
282
- // Freed outside the lock. Dropping several megabytes of buffers is not something
283
- // to do while the JS thread might be waiting on the same mutex.
284
- }
285
-
286
- NSInteger PdfCanvasSlotsCount(void) {
287
- std::lock_guard<std::mutex> lock(g_slotsMutex);
288
- return static_cast<NSInteger>(g_slots.size());
289
- }
290
-
291
- NSData *_Nullable PdfCanvasSlotsTakeForTest(int64_t slot) {
292
- std::shared_ptr<SlotBuffer> buffer = takeSlot(slot);
293
- if (buffer == nullptr) {
294
- return nil;
295
- }
296
- return [NSData dataWithBytes:buffer->data() length:buffer->size()];
297
- }