@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.
Files changed (50) hide show
  1. package/README.md +61 -6
  2. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +64 -10
  3. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +22 -3
  4. package/android/src/main/cpp/pdfcanvas-jni.cpp +47 -3
  5. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +31 -1
  6. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +79 -9
  7. package/android/tools/compile-gate.sh +6 -4
  8. package/dist/controller.d.ts +23 -1
  9. package/dist/controller.js +33 -16
  10. package/dist/rasterizer/fake.d.ts +38 -4
  11. package/dist/rasterizer/fake.js +167 -37
  12. package/dist/rasterizer/native-bridge.d.ts +48 -1
  13. package/dist/rasterizer/native-bridge.js +96 -0
  14. package/dist/rasterizer/native.d.ts +2 -2
  15. package/dist/rasterizer/native.js +9 -7
  16. package/dist/rasterizer/text-search.d.ts +22 -0
  17. package/dist/rasterizer/text-search.js +28 -0
  18. package/dist/rasterizer/web/client.js +69 -49
  19. package/dist/rasterizer/web/engine.d.ts +3 -1
  20. package/dist/rasterizer/web/engine.js +225 -8
  21. package/dist/rasterizer/web/pdfium.d.ts +64 -2
  22. package/dist/rasterizer/web/pdfium.js +21 -0
  23. package/dist/rasterizer/web/protocol.d.ts +23 -6
  24. package/dist/rasterizer/web/protocol.js +4 -1
  25. package/dist/rasterizer/web/session.js +30 -13
  26. package/dist/react/PdfContentView.js +5 -0
  27. package/dist/react/usePdfDocument.d.ts +27 -2
  28. package/dist/react/usePdfDocument.js +65 -12
  29. package/dist/react/usePdfLayer.d.ts +19 -2
  30. package/dist/react/usePdfLayer.js +51 -8
  31. package/dist/rotation.d.ts +59 -0
  32. package/dist/rotation.js +85 -0
  33. package/dist/testing/index.d.ts +1 -1
  34. package/dist/testing/index.js +1 -1
  35. package/dist/types.d.ts +86 -0
  36. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +71 -2
  37. package/native/CMakeLists.txt +7 -0
  38. package/native/core/include/pdfcanvas/document.h +17 -0
  39. package/native/core/include/pdfcanvas/service.h +14 -1
  40. package/native/core/include/pdfcanvas/text_search.h +52 -0
  41. package/native/core/include/pdfcanvas/types.h +49 -0
  42. package/native/core/pdfcanvas-core.cmake +1 -0
  43. package/native/core/src/document.cpp +211 -8
  44. package/native/core/src/service.cpp +43 -28
  45. package/native/core/src/text_search.cpp +115 -0
  46. package/native/tests/fixtures.cpp +27 -0
  47. package/native/tests/fixtures.h +15 -0
  48. package/native/tests/test_document.cpp +486 -0
  49. package/native/tests/test_service.cpp +110 -0
  50. package/package.json +1 -1
@@ -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,
@@ -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. A pure lookup — a cancel for a token whose
399
- * render already finished finds nothing and does nothing.
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));
@@ -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
- /// Best-effort, and genuinely mid-render: sets the signal a render polls.
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 {
@@ -19,4 +19,5 @@ set(PDFCANVAS_CORE_SOURCES
19
19
  "${PDFCANVAS_CORE_DIR}/src/pixels.cpp"
20
20
  "${PDFCANVAS_CORE_DIR}/src/service.cpp"
21
21
  "${PDFCANVAS_CORE_DIR}/src/slots.cpp"
22
+ "${PDFCANVAS_CORE_DIR}/src/text_search.cpp"
22
23
  )