@reekon-tools/react-native-pdf-canvas 0.2.0 → 0.3.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/PdfCanvas.podspec +12 -4
- package/README.md +22 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +34 -10
- package/android/src/main/cpp/pdfcanvas-jni.cpp +4 -2
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +5 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +10 -3
- package/dist/controller.d.ts +23 -1
- package/dist/controller.js +28 -16
- package/dist/rasterizer/fake.d.ts +14 -2
- package/dist/rasterizer/fake.js +93 -36
- package/dist/rasterizer/native-bridge.d.ts +11 -0
- package/dist/rasterizer/native-bridge.js +1 -0
- package/dist/rasterizer/web/engine.js +26 -7
- package/dist/react/usePdfDocument.d.ts +11 -1
- package/dist/react/usePdfDocument.js +21 -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/types.d.ts +20 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +5 -0
- package/native/CMakeLists.txt +7 -0
- package/native/core/include/pdfcanvas/types.h +8 -0
- package/native/core/src/document.cpp +25 -5
- package/native/tests/test_document.cpp +178 -0
- package/package.json +1 -1
- package/scripts/fetch-pdfium.mjs +75 -3
- package/scripts/pdfium-manifest.json +2 -0
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
* likely to be real.
|
|
41
41
|
*/
|
|
42
42
|
import { FPDF_ANNOT, FPDF_BITMAP_BGRA, FPDF_REVERSE_BYTE_ORDER, loadError, } from './pdfium.js';
|
|
43
|
+
import { isPageRotation, ROTATIONS, swapsExtents } from '../../rotation.js';
|
|
43
44
|
import { PdfError } from '../../types.js';
|
|
44
45
|
/**
|
|
45
46
|
* MEASURED, not inherited from the other backends. An antialiased opaque-white
|
|
@@ -64,7 +65,6 @@ const OPAQUE_WHITE = 0xffffffff;
|
|
|
64
65
|
const TRANSPARENT = 0x00000000;
|
|
65
66
|
/** PDFium's ints are 32-bit; a request past this cannot be expressed at all. */
|
|
66
67
|
const MAX_INT32 = 0x7fffffff;
|
|
67
|
-
const ROTATIONS = [0, 90, 180, 270];
|
|
68
68
|
/* ------------------------------------------------------------------ *
|
|
69
69
|
* Open
|
|
70
70
|
* ------------------------------------------------------------------ */
|
|
@@ -215,6 +215,13 @@ function createDocument(api, document, dataPointer, pages) {
|
|
|
215
215
|
`${docRect.width}x${docRect.height} at ` +
|
|
216
216
|
`(${docRect.x}, ${docRect.y}).`);
|
|
217
217
|
}
|
|
218
|
+
// Read as `?? 0`, so a request built before the field existed renders
|
|
219
|
+
// as it always did; validated, because from here on it is arithmetic
|
|
220
|
+
// and `45 / 90` would silently be "upright".
|
|
221
|
+
const rotation = request.rotation ?? 0;
|
|
222
|
+
if (!isPageRotation(rotation)) {
|
|
223
|
+
throw new PdfError('backend-failure', `Rotation must be 0, 90, 180 or 270, got ${String(rotation)}.`);
|
|
224
|
+
}
|
|
218
225
|
// At least one pixel: a sub-pixel rect is a legitimate ask at low zoom,
|
|
219
226
|
// and a zero-sized bitmap is not a thing PDFium (or Skia) accepts.
|
|
220
227
|
const width = Math.max(1, Math.round(docRect.width * scale));
|
|
@@ -223,8 +230,18 @@ function createDocument(api, document, dataPointer, pages) {
|
|
|
223
230
|
// the negated one.
|
|
224
231
|
const startX = -Math.round(docRect.x * scale);
|
|
225
232
|
const startY = -Math.round(docRect.y * scale);
|
|
226
|
-
|
|
227
|
-
|
|
233
|
+
// THE PAGE BOX PDFIUM IS HANDED IS THE DISPLAYED ONE. `size_x` / `size_y`
|
|
234
|
+
// describe the box the page is drawn INTO after `rotate` is applied —
|
|
235
|
+
// `CPDF_Page::GetDisplayMatrix` maps the page's HEIGHT onto `size_x` for
|
|
236
|
+
// rotate 1 and 3 — so a quarter turn swaps the intrinsic extents here.
|
|
237
|
+
// Without the swap the turned page is squashed into the upright box and
|
|
238
|
+
// every pixel is stretched; with it, `docRect` (already in the turned
|
|
239
|
+
// page's space, see `RasterRequest.rotation`) needs nothing: the start
|
|
240
|
+
// offsets slide the whole turned page exactly as they slide an upright
|
|
241
|
+
// one.
|
|
242
|
+
const turned = swapsExtents(rotation);
|
|
243
|
+
const pageWidth = Math.round((turned ? geometry.height : geometry.width) * scale);
|
|
244
|
+
const pageHeight = Math.round((turned ? geometry.width : geometry.height) * scale);
|
|
228
245
|
if (width * height > MAX_INT32 / 4 ||
|
|
229
246
|
pageWidth > MAX_INT32 ||
|
|
230
247
|
pageHeight > MAX_INT32) {
|
|
@@ -255,10 +272,12 @@ function createDocument(api, document, dataPointer, pages) {
|
|
|
255
272
|
}
|
|
256
273
|
const page = loadPage(request.page);
|
|
257
274
|
api.FPDF_RenderPageBitmap(bitmap, page, startX, startY, pageWidth, pageHeight,
|
|
258
|
-
//
|
|
259
|
-
// page's own
|
|
260
|
-
//
|
|
261
|
-
|
|
275
|
+
// The HOST's rotation, in quarter turns clockwise — and never the
|
|
276
|
+
// page's own `/Rotate`. PDFium has already folded that into
|
|
277
|
+
// `geometry` and into the render, so passing it here would turn the
|
|
278
|
+
// page twice; `rotation` is layered on top of it, which is exactly
|
|
279
|
+
// what this argument exists for.
|
|
280
|
+
rotation / 90, FPDF_REVERSE_BYTE_ORDER | (request.annotations ? FPDF_ANNOT : 0));
|
|
262
281
|
// READ, NEVER ASSUMED. Measured tight (`width * 4`) at 1, 3, 7, 13, 1023
|
|
263
282
|
// and 1025 px — but "tight today" is not "tight always", and a wrong
|
|
264
283
|
// stride shears the image instead of failing.
|
|
@@ -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 } 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 {
|
|
@@ -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}`);
|
|
@@ -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
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -220,6 +220,26 @@ export interface RasterRequest {
|
|
|
220
220
|
*/
|
|
221
221
|
annotations: boolean;
|
|
222
222
|
background: 'white' | 'transparent';
|
|
223
|
+
/**
|
|
224
|
+
* DEVICE rotation applied on top of the page's own `/Rotate`, in degrees
|
|
225
|
+
* clockwise. Optional: absent means 0, and every backend reads it as
|
|
226
|
+
* `rotation ?? 0`, so a request built before the field existed renders
|
|
227
|
+
* exactly as it did.
|
|
228
|
+
*
|
|
229
|
+
* THE COORDINATE CONTRACT: `docRect` and `scale` are expressed in the
|
|
230
|
+
* ROTATED page's doc space. For 90 and 270 the page's extents are swapped —
|
|
231
|
+
* a 612x792 page rotated 90 is a 792x612 page whose origin is its displayed
|
|
232
|
+
* top-left — so a whole-page request for it is `{0, 0, 792, 612}`. That is
|
|
233
|
+
* what lets a backend keep the negative-offset rect mechanism unchanged: it
|
|
234
|
+
* describes the whole TURNED page at `scale` and slides it by
|
|
235
|
+
* `(-round(x·s), -round(y·s))`, exactly as it does an upright one. The only
|
|
236
|
+
* thing a backend swaps is the page box it hands the engine.
|
|
237
|
+
*
|
|
238
|
+
* This is NOT `PageGeometry.rotation`. That one is the document's `/Rotate`,
|
|
239
|
+
* already folded into every size and render by PDFium and informational
|
|
240
|
+
* here; this one is a host's choice, layered on top. See `./rotation.ts`.
|
|
241
|
+
*/
|
|
242
|
+
rotation?: PageRotation;
|
|
223
243
|
}
|
|
224
244
|
export interface RasterizerCapabilities {
|
|
225
245
|
/** Can render PDF-embedded annotations into the raster. */
|
|
@@ -324,6 +324,11 @@ RCT_EXPORT_METHOD(render
|
|
|
324
324
|
req.width = [request[@"width"] doubleValue];
|
|
325
325
|
req.height = [request[@"height"] doubleValue];
|
|
326
326
|
req.scale = [request[@"scale"] doubleValue];
|
|
327
|
+
// OPTIONAL, DEFAULTING TO 0: a JS bundle older than this binary never sends it,
|
|
328
|
+
// and a page must not turn because a key is missing. The class check also covers
|
|
329
|
+
// a JS `null`, which the bridge delivers as `NSNull` rather than `nil`. The core
|
|
330
|
+
// validates the value; this only decides what "absent" means.
|
|
331
|
+
req.rotation = [request[@"rotation"] isKindOfClass:NSNumber.class] ? [request[@"rotation"] intValue] : 0;
|
|
327
332
|
req.annotations = request[@"annotations"] == nil ? true : [request[@"annotations"] boolValue];
|
|
328
333
|
req.background = [@"transparent" isEqual:request[@"background"]] ? pdfcanvas::Background::Transparent
|
|
329
334
|
: pdfcanvas::Background::White;
|
package/native/CMakeLists.txt
CHANGED
|
@@ -80,6 +80,13 @@ if(WIN32)
|
|
|
80
80
|
elseif(APPLE)
|
|
81
81
|
set_target_properties(pdfcanvas_tests PROPERTIES
|
|
82
82
|
BUILD_RPATH "${PDFCANVAS_PDFIUM_DIR}/lib")
|
|
83
|
+
# The rpath above cannot help on its own: the release dylib's install name is
|
|
84
|
+
# the CWD-relative `./libpdfium.dylib` (`otool -D`), not `@rpath/…`, so dyld
|
|
85
|
+
# looks beside the working directory and nowhere else. ctest runs the binary
|
|
86
|
+
# from the build directory, so — as on Windows — the library is copied there.
|
|
87
|
+
add_custom_command(TARGET pdfcanvas_tests POST_BUILD
|
|
88
|
+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
|
89
|
+
"${PDFCANVAS_PDFIUM_DIR}/lib/libpdfium.dylib" "$<TARGET_FILE_DIR:pdfcanvas_tests>")
|
|
83
90
|
else()
|
|
84
91
|
set_target_properties(pdfcanvas_tests PROPERTIES
|
|
85
92
|
BUILD_RPATH "${PDFCANVAS_PDFIUM_DIR}/lib")
|
|
@@ -48,6 +48,14 @@ struct RasterRequest {
|
|
|
48
48
|
double scale = 1;
|
|
49
49
|
/// Draw PDF-embedded annotations (and form fields) into the raster.
|
|
50
50
|
bool annotations = true;
|
|
51
|
+
/// The HOST's rotation on top of the page's `/Rotate`, in degrees clockwise:
|
|
52
|
+
/// 0, 90, 180 or 270 — anything else is refused. `x`/`y`/`width`/`height`
|
|
53
|
+
/// are in the TURNED page's doc space (extents swapped for a quarter turn),
|
|
54
|
+
/// which is what keeps the negative-offset rect mechanism unchanged: the
|
|
55
|
+
/// only thing the render swaps is the page box it hands PDFium. Absent on
|
|
56
|
+
/// the wire means 0 on both bindings, so a JS/binary version skew cannot
|
|
57
|
+
/// turn a page by accident.
|
|
58
|
+
int rotation = 0;
|
|
51
59
|
Background background = Background::White;
|
|
52
60
|
|
|
53
61
|
/// Destination width in device pixels: ROUND, not ceil, and at least 1 —
|
|
@@ -365,6 +365,14 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
365
365
|
" at (" + std::to_string(request.x) + ", " + std::to_string(request.y) + ").");
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
if (request.rotation != 0 && request.rotation != 90 && request.rotation != 180 &&
|
|
369
|
+
request.rotation != 270) {
|
|
370
|
+
// Refused rather than folded: `45 / 90` is 0 in integer arithmetic, and a
|
|
371
|
+
// request that asked for a turn must not quietly come back upright.
|
|
372
|
+
throw PdfError(ErrorCode::BackendFailure,
|
|
373
|
+
"Rotation must be 0, 90, 180 or 270, got " + std::to_string(request.rotation) + ".");
|
|
374
|
+
}
|
|
375
|
+
|
|
368
376
|
const int pxW = request.pixelWidth();
|
|
369
377
|
const int pxH = request.pixelHeight();
|
|
370
378
|
const long long pixels = static_cast<long long>(pxW) * pxH;
|
|
@@ -377,8 +385,16 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
377
385
|
// byte-identical to the matrix form on every rect it tried.
|
|
378
386
|
const long long startX = -std::llround(request.x * request.scale);
|
|
379
387
|
const long long startY = -std::llround(request.y * request.scale);
|
|
380
|
-
|
|
381
|
-
|
|
388
|
+
// THE PAGE BOX HANDED TO PDFIUM IS THE DISPLAYED ONE. `size_x` / `size_y`
|
|
389
|
+
// describe the box the page is drawn INTO after `rotate` is applied —
|
|
390
|
+
// `CPDF_Page::GetDisplayMatrix` maps the page's HEIGHT onto `size_x` for
|
|
391
|
+
// rotate 1 and 3 — so a quarter turn swaps the intrinsic extents here. The
|
|
392
|
+
// request's rect is already in that space (see `RasterRequest::rotation`), so
|
|
393
|
+
// the start offsets above need nothing: they slide the whole turned page
|
|
394
|
+
// exactly as they slide an upright one.
|
|
395
|
+
const bool turned = request.rotation == 90 || request.rotation == 270;
|
|
396
|
+
const long long pageW = std::llround((turned ? geometry.height : geometry.width) * request.scale);
|
|
397
|
+
const long long pageH = std::llround((turned ? geometry.width : geometry.height) * request.scale);
|
|
382
398
|
if (pixels > kMaxRasterPixels) {
|
|
383
399
|
throw PdfError(ErrorCode::OutOfMemory,
|
|
384
400
|
"Refused a " + std::to_string(pxW) + "x" + std::to_string(pxH) + " raster (" +
|
|
@@ -451,6 +467,11 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
451
467
|
const int sy = static_cast<int>(startY);
|
|
452
468
|
const int pw = static_cast<int>(pageW);
|
|
453
469
|
const int ph = static_cast<int>(pageH);
|
|
470
|
+
// The HOST's rotation in quarter turns clockwise — never the page's own
|
|
471
|
+
// `/Rotate`, which PDFium has already folded into `geometry` and into the
|
|
472
|
+
// render. Passed to BOTH passes below, so a form field lands where the turned
|
|
473
|
+
// content says and not where the upright page had it.
|
|
474
|
+
const int rotate = request.rotation / 90;
|
|
454
475
|
|
|
455
476
|
auto abandonWith = [&](PdfError error) -> PdfError {
|
|
456
477
|
FPDFBitmap_Destroy(bitmap);
|
|
@@ -466,8 +487,7 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
466
487
|
// TOBECONTINUED when the pause adapter asked to stop, which it does only
|
|
467
488
|
// once the signal is set; a live render runs to DONE in one call.
|
|
468
489
|
PauseAdapter pause(signal);
|
|
469
|
-
int status = FPDF_RenderPageBitmap_Start(bitmap, page, sx, sy, pw, ph,
|
|
470
|
-
&pause.pause);
|
|
490
|
+
int status = FPDF_RenderPageBitmap_Start(bitmap, page, sx, sy, pw, ph, rotate, flags, &pause.pause);
|
|
471
491
|
while (status == FPDF_RENDER_TOBECONTINUED) {
|
|
472
492
|
if (signal != nullptr && signal->isCancelled()) {
|
|
473
493
|
FPDF_RenderPage_Close(page);
|
|
@@ -487,7 +507,7 @@ RasterPixels Document::render(const RasterRequest& request, Cancellation* signal
|
|
|
487
507
|
// state — with the same offsets, so they land exactly where the page put
|
|
488
508
|
// them.
|
|
489
509
|
if (request.annotations && impl_->form != nullptr) {
|
|
490
|
-
FPDF_FFLDraw(impl_->form, bitmap, page, sx, sy, pw, ph,
|
|
510
|
+
FPDF_FFLDraw(impl_->form, bitmap, page, sx, sy, pw, ph, rotate, flags);
|
|
491
511
|
}
|
|
492
512
|
|
|
493
513
|
FPDFBitmap_Destroy(bitmap);
|