@reekon-tools/react-native-pdf-canvas 0.2.1 → 0.4.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.
- package/README.md +61 -6
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +64 -10
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +22 -3
- package/android/src/main/cpp/pdfcanvas-jni.cpp +47 -3
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +31 -1
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +79 -9
- package/android/tools/compile-gate.sh +6 -4
- package/dist/controller.d.ts +23 -1
- package/dist/controller.js +33 -16
- package/dist/rasterizer/fake.d.ts +38 -4
- package/dist/rasterizer/fake.js +167 -37
- package/dist/rasterizer/native-bridge.d.ts +48 -1
- package/dist/rasterizer/native-bridge.js +96 -0
- package/dist/rasterizer/native.d.ts +2 -2
- package/dist/rasterizer/native.js +9 -7
- package/dist/rasterizer/text-search.d.ts +22 -0
- package/dist/rasterizer/text-search.js +28 -0
- package/dist/rasterizer/web/client.js +69 -49
- package/dist/rasterizer/web/engine.d.ts +3 -1
- package/dist/rasterizer/web/engine.js +225 -8
- package/dist/rasterizer/web/pdfium.d.ts +64 -2
- package/dist/rasterizer/web/pdfium.js +21 -0
- package/dist/rasterizer/web/protocol.d.ts +23 -6
- package/dist/rasterizer/web/protocol.js +4 -1
- package/dist/rasterizer/web/session.js +30 -13
- package/dist/react/PdfContentView.js +5 -0
- package/dist/react/usePdfDocument.d.ts +27 -2
- package/dist/react/usePdfDocument.js +65 -12
- package/dist/react/usePdfLayer.d.ts +19 -2
- package/dist/react/usePdfLayer.js +51 -8
- package/dist/rotation.d.ts +59 -0
- package/dist/rotation.js +85 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/types.d.ts +86 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +71 -2
- package/native/CMakeLists.txt +7 -0
- package/native/core/include/pdfcanvas/document.h +17 -0
- package/native/core/include/pdfcanvas/service.h +14 -1
- package/native/core/include/pdfcanvas/text_search.h +52 -0
- package/native/core/include/pdfcanvas/types.h +49 -0
- package/native/core/pdfcanvas-core.cmake +1 -0
- package/native/core/src/document.cpp +211 -8
- package/native/core/src/service.cpp +43 -28
- package/native/core/src/text_search.cpp +115 -0
- package/native/tests/fixtures.cpp +27 -0
- package/native/tests/fixtures.h +15 -0
- package/native/tests/test_document.cpp +486 -0
- package/native/tests/test_service.cpp +110 -0
- package/package.json +1 -1
package/dist/testing/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* directly and never `../rasterizer/index.js`, which pulls in the Skia ingest
|
|
9
9
|
* path.
|
|
10
10
|
*/
|
|
11
|
-
export { ARCH_D, ARCH_E, LETTER, createFakeRasterizer, decodeFakeDocRect, fakePageColor, fakePixelSize, } from '../rasterizer/fake.js';
|
|
11
|
+
export { ARCH_D, ARCH_E, FAKE_MATCH_RECT_ORIGIN, FAKE_MATCH_RECT_SIZE, FAKE_MATCH_RECT_STRIDE, LETTER, createFakeRasterizer, decodeFakeDocRect, decodeFakeRotation, fakePageColor, fakePixelSize, searchFakeText, } from '../rasterizer/fake.js';
|
|
12
12
|
export type { FakeFailureModes, FakeRasterizerOptions, } from '../rasterizer/fake.js';
|
|
13
13
|
/**
|
|
14
14
|
* The reference scenes BUILD STEP 1 produced: the composition contract as data,
|
package/dist/testing/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* directly and never `../rasterizer/index.js`, which pulls in the Skia ingest
|
|
9
9
|
* path.
|
|
10
10
|
*/
|
|
11
|
-
export { ARCH_D, ARCH_E, LETTER, createFakeRasterizer, decodeFakeDocRect, fakePageColor, fakePixelSize, } from '../rasterizer/fake.js';
|
|
11
|
+
export { ARCH_D, ARCH_E, FAKE_MATCH_RECT_ORIGIN, FAKE_MATCH_RECT_SIZE, FAKE_MATCH_RECT_STRIDE, LETTER, createFakeRasterizer, decodeFakeDocRect, decodeFakeRotation, fakePageColor, fakePixelSize, searchFakeText, } from '../rasterizer/fake.js';
|
|
12
12
|
/**
|
|
13
13
|
* The reference scenes BUILD STEP 1 produced: the composition contract as data,
|
|
14
14
|
* for a backend suite to replay against its own rasterizer. Pure — it reaches
|
package/dist/types.d.ts
CHANGED
|
@@ -220,6 +220,26 @@ export interface RasterRequest {
|
|
|
220
220
|
*/
|
|
221
221
|
annotations: boolean;
|
|
222
222
|
background: 'white' | 'transparent';
|
|
223
|
+
/**
|
|
224
|
+
* DEVICE rotation applied on top of the page's own `/Rotate`, in degrees
|
|
225
|
+
* clockwise. Optional: absent means 0, and every backend reads it as
|
|
226
|
+
* `rotation ?? 0`, so a request built before the field existed renders
|
|
227
|
+
* exactly as it did.
|
|
228
|
+
*
|
|
229
|
+
* THE COORDINATE CONTRACT: `docRect` and `scale` are expressed in the
|
|
230
|
+
* ROTATED page's doc space. For 90 and 270 the page's extents are swapped —
|
|
231
|
+
* a 612x792 page rotated 90 is a 792x612 page whose origin is its displayed
|
|
232
|
+
* top-left — so a whole-page request for it is `{0, 0, 792, 612}`. That is
|
|
233
|
+
* what lets a backend keep the negative-offset rect mechanism unchanged: it
|
|
234
|
+
* describes the whole TURNED page at `scale` and slides it by
|
|
235
|
+
* `(-round(x·s), -round(y·s))`, exactly as it does an upright one. The only
|
|
236
|
+
* thing a backend swaps is the page box it hands the engine.
|
|
237
|
+
*
|
|
238
|
+
* This is NOT `PageGeometry.rotation`. That one is the document's `/Rotate`,
|
|
239
|
+
* already folded into every size and render by PDFium and informational
|
|
240
|
+
* here; this one is a host's choice, layered on top. See `./rotation.ts`.
|
|
241
|
+
*/
|
|
242
|
+
rotation?: PageRotation;
|
|
223
243
|
}
|
|
224
244
|
export interface RasterizerCapabilities {
|
|
225
245
|
/** Can render PDF-embedded annotations into the raster. */
|
|
@@ -227,6 +247,13 @@ export interface RasterizerCapabilities {
|
|
|
227
247
|
/** Honours the abort signal mid-render, not merely between requests. */
|
|
228
248
|
interruptibleRender: boolean;
|
|
229
249
|
text: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* `RasterizerHandle.searchText` finds text and returns glyph rects. TRUE for
|
|
252
|
+
* every shipped backend since 0.4.0 — they are all PDFium, and its text API
|
|
253
|
+
* is what implements it — and for the fake, which searches a declared
|
|
254
|
+
* per-page text. False only for the self-describing missing-backend
|
|
255
|
+
* placeholder, whose `open()` never succeeds anyway.
|
|
256
|
+
*/
|
|
230
257
|
search: boolean;
|
|
231
258
|
links: boolean;
|
|
232
259
|
/**
|
|
@@ -278,8 +305,57 @@ export interface RasterizerHandle {
|
|
|
278
305
|
/** Cheap: must not load a page. */
|
|
279
306
|
pageGeometry(index: number): PageGeometry;
|
|
280
307
|
render(request: RasterRequest, signal?: AbortSignal): Promise<RasterPixels>;
|
|
308
|
+
/**
|
|
309
|
+
* Find every occurrence of `request.query` on one page.
|
|
310
|
+
*
|
|
311
|
+
* REQUIRED ON THE SEAM, and every shipped backend implements it (the fake
|
|
312
|
+
* included), so a host can build "find in document" on any of them. The
|
|
313
|
+
* rects come back in the same doc space the page's rasters use — see
|
|
314
|
+
* `TextMatch.rects` for the contract — which is what lets a host draw a hit
|
|
315
|
+
* over the page with the transform it already has. An empty query is the
|
|
316
|
+
* document's business to short-circuit (`PdfDocument.searchPage` does);
|
|
317
|
+
* a backend handed one may answer `[]` or search for nothing, both of which
|
|
318
|
+
* are `[]`.
|
|
319
|
+
*/
|
|
320
|
+
searchText(request: TextSearchRequest, signal?: AbortSignal): Promise<TextMatch[]>;
|
|
281
321
|
close(): void;
|
|
282
322
|
}
|
|
323
|
+
export interface TextSearchOptions {
|
|
324
|
+
/** Case-sensitive match. Default false. */
|
|
325
|
+
matchCase?: boolean;
|
|
326
|
+
/** Whole-word match. Default false. */
|
|
327
|
+
wholeWord?: boolean;
|
|
328
|
+
/**
|
|
329
|
+
* The HOST's rotation for this page (see RasterRequest.rotation), so rects
|
|
330
|
+
* come back in the TURNED page's doc space — the space its rasters use.
|
|
331
|
+
* Default 0.
|
|
332
|
+
*/
|
|
333
|
+
rotation?: PageRotation;
|
|
334
|
+
signal?: AbortSignal;
|
|
335
|
+
}
|
|
336
|
+
export interface TextMatch {
|
|
337
|
+
page: number;
|
|
338
|
+
/** PDFium character index of the match start on the page, and its length. */
|
|
339
|
+
charIndex: number;
|
|
340
|
+
charCount: number;
|
|
341
|
+
/**
|
|
342
|
+
* Bounding boxes of the matched glyphs, one per text line the match spans,
|
|
343
|
+
* in the page's doc space: PDF points, origin at the DISPLAYED page's
|
|
344
|
+
* top-left, y down, host rotation applied. Empty only for a match PDFium
|
|
345
|
+
* reports no rects for.
|
|
346
|
+
*/
|
|
347
|
+
rects: DocRect[];
|
|
348
|
+
/** The page text around the match, roughly 40 chars each side, whitespace collapsed. */
|
|
349
|
+
context: string;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* What a backend is handed: the options RESOLVED, so no backend has a default
|
|
353
|
+
* of its own that could drift from `PdfDocument.searchPage`'s.
|
|
354
|
+
*/
|
|
355
|
+
export interface TextSearchRequest extends Required<Pick<TextSearchOptions, 'matchCase' | 'wholeWord' | 'rotation'>> {
|
|
356
|
+
page: number;
|
|
357
|
+
query: string;
|
|
358
|
+
}
|
|
283
359
|
export type RasterRole = 'base' | 'detail';
|
|
284
360
|
/**
|
|
285
361
|
* One rasterized piece, ready to draw.
|
|
@@ -317,6 +393,16 @@ export interface PdfPageContent {
|
|
|
317
393
|
readonly page: number;
|
|
318
394
|
/** Where this page sits in doc space, per the layout function. */
|
|
319
395
|
readonly pageRect: DocRect;
|
|
396
|
+
/**
|
|
397
|
+
* Whether this page may be painted at all — false for a page outside the
|
|
398
|
+
* controller's `allowedPages`. Such a page never holds a raster, and
|
|
399
|
+
* `PdfContentView` paints NO PAPER for it either: under a stacked layout
|
|
400
|
+
* (`singlePage()`) every page's rect sits at the origin, so paper for an
|
|
401
|
+
* unselected page of different extents (a page the host turned a quarter)
|
|
402
|
+
* would show through beside the selected one. The entry stays in `pages`
|
|
403
|
+
* so the array remains index-aligned with page numbers.
|
|
404
|
+
*/
|
|
405
|
+
readonly drawable: boolean;
|
|
320
406
|
/**
|
|
321
407
|
* The whole page at fit scale, as one or more pieces whose union is exactly
|
|
322
408
|
* `pageRect`.
|
|
@@ -324,6 +324,11 @@ RCT_EXPORT_METHOD(render
|
|
|
324
324
|
req.width = [request[@"width"] doubleValue];
|
|
325
325
|
req.height = [request[@"height"] doubleValue];
|
|
326
326
|
req.scale = [request[@"scale"] doubleValue];
|
|
327
|
+
// OPTIONAL, DEFAULTING TO 0: a JS bundle older than this binary never sends it,
|
|
328
|
+
// and a page must not turn because a key is missing. The class check also covers
|
|
329
|
+
// a JS `null`, which the bridge delivers as `NSNull` rather than `nil`. The core
|
|
330
|
+
// validates the value; this only decides what "absent" means.
|
|
331
|
+
req.rotation = [request[@"rotation"] isKindOfClass:NSNumber.class] ? [request[@"rotation"] intValue] : 0;
|
|
327
332
|
req.annotations = request[@"annotations"] == nil ? true : [request[@"annotations"] boolValue];
|
|
328
333
|
req.background = [@"transparent" isEqual:request[@"background"]] ? pdfcanvas::Background::Transparent
|
|
329
334
|
: pdfcanvas::Background::White;
|
|
@@ -392,11 +397,75 @@ RCT_EXPORT_METHOD(render
|
|
|
392
397
|
}];
|
|
393
398
|
}
|
|
394
399
|
|
|
400
|
+
/* ---------------------------------------------------------------- *
|
|
401
|
+
* search
|
|
402
|
+
* ---------------------------------------------------------------- */
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Finds text on one page and resolves with the core's JSON — ONE string, however
|
|
406
|
+
* many hits, so a find pass over a dense drawing is one bridge value rather than
|
|
407
|
+
* thousands of dictionaries. `native-bridge.ts` parses and checks it.
|
|
408
|
+
*
|
|
409
|
+
* THE SAME QUEUE AS RENDER, deliberately: PDFium is not thread-safe and the core
|
|
410
|
+
* serialises everything behind one lock anyway. The same (handle, token) registry
|
|
411
|
+
* too, so `cancel` stops a superseded search between matches.
|
|
412
|
+
*/
|
|
413
|
+
RCT_EXPORT_METHOD(search
|
|
414
|
+
: (double)handle request
|
|
415
|
+
: (NSDictionary *)request token
|
|
416
|
+
: (double)token resolve
|
|
417
|
+
: (RCTPromiseResolveBlock)resolve reject
|
|
418
|
+
: (RCTPromiseRejectBlock)reject) {
|
|
419
|
+
pdfcanvas::TextSearchRequest req;
|
|
420
|
+
req.page = [request[@"page"] intValue];
|
|
421
|
+
// UTF-16 straight out of the NSString, which already is UTF-16 — no UTF-8 step
|
|
422
|
+
// that could mangle a supplementary character on the way to PDFium.
|
|
423
|
+
NSString *query = [request[@"query"] isKindOfClass:NSString.class] ? request[@"query"] : @"";
|
|
424
|
+
req.query.resize(query.length);
|
|
425
|
+
if (query.length > 0) {
|
|
426
|
+
static_assert(sizeof(unichar) == sizeof(char16_t), "unichar is a UTF-16 unit");
|
|
427
|
+
[query getCharacters:reinterpret_cast<unichar *>(&req.query[0]) range:NSMakeRange(0, query.length)];
|
|
428
|
+
}
|
|
429
|
+
req.matchCase = [request[@"matchCase"] isKindOfClass:NSNumber.class] && [request[@"matchCase"] boolValue];
|
|
430
|
+
req.wholeWord = [request[@"wholeWord"] isKindOfClass:NSNumber.class] && [request[@"wholeWord"] boolValue];
|
|
431
|
+
// OPTIONAL, DEFAULTING TO 0, for the reason render's is.
|
|
432
|
+
req.rotation = [request[@"rotation"] isKindOfClass:NSNumber.class] ? [request[@"rotation"] intValue] : 0;
|
|
433
|
+
const int nativeHandle = static_cast<int>(handle);
|
|
434
|
+
const int64_t nativeToken = static_cast<int64_t>(token);
|
|
435
|
+
|
|
436
|
+
// Registered BEFORE the operation runs — see render.
|
|
437
|
+
_service->registerCancellation(nativeHandle, nativeToken);
|
|
438
|
+
|
|
439
|
+
std::shared_ptr<pdfcanvas::Service> service = _service;
|
|
440
|
+
[_renderQueue addOperationWithBlock:^{
|
|
441
|
+
@try {
|
|
442
|
+
std::string json;
|
|
443
|
+
try {
|
|
444
|
+
json = service->searchJson(nativeHandle, req, nativeToken);
|
|
445
|
+
} catch (const pdfcanvas::PdfError &error) {
|
|
446
|
+
RejectWithPdfError(reject, error);
|
|
447
|
+
return;
|
|
448
|
+
} catch (const std::exception &error) {
|
|
449
|
+
RejectWith(reject, pdfcanvas::ErrorCode::BackendFailure,
|
|
450
|
+
[NSString stringWithUTF8String:error.what()]);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// PURE ASCII by construction (`textMatchesToJson`), so UTF-8 decoding it is
|
|
454
|
+
// the identity.
|
|
455
|
+
resolve([NSString stringWithUTF8String:json.c_str()]);
|
|
456
|
+
} @catch (NSException *exception) {
|
|
457
|
+
RejectWith(reject, pdfcanvas::ErrorCode::BackendFailure,
|
|
458
|
+
[NSString stringWithFormat:@"%@: %@", exception.name, exception.reason]);
|
|
459
|
+
}
|
|
460
|
+
}];
|
|
461
|
+
}
|
|
462
|
+
|
|
395
463
|
/**
|
|
396
464
|
* Genuinely mid-render now, not best-effort: the core polls the signal from
|
|
397
465
|
* PDFium's progressive renderer between batches of page objects, so a superseded
|
|
398
|
-
* tile stops drawing within one batch
|
|
399
|
-
*
|
|
466
|
+
* tile stops drawing within one batch — and a search between matches. A pure
|
|
467
|
+
* lookup — a cancel for a token whose job already finished finds nothing and does
|
|
468
|
+
* nothing.
|
|
400
469
|
*/
|
|
401
470
|
RCT_EXPORT_METHOD(cancel : (double)handle token : (double)token) {
|
|
402
471
|
_service->cancel(static_cast<int>(handle), static_cast<int64_t>(token));
|
package/native/CMakeLists.txt
CHANGED
|
@@ -80,6 +80,13 @@ if(WIN32)
|
|
|
80
80
|
elseif(APPLE)
|
|
81
81
|
set_target_properties(pdfcanvas_tests PROPERTIES
|
|
82
82
|
BUILD_RPATH "${PDFCANVAS_PDFIUM_DIR}/lib")
|
|
83
|
+
# The rpath above cannot help on its own: the release dylib's install name is
|
|
84
|
+
# the CWD-relative `./libpdfium.dylib` (`otool -D`), not `@rpath/…`, so dyld
|
|
85
|
+
# looks beside the working directory and nowhere else. ctest runs the binary
|
|
86
|
+
# from the build directory, so — as on Windows — the library is copied there.
|
|
87
|
+
add_custom_command(TARGET pdfcanvas_tests POST_BUILD
|
|
88
|
+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
|
89
|
+
"${PDFCANVAS_PDFIUM_DIR}/lib/libpdfium.dylib" "$<TARGET_FILE_DIR:pdfcanvas_tests>")
|
|
83
90
|
else()
|
|
84
91
|
set_target_properties(pdfcanvas_tests PROPERTIES
|
|
85
92
|
BUILD_RPATH "${PDFCANVAS_PDFIUM_DIR}/lib")
|
|
@@ -74,6 +74,23 @@ class Document {
|
|
|
74
74
|
RasterPixels render(const RasterRequest& request, Cancellation* signal,
|
|
75
75
|
PixelSink* sink);
|
|
76
76
|
|
|
77
|
+
/// Every occurrence of `request.query` on one page, in reading order.
|
|
78
|
+
///
|
|
79
|
+
/// PDFium's own search (`FPDFText_FindStart` / `FindNext`), on a text page
|
|
80
|
+
/// parsed once and cached with the page it belongs to — a find box searches
|
|
81
|
+
/// again on every keystroke, and `FPDFText_LoadPage` is the call that walks
|
|
82
|
+
/// the content stream. Each match's rects are mapped through the SAME
|
|
83
|
+
/// display matrix the rasters use (`FPDF_PageToDevice` with the displayed
|
|
84
|
+
/// box and the host's rotate), so a hit drawn at a rect lands on the glyphs.
|
|
85
|
+
///
|
|
86
|
+
/// `signal` may be null. It is observed before the lock, after it, and
|
|
87
|
+
/// between matches — a superseded search holds the lock for at most one
|
|
88
|
+
/// more match. An empty query answers `{}` without touching the page.
|
|
89
|
+
/// Throws `not-found` for a missing page, `backend-failure` for a closed
|
|
90
|
+
/// document or a rotation that is not a quarter turn, `corrupt` when PDFium
|
|
91
|
+
/// cannot read the page's text, `cancelled`.
|
|
92
|
+
std::vector<TextMatch> searchText(const TextSearchRequest& request, Cancellation* signal);
|
|
93
|
+
|
|
77
94
|
/// Releases every PDFium object. Blocks until an in-flight render on this
|
|
78
95
|
/// document finishes (it holds the library lock), and is therefore free of
|
|
79
96
|
/// the use-after-free window the Android pool's close had. Idempotent.
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
#include <cstdint>
|
|
17
17
|
#include <memory>
|
|
18
18
|
#include <mutex>
|
|
19
|
+
#include <string>
|
|
19
20
|
#include <unordered_map>
|
|
20
21
|
#include <vector>
|
|
21
22
|
|
|
@@ -55,7 +56,15 @@ class Service {
|
|
|
55
56
|
RasterPixels render(int handle, const RasterRequest& request, int64_t token,
|
|
56
57
|
bool useSlot);
|
|
57
58
|
|
|
58
|
-
///
|
|
59
|
+
/// Searches one page on the calling thread and returns the WIRE FORM — the
|
|
60
|
+
/// JSON `textMatchesToJson` writes, pure ASCII, one bridge value however
|
|
61
|
+
/// many hits there are. The SAME (handle, token) registry as `render`:
|
|
62
|
+
/// register first, and `cancel` stops the search between matches. The
|
|
63
|
+
/// signal is consumed and unregistered on every path out.
|
|
64
|
+
std::string searchJson(int handle, const TextSearchRequest& request, int64_t token);
|
|
65
|
+
|
|
66
|
+
/// Best-effort, and genuinely mid-render: sets the signal a render (or a
|
|
67
|
+
/// search) polls.
|
|
59
68
|
void cancel(int handle, int64_t token);
|
|
60
69
|
|
|
61
70
|
/// Closes one document. Blocks until its in-flight render finishes.
|
|
@@ -74,6 +83,10 @@ class Service {
|
|
|
74
83
|
size_t openCount() const;
|
|
75
84
|
|
|
76
85
|
private:
|
|
86
|
+
/// One job's (signal, document) pair, unregistering the signal on
|
|
87
|
+
/// destruction. Shared by `render` and `searchJson`.
|
|
88
|
+
struct Lease;
|
|
89
|
+
|
|
77
90
|
static int64_t signalKey(int handle, int64_t token) noexcept;
|
|
78
91
|
std::shared_ptr<Document> documentFor(int handle);
|
|
79
92
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The two text-search rules that are not PDFium calls: how a match's context
|
|
2
|
+
// reads, and how a page's matches cross the bridge.
|
|
3
|
+
//
|
|
4
|
+
// PURE — no PDFium, no lock, no document — so both are provable on their own,
|
|
5
|
+
// and both have a TypeScript twin that must agree with them:
|
|
6
|
+
// `src/rasterizer/text-search.ts` (the context rule, used by the web engine
|
|
7
|
+
// and the fake) and `parseTextMatches` in `src/rasterizer/native-bridge.ts`
|
|
8
|
+
// (the reader of the JSON written here).
|
|
9
|
+
#pragma once
|
|
10
|
+
|
|
11
|
+
#include <string>
|
|
12
|
+
#include <vector>
|
|
13
|
+
|
|
14
|
+
#include "pdfcanvas/types.h"
|
|
15
|
+
|
|
16
|
+
namespace pdfcanvas {
|
|
17
|
+
|
|
18
|
+
/// Characters of page text a match carries either side of itself. Mirrors
|
|
19
|
+
/// `CONTEXT_CHARS` in `src/rasterizer/text-search.ts`.
|
|
20
|
+
constexpr int kContextChars = 40;
|
|
21
|
+
|
|
22
|
+
/// Whether a UTF-16 code unit is whitespace as ECMAScript's `\s` defines it —
|
|
23
|
+
/// the exact set `collapseWhitespace` in `text-search.ts` collapses, so a
|
|
24
|
+
/// context reads the same from every engine.
|
|
25
|
+
bool isJsWhitespace(char16_t unit) noexcept;
|
|
26
|
+
|
|
27
|
+
/// Runs of whitespace collapsed to one U+0020, both ends trimmed. PDFium's
|
|
28
|
+
/// text page carries `\r\n` at every line break and a "find" list wants one
|
|
29
|
+
/// line per hit.
|
|
30
|
+
std::u16string collapseWhitespace(const std::u16string& text);
|
|
31
|
+
|
|
32
|
+
/// `TextMatch[]` as ONE JSON string, the shape `parseTextMatches` reads:
|
|
33
|
+
///
|
|
34
|
+
/// [{"page":0,"charIndex":6,"charCount":8,
|
|
35
|
+
/// "rects":[{"x":..,"y":..,"width":..,"height":..}],"context":".."}]
|
|
36
|
+
///
|
|
37
|
+
/// THE OUTPUT IS PURE ASCII: every string character outside printable ASCII —
|
|
38
|
+
/// and `"` and `\` — is written as a `\uXXXX` escape of its UTF-16 unit, which
|
|
39
|
+
/// `JSON.parse` reassembles, surrogate pairs included. That is what lets both
|
|
40
|
+
/// bindings hand it over without a transcoding step: JNI's `NewStringUTF`
|
|
41
|
+
/// takes "modified UTF-8", which is identical to ASCII and to nothing else,
|
|
42
|
+
/// and `+[NSString stringWithUTF8String:]` reads ASCII as itself.
|
|
43
|
+
///
|
|
44
|
+
/// NUMBERS ARE LOCALE-PROOF. `%.17g` round-trips a double exactly, but printf
|
|
45
|
+
/// honours `LC_NUMERIC`, and a host that has called `setlocale` for a
|
|
46
|
+
/// comma-decimal locale would get `72,5` — which is not JSON. The one
|
|
47
|
+
/// separator `%g` can emit is replaced back. A non-finite value (which the
|
|
48
|
+
/// search never produces) is written as 0 rather than as the invalid token
|
|
49
|
+
/// `nan`.
|
|
50
|
+
std::string textMatchesToJson(const std::vector<TextMatch>& matches);
|
|
51
|
+
|
|
52
|
+
} // namespace pdfcanvas
|
|
@@ -48,6 +48,14 @@ struct RasterRequest {
|
|
|
48
48
|
double scale = 1;
|
|
49
49
|
/// Draw PDF-embedded annotations (and form fields) into the raster.
|
|
50
50
|
bool annotations = true;
|
|
51
|
+
/// The HOST's rotation on top of the page's `/Rotate`, in degrees clockwise:
|
|
52
|
+
/// 0, 90, 180 or 270 — anything else is refused. `x`/`y`/`width`/`height`
|
|
53
|
+
/// are in the TURNED page's doc space (extents swapped for a quarter turn),
|
|
54
|
+
/// which is what keeps the negative-offset rect mechanism unchanged: the
|
|
55
|
+
/// only thing the render swaps is the page box it hands PDFium. Absent on
|
|
56
|
+
/// the wire means 0 on both bindings, so a JS/binary version skew cannot
|
|
57
|
+
/// turn a page by accident.
|
|
58
|
+
int rotation = 0;
|
|
51
59
|
Background background = Background::White;
|
|
52
60
|
|
|
53
61
|
/// Destination width in device pixels: ROUND, not ceil, and at least 1 —
|
|
@@ -59,6 +67,47 @@ struct RasterRequest {
|
|
|
59
67
|
int pixelHeight() const;
|
|
60
68
|
};
|
|
61
69
|
|
|
70
|
+
/// A rect in page-local doc space: PDF points, y-down, origin at the DISPLAYED
|
|
71
|
+
/// page's top-left. `DocRect` in `src/types.ts`.
|
|
72
|
+
struct DocRect {
|
|
73
|
+
double x = 0;
|
|
74
|
+
double y = 0;
|
|
75
|
+
double width = 0;
|
|
76
|
+
double height = 0;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/// One page's search, mirroring `TextSearchRequest` in `src/types.ts`: every
|
|
80
|
+
/// option already resolved by `PdfDocument.searchPage`, so no default here can
|
|
81
|
+
/// drift from the TypeScript one.
|
|
82
|
+
struct TextSearchRequest {
|
|
83
|
+
int page = 0;
|
|
84
|
+
/// UTF-16 code units, exactly as JavaScript holds them. Neither binding
|
|
85
|
+
/// transcodes — JNI's `GetStringChars` and `-[NSString getCharacters:]` both
|
|
86
|
+
/// hand over UTF-16 — and PDFium's `FPDF_WIDESTRING` is UTF-16LE, so the
|
|
87
|
+
/// query reaches `FPDFText_FindStart` with no UTF-8 step anywhere that could
|
|
88
|
+
/// mangle a supplementary character (JNI's "modified UTF-8" would).
|
|
89
|
+
std::u16string query;
|
|
90
|
+
bool matchCase = false;
|
|
91
|
+
bool wholeWord = false;
|
|
92
|
+
/// The HOST's rotation, exactly as `RasterRequest::rotation`: 0/90/180/270,
|
|
93
|
+
/// anything else refused. Rects come back in the TURNED page's doc space —
|
|
94
|
+
/// the space that page's rasters are drawn in.
|
|
95
|
+
int rotation = 0;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/// One hit, mirroring `TextMatch` in `src/types.ts`.
|
|
99
|
+
struct TextMatch {
|
|
100
|
+
int page = 0;
|
|
101
|
+
/// PDFium's character index of the first matched character, and the count.
|
|
102
|
+
int charIndex = 0;
|
|
103
|
+
int charCount = 0;
|
|
104
|
+
/// One per text line the match spans, in the request's doc space.
|
|
105
|
+
std::vector<DocRect> rects;
|
|
106
|
+
/// `kContextChars` of page text either side, whitespace collapsed. UTF-16,
|
|
107
|
+
/// for the same reason the query is.
|
|
108
|
+
std::u16string context;
|
|
109
|
+
};
|
|
110
|
+
|
|
62
111
|
/// Where a document's bytes come from. Exactly one of `path` / `bytes` is used;
|
|
63
112
|
/// a non-empty path wins.
|
|
64
113
|
struct Source {
|