@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.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +77 -40
- package/README.md +218 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +559 -0
- package/scripts/pdfium-manifest.json +48 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
#import "PdfCanvasDocumentPool.h"
|
|
2
|
-
|
|
3
|
-
#import "PdfCanvasPreflight.h"
|
|
4
|
-
|
|
5
|
-
const NSInteger kPdfCanvasMaxPoolSize = 4;
|
|
6
|
-
|
|
7
|
-
/* ------------------------------------------------------------------ *
|
|
8
|
-
* Private surfaces
|
|
9
|
-
* ------------------------------------------------------------------ */
|
|
10
|
-
|
|
11
|
-
@interface PdfCanvasDocumentLease ()
|
|
12
|
-
/**
|
|
13
|
-
* @param document retained by the lease and released on dealloc, which is what
|
|
14
|
-
* makes close-while-leased safe — see the pool's header note. The caller
|
|
15
|
-
* keeps its own reference and must balance its own create.
|
|
16
|
-
*/
|
|
17
|
-
- (instancetype)initWithDocument:(CGPDFDocumentRef)document
|
|
18
|
-
fileURL:(nullable NSURL *)fileURL
|
|
19
|
-
data:(nullable NSData *)data
|
|
20
|
-
password:(nullable NSString *)password;
|
|
21
|
-
@end
|
|
22
|
-
|
|
23
|
-
@interface PdfCanvasDocumentPool ()
|
|
24
|
-
+ (NSInteger)clampSize:(NSInteger)size;
|
|
25
|
-
+ (nullable instancetype)poolWithLeases:
|
|
26
|
-
(NSArray<PdfCanvasDocumentLease *> *)leases
|
|
27
|
-
label:(NSString *)label
|
|
28
|
-
error:(NSError *_Nullable *_Nullable)error;
|
|
29
|
-
- (instancetype)initWithLeases:(NSArray<PdfCanvasDocumentLease *> *)leases
|
|
30
|
-
pageCount:(NSInteger)pageCount;
|
|
31
|
-
@end
|
|
32
|
-
|
|
33
|
-
/* ------------------------------------------------------------------ *
|
|
34
|
-
* Opening one document
|
|
35
|
-
* ------------------------------------------------------------------ */
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Unlocks a CGPDF document, or explains why it could not be.
|
|
39
|
-
*
|
|
40
|
-
* The three-way answer is the point, and CoreGraphics gives it cleanly where
|
|
41
|
-
* Android has to infer it from a `SecurityException`:
|
|
42
|
-
*
|
|
43
|
-
* - not encrypted, or encrypted with an EMPTY user password (owner-password-only
|
|
44
|
-
* files, which are common) — `CGPDFDocumentIsUnlocked` is already true and
|
|
45
|
-
* there is nothing to do. Refusing these because `IsEncrypted` is true would
|
|
46
|
-
* reject a large share of real documents that every reader opens.
|
|
47
|
-
* - encrypted, password supplied, `CGPDFDocumentUnlockWithPassword` false →
|
|
48
|
-
* `password-incorrect`.
|
|
49
|
-
* - encrypted, no password → `password-required`.
|
|
50
|
-
*
|
|
51
|
-
* Note what this backend can do that Android cannot do at all: `PdfRendererPool`
|
|
52
|
-
* REJECTS a non-nil password with `unsupported`, because
|
|
53
|
-
* `android.graphics.pdf.LoadParams` — the only way to supply one — landed at API
|
|
54
|
-
* 35. Encrypted documents work here.
|
|
55
|
-
*/
|
|
56
|
-
static BOOL UnlockCGDocument(
|
|
57
|
-
CGPDFDocumentRef document,
|
|
58
|
-
NSString *_Nullable password,
|
|
59
|
-
NSString *label,
|
|
60
|
-
NSError *_Nullable *_Nullable error) {
|
|
61
|
-
if (CGPDFDocumentIsUnlocked(document)) {
|
|
62
|
-
return YES;
|
|
63
|
-
}
|
|
64
|
-
if (password.length > 0) {
|
|
65
|
-
if (CGPDFDocumentUnlockWithPassword(document, password.UTF8String)) {
|
|
66
|
-
return YES;
|
|
67
|
-
}
|
|
68
|
-
PdfCanvasFail(
|
|
69
|
-
error, PdfCanvasErrorCodePasswordIncorrect,
|
|
70
|
-
[NSString stringWithFormat:@"The password for %@ was rejected.", label],
|
|
71
|
-
nil);
|
|
72
|
-
return NO;
|
|
73
|
-
}
|
|
74
|
-
PdfCanvasFail(
|
|
75
|
-
error, PdfCanvasErrorCodePasswordRequired,
|
|
76
|
-
[NSString stringWithFormat:@"%@ is encrypted and needs a password.", label],
|
|
77
|
-
nil);
|
|
78
|
-
return NO;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/* ------------------------------------------------------------------ *
|
|
82
|
-
* The lease
|
|
83
|
-
* ------------------------------------------------------------------ */
|
|
84
|
-
|
|
85
|
-
@implementation PdfCanvasDocumentLease {
|
|
86
|
-
CGPDFDocumentRef _cgDocument;
|
|
87
|
-
/** Whichever of the two the pool was opened from; exactly one is non-nil. */
|
|
88
|
-
NSURL *_fileURL;
|
|
89
|
-
NSData *_data;
|
|
90
|
-
NSString *_password;
|
|
91
|
-
PDFDocument *_pdfKitDocument;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
- (instancetype)initWithDocument:(CGPDFDocumentRef)document
|
|
95
|
-
fileURL:(nullable NSURL *)fileURL
|
|
96
|
-
data:(nullable NSData *)data
|
|
97
|
-
password:(nullable NSString *)password {
|
|
98
|
-
self = [super init];
|
|
99
|
-
if (self != nil) {
|
|
100
|
-
_cgDocument = CGPDFDocumentRetain(document);
|
|
101
|
-
_fileURL = fileURL;
|
|
102
|
-
_data = data;
|
|
103
|
-
_password = [password copy];
|
|
104
|
-
}
|
|
105
|
-
return self;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
- (void)dealloc {
|
|
109
|
-
if (_cgDocument != NULL) {
|
|
110
|
-
CGPDFDocumentRelease(_cgDocument);
|
|
111
|
-
_cgDocument = NULL;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
- (CGPDFDocumentRef)cgDocument {
|
|
116
|
-
return _cgDocument;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
- (nullable PDFDocument *)pdfKitDocumentWithError:
|
|
120
|
-
(NSError *_Nullable *_Nullable)error {
|
|
121
|
-
if (_pdfKitDocument != nil) {
|
|
122
|
-
return _pdfKitDocument;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
PDFDocument *document = _fileURL != nil
|
|
126
|
-
? [[PDFDocument alloc] initWithURL:_fileURL]
|
|
127
|
-
: [[PDFDocument alloc] initWithData:_data];
|
|
128
|
-
if (document == nil) {
|
|
129
|
-
// CoreGraphics accepted this file and PDFKit did not, which does happen — the
|
|
130
|
-
// two have separate parsers. Named as such rather than reported as a generic
|
|
131
|
-
// failure, because the actionable answer is "ask for this raster without
|
|
132
|
-
// annotations", and only a caller can decide that.
|
|
133
|
-
return PdfCanvasFail(
|
|
134
|
-
error, PdfCanvasErrorCodeCorrupt,
|
|
135
|
-
@"PDFKit refused a document CoreGraphics accepted, so annotations "
|
|
136
|
-
@"cannot be drawn for it. Request the raster with annotations: false to "
|
|
137
|
-
@"use the CoreGraphics path instead.",
|
|
138
|
-
nil);
|
|
139
|
-
}
|
|
140
|
-
if (document.isLocked) {
|
|
141
|
-
if (_password.length > 0) {
|
|
142
|
-
if (![document unlockWithPassword:_password]) {
|
|
143
|
-
return PdfCanvasFail(
|
|
144
|
-
error, PdfCanvasErrorCodePasswordIncorrect,
|
|
145
|
-
@"PDFKit rejected the password, though CoreGraphics accepted it.",
|
|
146
|
-
nil);
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
return PdfCanvasFail(
|
|
150
|
-
error, PdfCanvasErrorCodePasswordRequired,
|
|
151
|
-
@"PDFKit reports this document as locked and no password was given.",
|
|
152
|
-
nil);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
_pdfKitDocument = document;
|
|
156
|
-
return document;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
@end
|
|
160
|
-
|
|
161
|
-
/* ------------------------------------------------------------------ *
|
|
162
|
-
* The pool
|
|
163
|
-
* ------------------------------------------------------------------ */
|
|
164
|
-
|
|
165
|
-
@implementation PdfCanvasDocumentPool {
|
|
166
|
-
NSMutableArray<PdfCanvasDocumentLease *> *_free;
|
|
167
|
-
/** Guards `_free` and `_closed`. Never held across a render or an open. */
|
|
168
|
-
NSLock *_lock;
|
|
169
|
-
/** Counts available lanes, so `lease` blocks rather than spins. */
|
|
170
|
-
dispatch_semaphore_t _available;
|
|
171
|
-
NSInteger _pageCount;
|
|
172
|
-
NSInteger _size;
|
|
173
|
-
BOOL _closed;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
+ (NSInteger)clampSize:(NSInteger)size {
|
|
177
|
-
if (size < 1) return 1;
|
|
178
|
-
if (size > kPdfCanvasMaxPoolSize) return kPdfCanvasMaxPoolSize;
|
|
179
|
-
return size;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
+ (nullable instancetype)openFileURL:(NSURL *)url
|
|
183
|
-
password:(nullable NSString *)password
|
|
184
|
-
size:(NSInteger)size
|
|
185
|
-
error:(NSError *_Nullable *_Nullable)error {
|
|
186
|
-
if (![PdfCanvasPreflight checkFileURL:url error:error]) {
|
|
187
|
-
return nil;
|
|
188
|
-
}
|
|
189
|
-
const NSInteger lanes = [self clampSize:size];
|
|
190
|
-
NSMutableArray<PdfCanvasDocumentLease *> *leases =
|
|
191
|
-
[NSMutableArray arrayWithCapacity:(NSUInteger)lanes];
|
|
192
|
-
NSString *label = url.lastPathComponent.length > 0 ? url.lastPathComponent
|
|
193
|
-
: url.path;
|
|
194
|
-
|
|
195
|
-
for (NSInteger i = 0; i < lanes; i++) {
|
|
196
|
-
CGPDFDocumentRef document =
|
|
197
|
-
CGPDFDocumentCreateWithURL((__bridge CFURLRef)url);
|
|
198
|
-
if (document == NULL) {
|
|
199
|
-
// Preflight already ruled out "missing", "unreadable", "empty" and "no
|
|
200
|
-
// header", so what is left is structure CoreGraphics could not parse.
|
|
201
|
-
PdfCanvasFail(
|
|
202
|
-
error, PdfCanvasErrorCodeCorrupt,
|
|
203
|
-
[NSString stringWithFormat:
|
|
204
|
-
@"CoreGraphics could not parse %@ — it has a %%PDF- "
|
|
205
|
-
@"header but no usable cross-reference table or page "
|
|
206
|
-
@"tree.",
|
|
207
|
-
label],
|
|
208
|
-
nil);
|
|
209
|
-
return nil;
|
|
210
|
-
}
|
|
211
|
-
const BOOL unlocked = UnlockCGDocument(document, password, label, error);
|
|
212
|
-
if (!unlocked) {
|
|
213
|
-
CGPDFDocumentRelease(document);
|
|
214
|
-
return nil;
|
|
215
|
-
}
|
|
216
|
-
[leases addObject:[[PdfCanvasDocumentLease alloc] initWithDocument:document
|
|
217
|
-
fileURL:url
|
|
218
|
-
data:nil
|
|
219
|
-
password:password]];
|
|
220
|
-
// The lease retained it; this balances the create above.
|
|
221
|
-
CGPDFDocumentRelease(document);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return [self poolWithLeases:leases label:label error:error];
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
+ (nullable instancetype)openData:(NSData *)data
|
|
228
|
-
password:(nullable NSString *)password
|
|
229
|
-
size:(NSInteger)size
|
|
230
|
-
error:(NSError *_Nullable *_Nullable)error {
|
|
231
|
-
if (![PdfCanvasPreflight checkData:data error:error]) {
|
|
232
|
-
return nil;
|
|
233
|
-
}
|
|
234
|
-
const NSInteger lanes = [self clampSize:size];
|
|
235
|
-
NSMutableArray<PdfCanvasDocumentLease *> *leases =
|
|
236
|
-
[NSMutableArray arrayWithCapacity:(NSUInteger)lanes];
|
|
237
|
-
|
|
238
|
-
for (NSInteger i = 0; i < lanes; i++) {
|
|
239
|
-
// One provider per lane over the SAME immutable NSData. The provider retains
|
|
240
|
-
// the data, so the bytes outlive this scope, and nothing writes to them.
|
|
241
|
-
CGDataProviderRef provider =
|
|
242
|
-
CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
|
|
243
|
-
if (provider == NULL) {
|
|
244
|
-
PdfCanvasFail(error, PdfCanvasErrorCodeOutOfMemory,
|
|
245
|
-
@"Could not wrap the document bytes in a CGDataProvider.",
|
|
246
|
-
nil);
|
|
247
|
-
return nil;
|
|
248
|
-
}
|
|
249
|
-
CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider);
|
|
250
|
-
CGDataProviderRelease(provider);
|
|
251
|
-
if (document == NULL) {
|
|
252
|
-
PdfCanvasFail(
|
|
253
|
-
error, PdfCanvasErrorCodeCorrupt,
|
|
254
|
-
@"CoreGraphics could not parse the document bytes — they carry a "
|
|
255
|
-
@"%PDF- header but no usable cross-reference table or page tree.",
|
|
256
|
-
nil);
|
|
257
|
-
return nil;
|
|
258
|
-
}
|
|
259
|
-
const BOOL unlocked =
|
|
260
|
-
UnlockCGDocument(document, password, @"the document", error);
|
|
261
|
-
if (!unlocked) {
|
|
262
|
-
CGPDFDocumentRelease(document);
|
|
263
|
-
return nil;
|
|
264
|
-
}
|
|
265
|
-
[leases addObject:[[PdfCanvasDocumentLease alloc] initWithDocument:document
|
|
266
|
-
fileURL:nil
|
|
267
|
-
data:data
|
|
268
|
-
password:password]];
|
|
269
|
-
CGPDFDocumentRelease(document);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return [self poolWithLeases:leases label:@"the document" error:error];
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
+ (nullable instancetype)poolWithLeases:
|
|
276
|
-
(NSArray<PdfCanvasDocumentLease *> *)leases
|
|
277
|
-
label:(NSString *)label
|
|
278
|
-
error:(NSError *_Nullable *_Nullable)error {
|
|
279
|
-
const size_t pages =
|
|
280
|
-
CGPDFDocumentGetNumberOfPages(leases.firstObject.cgDocument);
|
|
281
|
-
if (pages == 0) {
|
|
282
|
-
// Reachable for a document that parsed but has an empty or unreadable page
|
|
283
|
-
// tree, and for one that is still locked — `CGPDFDocumentGetNumberOfPages`
|
|
284
|
-
// reports 0 rather than failing. The unlock above has already ruled the
|
|
285
|
-
// second out, so this is the first.
|
|
286
|
-
return PdfCanvasFail(
|
|
287
|
-
error, PdfCanvasErrorCodeCorrupt,
|
|
288
|
-
[NSString stringWithFormat:@"%@ reports 0 pages.", label], nil);
|
|
289
|
-
}
|
|
290
|
-
return [[self alloc] initWithLeases:leases pageCount:(NSInteger)pages];
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
- (instancetype)initWithLeases:(NSArray<PdfCanvasDocumentLease *> *)leases
|
|
294
|
-
pageCount:(NSInteger)pageCount {
|
|
295
|
-
self = [super init];
|
|
296
|
-
if (self != nil) {
|
|
297
|
-
_free = [leases mutableCopy];
|
|
298
|
-
_lock = [[NSLock alloc] init];
|
|
299
|
-
_available = dispatch_semaphore_create((long)leases.count);
|
|
300
|
-
_pageCount = pageCount;
|
|
301
|
-
_size = (NSInteger)leases.count;
|
|
302
|
-
}
|
|
303
|
-
return self;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
- (NSInteger)pageCount {
|
|
307
|
-
return _pageCount;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
- (NSInteger)size {
|
|
311
|
-
return _size;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
- (BOOL)isClosed {
|
|
315
|
-
[_lock lock];
|
|
316
|
-
const BOOL closed = _closed;
|
|
317
|
-
[_lock unlock];
|
|
318
|
-
return closed;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
- (nullable PdfCanvasDocumentLease *)leaseWithError:
|
|
322
|
-
(NSError *_Nullable *_Nullable)error {
|
|
323
|
-
if (self.isClosed) {
|
|
324
|
-
return PdfCanvasFail(error, PdfCanvasErrorCodeBackendFailure,
|
|
325
|
-
@"Document handle is closed.", nil);
|
|
326
|
-
}
|
|
327
|
-
dispatch_semaphore_wait(_available, DISPATCH_TIME_FOREVER);
|
|
328
|
-
|
|
329
|
-
[_lock lock];
|
|
330
|
-
PdfCanvasDocumentLease *lease = _free.lastObject;
|
|
331
|
-
if (lease != nil) {
|
|
332
|
-
[_free removeLastObject];
|
|
333
|
-
}
|
|
334
|
-
const BOOL closed = _closed;
|
|
335
|
-
[_lock unlock];
|
|
336
|
-
|
|
337
|
-
if (closed || lease == nil) {
|
|
338
|
-
// `close` raced us and emptied the free list. The count consumed above is
|
|
339
|
-
// handed back so a second waiter is not left blocked forever.
|
|
340
|
-
dispatch_semaphore_signal(_available);
|
|
341
|
-
return PdfCanvasFail(error, PdfCanvasErrorCodeBackendFailure,
|
|
342
|
-
@"Document handle was closed while waiting for a lane.",
|
|
343
|
-
nil);
|
|
344
|
-
}
|
|
345
|
-
return lease;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
- (void)returnLease:(nullable PdfCanvasDocumentLease *)lease {
|
|
349
|
-
if (lease == nil) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
[_lock lock];
|
|
353
|
-
// A lease returned to a CLOSED pool is dropped rather than re-queued, which is
|
|
354
|
-
// where its last reference goes and therefore where the parsed document is
|
|
355
|
-
// finally freed. The semaphore is still signalled: an unbalanced wait would
|
|
356
|
-
// wedge any render that raced the close.
|
|
357
|
-
const BOOL requeue = !_closed;
|
|
358
|
-
if (requeue) {
|
|
359
|
-
[_free addObject:lease];
|
|
360
|
-
}
|
|
361
|
-
[_lock unlock];
|
|
362
|
-
dispatch_semaphore_signal(_available);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
- (void)close {
|
|
366
|
-
// Held OUTSIDE the lock, deliberately: `dropped` keeps the leases alive past
|
|
367
|
-
// the unlock so that releasing several parsed ARCH-E documents does not happen
|
|
368
|
-
// while a render worker might be waiting on the same lock. Assigning nil after
|
|
369
|
-
// the unlock is what forces that — ARC is free to shorten a local's lifetime to
|
|
370
|
-
// its last use, so relying on end-of-scope here would be relying on the
|
|
371
|
-
// optimiser not to do its job.
|
|
372
|
-
NSArray<PdfCanvasDocumentLease *> *dropped = nil;
|
|
373
|
-
[_lock lock];
|
|
374
|
-
if (_closed) {
|
|
375
|
-
[_lock unlock];
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
_closed = YES;
|
|
379
|
-
// Dropping the FREE leases releases their documents. A lease that is currently
|
|
380
|
-
// OUT is untouched: the render worker owns it, and it is freed when
|
|
381
|
-
// `returnLease:` declines to re-queue it. Non-blocking, and with no window —
|
|
382
|
-
// see the header.
|
|
383
|
-
dropped = [_free copy];
|
|
384
|
-
[_free removeAllObjects];
|
|
385
|
-
[_lock unlock];
|
|
386
|
-
dropped = nil;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
@end
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#import "PdfCanvasErrorCode.h"
|
|
2
|
-
|
|
3
|
-
NSErrorDomain const PdfCanvasErrorDomain = @"tools.reekon.pdfcanvas";
|
|
4
|
-
|
|
5
|
-
NSString *PdfCanvasErrorCodeWire(PdfCanvasErrorCode code) {
|
|
6
|
-
switch (code) {
|
|
7
|
-
case PdfCanvasErrorCodeNotFound:
|
|
8
|
-
return @"not-found";
|
|
9
|
-
case PdfCanvasErrorCodePasswordRequired:
|
|
10
|
-
return @"password-required";
|
|
11
|
-
case PdfCanvasErrorCodePasswordIncorrect:
|
|
12
|
-
return @"password-incorrect";
|
|
13
|
-
case PdfCanvasErrorCodeCorrupt:
|
|
14
|
-
return @"corrupt";
|
|
15
|
-
case PdfCanvasErrorCodeUnsupported:
|
|
16
|
-
return @"unsupported";
|
|
17
|
-
case PdfCanvasErrorCodeCancelled:
|
|
18
|
-
return @"cancelled";
|
|
19
|
-
case PdfCanvasErrorCodeOutOfMemory:
|
|
20
|
-
return @"out-of-memory";
|
|
21
|
-
case PdfCanvasErrorCodeBackendFailure:
|
|
22
|
-
return @"backend-failure";
|
|
23
|
-
}
|
|
24
|
-
// No `default:` above, so a new member is a compiler warning rather than a
|
|
25
|
-
// silent fall-through to this line. This exists only because the enum is an
|
|
26
|
-
// NSInteger and a caller could cast anything into it.
|
|
27
|
-
return @"backend-failure";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
NSError *PdfCanvasMakeError(
|
|
31
|
-
PdfCanvasErrorCode code, NSString *message, NSError *_Nullable underlying) {
|
|
32
|
-
NSMutableDictionary<NSErrorUserInfoKey, id> *info =
|
|
33
|
-
[NSMutableDictionary dictionaryWithCapacity:3];
|
|
34
|
-
info[NSLocalizedDescriptionKey] = message ?: @"PDF Canvas failed.";
|
|
35
|
-
if (underlying != nil) {
|
|
36
|
-
info[NSUnderlyingErrorKey] = underlying;
|
|
37
|
-
}
|
|
38
|
-
return [NSError errorWithDomain:PdfCanvasErrorDomain code:code userInfo:info];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
PdfCanvasErrorCode PdfCanvasErrorCodeOf(NSError *error) {
|
|
42
|
-
if (![error.domain isEqualToString:PdfCanvasErrorDomain]) {
|
|
43
|
-
return PdfCanvasErrorCodeBackendFailure;
|
|
44
|
-
}
|
|
45
|
-
return (PdfCanvasErrorCode)error.code;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
id _Nullable PdfCanvasFail(
|
|
49
|
-
NSError *_Nullable *_Nullable out,
|
|
50
|
-
PdfCanvasErrorCode code,
|
|
51
|
-
NSString *message,
|
|
52
|
-
NSError *_Nullable underlying) {
|
|
53
|
-
if (out != NULL) {
|
|
54
|
-
*out = PdfCanvasMakeError(code, message, underlying);
|
|
55
|
-
}
|
|
56
|
-
return nil;
|
|
57
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#import "PdfCanvasPageGeometry.h"
|
|
2
|
-
|
|
3
|
-
NSInteger PdfCanvasNormalizeRotation(NSInteger degrees) {
|
|
4
|
-
NSInteger wrapped = degrees % 360;
|
|
5
|
-
if (wrapped < 0) {
|
|
6
|
-
wrapped += 360;
|
|
7
|
-
}
|
|
8
|
-
return (wrapped % 90 == 0) ? wrapped : 0;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
@implementation PdfCanvasPageGeometry
|
|
12
|
-
|
|
13
|
-
- (instancetype)initWithIndex:(NSInteger)index
|
|
14
|
-
cropBox:(CGRect)cropBox
|
|
15
|
-
rotationDegrees:(NSInteger)rotationDegrees {
|
|
16
|
-
self = [super init];
|
|
17
|
-
if (self != nil) {
|
|
18
|
-
_index = index;
|
|
19
|
-
_cropBox = cropBox;
|
|
20
|
-
_rotationDegrees = PdfCanvasNormalizeRotation(rotationDegrees);
|
|
21
|
-
// The one line of arithmetic that makes this type post-rotation. See the
|
|
22
|
-
// header: CoreGraphics reports the box unrotated, so nobody else does it.
|
|
23
|
-
const BOOL quarterTurn = (_rotationDegrees == 90 || _rotationDegrees == 270);
|
|
24
|
-
_widthPoints = quarterTurn ? cropBox.size.height : cropBox.size.width;
|
|
25
|
-
_heightPoints = quarterTurn ? cropBox.size.width : cropBox.size.height;
|
|
26
|
-
}
|
|
27
|
-
return self;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
- (NSString *)description {
|
|
31
|
-
// The rect is formatted by hand rather than with `NSStringFromCGRect`, which
|
|
32
|
-
// lives in UIKit's UIGeometry.h and therefore does not exist on macOS — where
|
|
33
|
-
// this whole target is compiled by `ios/Package.swift` so that `swift test`
|
|
34
|
-
// can prove it without a device. Nothing in PdfCanvasCore may reach for UIKit.
|
|
35
|
-
return [NSString
|
|
36
|
-
stringWithFormat:@"PdfCanvasPageGeometry{%ld %gx%gpt rot=%ld "
|
|
37
|
-
@"crop=(%g,%g %gx%g)}",
|
|
38
|
-
(long)_index, _widthPoints, _heightPoints,
|
|
39
|
-
(long)_rotationDegrees, _cropBox.origin.x,
|
|
40
|
-
_cropBox.origin.y, _cropBox.size.width,
|
|
41
|
-
_cropBox.size.height];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@end
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
#import "PdfCanvasPageTransform.h"
|
|
2
|
-
|
|
3
|
-
#import "PdfCanvasPageGeometry.h"
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
* Every case below was derived by mapping the page's four corners and then
|
|
7
|
-
* checked against a rendered fixture, so the derivation is worth keeping next to
|
|
8
|
-
* the code.
|
|
9
|
-
*
|
|
10
|
-
* Write the unrotated page in y-DOWN box-local coordinates:
|
|
11
|
-
*
|
|
12
|
-
* u = px - bx in [0, bw]
|
|
13
|
-
* v = (by + bh) - py in [0, bh]
|
|
14
|
-
*
|
|
15
|
-
* `/Rotate r` means "rotate the page r degrees CLOCKWISE when displayed", so the
|
|
16
|
-
* displayed image is the (u, v) image turned clockwise by r. Turning an image
|
|
17
|
-
* clockwise by 90 sends a source pixel at (u, v) to (H_src - v, u), where H_src is
|
|
18
|
-
* the source HEIGHT, which is the rotated image's WIDTH. Substituting u and v
|
|
19
|
-
* back gives each case:
|
|
20
|
-
*
|
|
21
|
-
* r = 0 doc = (u, v) = (px - bx, by + bh - py)
|
|
22
|
-
* r = 90 doc = (bh - v, u) = (py - by, px - bx)
|
|
23
|
-
* r = 180 doc = (bw - u, bh - v) = (bw + bx - px, py - by)
|
|
24
|
-
* r = 270 doc = (v, bw - u) = (by + bh - py, bw + bx - px)
|
|
25
|
-
*
|
|
26
|
-
* CGAffineTransform applies as `x' = a*x + c*y + tx`, `y' = b*x + d*y + ty`, so
|
|
27
|
-
* `a` and `b` are the px column and `c` and `d` are the py column. Reading the
|
|
28
|
-
* four expressions off that way is what the literals below are.
|
|
29
|
-
*
|
|
30
|
-
* Corner checks, for a 200 x 100 box at the origin:
|
|
31
|
-
*
|
|
32
|
-
* r = 90, PDF bottom-left (0, 0) -> doc (0, 0) displayed TOP-LEFT
|
|
33
|
-
* r = 90, PDF bottom-right (200, 0) -> doc (0, 200) displayed BOTTOM-LEFT
|
|
34
|
-
* r = 180, PDF bottom-left (0, 0) -> doc (200, 0) displayed TOP-RIGHT
|
|
35
|
-
* r = 270, PDF top-left (0, 100) -> doc (0, 200) displayed BOTTOM-LEFT
|
|
36
|
-
*
|
|
37
|
-
* All four match what turning a sheet of paper does, and all four are asserted
|
|
38
|
-
* individually in the Swift suite rather than being inferred from a render.
|
|
39
|
-
*/
|
|
40
|
-
CGAffineTransform PdfCanvasPdfToDocTransform(
|
|
41
|
-
CGRect cropBox, NSInteger rotationDegrees) {
|
|
42
|
-
const CGFloat bx = CGRectGetMinX(cropBox);
|
|
43
|
-
const CGFloat by = CGRectGetMinY(cropBox);
|
|
44
|
-
const CGFloat bw = CGRectGetWidth(cropBox);
|
|
45
|
-
const CGFloat bh = CGRectGetHeight(cropBox);
|
|
46
|
-
|
|
47
|
-
switch (PdfCanvasNormalizeRotation(rotationDegrees)) {
|
|
48
|
-
case 90:
|
|
49
|
-
// doc_x = py - by ; doc_y = px - bx
|
|
50
|
-
return CGAffineTransformMake(0, 1, 1, 0, -by, -bx);
|
|
51
|
-
case 180:
|
|
52
|
-
// doc_x = (bw + bx) - px ; doc_y = py - by
|
|
53
|
-
return CGAffineTransformMake(-1, 0, 0, 1, bw + bx, -by);
|
|
54
|
-
case 270:
|
|
55
|
-
// doc_x = (by + bh) - py ; doc_y = (bw + bx) - px
|
|
56
|
-
return CGAffineTransformMake(0, -1, -1, 0, by + bh, bw + bx);
|
|
57
|
-
case 0:
|
|
58
|
-
default:
|
|
59
|
-
// doc_x = px - bx ; doc_y = (by + bh) - py
|
|
60
|
-
return CGAffineTransformMake(1, 0, 0, -1, -bx, by + bh);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
CGAffineTransform PdfCanvasDocToRasterTransform(
|
|
65
|
-
double docX, double docY, double scale, NSInteger pixelHeight) {
|
|
66
|
-
// The CG-op sequence this is the closed form of, for anyone checking it by
|
|
67
|
-
// hand: translate(0, H); scale(1, -1); scale(s, s); translate(-docX, -docY).
|
|
68
|
-
//
|
|
69
|
-
// raster_x = (dx - docX) * s
|
|
70
|
-
// raster_y = H - (dy - docY) * s
|
|
71
|
-
return CGAffineTransformMake(
|
|
72
|
-
(CGFloat)scale, 0, 0, (CGFloat)-scale, (CGFloat)(-docX * scale),
|
|
73
|
-
(CGFloat)((double)pixelHeight + docY * scale));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
CGAffineTransform PdfCanvasDocToPdfKitTransform(double pageHeightPoints) {
|
|
77
|
-
// A pure y-flip through the page's own height, and nothing else: PDFKit
|
|
78
|
-
// applies the rotation and the box origin itself. See the header for the
|
|
79
|
-
// measurement that establishes both halves, and for why the height cannot come
|
|
80
|
-
// from `-[PDFPage boundsForBox:]`.
|
|
81
|
-
return CGAffineTransformMake(1, 0, 0, -1, 0, (CGFloat)pageHeightPoints);
|
|
82
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
#import "PdfCanvasPixelSink.h"
|
|
2
|
-
|
|
3
|
-
@implementation PdfCanvasHeapSink {
|
|
4
|
-
NSMutableData *_data;
|
|
5
|
-
BOOL _reserved;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
- (NSMutableData *)data {
|
|
9
|
-
return _data;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
- (nullable void *)pdfCanvasReserveBytes:(NSUInteger)length {
|
|
13
|
-
if (_reserved || length == 0) {
|
|
14
|
-
return NULL;
|
|
15
|
-
}
|
|
16
|
-
_reserved = YES;
|
|
17
|
-
// `dataWithLength:` is documented to zero the bytes, which is the sink
|
|
18
|
-
// contract. It returns nil rather than raising when the allocation cannot be
|
|
19
|
-
// had, and NULL here is what the rasterizer maps to `out-of-memory`.
|
|
20
|
-
_data = [NSMutableData dataWithLength:length];
|
|
21
|
-
if (_data == nil) {
|
|
22
|
-
return NULL;
|
|
23
|
-
}
|
|
24
|
-
return _data.mutableBytes;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
- (int64_t)pdfCanvasPublish {
|
|
28
|
-
// 0 is "heap-backed" on the wire — see `PdfCanvasRasterPixels.slot`. This sink
|
|
29
|
-
// has no identifier because `data` is how its bytes are reached.
|
|
30
|
-
return 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
- (void)pdfCanvasAbandon {
|
|
34
|
-
_data = nil;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@end
|