@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
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
#include "fpdf_formfill.h"
|
|
14
14
|
#include "fpdf_progressive.h"
|
|
15
|
+
#include "fpdf_text.h"
|
|
15
16
|
#include "fpdfview.h"
|
|
16
17
|
#include "pdfcanvas/library.h"
|
|
17
18
|
#include "pdfcanvas/slots.h"
|
|
19
|
+
#include "pdfcanvas/text_search.h"
|
|
18
20
|
|
|
19
21
|
namespace pdfcanvas {
|
|
20
22
|
|
|
@@ -33,6 +35,19 @@ constexpr FPDF_DWORD kTransparent = 0x00000000u;
|
|
|
33
35
|
/// PDFium's ints are 32-bit; a request past this cannot be expressed at all.
|
|
34
36
|
constexpr long long kMaxInt32 = (std::numeric_limits<int>::max)();
|
|
35
37
|
|
|
38
|
+
/// The device box a search rect is mapped through, in units per PDF point.
|
|
39
|
+
///
|
|
40
|
+
/// `FPDF_PageToDevice` is the display matrix the rasters use — `/Rotate`, the
|
|
41
|
+
/// CropBox origin and the host's `rotate` all folded in by PDFium itself, which
|
|
42
|
+
/// is the whole reason to go through it rather than restate the arithmetic —
|
|
43
|
+
/// but its outputs are `int*`: it ROUNDS TO WHOLE DEVICE UNITS. Handed the page
|
|
44
|
+
/// box in points, a rect would come back quantised to whole points, 4 px off
|
|
45
|
+
/// the glyphs at scale 4. So the box is handed over at this many units per
|
|
46
|
+
/// point and the answer divided back down: 1/1024 pt. The matrix is linear in
|
|
47
|
+
/// the box, so this is exactly the scale-1 mapping with more digits. Mirrors
|
|
48
|
+
/// `SEARCH_UNITS_PER_POINT` in `src/rasterizer/web/engine.ts`.
|
|
49
|
+
constexpr double kSearchUnitsPerPoint = 1024;
|
|
50
|
+
|
|
36
51
|
/* ------------------------------------------------------------------ *
|
|
37
52
|
* Errors
|
|
38
53
|
* ------------------------------------------------------------------ */
|
|
@@ -103,12 +118,16 @@ void preflightPath(const std::string& path) {
|
|
|
103
118
|
}
|
|
104
119
|
}
|
|
105
120
|
|
|
106
|
-
void checkCancelled(Cancellation* signal, const char* stage) {
|
|
121
|
+
void checkCancelled(Cancellation* signal, const char* stage, const char* what = "Render") {
|
|
107
122
|
if (signal != nullptr && signal->isCancelled()) {
|
|
108
|
-
throw PdfError(ErrorCode::Cancelled, std::string("
|
|
123
|
+
throw PdfError(ErrorCode::Cancelled, std::string(what) + " cancelled (" + stage + ").");
|
|
109
124
|
}
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
bool isQuarterTurn(int rotation) {
|
|
128
|
+
return rotation == 0 || rotation == 90 || rotation == 180 || rotation == 270;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
/* ------------------------------------------------------------------ *
|
|
113
132
|
* Geometry
|
|
114
133
|
* ------------------------------------------------------------------ */
|
|
@@ -192,6 +211,10 @@ struct Document::Impl {
|
|
|
192
211
|
struct LoadedPage {
|
|
193
212
|
int index = -1;
|
|
194
213
|
FPDF_PAGE page = nullptr;
|
|
214
|
+
/// The page's TEXT, parsed on its first search and then held for as long
|
|
215
|
+
/// as the page is: a find box re-searches on every keystroke. Null until
|
|
216
|
+
/// then — a page that is only ever rendered never pays for it.
|
|
217
|
+
FPDF_TEXTPAGE text = nullptr;
|
|
195
218
|
uint64_t lastUse = 0;
|
|
196
219
|
};
|
|
197
220
|
std::vector<LoadedPage> cache;
|
|
@@ -230,12 +253,40 @@ struct Document::Impl {
|
|
|
230
253
|
// FORM_OnBeforeClosePage in unloadPage.
|
|
231
254
|
FORM_OnAfterLoadPage(page, form);
|
|
232
255
|
}
|
|
233
|
-
|
|
256
|
+
LoadedPage entry;
|
|
257
|
+
entry.index = index;
|
|
258
|
+
entry.page = page;
|
|
259
|
+
entry.lastUse = ++useCounter;
|
|
260
|
+
cache.push_back(entry);
|
|
234
261
|
return page;
|
|
235
262
|
}
|
|
236
263
|
|
|
264
|
+
/// Requires the library lock. The text page of `index`, loading the page
|
|
265
|
+
/// (and making it the most recently used) first.
|
|
266
|
+
FPDF_TEXTPAGE loadTextPage(int index) {
|
|
267
|
+
loadPage(index);
|
|
268
|
+
for (auto& entry : cache) {
|
|
269
|
+
if (entry.index != index) continue;
|
|
270
|
+
if (entry.text == nullptr) {
|
|
271
|
+
entry.text = FPDFText_LoadPage(entry.page);
|
|
272
|
+
if (entry.text == nullptr) {
|
|
273
|
+
throw PdfError(ErrorCode::Corrupt,
|
|
274
|
+
"PDFium could not read the text of page " + std::to_string(index) + ".");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return entry.text;
|
|
278
|
+
}
|
|
279
|
+
// Unreachable: loadPage either put the entry there or threw.
|
|
280
|
+
throw PdfError(ErrorCode::BackendFailure, "Page " + std::to_string(index) + " left the cache.");
|
|
281
|
+
}
|
|
282
|
+
|
|
237
283
|
void unloadPage(LoadedPage& entry) {
|
|
238
284
|
if (entry.page == nullptr) return;
|
|
285
|
+
// The text page FIRST: PDFium requires it closed before its page.
|
|
286
|
+
if (entry.text != nullptr) {
|
|
287
|
+
FPDFText_ClosePage(entry.text);
|
|
288
|
+
entry.text = nullptr;
|
|
289
|
+
}
|
|
239
290
|
if (form != nullptr) FORM_OnBeforeClosePage(entry.page, form);
|
|
240
291
|
FPDF_ClosePage(entry.page);
|
|
241
292
|
entry.page = nullptr;
|
|
@@ -365,6 +416,13 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
365
416
|
" at (" + std::to_string(request.x) + ", " + std::to_string(request.y) + ").");
|
|
366
417
|
}
|
|
367
418
|
|
|
419
|
+
if (!isQuarterTurn(request.rotation)) {
|
|
420
|
+
// Refused rather than folded: `45 / 90` is 0 in integer arithmetic, and a
|
|
421
|
+
// request that asked for a turn must not quietly come back upright.
|
|
422
|
+
throw PdfError(ErrorCode::BackendFailure,
|
|
423
|
+
"Rotation must be 0, 90, 180 or 270, got " + std::to_string(request.rotation) + ".");
|
|
424
|
+
}
|
|
425
|
+
|
|
368
426
|
const int pxW = request.pixelWidth();
|
|
369
427
|
const int pxH = request.pixelHeight();
|
|
370
428
|
const long long pixels = static_cast<long long>(pxW) * pxH;
|
|
@@ -377,8 +435,16 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
377
435
|
// byte-identical to the matrix form on every rect it tried.
|
|
378
436
|
const long long startX = -std::llround(request.x * request.scale);
|
|
379
437
|
const long long startY = -std::llround(request.y * request.scale);
|
|
380
|
-
|
|
381
|
-
|
|
438
|
+
// THE PAGE BOX HANDED TO PDFIUM IS THE DISPLAYED ONE. `size_x` / `size_y`
|
|
439
|
+
// describe the box the page is drawn INTO after `rotate` is applied —
|
|
440
|
+
// `CPDF_Page::GetDisplayMatrix` maps the page's HEIGHT onto `size_x` for
|
|
441
|
+
// rotate 1 and 3 — so a quarter turn swaps the intrinsic extents here. The
|
|
442
|
+
// request's rect is already in that space (see `RasterRequest::rotation`), so
|
|
443
|
+
// the start offsets above need nothing: they slide the whole turned page
|
|
444
|
+
// exactly as they slide an upright one.
|
|
445
|
+
const bool turned = request.rotation == 90 || request.rotation == 270;
|
|
446
|
+
const long long pageW = std::llround((turned ? geometry.height : geometry.width) * request.scale);
|
|
447
|
+
const long long pageH = std::llround((turned ? geometry.width : geometry.height) * request.scale);
|
|
382
448
|
if (pixels > kMaxRasterPixels) {
|
|
383
449
|
throw PdfError(ErrorCode::OutOfMemory,
|
|
384
450
|
"Refused a " + std::to_string(pxW) + "x" + std::to_string(pxH) + " raster (" +
|
|
@@ -451,6 +517,11 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
451
517
|
const int sy = static_cast<int>(startY);
|
|
452
518
|
const int pw = static_cast<int>(pageW);
|
|
453
519
|
const int ph = static_cast<int>(pageH);
|
|
520
|
+
// The HOST's rotation in quarter turns clockwise — never the page's own
|
|
521
|
+
// `/Rotate`, which PDFium has already folded into `geometry` and into the
|
|
522
|
+
// render. Passed to BOTH passes below, so a form field lands where the turned
|
|
523
|
+
// content says and not where the upright page had it.
|
|
524
|
+
const int rotate = request.rotation / 90;
|
|
454
525
|
|
|
455
526
|
auto abandonWith = [&](PdfError error) -> PdfError {
|
|
456
527
|
FPDFBitmap_Destroy(bitmap);
|
|
@@ -466,8 +537,7 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
466
537
|
// TOBECONTINUED when the pause adapter asked to stop, which it does only
|
|
467
538
|
// once the signal is set; a live render runs to DONE in one call.
|
|
468
539
|
PauseAdapter pause(signal);
|
|
469
|
-
int status = FPDF_RenderPageBitmap_Start(bitmap, page, sx, sy, pw, ph,
|
|
470
|
-
&pause.pause);
|
|
540
|
+
int status = FPDF_RenderPageBitmap_Start(bitmap, page, sx, sy, pw, ph, rotate, flags, &pause.pause);
|
|
471
541
|
while (status == FPDF_RENDER_TOBECONTINUED) {
|
|
472
542
|
if (signal != nullptr && signal->isCancelled()) {
|
|
473
543
|
FPDF_RenderPage_Close(page);
|
|
@@ -487,7 +557,7 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
487
557
|
// state — with the same offsets, so they land exactly where the page put
|
|
488
558
|
// them.
|
|
489
559
|
if (request.annotations && impl_->form != nullptr) {
|
|
490
|
-
FPDF_FFLDraw(impl_->form, bitmap, page, sx, sy, pw, ph,
|
|
560
|
+
FPDF_FFLDraw(impl_->form, bitmap, page, sx, sy, pw, ph, rotate, flags);
|
|
491
561
|
}
|
|
492
562
|
|
|
493
563
|
FPDFBitmap_Destroy(bitmap);
|
|
@@ -513,4 +583,137 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
513
583
|
return out;
|
|
514
584
|
}
|
|
515
585
|
|
|
586
|
+
/* ------------------------------------------------------------------ *
|
|
587
|
+
* Text search
|
|
588
|
+
* ------------------------------------------------------------------ */
|
|
589
|
+
|
|
590
|
+
namespace {
|
|
591
|
+
|
|
592
|
+
/// `kContextChars` either side of `[charIndex, charIndex + charCount)`, out of
|
|
593
|
+
/// the text page, collapsed. The window is clamped to the page's text, so a
|
|
594
|
+
/// match near an edge carries less on that side rather than padding. Exactly
|
|
595
|
+
/// `readContext` in `src/rasterizer/web/engine.ts`.
|
|
596
|
+
std::u16string readContext(FPDF_TEXTPAGE text, int totalChars, int charIndex, int charCount) {
|
|
597
|
+
const int start = (std::max)(0, charIndex - kContextChars);
|
|
598
|
+
const int end = (std::min)(totalChars, charIndex + charCount + kContextChars);
|
|
599
|
+
const int count = end - start;
|
|
600
|
+
if (count <= 0) return {};
|
|
601
|
+
// `count + 1` units: PDFium writes the terminator, and its return value
|
|
602
|
+
// counts it.
|
|
603
|
+
std::vector<unsigned short> buffer(static_cast<size_t>(count) + 1, 0);
|
|
604
|
+
const int written = FPDFText_GetText(text, start, count, buffer.data());
|
|
605
|
+
std::u16string raw;
|
|
606
|
+
for (int i = 0; i + 1 < written; i++) raw += static_cast<char16_t>(buffer[static_cast<size_t>(i)]);
|
|
607
|
+
return collapseWhitespace(raw);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
} // namespace
|
|
611
|
+
|
|
612
|
+
std::vector<TextMatch> Document::searchText(const TextSearchRequest& request, Cancellation* signal) {
|
|
613
|
+
checkCancelled(signal, "before start", "Search");
|
|
614
|
+
if (impl_->closed) {
|
|
615
|
+
throw PdfError(ErrorCode::BackendFailure, "Document handle is closed.");
|
|
616
|
+
}
|
|
617
|
+
const PageGeometry& geometry = this->geometry(request.page);
|
|
618
|
+
if (!isQuarterTurn(request.rotation)) {
|
|
619
|
+
// Refused, as a render refuses it: rects for a turn nobody draws would be
|
|
620
|
+
// rects in no space at all.
|
|
621
|
+
throw PdfError(ErrorCode::BackendFailure,
|
|
622
|
+
"Rotation must be 0, 90, 180 or 270, got " + std::to_string(request.rotation) + ".");
|
|
623
|
+
}
|
|
624
|
+
if (request.query.empty()) return {};
|
|
625
|
+
|
|
626
|
+
// THE DISPLAYED PAGE'S BOX, as the renderer hands it to PDFium: the
|
|
627
|
+
// post-/Rotate extents, swapped for a host quarter turn — then scaled up,
|
|
628
|
+
// because `FPDF_PageToDevice` rounds to whole units.
|
|
629
|
+
const bool turned = request.rotation == 90 || request.rotation == 270;
|
|
630
|
+
const double displayedWidth = turned ? geometry.height : geometry.width;
|
|
631
|
+
const double displayedHeight = turned ? geometry.width : geometry.height;
|
|
632
|
+
const long long sizeX = std::llround(displayedWidth * kSearchUnitsPerPoint);
|
|
633
|
+
const long long sizeY = std::llround(displayedHeight * kSearchUnitsPerPoint);
|
|
634
|
+
if (sizeX <= 0 || sizeY <= 0 || sizeX > kMaxInt32 || sizeY > kMaxInt32) {
|
|
635
|
+
throw PdfError(ErrorCode::BackendFailure,
|
|
636
|
+
"Page " + std::to_string(request.page) + " is too large to map search rects on.");
|
|
637
|
+
}
|
|
638
|
+
// Per axis, from the ROUNDED box, so the division undoes exactly the scale
|
|
639
|
+
// PDFium applied rather than the one that was asked for.
|
|
640
|
+
const double pointsPerUnitX = displayedWidth / static_cast<double>(sizeX);
|
|
641
|
+
const double pointsPerUnitY = displayedHeight / static_cast<double>(sizeY);
|
|
642
|
+
const int rotate = request.rotation / 90;
|
|
643
|
+
const unsigned long flags = (request.matchCase ? FPDF_MATCHCASE : 0u) |
|
|
644
|
+
(request.wholeWord ? FPDF_MATCHWHOLEWORD : 0u);
|
|
645
|
+
|
|
646
|
+
std::lock_guard<std::mutex> lock(Library::mutex());
|
|
647
|
+
checkCancelled(signal, "after lock", "Search");
|
|
648
|
+
if (impl_->closed) {
|
|
649
|
+
throw PdfError(ErrorCode::BackendFailure, "Document handle is closed.");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// The text page first: it loads (or re-uses) the page it belongs to, and a
|
|
653
|
+
// page loaded afterwards could evict it.
|
|
654
|
+
FPDF_TEXTPAGE text = impl_->loadTextPage(request.page);
|
|
655
|
+
FPDF_PAGE page = impl_->loadPage(request.page);
|
|
656
|
+
const int totalChars = FPDFText_CountChars(text);
|
|
657
|
+
|
|
658
|
+
// `c_str()` is terminated, which is what an FPDF_WIDESTRING must be.
|
|
659
|
+
// `char16_t` and `unsigned short` are the same 16 bits on every target.
|
|
660
|
+
static_assert(sizeof(char16_t) == sizeof(FPDF_WCHAR), "UTF-16 unit size");
|
|
661
|
+
FPDF_SCHHANDLE find =
|
|
662
|
+
FPDFText_FindStart(text, reinterpret_cast<FPDF_WIDESTRING>(request.query.c_str()), flags, 0);
|
|
663
|
+
if (find == nullptr) return {};
|
|
664
|
+
struct CloseFind {
|
|
665
|
+
FPDF_SCHHANDLE handle;
|
|
666
|
+
~CloseFind() { FPDFText_FindClose(handle); }
|
|
667
|
+
} closeFind{find};
|
|
668
|
+
|
|
669
|
+
const auto toDevice = [&](double pageX, double pageY, int& x, int& y) {
|
|
670
|
+
if (!FPDF_PageToDevice(page, 0, 0, static_cast<int>(sizeX), static_cast<int>(sizeY), rotate,
|
|
671
|
+
pageX, pageY, &x, &y)) {
|
|
672
|
+
throw PdfError(ErrorCode::BackendFailure,
|
|
673
|
+
"PDFium could not map a search rect on page " + std::to_string(request.page) +
|
|
674
|
+
" to device space.");
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
std::vector<TextMatch> matches;
|
|
679
|
+
while (FPDFText_FindNext(find)) {
|
|
680
|
+
// Between matches, through `poll()` so the test affordance can land a
|
|
681
|
+
// cancel INSIDE a search. A superseded "find" on a dense drawing stops
|
|
682
|
+
// here instead of collecting hundreds of hits nobody will see.
|
|
683
|
+
if (signal != nullptr && signal->poll()) {
|
|
684
|
+
throw PdfError(ErrorCode::Cancelled, "Search cancelled (between matches).");
|
|
685
|
+
}
|
|
686
|
+
const int charIndex = FPDFText_GetSchResultIndex(find);
|
|
687
|
+
const int charCount = FPDFText_GetSchCount(find);
|
|
688
|
+
// Cannot happen for a non-empty query; the guard is against looping
|
|
689
|
+
// forever if PDFium ever reported one.
|
|
690
|
+
if (charCount <= 0) break;
|
|
691
|
+
|
|
692
|
+
TextMatch match;
|
|
693
|
+
match.page = request.page;
|
|
694
|
+
match.charIndex = charIndex;
|
|
695
|
+
match.charCount = charCount;
|
|
696
|
+
const int rectCount = FPDFText_CountRects(text, charIndex, charCount);
|
|
697
|
+
for (int i = 0; i < rectCount; i++) {
|
|
698
|
+
double left = 0, top = 0, right = 0, bottom = 0;
|
|
699
|
+
if (!FPDFText_GetRect(text, i, &left, &top, &right, &bottom)) continue;
|
|
700
|
+
// User space, y up (top > bottom). Both corners through the display
|
|
701
|
+
// matrix; a quarter turn can swap which one is the top-left, so the
|
|
702
|
+
// rect is min/max of the two.
|
|
703
|
+
int x1 = 0, y1 = 0, x2 = 0, y2 = 0;
|
|
704
|
+
toDevice(left, top, x1, y1);
|
|
705
|
+
toDevice(right, bottom, x2, y2);
|
|
706
|
+
DocRect rect;
|
|
707
|
+
rect.x = (std::min)(x1, x2) * pointsPerUnitX;
|
|
708
|
+
rect.y = (std::min)(y1, y2) * pointsPerUnitY;
|
|
709
|
+
rect.width = std::abs(x2 - x1) * pointsPerUnitX;
|
|
710
|
+
rect.height = std::abs(y2 - y1) * pointsPerUnitY;
|
|
711
|
+
match.rects.push_back(rect);
|
|
712
|
+
}
|
|
713
|
+
match.context = readContext(text, totalChars, charIndex, charCount);
|
|
714
|
+
matches.push_back(std::move(match));
|
|
715
|
+
}
|
|
716
|
+
return matches;
|
|
717
|
+
}
|
|
718
|
+
|
|
516
719
|
} // namespace pdfcanvas
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include <string>
|
|
4
4
|
|
|
5
5
|
#include "pdfcanvas/slots.h"
|
|
6
|
+
#include "pdfcanvas/text_search.h"
|
|
6
7
|
|
|
7
8
|
namespace pdfcanvas {
|
|
8
9
|
|
|
@@ -46,52 +47,66 @@ void Service::registerCancellation(int handle, int64_t token) {
|
|
|
46
47
|
signals_[signalKey(handle, token)] = std::make_shared<Cancellation>();
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/// Looks up (or plants) the job's signal and its document under the service
|
|
51
|
+
/// mutex, and unregisters the signal on EVERY path out, including a throw —
|
|
52
|
+
/// otherwise a superseded epoch, which produces a stream of cancelled jobs,
|
|
53
|
+
/// grows the map for the life of the module.
|
|
54
|
+
struct Service::Lease {
|
|
55
|
+
Service& service;
|
|
56
|
+
int64_t key;
|
|
52
57
|
std::shared_ptr<Cancellation> signal;
|
|
53
58
|
std::shared_ptr<Document> document;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
|
|
60
|
+
Lease(Service& owner, int handle, int64_t token)
|
|
61
|
+
: service(owner), key(signalKey(handle, token)) {
|
|
62
|
+
std::lock_guard<std::mutex> lock(service.mutex_);
|
|
63
|
+
auto found = service.signals_.find(key);
|
|
64
|
+
if (found != service.signals_.end()) {
|
|
58
65
|
signal = found->second;
|
|
59
66
|
} else {
|
|
60
67
|
// Not registered ahead of time: a caller that dispatched without the
|
|
61
68
|
// register step. It still gets a signal, so a cancel that arrives while
|
|
62
|
-
// this
|
|
69
|
+
// this job runs is honoured.
|
|
63
70
|
signal = std::make_shared<Cancellation>();
|
|
64
|
-
signals_.emplace(key, signal);
|
|
71
|
+
service.signals_.emplace(key, signal);
|
|
65
72
|
}
|
|
66
|
-
auto doc = documents_.find(handle);
|
|
67
|
-
document = doc == documents_.end() ? nullptr : doc->second;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// life of the module.
|
|
73
|
-
struct Unregister {
|
|
74
|
-
Service* service;
|
|
75
|
-
int64_t key;
|
|
76
|
-
~Unregister() {
|
|
77
|
-
std::lock_guard<std::mutex> lock(service->mutex_);
|
|
78
|
-
service->signals_.erase(key);
|
|
73
|
+
auto doc = service.documents_.find(handle);
|
|
74
|
+
document = doc == service.documents_.end() ? nullptr : doc->second;
|
|
75
|
+
if (document == nullptr) {
|
|
76
|
+
service.signals_.erase(key);
|
|
77
|
+
throw PdfError(ErrorCode::BackendFailure,
|
|
78
|
+
"No open document for handle " + std::to_string(handle) + ".");
|
|
79
79
|
}
|
|
80
|
-
}
|
|
80
|
+
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
~Lease() {
|
|
83
|
+
std::lock_guard<std::mutex> lock(service.mutex_);
|
|
84
|
+
service.signals_.erase(key);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
Lease(const Lease&) = delete;
|
|
88
|
+
Lease& operator=(const Lease&) = delete;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
RasterPixels Service::render(int handle, const RasterRequest& request, int64_t token,
|
|
92
|
+
bool useSlot) {
|
|
93
|
+
Lease lease(*this, handle, token);
|
|
94
|
+
|
|
87
95
|
// Read ONCE per render: whether to reach for a slot. The sink may still
|
|
88
96
|
// refuse, in which case the raster comes back heap-backed and the caller
|
|
89
97
|
// branches on where the pixels ARE, not on what was asked for.
|
|
90
98
|
if (useSlot) {
|
|
91
99
|
SlotSink sink;
|
|
92
|
-
return document->render(request, signal.get(), &sink);
|
|
100
|
+
return lease.document->render(request, lease.signal.get(), &sink);
|
|
93
101
|
}
|
|
94
|
-
return document->render(request, signal.get(), nullptr);
|
|
102
|
+
return lease.document->render(request, lease.signal.get(), nullptr);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
std::string Service::searchJson(int handle, const TextSearchRequest& request, int64_t token) {
|
|
106
|
+
Lease lease(*this, handle, token);
|
|
107
|
+
// Serialised HERE rather than in either binding, so the two cannot
|
|
108
|
+
// disagree about the wire form and the JSON is proven on a laptop.
|
|
109
|
+
return textMatchesToJson(lease.document->searchText(request, lease.signal.get()));
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
void Service::cancel(int handle, int64_t token) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#include "pdfcanvas/text_search.h"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
#include <cstdio>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
namespace pdfcanvas {
|
|
8
|
+
|
|
9
|
+
namespace {
|
|
10
|
+
|
|
11
|
+
void appendNumber(std::string& out, double value) {
|
|
12
|
+
if (!std::isfinite(value)) {
|
|
13
|
+
out += '0';
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
char buffer[40];
|
|
17
|
+
const int written = std::snprintf(buffer, sizeof(buffer), "%.17g", value);
|
|
18
|
+
if (written <= 0) {
|
|
19
|
+
out += '0';
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (int i = 0; i < written && buffer[i] != '\0'; i++) {
|
|
23
|
+
// `%g` emits digits, a sign, an exponent and ONE locale decimal separator.
|
|
24
|
+
// Anything that is not one of the first three is that separator.
|
|
25
|
+
const char c = buffer[i];
|
|
26
|
+
const bool keep = (c >= '0' && c <= '9') || c == '-' || c == '+' || c == 'e' || c == 'E';
|
|
27
|
+
out += keep ? c : '.';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
void appendString(std::string& out, const std::u16string& text) {
|
|
32
|
+
static const char kHex[] = "0123456789abcdef";
|
|
33
|
+
out += '"';
|
|
34
|
+
for (const char16_t unit : text) {
|
|
35
|
+
if (unit >= 0x20 && unit < 0x7F && unit != u'"' && unit != u'\\') {
|
|
36
|
+
out += static_cast<char>(unit);
|
|
37
|
+
} else {
|
|
38
|
+
out += "\\u";
|
|
39
|
+
out += kHex[(unit >> 12) & 0xF];
|
|
40
|
+
out += kHex[(unit >> 8) & 0xF];
|
|
41
|
+
out += kHex[(unit >> 4) & 0xF];
|
|
42
|
+
out += kHex[unit & 0xF];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
out += '"';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
} // namespace
|
|
49
|
+
|
|
50
|
+
bool isJsWhitespace(char16_t unit) noexcept {
|
|
51
|
+
// ECMAScript WhiteSpace + LineTerminator: TAB VT FF SP NBSP ZWNBSP, the
|
|
52
|
+
// Unicode Space_Separator category, LF CR LS PS.
|
|
53
|
+
switch (unit) {
|
|
54
|
+
case 0x0009: case 0x000A: case 0x000B: case 0x000C: case 0x000D:
|
|
55
|
+
case 0x0020: case 0x00A0: case 0x1680:
|
|
56
|
+
case 0x2028: case 0x2029: case 0x202F: case 0x205F: case 0x3000:
|
|
57
|
+
case 0xFEFF:
|
|
58
|
+
return true;
|
|
59
|
+
default:
|
|
60
|
+
return unit >= 0x2000 && unit <= 0x200A;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
std::u16string collapseWhitespace(const std::u16string& text) {
|
|
65
|
+
std::u16string out;
|
|
66
|
+
out.reserve(text.size());
|
|
67
|
+
bool pendingSpace = false;
|
|
68
|
+
for (const char16_t unit : text) {
|
|
69
|
+
if (isJsWhitespace(unit)) {
|
|
70
|
+
pendingSpace = !out.empty(); // leading whitespace is dropped
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (pendingSpace) out += u' ';
|
|
74
|
+
pendingSpace = false;
|
|
75
|
+
out += unit;
|
|
76
|
+
}
|
|
77
|
+
return out; // trailing whitespace never emitted its space
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
std::string textMatchesToJson(const std::vector<TextMatch>& matches) {
|
|
81
|
+
std::string out;
|
|
82
|
+
out.reserve(64 + matches.size() * 200);
|
|
83
|
+
out += '[';
|
|
84
|
+
for (size_t i = 0; i < matches.size(); i++) {
|
|
85
|
+
const TextMatch& match = matches[i];
|
|
86
|
+
if (i > 0) out += ',';
|
|
87
|
+
out += "{\"page\":";
|
|
88
|
+
out += std::to_string(match.page);
|
|
89
|
+
out += ",\"charIndex\":";
|
|
90
|
+
out += std::to_string(match.charIndex);
|
|
91
|
+
out += ",\"charCount\":";
|
|
92
|
+
out += std::to_string(match.charCount);
|
|
93
|
+
out += ",\"rects\":[";
|
|
94
|
+
for (size_t j = 0; j < match.rects.size(); j++) {
|
|
95
|
+
const DocRect& rect = match.rects[j];
|
|
96
|
+
if (j > 0) out += ',';
|
|
97
|
+
out += "{\"x\":";
|
|
98
|
+
appendNumber(out, rect.x);
|
|
99
|
+
out += ",\"y\":";
|
|
100
|
+
appendNumber(out, rect.y);
|
|
101
|
+
out += ",\"width\":";
|
|
102
|
+
appendNumber(out, rect.width);
|
|
103
|
+
out += ",\"height\":";
|
|
104
|
+
appendNumber(out, rect.height);
|
|
105
|
+
out += '}';
|
|
106
|
+
}
|
|
107
|
+
out += "],\"context\":";
|
|
108
|
+
appendString(out, match.context);
|
|
109
|
+
out += '}';
|
|
110
|
+
}
|
|
111
|
+
out += ']';
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
} // namespace pdfcanvas
|
|
@@ -204,6 +204,33 @@ Bytes formFieldPage() {
|
|
|
204
204
|
return w.finish(catalog);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
namespace {
|
|
208
|
+
|
|
209
|
+
Bytes helveticaPage(const std::string& content, int rotate) {
|
|
210
|
+
Writer w;
|
|
211
|
+
const int contents = w.add(streamObject(content + "\n"));
|
|
212
|
+
// Indirect, as the spec prefers for a font resource; written before the page
|
|
213
|
+
// so /Resources can name the number it actually got.
|
|
214
|
+
const int font = w.add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
|
|
215
|
+
const int pagesNumber = w.count() + 2;
|
|
216
|
+
std::string dict = "<< /Type /Page /Parent " + std::to_string(pagesNumber) + " 0 R /MediaBox [0 0 " +
|
|
217
|
+
std::to_string(kLetterW) + " " + std::to_string(kLetterH) + "]";
|
|
218
|
+
if (rotate != 0) dict += " /Rotate " + std::to_string(rotate);
|
|
219
|
+
dict += " /Contents " + std::to_string(contents) + " 0 R /Resources << /Font << /F1 " +
|
|
220
|
+
std::to_string(font) + " 0 R >> >> >>";
|
|
221
|
+
const int pageObj = w.add(dict);
|
|
222
|
+
const int pages = w.add("<< /Type /Pages /Kids [" + std::to_string(pageObj) + " 0 R] /Count 1 >>");
|
|
223
|
+
const int catalog = w.add("<< /Type /Catalog /Pages " + std::to_string(pages) + " 0 R >>");
|
|
224
|
+
if (pages != pagesNumber) throw std::logic_error("pages object number drifted");
|
|
225
|
+
return w.finish(catalog);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
} // namespace
|
|
229
|
+
|
|
230
|
+
Bytes textPage(int rotate) { return helveticaPage(kTextContent, rotate); }
|
|
231
|
+
|
|
232
|
+
Bytes twoLineTextPage() { return helveticaPage(kTwoLineTextContent, 0); }
|
|
233
|
+
|
|
207
234
|
/* ------------------------------------------------------------------ *
|
|
208
235
|
* Multi-page fixtures
|
|
209
236
|
* ------------------------------------------------------------------ */
|
package/native/tests/fixtures.h
CHANGED
|
@@ -61,6 +61,21 @@ Bytes annotatedPage();
|
|
|
61
61
|
/// through the document's AcroForm. Content: white page with a red marker.
|
|
62
62
|
Bytes formFieldPage();
|
|
63
63
|
|
|
64
|
+
/// ONE LINE OF REAL TEXT: "Hello measured world", 24pt Helvetica, baseline at
|
|
65
|
+
/// user-space (72, 700) on a 612x792 page carrying `/Rotate rotate`. The font is
|
|
66
|
+
/// NOT embedded — Helvetica is one of the standard 14, which PDFium substitutes
|
|
67
|
+
/// from its built-in fonts, so it draws and extracts with no font file. The same
|
|
68
|
+
/// bytes as `textPdf` in `src/__tests__/support/pdfium.ts`, so both engines are
|
|
69
|
+
/// asked the same question.
|
|
70
|
+
constexpr const char* kTextContent = "BT /F1 24 Tf 72 700 Td (Hello measured world) Tj ET";
|
|
71
|
+
Bytes textPage(int rotate = 0);
|
|
72
|
+
/// The same line plus "on a second line" 30pt below it, so PDFium's text page
|
|
73
|
+
/// carries a generated line break a match's context must collapse. The same
|
|
74
|
+
/// bytes as `twoLineTextPdf` in `src/__tests__/support/pdfium.ts`.
|
|
75
|
+
constexpr const char* kTwoLineTextContent =
|
|
76
|
+
"BT /F1 24 Tf 72 700 Td (Hello measured world) Tj 0 -30 Td (on a second line) Tj ET";
|
|
77
|
+
Bytes twoLineTextPage();
|
|
78
|
+
|
|
64
79
|
/// A single page with explicit boxes, rotation and content.
|
|
65
80
|
Bytes page(Box mediaBox, const Box* cropBox, int rotate, const std::string& content);
|
|
66
81
|
|