@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,548 +0,0 @@
1
- #import "PdfCanvasRasterizer.h"
2
-
3
- #import <PDFKit/PDFKit.h>
4
- #import <math.h>
5
-
6
- #import "PdfCanvasDocumentPool.h"
7
- #import "PdfCanvasPageTransform.h"
8
-
9
- static const NSInteger kPdfCanvasMaxRasterPixels = 64000000;
10
-
11
- /* ------------------------------------------------------------------ *
12
- * Private surfaces
13
- * ------------------------------------------------------------------ */
14
-
15
- @interface PdfCanvasRasterizer ()
16
- + (nullable instancetype)withPool:(PdfCanvasDocumentPool *)pool
17
- error:(NSError *_Nullable *_Nullable)error;
18
- - (instancetype)initWithPool:(PdfCanvasDocumentPool *)pool
19
- pages:(NSArray<PdfCanvasPageGeometry *> *)pages;
20
- - (nullable PdfCanvasRasterPixels *)
21
- renderLeased:(PdfCanvasDocumentLease *)lease
22
- request:(PdfCanvasRasterRequest *)request
23
- geometry:(PdfCanvasPageGeometry *)geometry
24
- pxW:(NSInteger)pxW
25
- pxH:(NSInteger)pxH
26
- signal:(nullable PdfCanvasCancellation *)signal
27
- sink:(nullable id<PdfCanvasPixelSink>)sink
28
- error:(NSError *_Nullable *_Nullable)error;
29
- - (BOOL)drawWithCoreGraphics:(PdfCanvasDocumentLease *)lease
30
- request:(PdfCanvasRasterRequest *)request
31
- geometry:(PdfCanvasPageGeometry *)geometry
32
- context:(CGContextRef)ctx
33
- error:(NSError *_Nullable *_Nullable)error;
34
- - (BOOL)drawWithPdfKit:(PdfCanvasDocumentLease *)lease
35
- request:(PdfCanvasRasterRequest *)request
36
- geometry:(PdfCanvasPageGeometry *)geometry
37
- context:(CGContextRef)ctx
38
- error:(NSError *_Nullable *_Nullable)error;
39
- @end
40
-
41
- /**
42
- * The one bitmap configuration this library creates, stated once.
43
- *
44
- * `kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big` is RGBA with
45
- * premultiplied alpha — `PdfCanvasFormatRGBA8888`, and `AlphaType.Premul` after
46
- * `ingest.ts` maps it.
47
- *
48
- * PREMULTIPLIED IS NOT A CHOICE. MEASURED: `CGBitmapContextCreate` REFUSES
49
- * `kCGImageAlphaLast` and `kCGImageAlphaFirst` — straight alpha, either byte order
50
- * — returning NULL, while `premultipliedLast | 32Big`,
51
- * `premultipliedFirst | 32Little` and `noneSkipLast` are all accepted. So the only
52
- * two things an 8-bit RGBA context can be are premultiplied or alpha-less, and
53
- * alpha-less would break `background: 'transparent'`, which is in the
54
- * `RasterRequest` contract. This is the cell the spec's alpha table had marked
55
- * NOT MEASURED; `src/rasterizer/ios.ts` declares the answer and
56
- * `alphaIsPremultiplied` in the Swift suite reads the bytes back off an
57
- * antialiased edge to confirm it (a 62%-covered opaque-white edge reads
58
- * `[a, a, a, a]`, not `[255, 255, 255, a]`).
59
- *
60
- * RGBA RATHER THAN THE APPLE-NATIVE BGRA, deliberately. Both are accepted, and
61
- * BGRA is the platform's preferred order — but cost is flat in scale on this
62
- * engine, so rasterizing dominates and the byte order is not where the time goes,
63
- * whereas matching Android's order means one fewer axis for a cross-platform pixel
64
- * test to account for.
65
- */
66
- static const CGBitmapInfo kPdfCanvasBitmapInfo =
67
- (CGBitmapInfo)kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big;
68
-
69
- /**
70
- * sRGB, named explicitly rather than `CGColorSpaceCreateDeviceRGB()`.
71
- *
72
- * `Skia.Image.MakeImage` is handed an `SkImageInfo` with no colour space, which
73
- * Skia treats as legacy/sRGB, and Android's `ARGB_8888` bitmaps are sRGB. A
74
- * wide-gamut context here would make the identical PDF read differently on iOS
75
- * than on Android with nothing in the pipeline saying so — the numbers would
76
- * simply be different, and every cross-platform pixel comparison would drift.
77
- */
78
- static CGColorSpaceRef PdfCanvasColorSpace(void) {
79
- static CGColorSpaceRef space;
80
- static dispatch_once_t once;
81
- dispatch_once(&once, ^{
82
- space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
83
- });
84
- return space;
85
- }
86
-
87
- @implementation PdfCanvasRasterizer {
88
- PdfCanvasDocumentPool *_pool;
89
- NSArray<PdfCanvasPageGeometry *> *_pages;
90
- BOOL _closed;
91
- }
92
-
93
- + (NSInteger)maxRasterPixels {
94
- return kPdfCanvasMaxRasterPixels;
95
- }
96
-
97
- + (NSString *)rotationSupport {
98
- return @"supported";
99
- }
100
-
101
- /* ---------------------------------------------------------------- *
102
- * Opening
103
- * ---------------------------------------------------------------- */
104
-
105
- + (nullable instancetype)openFileURL:(NSURL *)url
106
- password:(nullable NSString *)password
107
- poolSize:(NSInteger)poolSize
108
- error:(NSError *_Nullable *_Nullable)error {
109
- PdfCanvasDocumentPool *pool = [PdfCanvasDocumentPool openFileURL:url
110
- password:password
111
- size:poolSize
112
- error:error];
113
- return pool == nil ? nil : [self withPool:pool error:error];
114
- }
115
-
116
- + (nullable instancetype)openData:(NSData *)data
117
- password:(nullable NSString *)password
118
- poolSize:(NSInteger)poolSize
119
- error:(NSError *_Nullable *_Nullable)error {
120
- PdfCanvasDocumentPool *pool = [PdfCanvasDocumentPool openData:data
121
- password:password
122
- size:poolSize
123
- error:error];
124
- return pool == nil ? nil : [self withPool:pool error:error];
125
- }
126
-
127
- + (nullable instancetype)withPool:(PdfCanvasDocumentPool *)pool
128
- error:(NSError *_Nullable *_Nullable)error {
129
- PdfCanvasDocumentLease *lease = [pool leaseWithError:error];
130
- if (lease == nil) {
131
- [pool close];
132
- return nil;
133
- }
134
- NSMutableArray<PdfCanvasPageGeometry *> *pages =
135
- [NSMutableArray arrayWithCapacity:(NSUInteger)pool.pageCount];
136
- for (NSInteger i = 0; i < pool.pageCount; i++) {
137
- // CGPDFDocumentGetPage is 1-BASED. Every other index in this package is
138
- // 0-based, so the conversion happens here, once, and nowhere else.
139
- CGPDFPageRef page = CGPDFDocumentGetPage(lease.cgDocument, (size_t)(i + 1));
140
- if (page == NULL) {
141
- [pool returnLease:lease];
142
- [pool close];
143
- // A page the document's own page count promised and cannot produce. Failing
144
- // the OPEN rather than papering over it with a zero size is deliberate: a
145
- // `PageGeometry` is what the layout function turns into the document's whole
146
- // coordinate space, so a wrong one is not a missing page, it is every page
147
- // after it in the wrong place.
148
- return PdfCanvasFail(
149
- error, PdfCanvasErrorCodeCorrupt,
150
- [NSString stringWithFormat:
151
- @"Could not read page %ld of %ld — the page tree "
152
- @"promises it and CoreGraphics cannot produce it.",
153
- (long)i, (long)pool.pageCount],
154
- nil);
155
- }
156
- [pages addObject:[[PdfCanvasPageGeometry alloc]
157
- initWithIndex:i
158
- cropBox:PdfCanvasEffectiveCropBox(page)
159
- rotationDegrees:CGPDFPageGetRotationAngle(page)]];
160
- }
161
- [pool returnLease:lease];
162
- return [[self alloc] initWithPool:pool pages:pages];
163
- }
164
-
165
- - (instancetype)initWithPool:(PdfCanvasDocumentPool *)pool
166
- pages:(NSArray<PdfCanvasPageGeometry *> *)pages {
167
- self = [super init];
168
- if (self != nil) {
169
- _pool = pool;
170
- _pages = pages;
171
- }
172
- return self;
173
- }
174
-
175
- /* ---------------------------------------------------------------- *
176
- * Cheap accessors
177
- * ---------------------------------------------------------------- */
178
-
179
- - (NSInteger)pageCount {
180
- return (NSInteger)_pages.count;
181
- }
182
-
183
- - (NSInteger)poolSize {
184
- return _pool.size;
185
- }
186
-
187
- - (BOOL)isClosed {
188
- return _closed || _pool.isClosed;
189
- }
190
-
191
- - (nullable PdfCanvasPageGeometry *)pageGeometryAtIndex:(NSInteger)index
192
- error:
193
- (NSError *_Nullable
194
- *_Nullable)error {
195
- if (index < 0 || index >= (NSInteger)_pages.count) {
196
- return PdfCanvasFail(
197
- error, PdfCanvasErrorCodeNotFound,
198
- [NSString stringWithFormat:@"Document has %lu pages; asked for %ld.",
199
- (unsigned long)_pages.count, (long)index],
200
- nil);
201
- }
202
- return _pages[(NSUInteger)index];
203
- }
204
-
205
- /* ---------------------------------------------------------------- *
206
- * Rendering
207
- * ---------------------------------------------------------------- */
208
-
209
- - (nullable PdfCanvasRasterPixels *)render:(PdfCanvasRasterRequest *)request
210
- cancellation:
211
- (nullable PdfCanvasCancellation *)signal
212
- sink:(nullable id<PdfCanvasPixelSink>)sink
213
- error:(NSError *_Nullable *_Nullable)error {
214
- if (!PdfCanvasCheckCancelled(signal, @"before start", error)) {
215
- return nil;
216
- }
217
- if (self.isClosed) {
218
- return PdfCanvasFail(error, PdfCanvasErrorCodeBackendFailure,
219
- @"Document handle is closed.", nil);
220
- }
221
-
222
- // Validates the page index before anything expensive is allocated or leased.
223
- PdfCanvasPageGeometry *geometry = [self pageGeometryAtIndex:request.page
224
- error:error];
225
- if (geometry == nil) {
226
- return nil;
227
- }
228
-
229
- if (!(request.scale > 0) || !isfinite(request.scale)) {
230
- return PdfCanvasFail(
231
- error, PdfCanvasErrorCodeBackendFailure,
232
- [NSString stringWithFormat:@"Scale must be finite and positive: %g.",
233
- request.scale],
234
- nil);
235
- }
236
- if (!(request.docWidth > 0) || !(request.docHeight > 0) ||
237
- !isfinite(request.docWidth) || !isfinite(request.docHeight)) {
238
- return PdfCanvasFail(
239
- error, PdfCanvasErrorCodeBackendFailure,
240
- [NSString stringWithFormat:@"Doc rect has no area: %gx%g.",
241
- request.docWidth, request.docHeight],
242
- nil);
243
- }
244
- if (!isfinite(request.docX) || !isfinite(request.docY)) {
245
- return PdfCanvasFail(
246
- error, PdfCanvasErrorCodeBackendFailure,
247
- [NSString stringWithFormat:@"Doc rect origin is not finite: %g,%g.",
248
- request.docX, request.docY],
249
- nil);
250
- }
251
-
252
- const NSInteger pxW = request.pixelWidth;
253
- const NSInteger pxH = request.pixelHeight;
254
- const long long pixels = (long long)pxW * (long long)pxH;
255
- if (pixels > kPdfCanvasMaxRasterPixels) {
256
- return PdfCanvasFail(
257
- error, PdfCanvasErrorCodeOutOfMemory,
258
- [NSString stringWithFormat:
259
- @"Refused a %ldx%ld raster (%lld pixels > %ld).",
260
- (long)pxW, (long)pxH, pixels,
261
- (long)kPdfCanvasMaxRasterPixels],
262
- nil);
263
- }
264
-
265
- PdfCanvasDocumentLease *lease = [_pool leaseWithError:error];
266
- if (lease == nil) {
267
- return nil;
268
- }
269
- @try {
270
- // The lease can block for the length of another render, so this is the check
271
- // that actually saves work when a raster is superseded mid-queue.
272
- if (!PdfCanvasCheckCancelled(signal, @"after pool wait", error)) {
273
- return nil;
274
- }
275
- if (self.isClosed) {
276
- return PdfCanvasFail(error, PdfCanvasErrorCodeBackendFailure,
277
- @"Document handle is closed.", nil);
278
- }
279
- return [self renderLeased:lease
280
- request:request
281
- geometry:geometry
282
- pxW:pxW
283
- pxH:pxH
284
- signal:signal
285
- sink:sink
286
- error:error];
287
- } @finally {
288
- [_pool returnLease:lease];
289
- }
290
- }
291
-
292
- - (nullable PdfCanvasRasterPixels *)
293
- renderLeased:(PdfCanvasDocumentLease *)lease
294
- request:(PdfCanvasRasterRequest *)request
295
- geometry:(PdfCanvasPageGeometry *)geometry
296
- pxW:(NSInteger)pxW
297
- pxH:(NSInteger)pxH
298
- signal:(nullable PdfCanvasCancellation *)signal
299
- sink:(nullable id<PdfCanvasPixelSink>)sink
300
- error:(NSError *_Nullable *_Nullable)error {
301
- // TIGHT, and `CGBitmapContextCreate` constrains what else is possible: MEASURED,
302
- // it refuses a `bytesPerRow` that is not a multiple of 4 at 32 bits per pixel.
303
- // `width * 4` always is.
304
- const NSInteger rowBytes = pxW * 4;
305
- const NSUInteger byteCount = (NSUInteger)(rowBytes * pxH);
306
-
307
- // RESERVE BEFORE DRAWING, and fall back before drawing too. A sink refusal here
308
- // costs nothing; on Android the equivalent refusal can only be discovered after
309
- // the raster exists, which is why that code has a heap path it falls into
310
- // afterwards.
311
- id<PdfCanvasPixelSink> destination = sink;
312
- void *bytes = destination == nil
313
- ? NULL
314
- : [destination pdfCanvasReserveBytes:byteCount];
315
- if (bytes == NULL) {
316
- destination = [[PdfCanvasHeapSink alloc] init];
317
- bytes = [destination pdfCanvasReserveBytes:byteCount];
318
- }
319
- if (bytes == NULL) {
320
- return PdfCanvasFail(
321
- error, PdfCanvasErrorCodeOutOfMemory,
322
- [NSString stringWithFormat:
323
- @"Could not allocate %lu bytes for a %ldx%ld raster.",
324
- (unsigned long)byteCount, (long)pxW, (long)pxH],
325
- nil);
326
- }
327
-
328
- CGContextRef ctx = CGBitmapContextCreate(
329
- bytes, (size_t)pxW, (size_t)pxH, 8, (size_t)rowBytes,
330
- PdfCanvasColorSpace(), kPdfCanvasBitmapInfo);
331
- if (ctx == NULL) {
332
- [destination pdfCanvasAbandon];
333
- return PdfCanvasFail(
334
- error, PdfCanvasErrorCodeBackendFailure,
335
- [NSString stringWithFormat:
336
- @"CGBitmapContextCreate refused %ldx%ld at rowBytes %ld.",
337
- (long)pxW, (long)pxH, (long)rowBytes],
338
- nil);
339
- }
340
-
341
- // THE FILL IS NOT OPTIONAL AND IT IS NOT DEFENSIVE. Neither engine clears the
342
- // destination, and a zeroed RGBA buffer is transparent BLACK — which composites
343
- // under the app's annotation layers as a black page, not a blank one. Ingest
344
- // cannot repair it afterwards: it has no way to tell "the PDF painted black
345
- // here" from "nothing painted here".
346
- //
347
- // `memset` rather than `CGContextFillRect`: 0xFF across the whole buffer is
348
- // exactly opaque white in this configuration, it costs one pass with no CG
349
- // state involved, and it covers any row padding a future alignment change might
350
- // add. The transparent case needs nothing, because both sinks reserve zeroed
351
- // memory — which is a documented part of the sink contract rather than luck.
352
- if (request.background == PdfCanvasBackgroundWhite) {
353
- memset(bytes, 0xFF, byteCount);
354
- }
355
-
356
- CGContextSetInterpolationQuality(ctx, kCGInterpolationHigh);
357
- CGContextSetShouldAntialias(ctx, true);
358
- // Explicitly OFF. Subpixel text positioning changes glyph rasterization in a
359
- // way that depends on the destination's pixel grid, and every seam in this
360
- // package is two rasters of the same geometry into differently-sized surfaces.
361
- // Leaving it on would widen the sub-pixel disagreement the overlap rule already
362
- // has to tolerate. It has no effect on a bitmap context on iOS in any case, so
363
- // this is a statement of intent that costs nothing.
364
- CGContextSetShouldSubpixelPositionFonts(ctx, false);
365
- CGContextSetShouldSubpixelQuantizeFonts(ctx, false);
366
-
367
- CGContextConcatCTM(
368
- ctx, PdfCanvasDocToRasterTransform(request.docX, request.docY,
369
- request.scale, pxH));
370
-
371
- BOOL drawn = NO;
372
- if (request.annotations) {
373
- drawn = [self drawWithPdfKit:lease
374
- request:request
375
- geometry:geometry
376
- context:ctx
377
- error:error];
378
- } else {
379
- drawn = [self drawWithCoreGraphics:lease
380
- request:request
381
- geometry:geometry
382
- context:ctx
383
- error:error];
384
- }
385
- // FLUSHED BEFORE THE BYTES ARE READ BY ANYBODY. A bitmap context is documented
386
- // to be safe to read after the drawing calls return, but a flush is what makes
387
- // that true across an OS that may have deferred work, and it is free relative
388
- // to the draw. It must happen before the context is released, not after.
389
- CGContextFlush(ctx);
390
- CGContextRelease(ctx);
391
-
392
- if (!drawn) {
393
- [destination pdfCanvasAbandon];
394
- return nil;
395
- }
396
-
397
- // Post-check. This saves no drawing — the pixels above are already finished — it
398
- // only stops a superseded raster being handed back and ingested into Skia, which
399
- // is the expensive half on the JS side.
400
- //
401
- // THE ABANDON IS NOT OPTIONAL. A heap-backed raster would simply be collected,
402
- // but a slot-backed one is about to be published to a process-wide map where
403
- // nothing else will ever look for it. A superseded epoch produces a stream of
404
- // these, so leaking here is megabytes per gesture. The check stays AFTER the
405
- // draw rather than moving in front of it so the cancel window it covers does not
406
- // shrink.
407
- if (!PdfCanvasCheckCancelled(signal, @"after render", error)) {
408
- [destination pdfCanvasAbandon];
409
- return nil;
410
- }
411
-
412
- const int64_t slot = [destination pdfCanvasPublish];
413
- NSData *heapBytes = [destination isKindOfClass:PdfCanvasHeapSink.class]
414
- ? ((PdfCanvasHeapSink *)destination).data
415
- : nil;
416
- if (slot == 0 && heapBytes == nil) {
417
- // A sink that reserved successfully, published no identifier, and is not the
418
- // heap sink — so the raster exists and there is no way to reach it. Unreachable
419
- // with the two sinks that exist, and named here anyway: without this the failure
420
- // surfaces two layers up as "the native module returned neither `base64` nor
421
- // `pixelsSlot`", which sends a reader looking at the bridge instead of at their
422
- // sink.
423
- [destination pdfCanvasAbandon];
424
- return PdfCanvasFail(
425
- error, PdfCanvasErrorCodeBackendFailure,
426
- @"A PdfCanvasPixelSink published slot 0 without being a PdfCanvasHeapSink, "
427
- @"so the finished raster is unreachable. A sink must either return a "
428
- @"non-zero identifier from pdfCanvasPublish or expose its bytes the way "
429
- @"PdfCanvasHeapSink does.",
430
- nil);
431
- }
432
- return [[PdfCanvasRasterPixels alloc] initWithSink:destination
433
- slot:slot
434
- bytes:heapBytes
435
- width:pxW
436
- height:pxH
437
- rowBytes:rowBytes
438
- format:PdfCanvasFormatRGBA8888];
439
- }
440
-
441
- /**
442
- * The tile path: content stream only, no annotations.
443
- *
444
- * `CGContextDrawPDFPage` applies NEITHER the crop box origin NOR `/Rotate`
445
- * (measured — see `PdfCanvasPageTransform.h`), so both are in the transform, and
446
- * the clip is what keeps content outside the crop box out of the raster. The clip
447
- * is applied AFTER the page transform, so its rect is in PDF user space.
448
- */
449
- - (BOOL)drawWithCoreGraphics:(PdfCanvasDocumentLease *)lease
450
- request:(PdfCanvasRasterRequest *)request
451
- geometry:(PdfCanvasPageGeometry *)geometry
452
- context:(CGContextRef)ctx
453
- error:(NSError *_Nullable *_Nullable)error {
454
- CGPDFPageRef page =
455
- CGPDFDocumentGetPage(lease.cgDocument, (size_t)(request.page + 1));
456
- if (page == NULL) {
457
- PdfCanvasFail(
458
- error, PdfCanvasErrorCodeNotFound,
459
- [NSString stringWithFormat:@"No page %ld in this document.",
460
- (long)request.page],
461
- nil);
462
- return NO;
463
- }
464
- CGContextConcatCTM(
465
- ctx, PdfCanvasPdfToDocTransform(geometry.cropBox,
466
- geometry.rotationDegrees));
467
- CGContextClipToRect(ctx, geometry.cropBox);
468
- CGContextDrawPDFPage(ctx, page);
469
- return YES;
470
- }
471
-
472
- /**
473
- * The annotation path: PDFKit, which composites appearance streams.
474
- *
475
- * NOTHING HERE ROTATES ANYTHING. PDFKit applies `/Rotate` and normalises the box
476
- * origin itself, so the only transform is a y-flip — and it must be through the
477
- * POST-ROTATION height, which is `geometry.heightPoints` and emphatically not
478
- * `-[PDFPage boundsForBox:].size.height`. That method does not apply `/Rotate`
479
- * while `drawWithBox:` does, and flipping by the wrong one draws the page rotated,
480
- * off-centre and clipped with no error anywhere. See `PdfCanvasPageTransform.h`.
481
- *
482
- * NO CLIP. PDFKit draws the box it is given and nothing outside it, so the clip
483
- * the CoreGraphics path needs would be redundant here — and a clip in the wrong
484
- * space (this one is doc space, not PDF user space) would silently cut the page.
485
- */
486
- - (BOOL)drawWithPdfKit:(PdfCanvasDocumentLease *)lease
487
- request:(PdfCanvasRasterRequest *)request
488
- geometry:(PdfCanvasPageGeometry *)geometry
489
- context:(CGContextRef)ctx
490
- error:(NSError *_Nullable *_Nullable)error {
491
- PDFDocument *document = [lease pdfKitDocumentWithError:error];
492
- if (document == nil) {
493
- return NO;
494
- }
495
- PDFPage *page = [document pageAtIndex:(NSUInteger)request.page];
496
- if (page == nil) {
497
- PdfCanvasFail(
498
- error, PdfCanvasErrorCodeNotFound,
499
- [NSString stringWithFormat:@"PDFKit has no page %ld in this document.",
500
- (long)request.page],
501
- nil);
502
- return NO;
503
- }
504
- CGContextConcatCTM(
505
- ctx, PdfCanvasDocToPdfKitTransform(geometry.heightPoints));
506
- [page drawWithBox:kPDFDisplayBoxCropBox toContext:ctx];
507
- return YES;
508
- }
509
-
510
- /* ---------------------------------------------------------------- *
511
- * Teardown
512
- * ---------------------------------------------------------------- */
513
-
514
- - (void)close {
515
- if (_closed) {
516
- return;
517
- }
518
- _closed = YES;
519
- [_pool close];
520
- }
521
-
522
- @end
523
-
524
- /* ------------------------------------------------------------------ *
525
- * The crop box
526
- * ------------------------------------------------------------------ */
527
-
528
- CGRect PdfCanvasEffectiveCropBox(CGPDFPageRef page) {
529
- const CGRect media = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
530
- const CGRect crop = CGPDFPageGetBoxRect(page, kCGPDFCropBox);
531
- // INTERSECTED, per the PDF spec: "the crop box shall be intersected with the
532
- // media box". CoreGraphics returns the media box when there is no crop box, so
533
- // the common case is a no-op, and it does NOT do the intersection itself — a
534
- // file whose crop box overhangs its media box would otherwise produce a page
535
- // larger than the paper, and every rect the layout function derives from it
536
- // would be wrong by the overhang.
537
- //
538
- // A degenerate result falls back to the media box rather than to an empty rect:
539
- // a zero-area page would make `fitScale` produce a non-finite number and the
540
- // planner would emit nothing at all, which presents as a blank document with no
541
- // error. CGRectIntersection returns CGRectNull for non-overlapping rects, and
542
- // CGRectNull's width and height are 0.
543
- const CGRect intersected = CGRectIntersection(media, crop);
544
- if (CGRectIsNull(intersected) || CGRectIsEmpty(intersected)) {
545
- return media;
546
- }
547
- return intersected;
548
- }
@@ -1,48 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
-
3
- #import "PdfCanvasErrorCode.h"
4
-
5
- NS_ASSUME_NONNULL_BEGIN
6
-
7
- /**
8
- * Best-effort cancellation, and the emphasis is on BEST-EFFORT.
9
- *
10
- * `CGContextDrawPDFPage` and `-[PDFPage drawWithBox:toContext:]` are synchronous
11
- * and uninterruptible once entered. There is no cancel token, no progress
12
- * callback, and no safe way to kill the thread — CoreGraphics is mid-way through
13
- * writing into a buffer this process owns, and on the fast transport that buffer
14
- * is about to become a JS `ArrayBuffer`. So this class cannot stop a render; it
15
- * can only stop the NEXT one starting, which is exactly the shape the TS contract
16
- * already assumes for this platform — `RasterPolicy.minEpochIntervalMs` names
17
- * PDFKit in its own doc comment as the reason it exists.
18
- *
19
- * Concretely, a cancel is observed at four points and no others: on entry, after
20
- * the pool lease (the wait can be long when every document is busy), immediately
21
- * before the draw, and immediately after it. The last one does not save any work —
22
- * the pixels are already drawn — it only stops a superseded raster being handed
23
- * back and ingested into Skia, and it is the one that must release the slot.
24
- *
25
- * `_Atomic(bool)` rather than a lock: it is written by the JS thread through
26
- * `cancel(handle, token)` and read by a render worker, and there is nothing to
27
- * order it against. C11 atomics are available in a plain `.m` file, which keeps
28
- * `PdfCanvasCore` free of C++ and therefore importable from Swift.
29
- */
30
- @interface PdfCanvasCancellation : NSObject
31
-
32
- - (void)cancel;
33
- @property(nonatomic, readonly, getter=isCancelled) BOOL cancelled;
34
-
35
- @end
36
-
37
- /**
38
- * Returns NO and fills `error` with a `cancelled` code when the signal is set.
39
- *
40
- * @param stage named in the message so a cancellation reported from the
41
- * post-render check is distinguishable from one that saved real work.
42
- */
43
- FOUNDATION_EXPORT BOOL PdfCanvasCheckCancelled(
44
- PdfCanvasCancellation *_Nullable signal,
45
- NSString *stage,
46
- NSError *_Nullable *_Nullable error);
47
-
48
- NS_ASSUME_NONNULL_END
@@ -1,26 +0,0 @@
1
- /**
2
- * The umbrella header for `PdfCanvasCore` — the React-free iOS rasterizer.
3
- *
4
- * It exists so that both build systems land on the same module: SwiftPM prefers
5
- * `include/<TargetName>.h` as a target's umbrella header when it is present (and
6
- * would otherwise synthesise an umbrella DIRECTORY module, whose contents depend
7
- * on the file listing), and CocoaPods needs a modulemap to make
8
- * `import PdfCanvasCore` work from Swift at all. One hand-written list is
9
- * deterministic; two generated ones are not.
10
- *
11
- * NOTHING HERE IMPORTS UIKit, JSI, OR REACT NATIVE, and that is enforced by the
12
- * fact that `ios/Package.swift` builds this target for macOS so `swift test` can
13
- * run it on a laptop. A UIKit import would break that build immediately, which is
14
- * the failure mode worth having.
15
- */
16
-
17
- #import "PdfCanvasCancellation.h"
18
- #import "PdfCanvasDocumentPool.h"
19
- #import "PdfCanvasErrorCode.h"
20
- #import "PdfCanvasPageGeometry.h"
21
- #import "PdfCanvasPageTransform.h"
22
- #import "PdfCanvasPixelSink.h"
23
- #import "PdfCanvasPreflight.h"
24
- #import "PdfCanvasRasterPixels.h"
25
- #import "PdfCanvasRasterRequest.h"
26
- #import "PdfCanvasRasterizer.h"