@reekon-tools/react-native-pdf-canvas 0.3.0 → 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 +44 -6
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +30 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +22 -3
- package/android/src/main/cpp/pdfcanvas-jni.cpp +43 -1
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +26 -1
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +69 -6
- package/android/tools/compile-gate.sh +6 -4
- package/dist/controller.js +5 -0
- package/dist/rasterizer/fake.d.ts +24 -2
- package/dist/rasterizer/fake.js +74 -1
- package/dist/rasterizer/native-bridge.d.ts +37 -1
- package/dist/rasterizer/native-bridge.js +95 -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 +199 -1
- 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 +17 -2
- package/dist/react/usePdfDocument.js +44 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/types.d.ts +66 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +66 -2
- 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 +41 -0
- package/native/core/pdfcanvas-core.cmake +1 -0
- package/native/core/src/document.cpp +188 -5
- 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 +308 -0
- package/native/tests/test_service.cpp +110 -0
- package/package.json +1 -1
|
@@ -46,6 +46,9 @@ import type { PdfErrorCode } from '../../types.js';
|
|
|
46
46
|
export declare const FPDF_BITMAP_BGRA = 4;
|
|
47
47
|
/** Draw PDF-embedded annotations. Answers `RasterRequest.annotations`. */
|
|
48
48
|
export declare const FPDF_ANNOT = 1;
|
|
49
|
+
/** `FPDFText_FindStart` flags. Answer `TextSearchRequest.matchCase` / `.wholeWord`. */
|
|
50
|
+
export declare const FPDF_MATCHCASE = 1;
|
|
51
|
+
export declare const FPDF_MATCHWHOLEWORD = 2;
|
|
49
52
|
/**
|
|
50
53
|
* Write RGBA instead of BGRA.
|
|
51
54
|
*
|
|
@@ -95,11 +98,18 @@ export interface PdfiumHeap {
|
|
|
95
98
|
free(pointer: number): void;
|
|
96
99
|
/** Reads an `FS_SIZEF` component — PDFium's float32. */
|
|
97
100
|
getFloat(pointer: number): number;
|
|
101
|
+
/** Reads a `double*` out-parameter — `FPDFText_GetRect`'s four edges. */
|
|
102
|
+
getDouble(pointer: number): number;
|
|
103
|
+
/** Reads an `int*` out-parameter — `FPDF_PageToDevice`'s two coordinates. */
|
|
104
|
+
getInt32(pointer: number): number;
|
|
98
105
|
}
|
|
99
106
|
/**
|
|
100
107
|
* Exactly the PDFium entry points this backend calls. Nothing speculative:
|
|
101
|
-
*
|
|
102
|
-
*
|
|
108
|
+
* links and form filling are absent because neither is implemented, and
|
|
109
|
+
* `RasterizerCapabilities` says so. The `FPDFText_*` group is the text search
|
|
110
|
+
* surface (`RasterizerHandle.searchText`), and `FPDF_PageToDevice` is how its
|
|
111
|
+
* rects are mapped into the displayed page's doc space — see `search` in
|
|
112
|
+
* `./engine.ts`.
|
|
103
113
|
*/
|
|
104
114
|
export interface PdfiumBinding extends PdfiumHeap {
|
|
105
115
|
/**
|
|
@@ -149,6 +159,40 @@ export interface PdfiumBinding extends PdfiumHeap {
|
|
|
149
159
|
FPDFBitmap_GetBuffer(bitmap: number): number;
|
|
150
160
|
FPDFBitmap_GetStride(bitmap: number): number;
|
|
151
161
|
FPDF_RenderPageBitmap(bitmap: number, page: number, startX: number, startY: number, sizeX: number, sizeY: number, rotate: number, flags: number): void;
|
|
162
|
+
/**
|
|
163
|
+
* Maps a point in the page's USER space onto a device box. `sizeX`/`sizeY`
|
|
164
|
+
* are the box the page is displayed INTO after `rotate` — the same box the
|
|
165
|
+
* render call takes, extents swapped for a quarter turn — and the output is
|
|
166
|
+
* ROUNDED TO WHOLE DEVICE UNITS by PDFium (`FXSYS_roundf` on both), which is
|
|
167
|
+
* why `search` scales the box up before calling it.
|
|
168
|
+
*/
|
|
169
|
+
FPDF_PageToDevice(page: number, startX: number, startY: number, sizeX: number, sizeY: number, rotate: number, pageX: number, pageY: number, deviceXOut: number, deviceYOut: number): boolean;
|
|
170
|
+
/** Parses the page's text. Balanced by `FPDFText_ClosePage`. */
|
|
171
|
+
FPDFText_LoadPage(page: number): number;
|
|
172
|
+
FPDFText_ClosePage(textPage: number): void;
|
|
173
|
+
FPDFText_CountChars(textPage: number): number;
|
|
174
|
+
/**
|
|
175
|
+
* Writes `count` UTF-16 code units plus a terminating 0 to `buffer` (which
|
|
176
|
+
* must hold `count + 1` units) and returns how many units it wrote, the
|
|
177
|
+
* terminator included.
|
|
178
|
+
*/
|
|
179
|
+
FPDFText_GetText(textPage: number, startIndex: number, count: number, buffer: number): number;
|
|
180
|
+
/**
|
|
181
|
+
* `findWhat` is an `FPDF_WIDESTRING`: UTF-16LE code units in the heap with a
|
|
182
|
+
* terminating 0. Returns a search handle, or 0 for an empty query.
|
|
183
|
+
*/
|
|
184
|
+
FPDFText_FindStart(textPage: number, findWhat: number, flags: number, startIndex: number): number;
|
|
185
|
+
FPDFText_FindNext(handle: number): boolean;
|
|
186
|
+
FPDFText_FindClose(handle: number): void;
|
|
187
|
+
FPDFText_GetSchResultIndex(handle: number): number;
|
|
188
|
+
FPDFText_GetSchCount(handle: number): number;
|
|
189
|
+
/** Counts the rects a char range occupies and caches them for `GetRect`. */
|
|
190
|
+
FPDFText_CountRects(textPage: number, startIndex: number, count: number): number;
|
|
191
|
+
/**
|
|
192
|
+
* One cached rect, as four `double*` out-parameters in the page's USER space
|
|
193
|
+
* (y up, so `top > bottom`).
|
|
194
|
+
*/
|
|
195
|
+
FPDFText_GetRect(textPage: number, rectIndex: number, leftOut: number, topOut: number, rightOut: number, bottomOut: number): boolean;
|
|
152
196
|
}
|
|
153
197
|
/**
|
|
154
198
|
* The shape of `@embedpdf/pdfium`'s `WrappedPdfiumModule`, restated as the
|
|
@@ -189,6 +233,24 @@ export interface PdfiumWasmModule {
|
|
|
189
233
|
FPDFBitmap_GetBuffer(bitmap: number): number;
|
|
190
234
|
FPDFBitmap_GetStride(bitmap: number): number;
|
|
191
235
|
FPDF_RenderPageBitmap(bitmap: number, page: number, startX: number, startY: number, sizeX: number, sizeY: number, rotate: number, flags: number): null;
|
|
236
|
+
FPDF_PageToDevice(page: number, startX: number, startY: number, sizeX: number, sizeY: number, rotate: number, pageX: number, pageY: number, deviceXOut: number, deviceYOut: number): boolean;
|
|
237
|
+
FPDFText_LoadPage(page: number): number;
|
|
238
|
+
FPDFText_ClosePage(textPage: number): null;
|
|
239
|
+
FPDFText_CountChars(textPage: number): number;
|
|
240
|
+
FPDFText_GetText(textPage: number, startIndex: number, count: number, buffer: number): number;
|
|
241
|
+
/**
|
|
242
|
+
* `findWhat` is a POINTER (`number`), not a JS string: the real module's
|
|
243
|
+
* signature is `["number", "number", "number", "number"]`, so the UTF-16LE
|
|
244
|
+
* query is written into the heap by the caller. A `"string"` here would
|
|
245
|
+
* have `tsc` accept a call the wasm would read as a garbage pointer.
|
|
246
|
+
*/
|
|
247
|
+
FPDFText_FindStart(textPage: number, findWhat: number, flags: number, startIndex: number): number;
|
|
248
|
+
FPDFText_FindNext(handle: number): boolean;
|
|
249
|
+
FPDFText_FindClose(handle: number): null;
|
|
250
|
+
FPDFText_GetSchResultIndex(handle: number): number;
|
|
251
|
+
FPDFText_GetSchCount(handle: number): number;
|
|
252
|
+
FPDFText_CountRects(textPage: number, startIndex: number, count: number): number;
|
|
253
|
+
FPDFText_GetRect(textPage: number, rectIndex: number, leftOut: number, topOut: number, rightOut: number, bottomOut: number): boolean;
|
|
192
254
|
}
|
|
193
255
|
/**
|
|
194
256
|
* Turn an initialised `@embedpdf/pdfium` module into a `PdfiumBinding`.
|
|
@@ -48,6 +48,9 @@ import { PdfError } from '../../types.js';
|
|
|
48
48
|
export const FPDF_BITMAP_BGRA = 4;
|
|
49
49
|
/** Draw PDF-embedded annotations. Answers `RasterRequest.annotations`. */
|
|
50
50
|
export const FPDF_ANNOT = 0x01;
|
|
51
|
+
/** `FPDFText_FindStart` flags. Answer `TextSearchRequest.matchCase` / `.wholeWord`. */
|
|
52
|
+
export const FPDF_MATCHCASE = 0x01;
|
|
53
|
+
export const FPDF_MATCHWHOLEWORD = 0x02;
|
|
51
54
|
/**
|
|
52
55
|
* Write RGBA instead of BGRA.
|
|
53
56
|
*
|
|
@@ -96,6 +99,8 @@ export function bindPdfium(module) {
|
|
|
96
99
|
malloc: byteLength => pdfium.wasmExports.malloc(byteLength),
|
|
97
100
|
free: pointer => pdfium.wasmExports.free(pointer),
|
|
98
101
|
getFloat: pointer => Number(pdfium.getValue(pointer, 'float')),
|
|
102
|
+
getDouble: pointer => Number(pdfium.getValue(pointer, 'double')),
|
|
103
|
+
getInt32: pointer => Number(pdfium.getValue(pointer, 'i32')),
|
|
99
104
|
FPDF_LoadMemDocument: (pointer, byteLength, password) => module.FPDF_LoadMemDocument(pointer, byteLength, password),
|
|
100
105
|
FPDF_CloseDocument: document => {
|
|
101
106
|
module.FPDF_CloseDocument(document);
|
|
@@ -118,6 +123,22 @@ export function bindPdfium(module) {
|
|
|
118
123
|
FPDF_RenderPageBitmap: (bitmap, page, startX, startY, sizeX, sizeY, rotate, flags) => {
|
|
119
124
|
module.FPDF_RenderPageBitmap(bitmap, page, startX, startY, sizeX, sizeY, rotate, flags);
|
|
120
125
|
},
|
|
126
|
+
FPDF_PageToDevice: (page, startX, startY, sizeX, sizeY, rotate, pageX, pageY, deviceXOut, deviceYOut) => module.FPDF_PageToDevice(page, startX, startY, sizeX, sizeY, rotate, pageX, pageY, deviceXOut, deviceYOut),
|
|
127
|
+
FPDFText_LoadPage: page => module.FPDFText_LoadPage(page),
|
|
128
|
+
FPDFText_ClosePage: textPage => {
|
|
129
|
+
module.FPDFText_ClosePage(textPage);
|
|
130
|
+
},
|
|
131
|
+
FPDFText_CountChars: textPage => module.FPDFText_CountChars(textPage),
|
|
132
|
+
FPDFText_GetText: (textPage, startIndex, count, buffer) => module.FPDFText_GetText(textPage, startIndex, count, buffer),
|
|
133
|
+
FPDFText_FindStart: (textPage, findWhat, flags, startIndex) => module.FPDFText_FindStart(textPage, findWhat, flags, startIndex),
|
|
134
|
+
FPDFText_FindNext: handle => module.FPDFText_FindNext(handle),
|
|
135
|
+
FPDFText_FindClose: handle => {
|
|
136
|
+
module.FPDFText_FindClose(handle);
|
|
137
|
+
},
|
|
138
|
+
FPDFText_GetSchResultIndex: handle => module.FPDFText_GetSchResultIndex(handle),
|
|
139
|
+
FPDFText_GetSchCount: handle => module.FPDFText_GetSchCount(handle),
|
|
140
|
+
FPDFText_CountRects: (textPage, startIndex, count) => module.FPDFText_CountRects(textPage, startIndex, count),
|
|
141
|
+
FPDFText_GetRect: (textPage, rectIndex, leftOut, topOut, rightOut, bottomOut) => module.FPDFText_GetRect(textPage, rectIndex, leftOut, topOut, rightOut, bottomOut),
|
|
121
142
|
};
|
|
122
143
|
}
|
|
123
144
|
/* ------------------------------------------------------------------ *
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* exists but has not been told where its wasm is" — that could otherwise be
|
|
22
22
|
* observed by a `render` arriving first.
|
|
23
23
|
*/
|
|
24
|
-
import type { AlphaEncoding, PageGeometry, PdfErrorCode, PixelFormat, RasterRequest } from '../../types.js';
|
|
24
|
+
import type { AlphaEncoding, PageGeometry, PdfErrorCode, PixelFormat, RasterRequest, TextMatch, TextSearchRequest } from '../../types.js';
|
|
25
25
|
/**
|
|
26
26
|
* Where the worker gets `pdfium.wasm`.
|
|
27
27
|
*
|
|
@@ -65,11 +65,22 @@ export interface RenderRequest {
|
|
|
65
65
|
request: RasterRequest;
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* Find text on one page. Routed exactly as a render is — the same queue, the
|
|
69
|
+
* same yield, the same `cancel` by id — so a search never overtakes a render
|
|
70
|
+
* that was posted before it and a superseded search can still be dropped
|
|
71
|
+
* before it starts.
|
|
72
|
+
*/
|
|
73
|
+
export interface SearchRequest {
|
|
74
|
+
kind: 'search';
|
|
75
|
+
id: number;
|
|
76
|
+
request: TextSearchRequest;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Drop a render or a search that has not started yet.
|
|
69
80
|
*
|
|
70
81
|
* BEST EFFORT AND HONEST ABOUT IT. The worker renders synchronously, so a
|
|
71
|
-
* `cancel` can only ever be delivered BETWEEN
|
|
72
|
-
* genuinely does is stop a QUEUED
|
|
82
|
+
* `cancel` can only ever be delivered BETWEEN jobs — never into one. What it
|
|
83
|
+
* genuinely does is stop a QUEUED job from starting, which is why the worker
|
|
73
84
|
* yields to its event loop between jobs (see `./session.ts`): without that
|
|
74
85
|
* yield, messages queued behind a burst of renders would not be read until the
|
|
75
86
|
* burst had finished and this message would be worthless.
|
|
@@ -94,7 +105,7 @@ export interface CancelRequest {
|
|
|
94
105
|
export interface CloseRequest {
|
|
95
106
|
kind: 'close';
|
|
96
107
|
}
|
|
97
|
-
export type WorkerRequest = OpenRequest | RenderRequest | CancelRequest | CloseRequest;
|
|
108
|
+
export type WorkerRequest = OpenRequest | RenderRequest | SearchRequest | CancelRequest | CloseRequest;
|
|
98
109
|
export interface OpenedResponse {
|
|
99
110
|
kind: 'opened';
|
|
100
111
|
id: number;
|
|
@@ -119,6 +130,12 @@ export interface RenderedResponse {
|
|
|
119
130
|
*/
|
|
120
131
|
alpha: AlphaEncoding;
|
|
121
132
|
}
|
|
133
|
+
/** Plain data, structured-cloned whole: `DocRect`s and strings. */
|
|
134
|
+
export interface SearchedResponse {
|
|
135
|
+
kind: 'searched';
|
|
136
|
+
id: number;
|
|
137
|
+
matches: TextMatch[];
|
|
138
|
+
}
|
|
122
139
|
export interface ErrorResponse {
|
|
123
140
|
kind: 'error';
|
|
124
141
|
/** Null for a failure that belongs to no single request. */
|
|
@@ -126,7 +143,7 @@ export interface ErrorResponse {
|
|
|
126
143
|
code: PdfErrorCode;
|
|
127
144
|
message: string;
|
|
128
145
|
}
|
|
129
|
-
export type WorkerResponse = OpenedResponse | RenderedResponse | ErrorResponse;
|
|
146
|
+
export type WorkerResponse = OpenedResponse | RenderedResponse | SearchedResponse | ErrorResponse;
|
|
130
147
|
/**
|
|
131
148
|
* Is this a message from our worker?
|
|
132
149
|
*
|
|
@@ -33,5 +33,8 @@ export function isWorkerResponse(value) {
|
|
|
33
33
|
if (typeof value !== 'object' || value === null)
|
|
34
34
|
return false;
|
|
35
35
|
const kind = value.kind;
|
|
36
|
-
return kind === 'opened' ||
|
|
36
|
+
return (kind === 'opened' ||
|
|
37
|
+
kind === 'rendered' ||
|
|
38
|
+
kind === 'searched' ||
|
|
39
|
+
kind === 'error');
|
|
37
40
|
}
|
|
@@ -180,6 +180,13 @@ export function createWorkerSession(deps) {
|
|
|
180
180
|
let opening = null;
|
|
181
181
|
let document = null;
|
|
182
182
|
let disposed = false;
|
|
183
|
+
/**
|
|
184
|
+
* Renders AND searches, one queue. A search is a synchronous PDFium call
|
|
185
|
+
* like a render, wants the same between-jobs cancel window, and must not
|
|
186
|
+
* overtake a render posted before it — a page's text is parsed from the
|
|
187
|
+
* same loaded page the render uses, so keeping them in order keeps the
|
|
188
|
+
* engine's one-page cache hot for both.
|
|
189
|
+
*/
|
|
183
190
|
const queue = [];
|
|
184
191
|
/**
|
|
185
192
|
* Ids the main thread has given up on.
|
|
@@ -286,18 +293,27 @@ export function createWorkerSession(deps) {
|
|
|
286
293
|
if (cancelled.delete(job.id))
|
|
287
294
|
continue;
|
|
288
295
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
if (job.kind === 'search') {
|
|
297
|
+
deps.post({
|
|
298
|
+
kind: 'searched',
|
|
299
|
+
id: job.id,
|
|
300
|
+
matches: target.search(job.request),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const raster = target.render(job.request);
|
|
305
|
+
const buffer = raster.bytes.buffer;
|
|
306
|
+
deps.post({
|
|
307
|
+
kind: 'rendered',
|
|
308
|
+
id: job.id,
|
|
309
|
+
bytes: buffer,
|
|
310
|
+
width: raster.width,
|
|
311
|
+
height: raster.height,
|
|
312
|
+
rowBytes: raster.rowBytes,
|
|
313
|
+
format: raster.format,
|
|
314
|
+
alpha: raster.alpha,
|
|
315
|
+
}, [buffer]);
|
|
316
|
+
}
|
|
301
317
|
}
|
|
302
318
|
catch (error) {
|
|
303
319
|
postError(job.id, error);
|
|
@@ -387,7 +403,8 @@ export function createWorkerSession(deps) {
|
|
|
387
403
|
});
|
|
388
404
|
return;
|
|
389
405
|
}
|
|
390
|
-
case 'render':
|
|
406
|
+
case 'render':
|
|
407
|
+
case 'search': {
|
|
391
408
|
queue.push(request);
|
|
392
409
|
void drain();
|
|
393
410
|
return;
|
|
@@ -158,6 +158,11 @@ export function PdfContentView({ content, zIndex, sampling = 'linear', paperColo
|
|
|
158
158
|
const out = [];
|
|
159
159
|
if (paperColor != null) {
|
|
160
160
|
for (const page of content.pages) {
|
|
161
|
+
// A page outside the allow-list holds nothing and gets no paper: in a
|
|
162
|
+
// stacked layout its sheet would sit at the origin beside the one page
|
|
163
|
+
// that is meant to show (see PdfPageContent.drawable).
|
|
164
|
+
if (!page.drawable)
|
|
165
|
+
continue;
|
|
161
166
|
const { pageRect } = page;
|
|
162
167
|
out.push(_jsx(Rect, { x: pageRect.x, y: pageRect.y, width: pageRect.width, height: pageRect.height, color: paperColor }, `paper:${page.page}`));
|
|
163
168
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { SkImage } from '@shopify/react-native-skia';
|
|
18
18
|
import { PdfError } from '../types.js';
|
|
19
|
-
import type { DocRect, DocSize, PageGeometry, PageRasterizer, PageRotation, PdfSource, PixelSize, RasterPixels, RasterizerCapabilities, RasterizerHandle } from '../types.js';
|
|
19
|
+
import type { DocRect, DocSize, PageGeometry, PageRasterizer, PageRotation, PdfSource, PixelSize, RasterPixels, RasterizerCapabilities, RasterizerHandle, TextMatch, TextSearchOptions } from '../types.js';
|
|
20
20
|
/** Install the rasterizer used when `usePdfDocument` / `openPdfDocument` are not
|
|
21
21
|
* given one explicitly. Returns the previous value so a test can restore it. */
|
|
22
22
|
export declare function setDefaultRasterizer(rasterizer: PageRasterizer | null): PageRasterizer | null;
|
|
@@ -83,7 +83,22 @@ export interface PdfDocument {
|
|
|
83
83
|
/** Intrinsic sizes of every page, in the order a `PdfPageLayout` expects. */
|
|
84
84
|
pageSizes(): DocSize[];
|
|
85
85
|
renderPage(index: number, options?: PdfRenderOptions): Promise<PdfPageRender>;
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Every occurrence of `query` on one page, in reading order.
|
|
88
|
+
*
|
|
89
|
+
* `TextMatch.rects` are in the page's doc space — PDF points, origin at the
|
|
90
|
+
* DISPLAYED page's top-left, y down — with `options.rotation` applied, so
|
|
91
|
+
* pass the same rotation the page's layer or `renderPage` uses and a hit
|
|
92
|
+
* drawn at a rect lands on the glyphs. An empty or whitespace-only query
|
|
93
|
+
* resolves `[]` without asking the backend. Rejects `backend-failure` for a
|
|
94
|
+
* page index out of range or a closed document, `cancelled` when
|
|
95
|
+
* `options.signal` aborts.
|
|
96
|
+
*/
|
|
97
|
+
searchPage(page: number, query: string, options?: TextSearchOptions): Promise<TextMatch[]>;
|
|
98
|
+
/**
|
|
99
|
+
* True once `close()` has run. Every further `renderPage` rejects with
|
|
100
|
+
* `cancelled`, every further `searchPage` with `backend-failure`.
|
|
101
|
+
*/
|
|
87
102
|
readonly closed: boolean;
|
|
88
103
|
/**
|
|
89
104
|
* Release the underlying file handle. `usePdfDocument` owns this for documents
|
|
@@ -210,6 +210,50 @@ export async function openPdfDocument(source, rasterizer) {
|
|
|
210
210
|
},
|
|
211
211
|
};
|
|
212
212
|
},
|
|
213
|
+
async searchPage(page, query, options) {
|
|
214
|
+
// `backend-failure`, not `renderPage`'s `cancelled`: nothing was
|
|
215
|
+
// superseded — the caller asked a closed document a question, which is
|
|
216
|
+
// a bug in the caller, and "cancelled" is the one code a host is
|
|
217
|
+
// expected to swallow silently.
|
|
218
|
+
if (closed) {
|
|
219
|
+
throw new PdfError('backend-failure', 'searchPage called on a closed PdfDocument');
|
|
220
|
+
}
|
|
221
|
+
if (!Number.isInteger(page) || page < 0 || page >= handle.pageCount) {
|
|
222
|
+
throw new PdfError('backend-failure', `page index ${page} is out of range (pageCount ${handle.pageCount})`);
|
|
223
|
+
}
|
|
224
|
+
// Nothing to find, and every engine would either refuse the query or
|
|
225
|
+
// answer it with nothing — so neither is asked. This is also what makes
|
|
226
|
+
// a find box that clears itself free.
|
|
227
|
+
if (query.trim().length === 0) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
if (isAborted(options?.signal)) {
|
|
231
|
+
throw new PdfError('cancelled', 'searchPage aborted before it started');
|
|
232
|
+
}
|
|
233
|
+
// RESOLVED HERE, once, for every backend: a backend is handed the three
|
|
234
|
+
// options with no defaults of its own left to drift.
|
|
235
|
+
const request = {
|
|
236
|
+
page,
|
|
237
|
+
query,
|
|
238
|
+
matchCase: options?.matchCase ?? false,
|
|
239
|
+
wholeWord: options?.wholeWord ?? false,
|
|
240
|
+
rotation: options?.rotation ?? 0,
|
|
241
|
+
};
|
|
242
|
+
let matches;
|
|
243
|
+
try {
|
|
244
|
+
matches = await handle.searchText(request, options?.signal);
|
|
245
|
+
}
|
|
246
|
+
catch (cause) {
|
|
247
|
+
throw toPdfError(cause, `Failed to search page ${page}`);
|
|
248
|
+
}
|
|
249
|
+
// A backend that answered anyway — the web worker cannot interrupt a
|
|
250
|
+
// search it has started — must not hand hits to a caller that already
|
|
251
|
+
// moved on to the next keystroke.
|
|
252
|
+
if (isAborted(options?.signal)) {
|
|
253
|
+
throw new PdfError('cancelled', 'searchPage aborted');
|
|
254
|
+
}
|
|
255
|
+
return matches;
|
|
256
|
+
},
|
|
213
257
|
close() {
|
|
214
258
|
if (closed) {
|
|
215
259
|
return;
|
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
|
@@ -247,6 +247,13 @@ export interface RasterizerCapabilities {
|
|
|
247
247
|
/** Honours the abort signal mid-render, not merely between requests. */
|
|
248
248
|
interruptibleRender: boolean;
|
|
249
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
|
+
*/
|
|
250
257
|
search: boolean;
|
|
251
258
|
links: boolean;
|
|
252
259
|
/**
|
|
@@ -298,8 +305,57 @@ export interface RasterizerHandle {
|
|
|
298
305
|
/** Cheap: must not load a page. */
|
|
299
306
|
pageGeometry(index: number): PageGeometry;
|
|
300
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[]>;
|
|
301
321
|
close(): void;
|
|
302
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
|
+
}
|
|
303
359
|
export type RasterRole = 'base' | 'detail';
|
|
304
360
|
/**
|
|
305
361
|
* One rasterized piece, ready to draw.
|
|
@@ -337,6 +393,16 @@ export interface PdfPageContent {
|
|
|
337
393
|
readonly page: number;
|
|
338
394
|
/** Where this page sits in doc space, per the layout function. */
|
|
339
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;
|
|
340
406
|
/**
|
|
341
407
|
* The whole page at fit scale, as one or more pieces whose union is exactly
|
|
342
408
|
* `pageRect`.
|
|
@@ -397,11 +397,75 @@ RCT_EXPORT_METHOD(render
|
|
|
397
397
|
}];
|
|
398
398
|
}
|
|
399
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
|
+
|
|
400
463
|
/**
|
|
401
464
|
* Genuinely mid-render now, not best-effort: the core polls the signal from
|
|
402
465
|
* PDFium's progressive renderer between batches of page objects, so a superseded
|
|
403
|
-
* tile stops drawing within one batch
|
|
404
|
-
*
|
|
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.
|
|
405
469
|
*/
|
|
406
470
|
RCT_EXPORT_METHOD(cancel : (double)handle token : (double)token) {
|
|
407
471
|
_service->cancel(static_cast<int>(handle), static_cast<int64_t>(token));
|
|
@@ -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
|
|