@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
package/PdfCanvas.podspec
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
3
|
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
pdfium = JSON.parse(File.read(File.join(__dir__, "scripts", "pdfium-manifest.json")))
|
|
4
5
|
|
|
5
6
|
# FETCH PDFIUM WHILE THE PODSPEC IS EVALUATED.
|
|
6
7
|
#
|
|
@@ -50,10 +51,17 @@ Pod::Spec.new do |s|
|
|
|
50
51
|
s.authors = { "REEKON Tools" => "hello@reekon.tools" }
|
|
51
52
|
s.source = { :git => "https://gitlab.com/reekon-tools/software/rock-pro/react-native-pdf-canvas.git", :tag => "#{s.version}" }
|
|
52
53
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
54
|
+
# THE FLOOR IS THE ENGINE'S, NOT OURS. Nothing this pod compiles needs anything
|
|
55
|
+
# newer than iOS 13, but the vendored PDFium dylib is built for whatever
|
|
56
|
+
# `ios_deployment_target` bblanchon used (17.0 since 2026-08-14), and that is
|
|
57
|
+
# what dyld enforces on the device and what App Store processing checks
|
|
58
|
+
# against the framework's Info.plist (ITMS-90208). The value is declared once
|
|
59
|
+
# in scripts/pdfium-manifest.json, verified against every slice's Mach-O by
|
|
60
|
+
# fetch-pdfium.mjs above, and written into the framework plist from the same
|
|
61
|
+
# field — so it is read here rather than restated. Declaring it as the pod's
|
|
62
|
+
# platform makes a host with a lower deployment target fail at `pod install`
|
|
63
|
+
# ("PdfCanvas does not support iOS 16.4") instead of at upload.
|
|
64
|
+
s.platforms = { :ios => pdfium["iosMinimumOSVersion"] }
|
|
57
65
|
s.requires_arc = true
|
|
58
66
|
|
|
59
67
|
# THE CORE IS C++ AND SHARED WITH ANDROID: `native/core` is compiled into this
|
package/README.md
CHANGED
|
@@ -80,6 +80,11 @@ during CocoaPods podspec evaluation; Android retrieves it from a Gradle task. Ai
|
|
|
80
80
|
may direct `PDFCANVAS_PDFIUM_CACHE` or `PDFCANVAS_PDFIUM_MIRROR` at a local copy, which is still
|
|
81
81
|
verified against the manifest.
|
|
82
82
|
|
|
83
|
+
The prebuilt engine sets the native floors: **iOS 17.0** (`iosMinimumOSVersion` in the manifest,
|
|
84
|
+
declared by the pod and checked against the dylib at fetch time) and **Android API 24**. A host app
|
|
85
|
+
with a lower iOS deployment target fails at `pod install`; raise the target rather than the
|
|
86
|
+
framework's plist, since the dylib will not load on older systems either.
|
|
87
|
+
|
|
83
88
|
## Installation
|
|
84
89
|
|
|
85
90
|
```sh
|
|
@@ -245,6 +250,23 @@ usePdfLayer({document, policy: {sharpnessBand: 1.2, quietZoomMs: 400}});
|
|
|
245
250
|
observed p95 raster time. A value set too low permits a user adjusting the zoom to supersede
|
|
246
251
|
each render before it completes.
|
|
247
252
|
|
|
253
|
+
### Page rotation
|
|
254
|
+
|
|
255
|
+
A host may show a page turned a quarter, half or three-quarter turn clockwise on top of the
|
|
256
|
+
document's own `/Rotate`, per page:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// Page 0 shown turned 90° clockwise; pages without an entry stay upright.
|
|
260
|
+
usePdfLayer({document, rotations: [90]});
|
|
261
|
+
// A matching thumbnail. `widthPx` is the width of the TURNED raster.
|
|
262
|
+
document.renderPage(0, {widthPx: 160, rotation: 90});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Doc space for a turned page is the turned page's: a 612x792 page at 90 is laid out as 792x612,
|
|
266
|
+
`content.pages[i].pageRect` reports that rect, and anything the host draws over the page is
|
|
267
|
+
positioned in the same space. Changing a page's rotation rebuilds the layer's raster cache,
|
|
268
|
+
exactly as changing `annotations` does.
|
|
269
|
+
|
|
248
270
|
## Platform support
|
|
249
271
|
|
|
250
272
|
| Platform | Engine | Backend identifier |
|
|
@@ -102,7 +102,7 @@ public class PdfCanvasNativeTest {
|
|
|
102
102
|
|
|
103
103
|
long[] shape = new long[4];
|
|
104
104
|
byte[] bytes = PdfCanvasNative.render(
|
|
105
|
-
service, handle, 0, 0, 0, 200, 100, 2, false, false, 1, false, shape);
|
|
105
|
+
service, handle, 0, 0, 0, 200, 100, 2, 0, false, false, 1, false, shape);
|
|
106
106
|
assertNotNull("a heap render returns bytes", bytes);
|
|
107
107
|
assertEquals(0L, shape[0]);
|
|
108
108
|
assertEquals(400L, shape[1]);
|
|
@@ -122,14 +122,38 @@ public class PdfCanvasNativeTest {
|
|
|
122
122
|
public void annotationsFlagReachesTheCore() throws Exception {
|
|
123
123
|
int handle = open(TestPdfs.annotated(ctx(), "jni-annotated.pdf"));
|
|
124
124
|
long[] shape = new long[4];
|
|
125
|
-
byte[] with = PdfCanvasNative.render(service, handle, 0, 0, 0, 200, 200, 1, true, false, 1, false, shape);
|
|
126
|
-
byte[] without = PdfCanvasNative.render(service, handle, 0, 0, 0, 200, 200, 1, false, false, 2, false, shape);
|
|
125
|
+
byte[] with = PdfCanvasNative.render(service, handle, 0, 0, 0, 200, 200, 1, 0, true, false, 1, false, shape);
|
|
126
|
+
byte[] without = PdfCanvasNative.render(service, handle, 0, 0, 0, 200, 200, 1, 0, false, false, 2, false, shape);
|
|
127
127
|
int rowBytes = (int) shape[3];
|
|
128
128
|
assertPixel(with, rowBytes, 100, 100, 0, 255, 0);
|
|
129
129
|
assertPixel(without, rowBytes, 100, 100, 255, 255, 255);
|
|
130
130
|
PdfCanvasNative.close(service, handle);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* CATCHES: the rotation not surviving the JNI int hop, or landing in the
|
|
135
|
+
* wrong argument slot (a swapped {@code annotations} would still compile).
|
|
136
|
+
* Doc top-left is blue and bottom-left red; a quarter turn clockwise puts
|
|
137
|
+
* the bottom-left at the top-left and the top-left at the top-right. The
|
|
138
|
+
* rect is the TURNED page's: 100x200 for a 200x100 page.
|
|
139
|
+
*/
|
|
140
|
+
@Test
|
|
141
|
+
public void rotationReachesTheCore() throws Exception {
|
|
142
|
+
int handle = open(TestPdfs.quadrants(ctx(), "jni-rotated.pdf", 200, 100));
|
|
143
|
+
long[] shape = new long[4];
|
|
144
|
+
byte[] bytes =
|
|
145
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 100, 200, 2, 90, false, false, 1, false, shape);
|
|
146
|
+
assertNotNull(bytes);
|
|
147
|
+
assertEquals(200L, shape[1]);
|
|
148
|
+
assertEquals(400L, shape[2]);
|
|
149
|
+
int rowBytes = (int) shape[3];
|
|
150
|
+
assertPixel(bytes, rowBytes, 20, 20, 255, 0, 0); // red: doc bottom-left, now top-left
|
|
151
|
+
assertPixel(bytes, rowBytes, 180, 20, 0, 0, 255); // blue: doc top-left, now top-right
|
|
152
|
+
assertPixel(bytes, rowBytes, 180, 380, 0, 0, 0); // black: doc top-right, now bottom-right
|
|
153
|
+
assertPixel(bytes, rowBytes, 20, 380, 0, 255, 0); // green: doc bottom-right, now bottom-left
|
|
154
|
+
PdfCanvasNative.close(service, handle);
|
|
155
|
+
}
|
|
156
|
+
|
|
133
157
|
/* ================================================================ *
|
|
134
158
|
* 2. The transport
|
|
135
159
|
* ================================================================ */
|
|
@@ -144,11 +168,11 @@ public class PdfCanvasNativeTest {
|
|
|
144
168
|
public void slotBytesEqualHeapBytesForARealRaster() throws Exception {
|
|
145
169
|
int handle = open(TestPdfs.quadrants(ctx(), "jni-slot.pdf", 300, 200));
|
|
146
170
|
long[] heapShape = new long[4];
|
|
147
|
-
byte[] heap = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, false, false, 1, false, heapShape);
|
|
171
|
+
byte[] heap = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, 0, false, false, 1, false, heapShape);
|
|
148
172
|
assertNotNull(heap);
|
|
149
173
|
|
|
150
174
|
long[] slotShape = new long[4];
|
|
151
|
-
byte[] none = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, false, false, 2, true, slotShape);
|
|
175
|
+
byte[] none = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, 0, false, false, 2, true, slotShape);
|
|
152
176
|
assertNull("a slot render returns no bytes", none);
|
|
153
177
|
assertTrue("a slot render names a slot", slotShape[0] > 0);
|
|
154
178
|
assertEquals(heapShape[1], slotShape[1]);
|
|
@@ -215,7 +239,7 @@ public class PdfCanvasNativeTest {
|
|
|
215
239
|
}
|
|
216
240
|
int handle = open(TestPdfs.quadrants(ctx(), "jni-range.pdf", 40, 40));
|
|
217
241
|
try {
|
|
218
|
-
PdfCanvasNative.render(service, handle, 7, 0, 0, 10, 10, 1, false, false, 1, false, new long[4]);
|
|
242
|
+
PdfCanvasNative.render(service, handle, 7, 0, 0, 10, 10, 1, 0, false, false, 1, false, new long[4]);
|
|
219
243
|
fail("an out-of-range page must throw");
|
|
220
244
|
} catch (PdfRasterException e) {
|
|
221
245
|
assertEquals(PdfErrorCode.NOT_FOUND, e.code());
|
|
@@ -228,24 +252,24 @@ public class PdfCanvasNativeTest {
|
|
|
228
252
|
PdfCanvasNative.registerCancellation(service, handle, 5);
|
|
229
253
|
PdfCanvasNative.cancel(service, handle, 5);
|
|
230
254
|
try {
|
|
231
|
-
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 5, false, new long[4]);
|
|
255
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, 0, false, false, 5, false, new long[4]);
|
|
232
256
|
fail("a cancelled render must throw");
|
|
233
257
|
} catch (PdfRasterException e) {
|
|
234
258
|
assertEquals(PdfErrorCode.CANCELLED, e.code());
|
|
235
259
|
}
|
|
236
260
|
// The signal was consumed: the same token renders fine afterwards.
|
|
237
|
-
assertNotNull(PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 5, false, new long[4]));
|
|
261
|
+
assertNotNull(PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, 0, false, false, 5, false, new long[4]));
|
|
238
262
|
}
|
|
239
263
|
|
|
240
264
|
@Test
|
|
241
265
|
public void invalidateClosesEverythingAndRefusesAfterwards() throws Exception {
|
|
242
266
|
int handle = open(TestPdfs.quadrants(ctx(), "jni-invalidate.pdf", 40, 40));
|
|
243
|
-
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 1, true, new long[4]);
|
|
267
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, 0, false, false, 1, true, new long[4]);
|
|
244
268
|
assertEquals(1, PdfCanvasNative.slotCount());
|
|
245
269
|
PdfCanvasNative.invalidateService(service);
|
|
246
270
|
assertEquals("invalidate drains parked slots", 0, PdfCanvasNative.slotCount());
|
|
247
271
|
try {
|
|
248
|
-
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 2, false, new long[4]);
|
|
272
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, 0, false, false, 2, false, new long[4]);
|
|
249
273
|
fail("a render after invalidate must throw");
|
|
250
274
|
} catch (PdfRasterException e) {
|
|
251
275
|
assertEquals(PdfErrorCode.BACKEND_FAILURE, e.code());
|
|
@@ -172,8 +172,8 @@ JNIEXPORT void JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_registerCance
|
|
|
172
172
|
|
|
173
173
|
JNIEXPORT jbyteArray JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_render(
|
|
174
174
|
JNIEnv* env, jclass, jlong service, jint handle, jint page, jdouble x, jdouble y, jdouble width,
|
|
175
|
-
jdouble height, jdouble scale, jboolean annotations, jboolean transparent,
|
|
176
|
-
jboolean useSlot, jlongArray out) {
|
|
175
|
+
jdouble height, jdouble scale, jint rotation, jboolean annotations, jboolean transparent,
|
|
176
|
+
jlong token, jboolean useSlot, jlongArray out) {
|
|
177
177
|
return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray {
|
|
178
178
|
Service* s = serviceOf(service);
|
|
179
179
|
if (s == nullptr) throw PdfError(ErrorCode::BackendFailure, "The native service is gone.");
|
|
@@ -188,6 +188,8 @@ JNIEXPORT jbyteArray JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_render(
|
|
|
188
188
|
request.width = width;
|
|
189
189
|
request.height = height;
|
|
190
190
|
request.scale = scale;
|
|
191
|
+
// Degrees clockwise on top of the page's /Rotate; the core validates it.
|
|
192
|
+
request.rotation = rotation;
|
|
191
193
|
request.annotations = annotations == JNI_TRUE;
|
|
192
194
|
request.background = transparent == JNI_TRUE ? pdfcanvas::Background::Transparent
|
|
193
195
|
: pdfcanvas::Background::White;
|
|
@@ -107,6 +107,10 @@ public final class PdfCanvasNative {
|
|
|
107
107
|
/**
|
|
108
108
|
* Renders one raster on the calling thread.
|
|
109
109
|
*
|
|
110
|
+
* @param rotation the HOST's rotation on top of the page's {@code /Rotate},
|
|
111
|
+
* in degrees clockwise: 0, 90, 180 or 270. The rect is in the TURNED
|
|
112
|
+
* page's doc space — extents swapped for a quarter turn. The core refuses
|
|
113
|
+
* anything else.
|
|
110
114
|
* @param useSlot ask for the zero-copy transport. The store may still refuse,
|
|
111
115
|
* so the result says where the pixels ARE: a non-null return is heap
|
|
112
116
|
* bytes, a null return means {@code out[0]} names a slot.
|
|
@@ -122,6 +126,7 @@ public final class PdfCanvasNative {
|
|
|
122
126
|
double width,
|
|
123
127
|
double height,
|
|
124
128
|
double scale,
|
|
129
|
+
int rotation,
|
|
125
130
|
boolean annotations,
|
|
126
131
|
boolean transparent,
|
|
127
132
|
long token,
|
|
@@ -294,7 +294,7 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
296
|
// The whole request is turned into plain values HERE, on the bridge thread.
|
|
297
|
-
final int page;
|
|
297
|
+
final int page, rotation;
|
|
298
298
|
final double x, y, width, height, scale;
|
|
299
299
|
final boolean annotations, transparent;
|
|
300
300
|
try {
|
|
@@ -304,6 +304,13 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
304
304
|
width = request.getDouble("width");
|
|
305
305
|
height = request.getDouble("height");
|
|
306
306
|
scale = request.getDouble("scale");
|
|
307
|
+
// OPTIONAL, DEFAULTING TO 0: a JS bundle older than this binary never
|
|
308
|
+
// sends it, and a page must not turn because a key is missing. The core
|
|
309
|
+
// validates the value; this only decides what "absent" means.
|
|
310
|
+
rotation =
|
|
311
|
+
request.hasKey("rotation") && !request.isNull("rotation")
|
|
312
|
+
? request.getInt("rotation")
|
|
313
|
+
: 0;
|
|
307
314
|
annotations = !request.hasKey("annotations") || request.getBoolean("annotations");
|
|
308
315
|
transparent = "transparent".equals(request.getString("background"));
|
|
309
316
|
} catch (RuntimeException e) {
|
|
@@ -329,8 +336,8 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
329
336
|
final long[] shape = new long[4];
|
|
330
337
|
final byte[] bytes =
|
|
331
338
|
PdfCanvasNative.render(
|
|
332
|
-
service, handle, page, x, y, width, height, scale,
|
|
333
|
-
token, fast, shape);
|
|
339
|
+
service, handle, page, x, y, width, height, scale, rotation, annotations,
|
|
340
|
+
transparent, token, fast, shape);
|
|
334
341
|
slot = shape[0];
|
|
335
342
|
|
|
336
343
|
WritableMap result = Arguments.createMap();
|
package/dist/controller.d.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* which key comes from `./planner.js`. A private copy of either is how the two
|
|
38
38
|
* silently drift until the tested one is testing nothing that runs.
|
|
39
39
|
*/
|
|
40
|
-
import type { DocRect, PageGeometry, PdfContent, PdfController, PdfDiagnostic, PdfRaster, RasterizerHandle, RasterPixels, RasterPolicy, RasterRole } from './types.js';
|
|
40
|
+
import type { DocRect, PageGeometry, PageRotation, PdfContent, PdfController, PdfDiagnostic, PdfRaster, RasterizerHandle, RasterPixels, RasterPolicy, RasterRole } from './types.js';
|
|
41
41
|
import type { RasterCache } from './cache.js';
|
|
42
42
|
export interface ScheduledHandle {
|
|
43
43
|
cancel(): void;
|
|
@@ -62,6 +62,28 @@ export interface PdfControllerOptions {
|
|
|
62
62
|
pageRects: DocRect[];
|
|
63
63
|
/** Parallel to `pageRects`. Every page, in page order. */
|
|
64
64
|
pages: PageGeometry[];
|
|
65
|
+
/**
|
|
66
|
+
* The HOST's per-page rotation on top of the document's `/Rotate`, indexed
|
|
67
|
+
* by page like `pageRects`; a missing or undefined entry is 0. Stamped onto
|
|
68
|
+
* every `RasterRequest` for that page as `rotation` — and only that. The
|
|
69
|
+
* controller does no rotation arithmetic of its own.
|
|
70
|
+
*
|
|
71
|
+
* WHY A PARALLEL ARRAY AND NOT A FIELD ON `PageGeometry`. `PageGeometry` is
|
|
72
|
+
* the backend's statement of a page's intrinsic size, and its `rotation` is
|
|
73
|
+
* already taken: it is the document's own `/Rotate`. A host preference on
|
|
74
|
+
* the same object would give the type two rotations with different meanings,
|
|
75
|
+
* and would ride into every backend that constructs one (the fake, the web
|
|
76
|
+
* engine, the native bridge) for a value none of them produce. This
|
|
77
|
+
* controller already takes its per-page inputs as parallel arrays, so a
|
|
78
|
+
* third costs nothing new and touches nothing outside this file.
|
|
79
|
+
*
|
|
80
|
+
* THE GEOMETRY MUST ALREADY BE ROTATED. For a page turned 90 or 270, `pages`
|
|
81
|
+
* and `pageRects` must both carry the SWAPPED extents — see `../rotation.ts`
|
|
82
|
+
* — because `layoutScale` is `pageRect.width / geometry.width` and is only
|
|
83
|
+
* right when both sides describe the same orientation. `usePdfLayer` does
|
|
84
|
+
* the swap; a host driving this directly must too.
|
|
85
|
+
*/
|
|
86
|
+
rotations?: readonly PageRotation[];
|
|
65
87
|
policy: RasterPolicy;
|
|
66
88
|
ingest: (pixels: RasterPixels, meta: RasterIngestMeta) => PdfRaster;
|
|
67
89
|
cache: RasterCache;
|
package/dist/controller.js
CHANGED
|
@@ -137,6 +137,7 @@ export function createPdfController(options) {
|
|
|
137
137
|
page,
|
|
138
138
|
pageRect,
|
|
139
139
|
layoutScale: Number.isFinite(raw) && raw > 0 ? raw : 1,
|
|
140
|
+
rotation: options.rotations?.[page] ?? 0,
|
|
140
141
|
};
|
|
141
142
|
});
|
|
142
143
|
const listeners = new Set();
|
|
@@ -405,24 +406,35 @@ export function createPdfController(options) {
|
|
|
405
406
|
};
|
|
406
407
|
const renderOne = async (item, slot, info) => {
|
|
407
408
|
const startedAt = now();
|
|
409
|
+
const request = {
|
|
410
|
+
page: item.page,
|
|
411
|
+
// Page-LOCAL rect: the planner works in LAYOUT space, but a backend
|
|
412
|
+
// renders a page and knows nothing about where the layout put it.
|
|
413
|
+
// For a page the host has turned this is the ROTATED page's space:
|
|
414
|
+
// `pageRects` and `pages` both carry the swapped extents, which is
|
|
415
|
+
// what keeps `layoutScale` at 1 and lets the backend slide the whole
|
|
416
|
+
// turned page exactly as it slides an upright one.
|
|
417
|
+
docRect: {
|
|
418
|
+
x: (item.docRect.x - info.pageRect.x) / info.layoutScale,
|
|
419
|
+
y: (item.docRect.y - info.pageRect.y) / info.layoutScale,
|
|
420
|
+
width: item.docRect.width / info.layoutScale,
|
|
421
|
+
height: item.docRect.height / info.layoutScale,
|
|
422
|
+
},
|
|
423
|
+
scale: item.scale * info.layoutScale,
|
|
424
|
+
// See `PdfControllerOptions.annotations`: true unless the host
|
|
425
|
+
// draws the PDF's annotations itself.
|
|
426
|
+
annotations: drawAnnotations,
|
|
427
|
+
background: 'white',
|
|
428
|
+
};
|
|
429
|
+
// STAMPED ONLY WHEN IT IS NOT THE DEFAULT. A host that never rotates
|
|
430
|
+
// hands its backend the exact request it always did, key for key — a
|
|
431
|
+
// host-injected backend that compares requests structurally sees no
|
|
432
|
+
// change at all — and every shipped backend reads `rotation ?? 0`.
|
|
433
|
+
if (info.rotation !== 0)
|
|
434
|
+
request.rotation = info.rotation;
|
|
408
435
|
let pixels;
|
|
409
436
|
try {
|
|
410
|
-
pixels = await handle.render(
|
|
411
|
-
page: item.page,
|
|
412
|
-
// Page-LOCAL rect: the planner works in LAYOUT space, but a backend
|
|
413
|
-
// renders a page and knows nothing about where the layout put it.
|
|
414
|
-
docRect: {
|
|
415
|
-
x: (item.docRect.x - info.pageRect.x) / info.layoutScale,
|
|
416
|
-
y: (item.docRect.y - info.pageRect.y) / info.layoutScale,
|
|
417
|
-
width: item.docRect.width / info.layoutScale,
|
|
418
|
-
height: item.docRect.height / info.layoutScale,
|
|
419
|
-
},
|
|
420
|
-
scale: item.scale * info.layoutScale,
|
|
421
|
-
// See `PdfControllerOptions.annotations`: true unless the host
|
|
422
|
-
// draws the PDF's annotations itself.
|
|
423
|
-
annotations: drawAnnotations,
|
|
424
|
-
background: 'white',
|
|
425
|
-
}, state.abort.signal);
|
|
437
|
+
pixels = await handle.render(request, state.abort.signal);
|
|
426
438
|
}
|
|
427
439
|
catch (error) {
|
|
428
440
|
missing.push(item.key);
|
|
@@ -14,8 +14,14 @@
|
|
|
14
14
|
* checks and an accidental clip is visible as a blank margin;
|
|
15
15
|
* - each piece carries a 1px border, so a piece that has been inset or
|
|
16
16
|
* stretched shows its border in the wrong place;
|
|
17
|
-
* - each piece encodes its own doc rect
|
|
18
|
-
* read back what a raster BELIEVES it is without any
|
|
17
|
+
* - each piece encodes its own doc rect AND its rotation into a corner
|
|
18
|
+
* swatch, so a test can read back what a raster BELIEVES it is without any
|
|
19
|
+
* Skia in the loop;
|
|
20
|
+
* - the checker is drawn in the PAGE's own space and turned by the request's
|
|
21
|
+
* `rotation`, the way a real engine turns its page — so a raster at 90 is
|
|
22
|
+
* the raster at 0 turned a quarter clockwise, pixel for pixel outside the
|
|
23
|
+
* border and swatch, and a pipeline that rotated its geometry but forgot
|
|
24
|
+
* to stamp the request shows an unturned board in a turned frame.
|
|
19
25
|
*
|
|
20
26
|
* Everything here is a pure function of the request, so the same request always
|
|
21
27
|
* produces byte-identical output.
|
|
@@ -89,4 +95,10 @@ export declare function fakePageColor(page: number): [number, number, number];
|
|
|
89
95
|
* was too small to carry a swatch.
|
|
90
96
|
*/
|
|
91
97
|
export declare function decodeFakeDocRect(pixels: RasterPixels): DocRect | null;
|
|
98
|
+
/**
|
|
99
|
+
* Read the rotation a fake raster was rendered at, or null when the raster was
|
|
100
|
+
* too small to carry a swatch. What a pipeline test reads to prove the
|
|
101
|
+
* controller's stamp reached the backend.
|
|
102
|
+
*/
|
|
103
|
+
export declare function decodeFakeRotation(pixels: RasterPixels): PageRotation | null;
|
|
92
104
|
export declare function createFakeRasterizer(options?: FakeRasterizerOptions): PageRasterizer;
|
package/dist/rasterizer/fake.js
CHANGED
|
@@ -14,12 +14,19 @@
|
|
|
14
14
|
* checks and an accidental clip is visible as a blank margin;
|
|
15
15
|
* - each piece carries a 1px border, so a piece that has been inset or
|
|
16
16
|
* stretched shows its border in the wrong place;
|
|
17
|
-
* - each piece encodes its own doc rect
|
|
18
|
-
* read back what a raster BELIEVES it is without any
|
|
17
|
+
* - each piece encodes its own doc rect AND its rotation into a corner
|
|
18
|
+
* swatch, so a test can read back what a raster BELIEVES it is without any
|
|
19
|
+
* Skia in the loop;
|
|
20
|
+
* - the checker is drawn in the PAGE's own space and turned by the request's
|
|
21
|
+
* `rotation`, the way a real engine turns its page — so a raster at 90 is
|
|
22
|
+
* the raster at 0 turned a quarter clockwise, pixel for pixel outside the
|
|
23
|
+
* border and swatch, and a pipeline that rotated its geometry but forgot
|
|
24
|
+
* to stamp the request shows an unturned board in a turned frame.
|
|
19
25
|
*
|
|
20
26
|
* Everything here is a pure function of the request, so the same request always
|
|
21
27
|
* produces byte-identical output.
|
|
22
28
|
*/
|
|
29
|
+
import { isPageRotation } from '../rotation.js';
|
|
23
30
|
import { PdfError } from '../types.js';
|
|
24
31
|
/* ------------------------------------------------------------------ *
|
|
25
32
|
* Page sizes, in PDF points (1pt = 1/72in)
|
|
@@ -171,9 +178,10 @@ function writePixel(out, offset, r, g, b, a, format) {
|
|
|
171
178
|
* The corner swatch
|
|
172
179
|
* ------------------------------------------------------------------ */
|
|
173
180
|
/**
|
|
174
|
-
* The swatch is RAW BYTES, not a colour:
|
|
175
|
-
*
|
|
176
|
-
*
|
|
181
|
+
* The swatch is RAW BYTES, not a colour: five pixels, each carrying one value
|
|
182
|
+
* as a 24-bit fixed-point number in channels 0..2 with channel 3 held at 255 —
|
|
183
|
+
* the four doc rect components, then the request's rotation in degrees.
|
|
184
|
+
* Because it is written without the format swizzle, the decoder is
|
|
177
185
|
* format-independent — it reads back the same numbers from an RGBA and a BGRA
|
|
178
186
|
* raster.
|
|
179
187
|
*/
|
|
@@ -182,7 +190,9 @@ const SWATCH_FIXED_POINT = 16;
|
|
|
182
190
|
const SWATCH_BIAS = 0x800000;
|
|
183
191
|
const SWATCH_ORIGIN_X = 1;
|
|
184
192
|
const SWATCH_ROW = 1;
|
|
185
|
-
const SWATCH_PIXELS =
|
|
193
|
+
const SWATCH_PIXELS = 5;
|
|
194
|
+
/** Index of the rotation value within the swatch, after x, y, width, height. */
|
|
195
|
+
const SWATCH_ROTATION = 4;
|
|
186
196
|
/** Smallest raster that has room for the swatch inside its border. */
|
|
187
197
|
const SWATCH_MIN_WIDTH = SWATCH_ORIGIN_X + SWATCH_PIXELS + 1;
|
|
188
198
|
const SWATCH_MIN_HEIGHT = SWATCH_ROW + 2;
|
|
@@ -194,7 +204,7 @@ function encodeSwatchValue(out, offset, value) {
|
|
|
194
204
|
out[offset + 2] = clamped & 0xff;
|
|
195
205
|
out[offset + 3] = 255;
|
|
196
206
|
}
|
|
197
|
-
function encodeSwatch(out, rowBytes, width, height, docRect) {
|
|
207
|
+
function encodeSwatch(out, rowBytes, width, height, docRect, rotation) {
|
|
198
208
|
if (width < SWATCH_MIN_WIDTH || height < SWATCH_MIN_HEIGHT) {
|
|
199
209
|
return;
|
|
200
210
|
}
|
|
@@ -203,39 +213,81 @@ function encodeSwatch(out, rowBytes, width, height, docRect) {
|
|
|
203
213
|
encodeSwatchValue(out, base + BYTES_PER_PIXEL, docRect.y);
|
|
204
214
|
encodeSwatchValue(out, base + BYTES_PER_PIXEL * 2, docRect.width);
|
|
205
215
|
encodeSwatchValue(out, base + BYTES_PER_PIXEL * 3, docRect.height);
|
|
216
|
+
encodeSwatchValue(out, base + BYTES_PER_PIXEL * SWATCH_ROTATION, rotation);
|
|
217
|
+
}
|
|
218
|
+
/** One swatch value back out, or null when the raster is too small to carry a
|
|
219
|
+
* swatch at all. `component` is the pixel index within the swatch. */
|
|
220
|
+
function decodeSwatchValue(pixels, component) {
|
|
221
|
+
const { bytes, width, height, rowBytes } = pixels;
|
|
222
|
+
if (width < SWATCH_MIN_WIDTH || height < SWATCH_MIN_HEIGHT) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const o = SWATCH_ROW * rowBytes + (SWATCH_ORIGIN_X + component) * BYTES_PER_PIXEL;
|
|
226
|
+
// In range by the size guard above; `?? 0` satisfies
|
|
227
|
+
// noUncheckedIndexedAccess on TypedArray indexing.
|
|
228
|
+
const raw = ((bytes[o] ?? 0) << 16) | ((bytes[o + 1] ?? 0) << 8) | (bytes[o + 2] ?? 0);
|
|
229
|
+
return (raw - SWATCH_BIAS) / SWATCH_FIXED_POINT;
|
|
206
230
|
}
|
|
207
231
|
/**
|
|
208
232
|
* Read the doc rect a fake raster believes it covers, or null when the raster
|
|
209
233
|
* was too small to carry a swatch.
|
|
210
234
|
*/
|
|
211
235
|
export function decodeFakeDocRect(pixels) {
|
|
212
|
-
const
|
|
213
|
-
|
|
236
|
+
const x = decodeSwatchValue(pixels, 0);
|
|
237
|
+
const y = decodeSwatchValue(pixels, 1);
|
|
238
|
+
const width = decodeSwatchValue(pixels, 2);
|
|
239
|
+
const height = decodeSwatchValue(pixels, 3);
|
|
240
|
+
if (x === null || y === null || width === null || height === null) {
|
|
214
241
|
return null;
|
|
215
242
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
y: read(1),
|
|
229
|
-
width: read(2),
|
|
230
|
-
height: read(3),
|
|
231
|
-
};
|
|
243
|
+
return { x, y, width, height };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Read the rotation a fake raster was rendered at, or null when the raster was
|
|
247
|
+
* too small to carry a swatch. What a pipeline test reads to prove the
|
|
248
|
+
* controller's stamp reached the backend.
|
|
249
|
+
*/
|
|
250
|
+
export function decodeFakeRotation(pixels) {
|
|
251
|
+
const rotation = decodeSwatchValue(pixels, SWATCH_ROTATION);
|
|
252
|
+
if (rotation === null)
|
|
253
|
+
return null;
|
|
254
|
+
return isPageRotation(rotation) ? rotation : null;
|
|
232
255
|
}
|
|
233
256
|
/* ------------------------------------------------------------------ *
|
|
234
257
|
* Rendering
|
|
235
258
|
* ------------------------------------------------------------------ */
|
|
236
|
-
|
|
259
|
+
/**
|
|
260
|
+
* The inverse of the request's rotation: a point in the TURNED page's doc space
|
|
261
|
+
* back to the page's own. `pageWidth` / `pageHeight` are the page's intrinsic
|
|
262
|
+
* (unturned) extents.
|
|
263
|
+
*
|
|
264
|
+
* These are the inverses of PDFium's `CPDF_Page::GetDisplayMatrix` for rotate
|
|
265
|
+
* 1..3, restated in y-down doc units — the same mapping the real backends'
|
|
266
|
+
* tests hold a turned render to, so the fake and the engine agree on what "90
|
|
267
|
+
* clockwise" means.
|
|
268
|
+
*/
|
|
269
|
+
function unrotate(rotation, pageWidth, pageHeight) {
|
|
270
|
+
switch (rotation) {
|
|
271
|
+
case 90:
|
|
272
|
+
return (x, y) => [y, pageHeight - x];
|
|
273
|
+
case 180:
|
|
274
|
+
return (x, y) => [pageWidth - x, pageHeight - y];
|
|
275
|
+
case 270:
|
|
276
|
+
return (x, y) => [pageWidth - y, x];
|
|
277
|
+
default:
|
|
278
|
+
return (x, y) => [x, y];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function renderPixels(request, cfg, geometry) {
|
|
237
282
|
const { docRect, scale, page, background } = request;
|
|
238
283
|
const { width, height } = fakePixelSize(request);
|
|
284
|
+
// Read as `?? 0` and then VALIDATED, as every real backend does: the fake
|
|
285
|
+
// stands in for them in the controller suite, so it must refuse what they
|
|
286
|
+
// refuse rather than quietly draw an upright page.
|
|
287
|
+
const rotation = request.rotation ?? 0;
|
|
288
|
+
if (!isPageRotation(rotation)) {
|
|
289
|
+
throw new PdfError('backend-failure', `Fake rasterizer: rotation must be 0, 90, 180 or 270, got ${String(rotation)}.`);
|
|
290
|
+
}
|
|
239
291
|
if (width * height > cfg.maxPixels) {
|
|
240
292
|
throw new PdfError('out-of-memory', `Fake rasterizer refused a ${width}x${height} raster ` +
|
|
241
293
|
`(${width * height} pixels > maxPixels ${cfg.maxPixels}).`);
|
|
@@ -256,18 +308,23 @@ function renderPixels(request, cfg) {
|
|
|
256
308
|
writePixel(bytes, o, tintR, tintG, tintB, 255, cfg.format);
|
|
257
309
|
}
|
|
258
310
|
}
|
|
259
|
-
// Dark checker cells, in DOC space. Pixel CENTRES are converted
|
|
260
|
-
// coordinates — a corner-based conversion puts the sample exactly on
|
|
261
|
-
// boundary whenever the piece origin happens to align with one, and
|
|
262
|
-
// parity then flips on a floating-point tie.
|
|
311
|
+
// Dark checker cells, in the PAGE's DOC space. Pixel CENTRES are converted
|
|
312
|
+
// to doc coordinates — a corner-based conversion puts the sample exactly on
|
|
313
|
+
// a cell boundary whenever the piece origin happens to align with one, and
|
|
314
|
+
// the parity then flips on a floating-point tie. The request's rect is in
|
|
315
|
+
// the TURNED page's space, so each centre is mapped back through the inverse
|
|
316
|
+
// rotation before its cell is looked up: that is what makes the board turn
|
|
317
|
+
// with the request, the way a real engine's page does.
|
|
263
318
|
const cell = cfg.checkerDocSize;
|
|
319
|
+
const toPage = unrotate(rotation, geometry.width, geometry.height);
|
|
264
320
|
for (let py = 0; py < height; py++) {
|
|
265
|
-
const
|
|
266
|
-
const cellY = Math.floor(docY / cell);
|
|
321
|
+
const turnedY = docRect.y + (py + 0.5) / scale;
|
|
267
322
|
let offset = py * rowBytes;
|
|
268
323
|
for (let px = 0; px < width; px++, offset += BYTES_PER_PIXEL) {
|
|
269
|
-
const
|
|
324
|
+
const turnedX = docRect.x + (px + 0.5) / scale;
|
|
325
|
+
const [docX, docY] = toPage(turnedX, turnedY);
|
|
270
326
|
const cellX = Math.floor(docX / cell);
|
|
327
|
+
const cellY = Math.floor(docY / cell);
|
|
271
328
|
if (((cellX + cellY) & 1) === 1) {
|
|
272
329
|
writePixel(bytes, offset, r, g, b, 255, cfg.format);
|
|
273
330
|
}
|
|
@@ -288,7 +345,7 @@ function renderPixels(request, cfg) {
|
|
|
288
345
|
writePixel(bytes, row + lastCol, 0, 0, 0, 255, cfg.format);
|
|
289
346
|
}
|
|
290
347
|
}
|
|
291
|
-
encodeSwatch(bytes, rowBytes, width, height, docRect);
|
|
348
|
+
encodeSwatch(bytes, rowBytes, width, height, docRect, rotation);
|
|
292
349
|
return {
|
|
293
350
|
bytes,
|
|
294
351
|
width,
|
|
@@ -384,7 +441,7 @@ function createHandle(cfg) {
|
|
|
384
441
|
async render(request, signal) {
|
|
385
442
|
throwIfAborted(signal);
|
|
386
443
|
requireOpen();
|
|
387
|
-
requirePage(request.page);
|
|
444
|
+
const geometry = requirePage(request.page);
|
|
388
445
|
if (cfg.latencyMs > 0) {
|
|
389
446
|
await delay(cfg.latencyMs, signal);
|
|
390
447
|
}
|
|
@@ -396,7 +453,7 @@ function createHandle(cfg) {
|
|
|
396
453
|
if (failure) {
|
|
397
454
|
throw new PdfError(failure, `Fake rasterizer failed page ${request.page} by configuration.`);
|
|
398
455
|
}
|
|
399
|
-
return renderPixels(request, cfg);
|
|
456
|
+
return renderPixels(request, cfg, geometry);
|
|
400
457
|
},
|
|
401
458
|
close() {
|
|
402
459
|
closed = true;
|
|
@@ -81,6 +81,17 @@ export interface NativeRenderRequest {
|
|
|
81
81
|
scale: number;
|
|
82
82
|
annotations: boolean;
|
|
83
83
|
background: 'white' | 'transparent';
|
|
84
|
+
/**
|
|
85
|
+
* Degrees clockwise, 0 / 90 / 180 / 270, on top of the page's `/Rotate`.
|
|
86
|
+
*
|
|
87
|
+
* ALWAYS PRESENT on the wire — 0 when the `RasterRequest` carried none —
|
|
88
|
+
* while both native modules read it as optional-defaulting-to-0. That pair
|
|
89
|
+
* is what makes a version skew safe in both directions: a JS bundle newer
|
|
90
|
+
* than its binary sends a key the binary never looks for, and a binary newer
|
|
91
|
+
* than its bundle reads 0 for a key that never arrives. Neither can turn a
|
|
92
|
+
* page by accident.
|
|
93
|
+
*/
|
|
94
|
+
rotation: number;
|
|
84
95
|
}
|
|
85
96
|
export interface NativeSource {
|
|
86
97
|
uri?: string;
|
|
@@ -343,6 +343,7 @@ export function createNativeHandle(native, opened, transport, alpha, label) {
|
|
|
343
343
|
scale: request.scale,
|
|
344
344
|
annotations: request.annotations,
|
|
345
345
|
background: request.background,
|
|
346
|
+
rotation: request.rotation ?? 0,
|
|
346
347
|
}, token);
|
|
347
348
|
}
|
|
348
349
|
catch (error) {
|