@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
|
@@ -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, 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;
|
|
@@ -61,6 +61,16 @@ export interface PdfRenderOptions {
|
|
|
61
61
|
* compositing onto their own surface.
|
|
62
62
|
*/
|
|
63
63
|
background?: 'white' | 'transparent';
|
|
64
|
+
/**
|
|
65
|
+
* Turn the page this many degrees clockwise on top of its own `/Rotate`.
|
|
66
|
+
*
|
|
67
|
+
* Defaults to 0. For 90 and 270 the default `docRect` — and therefore the
|
|
68
|
+
* raster — has the page's extents SWAPPED, and a `docRect` you pass yourself
|
|
69
|
+
* is read in that turned space (see `RasterRequest.rotation`). `widthPx` is
|
|
70
|
+
* the width of the turned raster. This is what a thumbnail strip passes so
|
|
71
|
+
* its tiles match a layer the host has rotated.
|
|
72
|
+
*/
|
|
73
|
+
rotation?: PageRotation;
|
|
64
74
|
signal?: AbortSignal;
|
|
65
75
|
}
|
|
66
76
|
export interface PdfDocument {
|
|
@@ -73,7 +83,22 @@ export interface PdfDocument {
|
|
|
73
83
|
/** Intrinsic sizes of every page, in the order a `PdfPageLayout` expects. */
|
|
74
84
|
pageSizes(): DocSize[];
|
|
75
85
|
renderPage(index: number, options?: PdfRenderOptions): Promise<PdfPageRender>;
|
|
76
|
-
/**
|
|
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
|
+
*/
|
|
77
102
|
readonly closed: boolean;
|
|
78
103
|
/**
|
|
79
104
|
* Release the underlying file handle. `usePdfDocument` owns this for documents
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { useEffect, useState } from 'react';
|
|
18
18
|
// Through the `./skia` barrel — see the note in `usePdfLayer.ts`.
|
|
19
19
|
import { imageFromPixels } from '../skia/index.js';
|
|
20
|
+
import { rotatedSize } from '../rotation.js';
|
|
20
21
|
import { PdfError } from '../types.js';
|
|
21
22
|
/* ------------------------------------------------------------------ *
|
|
22
23
|
* Default rasterizer registry
|
|
@@ -145,11 +146,15 @@ export async function openPdfDocument(source, rasterizer) {
|
|
|
145
146
|
throw new PdfError('cancelled', 'renderPage aborted before it started');
|
|
146
147
|
}
|
|
147
148
|
const geometry = handle.pageGeometry(index);
|
|
149
|
+
const rotation = options?.rotation ?? 0;
|
|
150
|
+
// The whole page in the TURNED page's doc space: a 612x792 page at 90 is
|
|
151
|
+
// a 792x612 raster, and a caller's `widthPx` is the width of that.
|
|
152
|
+
const turned = rotatedSize(geometry, rotation);
|
|
148
153
|
const docRect = options?.docRect ?? {
|
|
149
154
|
x: 0,
|
|
150
155
|
y: 0,
|
|
151
|
-
width:
|
|
152
|
-
height:
|
|
156
|
+
width: turned.width,
|
|
157
|
+
height: turned.height,
|
|
153
158
|
};
|
|
154
159
|
if (!(docRect.width > 0) || !(docRect.height > 0)) {
|
|
155
160
|
throw new PdfError('backend-failure', `renderPage needs a positive docRect, got ${docRect.width}x${docRect.height}`);
|
|
@@ -161,18 +166,22 @@ export async function openPdfDocument(source, rasterizer) {
|
|
|
161
166
|
if (!(scale > 0) || !Number.isFinite(scale)) {
|
|
162
167
|
throw new PdfError('backend-failure', `renderPage needs a positive scale, got ${scale}`);
|
|
163
168
|
}
|
|
169
|
+
const request = {
|
|
170
|
+
page: index,
|
|
171
|
+
docRect,
|
|
172
|
+
scale,
|
|
173
|
+
// A backend that cannot draw annotations would otherwise silently
|
|
174
|
+
// ignore the flag; downgrading here keeps the request honest.
|
|
175
|
+
annotations: (options?.annotations ?? true) && backend.capabilities.annotations,
|
|
176
|
+
background: options?.background ?? 'white',
|
|
177
|
+
};
|
|
178
|
+
// Only when the caller asked for a turn: an unrotated request stays the
|
|
179
|
+
// request it always was, key for key (see the controller's stamp).
|
|
180
|
+
if (rotation !== 0)
|
|
181
|
+
request.rotation = rotation;
|
|
164
182
|
let pixels;
|
|
165
183
|
try {
|
|
166
|
-
pixels = await handle.render(
|
|
167
|
-
page: index,
|
|
168
|
-
docRect,
|
|
169
|
-
scale,
|
|
170
|
-
// A backend that cannot draw annotations would otherwise silently
|
|
171
|
-
// ignore the flag; downgrading here keeps the request honest.
|
|
172
|
-
annotations: (options?.annotations ?? true) &&
|
|
173
|
-
backend.capabilities.annotations,
|
|
174
|
-
background: options?.background ?? 'white',
|
|
175
|
-
}, options?.signal);
|
|
184
|
+
pixels = await handle.render(request, options?.signal);
|
|
176
185
|
}
|
|
177
186
|
catch (cause) {
|
|
178
187
|
throw toPdfError(cause, `Failed to render page ${index}`);
|
|
@@ -201,6 +210,50 @@ export async function openPdfDocument(source, rasterizer) {
|
|
|
201
210
|
},
|
|
202
211
|
};
|
|
203
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
|
+
},
|
|
204
257
|
close() {
|
|
205
258
|
if (closed) {
|
|
206
259
|
return;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* host's transform does the panning and zooming; this hook only ever hears about
|
|
21
21
|
* the viewport through the `controller` methods the host calls.
|
|
22
22
|
*/
|
|
23
|
-
import type { PdfContent, PdfController, PdfDiagnostic, PdfPageLayout, RasterPolicy } from '../types.js';
|
|
23
|
+
import type { PageRotation, PdfContent, PdfController, PdfDiagnostic, PdfPageLayout, RasterPolicy } from '../types.js';
|
|
24
24
|
import type { PdfDocument } from './usePdfDocument.js';
|
|
25
25
|
/**
|
|
26
26
|
* Only the pages the viewport actually intersects may hold rasters.
|
|
@@ -88,6 +88,23 @@ export interface UsePdfLayerOptions {
|
|
|
88
88
|
* and its raster cache: every cached tile was rendered one way or the other.
|
|
89
89
|
*/
|
|
90
90
|
annotations?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* A per-page rotation the HOST applies on top of the document's own
|
|
93
|
+
* `/Rotate`, in degrees clockwise, indexed by page. A missing or undefined
|
|
94
|
+
* entry is 0; absent altogether, nothing about the layer changes.
|
|
95
|
+
*
|
|
96
|
+
* The layout is handed the ROTATED page sizes (a 612x792 page turned 90 is
|
|
97
|
+
* laid out as 792x612), the controller is handed geometry in the same
|
|
98
|
+
* orientation, and every tile request carries the rotation so the backend
|
|
99
|
+
* draws the page turned. Doc space is therefore the rotated page's: a host
|
|
100
|
+
* positioning its own layers over a turned page positions them in that
|
|
101
|
+
* space, and `content.pages[i].pageRect` already reports it.
|
|
102
|
+
*
|
|
103
|
+
* Changing an entry REBUILDS the controller and clears its raster cache —
|
|
104
|
+
* every cached tile was drawn one way up — exactly as `annotations` does. An
|
|
105
|
+
* all-zero list is the same controller as no list at all.
|
|
106
|
+
*/
|
|
107
|
+
rotations?: readonly PageRotation[];
|
|
91
108
|
onDiagnostic?: (diagnostic: PdfDiagnostic) => void;
|
|
92
109
|
}
|
|
93
110
|
export interface PdfLayer {
|
|
@@ -100,4 +117,4 @@ export interface PdfLayer {
|
|
|
100
117
|
*/
|
|
101
118
|
controller: PdfController;
|
|
102
119
|
}
|
|
103
|
-
export declare function usePdfLayer({ document, pages: pageSelection, layout, policy, annotations, onDiagnostic, }: UsePdfLayerOptions): PdfLayer;
|
|
120
|
+
export declare function usePdfLayer({ document, pages: pageSelection, layout, policy, annotations, rotations, onDiagnostic, }: UsePdfLayerOptions): PdfLayer;
|
|
@@ -25,6 +25,7 @@ import { createRasterCache } from '../cache.js';
|
|
|
25
25
|
import { createPdfController } from '../controller.js';
|
|
26
26
|
import { continuousVertical, hasOverlappingPages } from '../layout.js';
|
|
27
27
|
import { resolvePolicy } from '../policy.js';
|
|
28
|
+
import { resolveRotations, rotatePageGeometry } from '../rotation.js';
|
|
28
29
|
// Through the `./skia` barrel, not `../rasterizer/ingest.js` directly: that
|
|
29
30
|
// barrel is the documented home of the ingest seam, and routing every internal
|
|
30
31
|
// consumer through it is what keeps "which modules can touch Skia" a list of one
|
|
@@ -116,11 +117,17 @@ function disposeRaster(raster) {
|
|
|
116
117
|
disposedImages.add(raster.image);
|
|
117
118
|
raster.image.dispose();
|
|
118
119
|
}
|
|
119
|
-
/**
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Geometry for every page, in page order, as the host wants it displayed: a
|
|
122
|
+
* quarter turn swaps a page's extents (see `../rotation.ts`). Cheap: loads no
|
|
123
|
+
* page. With no rotations it is the intrinsic geometry exactly, object for
|
|
124
|
+
* object — `rotatePageGeometry` returns its input for a rotation that changes
|
|
125
|
+
* nothing.
|
|
126
|
+
*/
|
|
127
|
+
function rotatedPageGeometry(document, rotations) {
|
|
121
128
|
const geometry = [];
|
|
122
129
|
for (let index = 0; index < document.pageCount; index += 1) {
|
|
123
|
-
geometry.push(document.pageGeometry(index));
|
|
130
|
+
geometry.push(rotatePageGeometry(document.pageGeometry(index), rotations?.[index] ?? 0));
|
|
124
131
|
}
|
|
125
132
|
return geometry;
|
|
126
133
|
}
|
|
@@ -181,7 +188,7 @@ export function usePdfLayer({ document,
|
|
|
181
188
|
// The PUBLIC name stays `pages`; locally it is the SELECTION, and the
|
|
182
189
|
// controller's own `pages` is the per-page geometry. Two very different things
|
|
183
190
|
// one rename apart.
|
|
184
|
-
pages: pageSelection = DEFAULT_PAGE_SELECTION, layout, policy, annotations = true, onDiagnostic, }) {
|
|
191
|
+
pages: pageSelection = DEFAULT_PAGE_SELECTION, layout, policy, annotations = true, rotations, onDiagnostic, }) {
|
|
185
192
|
// The diagnostic callback is almost always an inline arrow. Route it through a
|
|
186
193
|
// ref so a fresh identity on every render never counts as a reason to rebuild
|
|
187
194
|
// the controller.
|
|
@@ -194,14 +201,41 @@ pages: pageSelection = DEFAULT_PAGE_SELECTION, layout, policy, annotations = tru
|
|
|
194
201
|
}, []);
|
|
195
202
|
const pagesKey = pageSelectionKey(pageSelection);
|
|
196
203
|
const policySignature = policyKey(policy);
|
|
204
|
+
/**
|
|
205
|
+
* Normalised every render — the option is an inline literal at every
|
|
206
|
+
* realistic call site, so its identity means nothing — and its joined form
|
|
207
|
+
* is the structural key the memos below depend on instead. `undefined` for
|
|
208
|
+
* an absent AND an all-zero list, so neither spelling of "nothing turned"
|
|
209
|
+
* can tear the controller down (see `resolveRotations`).
|
|
210
|
+
*/
|
|
211
|
+
const resolvedRotations = resolveRotations(rotations, document?.pageCount ?? 0);
|
|
212
|
+
const rotationsSignature = resolvedRotations?.join(',') ?? '';
|
|
213
|
+
/**
|
|
214
|
+
* Every page's geometry as the host wants it shown. What the layout sizes
|
|
215
|
+
* pages from and what the controller divides page rects by, so the two are
|
|
216
|
+
* derived from ONE array and cannot disagree about a page's orientation.
|
|
217
|
+
*/
|
|
218
|
+
const pageGeometry = useMemo(() => document === null
|
|
219
|
+
? null
|
|
220
|
+
: rotatedPageGeometry(document, resolvedRotations),
|
|
221
|
+
// `resolvedRotations` is rebuilt every render; its signature is a complete
|
|
222
|
+
// description of it and stands in — the same trade `policy` makes below.
|
|
223
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
224
|
+
[document, rotationsSignature]);
|
|
197
225
|
/**
|
|
198
226
|
* Split out of the controller memo so the `__DEV__` check below can see the
|
|
199
227
|
* rects without re-running the layout, and so a warning lives in an EFFECT
|
|
200
228
|
* rather than inside a memo React is free to invoke twice.
|
|
201
229
|
*/
|
|
202
|
-
const pageRects = useMemo(() =>
|
|
230
|
+
const pageRects = useMemo(() => pageGeometry === null
|
|
203
231
|
? null
|
|
204
|
-
: (layout ?? DEFAULT_LAYOUT)(
|
|
232
|
+
: (layout ?? DEFAULT_LAYOUT)(
|
|
233
|
+
// The ROTATED sizes: a page the host turned a quarter is laid out
|
|
234
|
+
// at its displayed extents, and doc space is that page's.
|
|
235
|
+
pageGeometry.map(page => ({
|
|
236
|
+
width: page.width,
|
|
237
|
+
height: page.height,
|
|
238
|
+
}))), [pageGeometry, layout]);
|
|
205
239
|
/**
|
|
206
240
|
* A stacked layout (`singlePage()`) is correct ONLY with a one-element `pages`
|
|
207
241
|
* list. Any other selection lets every page hold a base at the same doc rect,
|
|
@@ -234,7 +268,7 @@ pages: pageSelection = DEFAULT_PAGE_SELECTION, layout, policy, annotations = tru
|
|
|
234
268
|
'continuousVertical() / spread().');
|
|
235
269
|
}, [document, pageRects, pageSelection]);
|
|
236
270
|
const layer = useMemo(() => {
|
|
237
|
-
if (document === null || pageRects === null) {
|
|
271
|
+
if (document === null || pageGeometry === null || pageRects === null) {
|
|
238
272
|
return null;
|
|
239
273
|
}
|
|
240
274
|
const resolvedPolicy = resolvePolicy(policy);
|
|
@@ -245,7 +279,11 @@ pages: pageSelection = DEFAULT_PAGE_SELECTION, layout, policy, annotations = tru
|
|
|
245
279
|
const controller = createPdfController({
|
|
246
280
|
handle: document.handle,
|
|
247
281
|
pageRects,
|
|
248
|
-
pages:
|
|
282
|
+
pages: pageGeometry,
|
|
283
|
+
// Undefined when nothing is turned, so the controller's default path
|
|
284
|
+
// — and the requests it builds — are exactly what they were before
|
|
285
|
+
// rotation existed.
|
|
286
|
+
rotations: resolvedRotations,
|
|
249
287
|
policy: resolvedPolicy,
|
|
250
288
|
ingest: ingestRaster,
|
|
251
289
|
cache,
|
|
@@ -277,12 +315,17 @@ pages: pageSelection = DEFAULT_PAGE_SELECTION, layout, policy, annotations = tru
|
|
|
277
315
|
// layout the resident cost goes from one page to as many as five. The measured
|
|
278
316
|
// cost of the teardown is one page's rasters, which is the page you are turning
|
|
279
317
|
// to and would have paid for anyway. See `__tests__/single-page.test.ts`.
|
|
318
|
+
//
|
|
319
|
+
// `rotations` follows the same rule as the two above: `rotationsSignature`
|
|
320
|
+
// stands in for it, and `resolvedRotations` is read inside on its strength.
|
|
280
321
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
281
322
|
[
|
|
282
323
|
document,
|
|
324
|
+
pageGeometry,
|
|
283
325
|
pageRects,
|
|
284
326
|
pagesKey,
|
|
285
327
|
policySignature,
|
|
328
|
+
rotationsSignature,
|
|
286
329
|
annotations,
|
|
287
330
|
emitDiagnostic,
|
|
288
331
|
]);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HOST's page rotation, and the one place its arithmetic lives.
|
|
3
|
+
*
|
|
4
|
+
* Two rotations exist in this package and they must not be confused:
|
|
5
|
+
*
|
|
6
|
+
* - `PageGeometry.rotation` is the document's own `/Rotate`. PDFium folds it
|
|
7
|
+
* into every size it reports and every render it draws, so from this
|
|
8
|
+
* package's vantage point it is already applied and purely informational.
|
|
9
|
+
* - `RasterRequest.rotation` — what this module is about — is a rotation the
|
|
10
|
+
* host layers ON TOP of that: "show me this page turned a quarter
|
|
11
|
+
* clockwise". It is PDFium's `rotate` argument, which every engine here
|
|
12
|
+
* hard-coded to 0 until this existed.
|
|
13
|
+
*
|
|
14
|
+
* THE CONTRACT, stated once and honoured by every backend: doc space for a
|
|
15
|
+
* rotated page is the ROTATED page's. A 612x792 page turned 90 is a 792x612
|
|
16
|
+
* page whose origin is its displayed top-left, and a request's `docRect` and
|
|
17
|
+
* `scale` are in that space. So the layout lays out swapped sizes, the planner
|
|
18
|
+
* plans in them, the controller divides swapped page rects by swapped geometry
|
|
19
|
+
* (and `layoutScale` stays 1), and a backend keeps its negative-offset rect
|
|
20
|
+
* mechanism unchanged — the only thing it swaps is the page box it hands the
|
|
21
|
+
* engine. This module does the swapping; nothing else in `src/` repeats it.
|
|
22
|
+
*
|
|
23
|
+
* Pure, and deliberately without React: `usePdfLayer` calls it, and so does the
|
|
24
|
+
* test that pins what the layout is handed.
|
|
25
|
+
*/
|
|
26
|
+
import type { DocSize, PageGeometry, PageRotation } from './types.js';
|
|
27
|
+
export declare const ROTATIONS: readonly PageRotation[];
|
|
28
|
+
/**
|
|
29
|
+
* True for exactly the four values the type admits. For a value that arrived
|
|
30
|
+
* from JavaScript rather than through the type — a backend validating a request
|
|
31
|
+
* it did not build.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isPageRotation(value: unknown): value is PageRotation;
|
|
34
|
+
/** A quarter turn either way swaps a page's extents; a half turn does not. */
|
|
35
|
+
export declare function swapsExtents(rotation: PageRotation): boolean;
|
|
36
|
+
/** A page's size as displayed under `rotation`. */
|
|
37
|
+
export declare function rotatedSize(size: DocSize, rotation: PageRotation): DocSize;
|
|
38
|
+
/**
|
|
39
|
+
* The geometry the controller and the layout see for a page a host has turned.
|
|
40
|
+
*
|
|
41
|
+
* Returns the SAME object for a rotation that changes nothing, so a document
|
|
42
|
+
* with no rotations hands downstream exactly the geometry it did before this
|
|
43
|
+
* existed. `rotation` — the document's `/Rotate` — is carried through
|
|
44
|
+
* untouched: it describes the file, not the host's view of it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function rotatePageGeometry(geometry: PageGeometry, rotation: PageRotation): PageGeometry;
|
|
47
|
+
/**
|
|
48
|
+
* A host's `rotations` option, normalised to one entry per page — or
|
|
49
|
+
* `undefined` when it would change nothing.
|
|
50
|
+
*
|
|
51
|
+
* `undefined` for BOTH an absent option and an all-zero list, on purpose: the
|
|
52
|
+
* two mean the same controller, and `usePdfLayer` keys its controller memo on
|
|
53
|
+
* the result, so a host toggling between "no rotations" and `[0, 0, 0]` must
|
|
54
|
+
* not tear its raster cache down over a distinction without a difference. A
|
|
55
|
+
* value the type does not admit (from JavaScript) reads as 0 rather than
|
|
56
|
+
* throwing: a malformed entry should leave that page upright, not blank the
|
|
57
|
+
* document.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveRotations(rotations: readonly PageRotation[] | undefined, pageCount: number): PageRotation[] | undefined;
|
package/dist/rotation.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HOST's page rotation, and the one place its arithmetic lives.
|
|
3
|
+
*
|
|
4
|
+
* Two rotations exist in this package and they must not be confused:
|
|
5
|
+
*
|
|
6
|
+
* - `PageGeometry.rotation` is the document's own `/Rotate`. PDFium folds it
|
|
7
|
+
* into every size it reports and every render it draws, so from this
|
|
8
|
+
* package's vantage point it is already applied and purely informational.
|
|
9
|
+
* - `RasterRequest.rotation` — what this module is about — is a rotation the
|
|
10
|
+
* host layers ON TOP of that: "show me this page turned a quarter
|
|
11
|
+
* clockwise". It is PDFium's `rotate` argument, which every engine here
|
|
12
|
+
* hard-coded to 0 until this existed.
|
|
13
|
+
*
|
|
14
|
+
* THE CONTRACT, stated once and honoured by every backend: doc space for a
|
|
15
|
+
* rotated page is the ROTATED page's. A 612x792 page turned 90 is a 792x612
|
|
16
|
+
* page whose origin is its displayed top-left, and a request's `docRect` and
|
|
17
|
+
* `scale` are in that space. So the layout lays out swapped sizes, the planner
|
|
18
|
+
* plans in them, the controller divides swapped page rects by swapped geometry
|
|
19
|
+
* (and `layoutScale` stays 1), and a backend keeps its negative-offset rect
|
|
20
|
+
* mechanism unchanged — the only thing it swaps is the page box it hands the
|
|
21
|
+
* engine. This module does the swapping; nothing else in `src/` repeats it.
|
|
22
|
+
*
|
|
23
|
+
* Pure, and deliberately without React: `usePdfLayer` calls it, and so does the
|
|
24
|
+
* test that pins what the layout is handed.
|
|
25
|
+
*/
|
|
26
|
+
export const ROTATIONS = Object.freeze([
|
|
27
|
+
0, 90, 180, 270,
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* True for exactly the four values the type admits. For a value that arrived
|
|
31
|
+
* from JavaScript rather than through the type — a backend validating a request
|
|
32
|
+
* it did not build.
|
|
33
|
+
*/
|
|
34
|
+
export function isPageRotation(value) {
|
|
35
|
+
return ROTATIONS.includes(value);
|
|
36
|
+
}
|
|
37
|
+
/** A quarter turn either way swaps a page's extents; a half turn does not. */
|
|
38
|
+
export function swapsExtents(rotation) {
|
|
39
|
+
return rotation === 90 || rotation === 270;
|
|
40
|
+
}
|
|
41
|
+
/** A page's size as displayed under `rotation`. */
|
|
42
|
+
export function rotatedSize(size, rotation) {
|
|
43
|
+
return swapsExtents(rotation)
|
|
44
|
+
? { width: size.height, height: size.width }
|
|
45
|
+
: { width: size.width, height: size.height };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The geometry the controller and the layout see for a page a host has turned.
|
|
49
|
+
*
|
|
50
|
+
* Returns the SAME object for a rotation that changes nothing, so a document
|
|
51
|
+
* with no rotations hands downstream exactly the geometry it did before this
|
|
52
|
+
* existed. `rotation` — the document's `/Rotate` — is carried through
|
|
53
|
+
* untouched: it describes the file, not the host's view of it.
|
|
54
|
+
*/
|
|
55
|
+
export function rotatePageGeometry(geometry, rotation) {
|
|
56
|
+
if (!swapsExtents(rotation))
|
|
57
|
+
return geometry;
|
|
58
|
+
return { ...geometry, width: geometry.height, height: geometry.width };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A host's `rotations` option, normalised to one entry per page — or
|
|
62
|
+
* `undefined` when it would change nothing.
|
|
63
|
+
*
|
|
64
|
+
* `undefined` for BOTH an absent option and an all-zero list, on purpose: the
|
|
65
|
+
* two mean the same controller, and `usePdfLayer` keys its controller memo on
|
|
66
|
+
* the result, so a host toggling between "no rotations" and `[0, 0, 0]` must
|
|
67
|
+
* not tear its raster cache down over a distinction without a difference. A
|
|
68
|
+
* value the type does not admit (from JavaScript) reads as 0 rather than
|
|
69
|
+
* throwing: a malformed entry should leave that page upright, not blank the
|
|
70
|
+
* document.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveRotations(rotations, pageCount) {
|
|
73
|
+
if (rotations === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const resolved = [];
|
|
76
|
+
let any = false;
|
|
77
|
+
for (let page = 0; page < pageCount; page += 1) {
|
|
78
|
+
const rotation = rotations[page];
|
|
79
|
+
const clean = isPageRotation(rotation) ? rotation : 0;
|
|
80
|
+
if (clean !== 0)
|
|
81
|
+
any = true;
|
|
82
|
+
resolved.push(clean);
|
|
83
|
+
}
|
|
84
|
+
return any ? resolved : undefined;
|
|
85
|
+
}
|