@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
package/README.md
CHANGED
|
@@ -50,15 +50,16 @@ and drawing review, and form overlays.
|
|
|
50
50
|
|
|
51
51
|
The package supplies no viewer chrome — no scroll view, page controls or navigation — and
|
|
52
52
|
deliberately requires the host to provide the canvas, the gesture handling and the page layout.
|
|
53
|
-
It does not extract text
|
|
53
|
+
It does not extract text or modify documents. Beyond finding text on a page (see "Text search"),
|
|
54
|
+
its function is rasterization.
|
|
54
55
|
|
|
55
56
|
## Project status
|
|
56
57
|
|
|
57
|
-
Version 0.
|
|
58
|
-
deterministic fake backend are implemented and tested. All three platform backends render
|
|
59
|
-
PDFium, so annotations, form fields, passwords
|
|
60
|
-
native core is covered by a host-machine C++ suite, and the web backend
|
|
61
|
-
PDFium WebAssembly under Node.
|
|
58
|
+
Version 0.4.0 is a pre-release. The core, the cadence controller, the React layer and a
|
|
59
|
+
deterministic fake backend are implemented and tested. All three platform backends render and
|
|
60
|
+
search with PDFium, so annotations, form fields, passwords, page rotation and text search behave
|
|
61
|
+
identically on each. The native core is covered by a host-machine C++ suite, and the web backend
|
|
62
|
+
by suites driving real PDFium WebAssembly under Node.
|
|
62
63
|
|
|
63
64
|
Two areas remain unverified: the web backend has not yet been exercised in a browser, and the
|
|
64
65
|
iOS binding requires a device pass.
|
|
@@ -250,6 +251,60 @@ usePdfLayer({document, policy: {sharpnessBand: 1.2, quietZoomMs: 400}});
|
|
|
250
251
|
observed p95 raster time. A value set too low permits a user adjusting the zoom to supersede
|
|
251
252
|
each render before it completes.
|
|
252
253
|
|
|
254
|
+
### Page rotation
|
|
255
|
+
|
|
256
|
+
A host may show a page turned a quarter, half or three-quarter turn clockwise on top of the
|
|
257
|
+
document's own `/Rotate`, per page:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// Page 0 shown turned 90° clockwise; pages without an entry stay upright.
|
|
261
|
+
usePdfLayer({document, rotations: [90]});
|
|
262
|
+
// A matching thumbnail. `widthPx` is the width of the TURNED raster.
|
|
263
|
+
document.renderPage(0, {widthPx: 160, rotation: 90});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Doc space for a turned page is the turned page's: a 612x792 page at 90 is laid out as 792x612,
|
|
267
|
+
`content.pages[i].pageRect` reports that rect, and anything the host draws over the page is
|
|
268
|
+
positioned in the same space. Changing a page's rotation rebuilds the layer's raster cache,
|
|
269
|
+
exactly as changing `annotations` does.
|
|
270
|
+
|
|
271
|
+
### Text search
|
|
272
|
+
|
|
273
|
+
`PdfDocument.searchPage` finds every occurrence of a string on one page, in reading order:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
const controller = new AbortController(); // abort it on the next keystroke
|
|
277
|
+
const hits = await document.searchPage(pageIndex, query, {
|
|
278
|
+
wholeWord: true, // default false; `matchCase` likewise
|
|
279
|
+
rotation: 90, // the SAME rotation the page's layer uses; default 0
|
|
280
|
+
signal: controller.signal,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Inside the same <Group transform={worldTransform}> as the PdfContentView.
|
|
284
|
+
// Rects are PAGE-LOCAL: offset them by the page's rect in the layout.
|
|
285
|
+
const {pageRect} = content.pages[pageIndex];
|
|
286
|
+
hits
|
|
287
|
+
.flatMap(hit => hit.rects)
|
|
288
|
+
.map((r, i) => (
|
|
289
|
+
<Rect
|
|
290
|
+
key={i}
|
|
291
|
+
x={pageRect.x + r.x}
|
|
292
|
+
y={pageRect.y + r.y}
|
|
293
|
+
width={r.width}
|
|
294
|
+
height={r.height}
|
|
295
|
+
color="rgba(255, 200, 0, 0.35)"
|
|
296
|
+
/>
|
|
297
|
+
));
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Each `TextMatch` carries `charIndex` / `charCount` (PDFium's character offsets on the page),
|
|
301
|
+
`context` (about 40 characters either side, whitespace collapsed, for a results list) and `rects`:
|
|
302
|
+
one per text line the match spans, in PDF points with the origin at the **displayed** page's
|
|
303
|
+
top-left and y pointing down, with the host rotation applied. That is the space the page's own
|
|
304
|
+
rasters are drawn in, so a highlight lands on the glyphs with no conversion. An empty or
|
|
305
|
+
whitespace-only query resolves `[]` without asking the backend. Every shipped backend declares
|
|
306
|
+
`capabilities.search`, and the fake searches the per-page `text` it is given.
|
|
307
|
+
|
|
253
308
|
## Platform support
|
|
254
309
|
|
|
255
310
|
| 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,68 @@ 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
|
+
|
|
157
|
+
/**
|
|
158
|
+
* CATCHES: the query mangled crossing JNI (it goes as UTF-16 via
|
|
159
|
+
* {@code GetStringRegion}, never modified UTF-8), a flag or the rotation in
|
|
160
|
+
* the wrong argument slot, and the JSON not surviving {@code NewStringUTF}.
|
|
161
|
+
* The match itself — index, rects, geometry — is proven by {@code native/tests};
|
|
162
|
+
* this checks the values cross.
|
|
163
|
+
*/
|
|
164
|
+
@Test
|
|
165
|
+
public void searchCrossesJni() throws Exception {
|
|
166
|
+
int handle = open(TestPdfs.text(ctx(), "jni-text.pdf"));
|
|
167
|
+
PdfCanvasNative.registerCancellation(service, handle, 1);
|
|
168
|
+
String json = PdfCanvasNative.search(service, handle, 0, "MEASURED", false, false, 0, 1);
|
|
169
|
+
assertTrue(json, json.startsWith("[{\"page\":0,\"charIndex\":6,\"charCount\":8,\"rects\":[{"));
|
|
170
|
+
assertTrue(json, json.contains("\"context\":\"Hello measured world\""));
|
|
171
|
+
// matchCase lands in its own slot: the same query now finds nothing.
|
|
172
|
+
assertEquals("[]", PdfCanvasNative.search(service, handle, 0, "MEASURED", true, false, 0, 2));
|
|
173
|
+
// wholeWord likewise.
|
|
174
|
+
assertEquals("[]", PdfCanvasNative.search(service, handle, 0, "meas", false, true, 0, 3));
|
|
175
|
+
// A supplementary character crosses intact and simply matches nothing.
|
|
176
|
+
assertEquals("[]", PdfCanvasNative.search(service, handle, 0, "\uD83D\uDE00", false, false, 0, 4));
|
|
177
|
+
// A refused rotation comes back as a typed error, not a crash.
|
|
178
|
+
try {
|
|
179
|
+
PdfCanvasNative.search(service, handle, 0, "measured", false, false, 45, 5);
|
|
180
|
+
fail("a 45-degree rotation must be refused");
|
|
181
|
+
} catch (PdfRasterException e) {
|
|
182
|
+
assertEquals(PdfErrorCode.BACKEND_FAILURE, e.code());
|
|
183
|
+
}
|
|
184
|
+
PdfCanvasNative.close(service, handle);
|
|
185
|
+
}
|
|
186
|
+
|
|
133
187
|
/* ================================================================ *
|
|
134
188
|
* 2. The transport
|
|
135
189
|
* ================================================================ */
|
|
@@ -144,11 +198,11 @@ public class PdfCanvasNativeTest {
|
|
|
144
198
|
public void slotBytesEqualHeapBytesForARealRaster() throws Exception {
|
|
145
199
|
int handle = open(TestPdfs.quadrants(ctx(), "jni-slot.pdf", 300, 200));
|
|
146
200
|
long[] heapShape = new long[4];
|
|
147
|
-
byte[] heap = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, false, false, 1, false, heapShape);
|
|
201
|
+
byte[] heap = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, 0, false, false, 1, false, heapShape);
|
|
148
202
|
assertNotNull(heap);
|
|
149
203
|
|
|
150
204
|
long[] slotShape = new long[4];
|
|
151
|
-
byte[] none = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, false, false, 2, true, slotShape);
|
|
205
|
+
byte[] none = PdfCanvasNative.render(service, handle, 0, 37, 23, 64, 41, 4, 0, false, false, 2, true, slotShape);
|
|
152
206
|
assertNull("a slot render returns no bytes", none);
|
|
153
207
|
assertTrue("a slot render names a slot", slotShape[0] > 0);
|
|
154
208
|
assertEquals(heapShape[1], slotShape[1]);
|
|
@@ -215,7 +269,7 @@ public class PdfCanvasNativeTest {
|
|
|
215
269
|
}
|
|
216
270
|
int handle = open(TestPdfs.quadrants(ctx(), "jni-range.pdf", 40, 40));
|
|
217
271
|
try {
|
|
218
|
-
PdfCanvasNative.render(service, handle, 7, 0, 0, 10, 10, 1, false, false, 1, false, new long[4]);
|
|
272
|
+
PdfCanvasNative.render(service, handle, 7, 0, 0, 10, 10, 1, 0, false, false, 1, false, new long[4]);
|
|
219
273
|
fail("an out-of-range page must throw");
|
|
220
274
|
} catch (PdfRasterException e) {
|
|
221
275
|
assertEquals(PdfErrorCode.NOT_FOUND, e.code());
|
|
@@ -228,24 +282,24 @@ public class PdfCanvasNativeTest {
|
|
|
228
282
|
PdfCanvasNative.registerCancellation(service, handle, 5);
|
|
229
283
|
PdfCanvasNative.cancel(service, handle, 5);
|
|
230
284
|
try {
|
|
231
|
-
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 5, false, new long[4]);
|
|
285
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, 0, false, false, 5, false, new long[4]);
|
|
232
286
|
fail("a cancelled render must throw");
|
|
233
287
|
} catch (PdfRasterException e) {
|
|
234
288
|
assertEquals(PdfErrorCode.CANCELLED, e.code());
|
|
235
289
|
}
|
|
236
290
|
// 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]));
|
|
291
|
+
assertNotNull(PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, 0, false, false, 5, false, new long[4]));
|
|
238
292
|
}
|
|
239
293
|
|
|
240
294
|
@Test
|
|
241
295
|
public void invalidateClosesEverythingAndRefusesAfterwards() throws Exception {
|
|
242
296
|
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]);
|
|
297
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, 0, false, false, 1, true, new long[4]);
|
|
244
298
|
assertEquals(1, PdfCanvasNative.slotCount());
|
|
245
299
|
PdfCanvasNative.invalidateService(service);
|
|
246
300
|
assertEquals("invalidate drains parked slots", 0, PdfCanvasNative.slotCount());
|
|
247
301
|
try {
|
|
248
|
-
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, false, false, 2, false, new long[4]);
|
|
302
|
+
PdfCanvasNative.render(service, handle, 0, 0, 0, 40, 40, 1, 0, false, false, 2, false, new long[4]);
|
|
249
303
|
fail("a render after invalidate must throw");
|
|
250
304
|
} catch (PdfRasterException e) {
|
|
251
305
|
assertEquals(PdfErrorCode.BACKEND_FAILURE, e.code());
|
|
@@ -18,9 +18,9 @@ import java.util.List;
|
|
|
18
18
|
* laptop by {@code native/tests} against the same PDFium build, with a much
|
|
19
19
|
* richer fixture set. What is left to prove ON DEVICE is that the bytes cross
|
|
20
20
|
* JNI intact and the transport's slot store hands back what PDFium wrote, and
|
|
21
|
-
*
|
|
22
|
-
* channel swap or a stride shear cannot hide)
|
|
23
|
-
* inside an annotation.
|
|
21
|
+
* three fixtures are enough for that: one whose channels are all saturated (a
|
|
22
|
+
* channel swap or a stride shear cannot hide), one whose green exists only
|
|
23
|
+
* inside an annotation, and one line of text for the search seam.
|
|
24
24
|
*/
|
|
25
25
|
final class TestPdfs {
|
|
26
26
|
|
|
@@ -124,6 +124,25 @@ final class TestPdfs {
|
|
|
124
124
|
return writeFile(ctx, name, wr.finish(catalog));
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* "Hello measured world" in unembedded 24pt Helvetica at (72, 700) on a
|
|
129
|
+
* 612x792 page — the same bytes {@code native/tests} searches.
|
|
130
|
+
*/
|
|
131
|
+
static File text(Context ctx, String name) throws IOException {
|
|
132
|
+
Writer wr = new Writer();
|
|
133
|
+
int contents = wr.add(stream("BT /F1 24 Tf 72 700 Td (Hello measured world) Tj ET\n"));
|
|
134
|
+
int font = wr.add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
|
|
135
|
+
int pagesNumber = wr.count() + 2;
|
|
136
|
+
int page = wr.add("<< /Type /Page /Parent " + pagesNumber + " 0 R /MediaBox [0 0 612 792] /Contents "
|
|
137
|
+
+ contents + " 0 R /Resources << /Font << /F1 " + font + " 0 R >> >> >>");
|
|
138
|
+
int pages = wr.add("<< /Type /Pages /Kids [" + page + " 0 R] /Count 1 >>");
|
|
139
|
+
int catalog = wr.add("<< /Type /Catalog /Pages " + pages + " 0 R >>");
|
|
140
|
+
if (pages != pagesNumber) {
|
|
141
|
+
throw new IllegalStateException("Pages object is " + pages + ", not " + pagesNumber);
|
|
142
|
+
}
|
|
143
|
+
return writeFile(ctx, name, wr.finish(catalog));
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
/** No %PDF header and no recoverable objects. */
|
|
128
147
|
static File notAPdf(Context ctx, String name) throws IOException {
|
|
129
148
|
StringBuilder sb = new StringBuilder();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `PdfError` into a `PdfRasterException` carrying the same wire code, and
|
|
14
14
|
* anything else into `backend-failure`.
|
|
15
15
|
*
|
|
16
|
-
* PLAIN JNI, NO FBJNI. The whole surface is
|
|
16
|
+
* PLAIN JNI, NO FBJNI. The whole surface is fourteen static methods on one
|
|
17
17
|
* class; fbjni would add a dependency and a registration dance to express
|
|
18
18
|
* `SetByteArrayRegion`.
|
|
19
19
|
*/
|
|
@@ -94,6 +94,21 @@ std::vector<uint8_t> toBytes(JNIEnv* env, jbyteArray value) {
|
|
|
94
94
|
return out;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/// UTF-16 straight out of the Java string, with no transcoding. NOT
|
|
98
|
+
/// `GetStringUTFChars`: that is "modified UTF-8", which encodes a
|
|
99
|
+
/// supplementary character as two three-byte surrogates and U+0000 as two
|
|
100
|
+
/// bytes, so a query containing either would reach PDFium as something else.
|
|
101
|
+
std::u16string toUtf16(JNIEnv* env, jstring value) {
|
|
102
|
+
if (value == nullptr) return {};
|
|
103
|
+
const jsize length = env->GetStringLength(value);
|
|
104
|
+
std::u16string out(static_cast<size_t>(length), u'\0');
|
|
105
|
+
if (length > 0) {
|
|
106
|
+
static_assert(sizeof(jchar) == sizeof(char16_t), "jchar is a UTF-16 unit");
|
|
107
|
+
env->GetStringRegion(value, 0, length, reinterpret_cast<jchar*>(&out[0]));
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
jbyteArray toJavaBytes(JNIEnv* env, const std::vector<uint8_t>& bytes) {
|
|
98
113
|
const jsize length = static_cast<jsize>(bytes.size());
|
|
99
114
|
jbyteArray out = env->NewByteArray(length);
|
|
@@ -172,8 +187,8 @@ JNIEXPORT void JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_registerCance
|
|
|
172
187
|
|
|
173
188
|
JNIEXPORT jbyteArray JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_render(
|
|
174
189
|
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) {
|
|
190
|
+
jdouble height, jdouble scale, jint rotation, jboolean annotations, jboolean transparent,
|
|
191
|
+
jlong token, jboolean useSlot, jlongArray out) {
|
|
177
192
|
return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray {
|
|
178
193
|
Service* s = serviceOf(service);
|
|
179
194
|
if (s == nullptr) throw PdfError(ErrorCode::BackendFailure, "The native service is gone.");
|
|
@@ -188,6 +203,8 @@ JNIEXPORT jbyteArray JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_render(
|
|
|
188
203
|
request.width = width;
|
|
189
204
|
request.height = height;
|
|
190
205
|
request.scale = scale;
|
|
206
|
+
// Degrees clockwise on top of the page's /Rotate; the core validates it.
|
|
207
|
+
request.rotation = rotation;
|
|
191
208
|
request.annotations = annotations == JNI_TRUE;
|
|
192
209
|
request.background = transparent == JNI_TRUE ? pdfcanvas::Background::Transparent
|
|
193
210
|
: pdfcanvas::Background::White;
|
|
@@ -207,6 +224,33 @@ JNIEXPORT jbyteArray JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_render(
|
|
|
207
224
|
});
|
|
208
225
|
}
|
|
209
226
|
|
|
227
|
+
JNIEXPORT jstring JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_search(
|
|
228
|
+
JNIEnv* env, jclass, jlong service, jint handle, jint page, jstring query, jboolean matchCase,
|
|
229
|
+
jboolean wholeWord, jint rotation, jlong token) {
|
|
230
|
+
return guarded<jstring>(env, nullptr, [&]() -> jstring {
|
|
231
|
+
Service* s = serviceOf(service);
|
|
232
|
+
if (s == nullptr) throw PdfError(ErrorCode::BackendFailure, "The native service is gone.");
|
|
233
|
+
|
|
234
|
+
pdfcanvas::TextSearchRequest request;
|
|
235
|
+
request.page = page;
|
|
236
|
+
request.query = toUtf16(env, query);
|
|
237
|
+
request.matchCase = matchCase == JNI_TRUE;
|
|
238
|
+
request.wholeWord = wholeWord == JNI_TRUE;
|
|
239
|
+
// Degrees clockwise, as for render; the core validates it.
|
|
240
|
+
request.rotation = rotation;
|
|
241
|
+
|
|
242
|
+
// PURE ASCII by construction (`textMatchesToJson`), which is the one input
|
|
243
|
+
// on which NewStringUTF's "modified UTF-8" and real UTF-8 agree.
|
|
244
|
+
const std::string json = s->searchJson(handle, request, token);
|
|
245
|
+
jstring out = env->NewStringUTF(json.c_str());
|
|
246
|
+
if (out == nullptr) {
|
|
247
|
+
throw PdfError(ErrorCode::OutOfMemory, "Could not allocate a Java string for " +
|
|
248
|
+
std::to_string(json.size()) + " bytes of search results.");
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
210
254
|
JNIEXPORT void JNICALL Java_tools_reekon_pdfcanvas_PdfCanvasNative_cancel(JNIEnv* env, jclass, jlong service,
|
|
211
255
|
jint handle, jlong token) {
|
|
212
256
|
guarded<int>(env, 0, [&] {
|
|
@@ -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,
|
|
@@ -129,7 +134,32 @@ public final class PdfCanvasNative {
|
|
|
129
134
|
long[] out)
|
|
130
135
|
throws PdfRasterException;
|
|
131
136
|
|
|
132
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Finds every occurrence of {@code query} on one page, on the calling thread.
|
|
139
|
+
* Register the token with {@link #registerCancellation} first, exactly as for
|
|
140
|
+
* a render: {@link #cancel} then stops the search between matches.
|
|
141
|
+
*
|
|
142
|
+
* @param query crosses JNI as UTF-16, untranscoded.
|
|
143
|
+
* @param rotation the HOST's rotation, as for {@link #render}: the rects come
|
|
144
|
+
* back in the TURNED page's doc space. The core refuses anything but 0,
|
|
145
|
+
* 90, 180 or 270.
|
|
146
|
+
* @return {@code TextMatch[]} as ONE JSON string, pure ASCII, written by the
|
|
147
|
+
* core ({@code textMatchesToJson}) — handed to JS as it is, and checked
|
|
148
|
+
* there by {@code parseTextMatches}.
|
|
149
|
+
* @throws PdfRasterException with the failure's {@link PdfErrorCode}.
|
|
150
|
+
*/
|
|
151
|
+
public static native String search(
|
|
152
|
+
long service,
|
|
153
|
+
int handle,
|
|
154
|
+
int page,
|
|
155
|
+
String query,
|
|
156
|
+
boolean matchCase,
|
|
157
|
+
boolean wholeWord,
|
|
158
|
+
int rotation,
|
|
159
|
+
long token)
|
|
160
|
+
throws PdfRasterException;
|
|
161
|
+
|
|
162
|
+
/** Sets the signal a render (or a search) polls. A pure lookup. */
|
|
133
163
|
public static native void cancel(long service, int handle, long token);
|
|
134
164
|
|
|
135
165
|
/** Closes one document. Idempotent. */
|
|
@@ -60,10 +60,10 @@ import tools.reekon.pdfcanvas.PdfRasterException;
|
|
|
60
60
|
* <h3>Threading</h3>
|
|
61
61
|
*
|
|
62
62
|
* {@code @ReactMethod} runs on the bridge's native-modules thread and a render
|
|
63
|
-
* is a long synchronous call, so every open and
|
|
64
|
-
* One, because PDFium is not thread-safe and the core serialises every
|
|
65
|
-
* behind a process-wide lock anyway; a second worker would only queue
|
|
66
|
-
* the first while holding a promise open.
|
|
63
|
+
* is a long synchronous call, so every open, render and search is handed to ONE
|
|
64
|
+
* worker. One, because PDFium is not thread-safe and the core serialises every
|
|
65
|
+
* call behind a process-wide lock anyway; a second worker would only queue
|
|
66
|
+
* behind the first while holding a promise open.
|
|
67
67
|
*
|
|
68
68
|
* <p>EVERY {@code ReadableMap} IS DRAINED ON THE CALLING THREAD, before the
|
|
69
69
|
* worker is handed anything: on the legacy bridge a {@code ReadableNativeMap} is
|
|
@@ -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();
|
|
@@ -371,11 +378,74 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
|
|
|
371
378
|
}
|
|
372
379
|
}
|
|
373
380
|
|
|
381
|
+
/* ---------------------------------------------------------------- *
|
|
382
|
+
* search
|
|
383
|
+
* ---------------------------------------------------------------- */
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Finds text on one page and resolves with the core's JSON — ONE string,
|
|
387
|
+
* however many hits, so a find pass over a dense drawing is one bridge value
|
|
388
|
+
* rather than thousands of maps. {@code native-bridge.ts} parses and checks
|
|
389
|
+
* it.
|
|
390
|
+
*
|
|
391
|
+
* <p>THE SAME WORKER AS RENDER, deliberately: PDFium is not thread-safe and
|
|
392
|
+
* the core serialises everything behind one lock anyway, and a search that
|
|
393
|
+
* jumped the queue would only wait on that lock while holding the text page
|
|
394
|
+
* a render is about to want. The same (handle, token) registry too, so
|
|
395
|
+
* {@link #cancel} stops a superseded search between matches.
|
|
396
|
+
*/
|
|
397
|
+
@ReactMethod
|
|
398
|
+
public void search(int handle, ReadableMap request, int token, Promise promise) {
|
|
399
|
+
if (!requireService(promise)) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
// Drained HERE, on the bridge thread, like render's request.
|
|
403
|
+
final int page, rotation;
|
|
404
|
+
final String query;
|
|
405
|
+
final boolean matchCase, wholeWord;
|
|
406
|
+
try {
|
|
407
|
+
page = request.getInt("page");
|
|
408
|
+
query = request.hasKey("query") && !request.isNull("query") ? request.getString("query") : "";
|
|
409
|
+
matchCase = request.hasKey("matchCase") && request.getBoolean("matchCase");
|
|
410
|
+
wholeWord = request.hasKey("wholeWord") && request.getBoolean("wholeWord");
|
|
411
|
+
// OPTIONAL, DEFAULTING TO 0, for the reason render's is.
|
|
412
|
+
rotation =
|
|
413
|
+
request.hasKey("rotation") && !request.isNull("rotation")
|
|
414
|
+
? request.getInt("rotation")
|
|
415
|
+
: 0;
|
|
416
|
+
} catch (RuntimeException e) {
|
|
417
|
+
reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Registered BEFORE the worker runs — see render.
|
|
422
|
+
PdfCanvasNative.registerCancellation(service, handle, token);
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
worker.execute(
|
|
426
|
+
() -> {
|
|
427
|
+
try {
|
|
428
|
+
promise.resolve(
|
|
429
|
+
PdfCanvasNative.search(
|
|
430
|
+
service, handle, page, query, matchCase, wholeWord, rotation, token));
|
|
431
|
+
} catch (PdfRasterException e) {
|
|
432
|
+
reject(promise, e);
|
|
433
|
+
} catch (Throwable t) {
|
|
434
|
+
reject(promise, PdfErrorCode.BACKEND_FAILURE, t);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
} catch (RejectedExecutionException e) {
|
|
438
|
+
// invalidate() shut the worker down between the register and here.
|
|
439
|
+
reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
374
443
|
/**
|
|
375
444
|
* Genuinely mid-render now, not best-effort: the core polls the signal from
|
|
376
445
|
* PDFium's progressive renderer between batches of page objects, so a
|
|
377
|
-
* superseded tile stops drawing within one batch
|
|
378
|
-
* for a token whose
|
|
446
|
+
* superseded tile stops drawing within one batch — and a search between
|
|
447
|
+
* matches. A pure lookup — a cancel for a token whose job already finished
|
|
448
|
+
* finds nothing and does nothing.
|
|
379
449
|
*/
|
|
380
450
|
@ReactMethod
|
|
381
451
|
public void cancel(int handle, int token) {
|
|
@@ -177,10 +177,12 @@ if [ -n "$CLANG" ] && [ -f "$PDFIUM/include/fpdfview.h" ]; then
|
|
|
177
177
|
if [ -n "$NM" ] && [ -f "$OUT/lib-aarch64-linux-android24.so" ]; then
|
|
178
178
|
exported=$("$NM" -D --defined-only "$OUT/lib-aarch64-linux-android24.so" 2>/dev/null \
|
|
179
179
|
| grep -c 'Java_tools_reekon_pdfcanvas_PdfCanvasNative_')
|
|
180
|
-
|
|
180
|
+
# 14 since 0.4.0 (`search`). A floor, not an equality: the prototype gate
|
|
181
|
+
# below is what pins declarations and definitions to each other.
|
|
182
|
+
if [ "${exported:-0}" -ge 14 ]; then
|
|
181
183
|
echo "PASS the linked .so exports $exported Java_tools_reekon_pdfcanvas_PdfCanvasNative_* symbols"
|
|
182
184
|
else
|
|
183
|
-
echo "FAIL the linked .so exports only ${exported:-0} JNI symbols (expected >=
|
|
185
|
+
echo "FAIL the linked .so exports only ${exported:-0} JNI symbols (expected >= 14)"; fail=1
|
|
184
186
|
fi
|
|
185
187
|
else
|
|
186
188
|
say_skip "exported JNI symbols" "no llvm-nm, or nothing linked"
|
|
@@ -249,8 +251,8 @@ if [ -n "$JAVAC" ] && [ -n "$AJAR" ] && [ -n "$CLANG" ] && [ -f "$PDFIUM/include
|
|
|
249
251
|
n=$((n + 1))
|
|
250
252
|
grep -q "${m}(" "$JNI_SRC" || missing="$missing $m"
|
|
251
253
|
done
|
|
252
|
-
if [ "$n" -lt
|
|
253
|
-
echo "FAIL the JNI gate found only $n declared native methods (expected >=
|
|
254
|
+
if [ "$n" -lt 14 ]; then
|
|
255
|
+
echo "FAIL the JNI gate found only $n declared native methods (expected >= 14)"; fail=1
|
|
254
256
|
elif [ -n "$missing" ]; then
|
|
255
257
|
echo "FAIL declared native in Java, undefined in C++:$missing"; fail=1
|
|
256
258
|
else
|
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;
|