@reekon-tools/react-native-pdf-canvas 0.1.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 +80 -0
- package/README.md +1391 -0
- package/android/build.gradle +175 -0
- package/android/consumer-rules.pro +3 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +2 -0
- package/android/gradlew +248 -0
- package/android/gradlew.bat +98 -0
- package/android/settings.gradle +34 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
- package/android/src/jsi/cpp/CMakeLists.txt +33 -0
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
- package/android/tools/compile-gate.sh +289 -0
- package/dist/cache.d.ts +161 -0
- package/dist/cache.js +389 -0
- package/dist/clock.d.ts +21 -0
- package/dist/clock.js +31 -0
- package/dist/controller.d.ts +131 -0
- package/dist/controller.js +1002 -0
- package/dist/geometry.d.ts +96 -0
- package/dist/geometry.js +248 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +50 -0
- package/dist/index.native.d.ts +21 -0
- package/dist/index.native.js +23 -0
- package/dist/index.web.d.ts +24 -0
- package/dist/index.web.js +26 -0
- package/dist/layout.d.ts +78 -0
- package/dist/layout.js +152 -0
- package/dist/peer-check.d.ts +83 -0
- package/dist/peer-check.js +183 -0
- package/dist/planner.d.ts +35 -0
- package/dist/planner.js +530 -0
- package/dist/policy.d.ts +14 -0
- package/dist/policy.js +168 -0
- package/dist/rasterizer/android.d.ts +121 -0
- package/dist/rasterizer/android.js +126 -0
- package/dist/rasterizer/buffer.d.ts +18 -0
- package/dist/rasterizer/buffer.js +18 -0
- package/dist/rasterizer/fake.d.ts +92 -0
- package/dist/rasterizer/fake.js +433 -0
- package/dist/rasterizer/index.d.ts +38 -0
- package/dist/rasterizer/index.js +69 -0
- package/dist/rasterizer/index.native.d.ts +19 -0
- package/dist/rasterizer/index.native.js +80 -0
- package/dist/rasterizer/index.web.d.ts +63 -0
- package/dist/rasterizer/index.web.js +75 -0
- package/dist/rasterizer/ingest.d.ts +180 -0
- package/dist/rasterizer/ingest.js +307 -0
- package/dist/rasterizer/ios.d.ts +84 -0
- package/dist/rasterizer/ios.js +155 -0
- package/dist/rasterizer/native-bridge.d.ts +261 -0
- package/dist/rasterizer/native-bridge.js +413 -0
- package/dist/rasterizer/web/client.d.ts +84 -0
- package/dist/rasterizer/web/client.js +359 -0
- package/dist/rasterizer/web/engine.d.ts +69 -0
- package/dist/rasterizer/web/engine.js +308 -0
- package/dist/rasterizer/web/index.d.ts +26 -0
- package/dist/rasterizer/web/index.js +21 -0
- package/dist/rasterizer/web/pdfium.d.ts +213 -0
- package/dist/rasterizer/web/pdfium.js +170 -0
- package/dist/rasterizer/web/protocol.d.ts +138 -0
- package/dist/rasterizer/web/protocol.js +37 -0
- package/dist/rasterizer/web/session.d.ts +106 -0
- package/dist/rasterizer/web/session.js +413 -0
- package/dist/rasterizer/web/worker.d.ts +44 -0
- package/dist/rasterizer/web/worker.js +76 -0
- package/dist/react/PdfContentView.d.ts +98 -0
- package/dist/react/PdfContentView.js +189 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +3 -0
- package/dist/react/usePdfDocument.d.ts +134 -0
- package/dist/react/usePdfDocument.js +275 -0
- package/dist/react/usePdfLayer.d.ts +94 -0
- package/dist/react/usePdfLayer.js +388 -0
- package/dist/skia/index.d.ts +26 -0
- package/dist/skia/index.js +25 -0
- package/dist/skia/index.native.d.ts +14 -0
- package/dist/skia/index.native.js +16 -0
- package/dist/skia/index.web.d.ts +12 -0
- package/dist/skia/index.web.js +14 -0
- package/dist/skia/resolve.native.d.ts +18 -0
- package/dist/skia/resolve.native.js +22 -0
- package/dist/skia/resolve.web.d.ts +41 -0
- package/dist/skia/resolve.web.js +62 -0
- package/dist/skia/skia-api.d.ts +94 -0
- package/dist/skia/skia-api.js +113 -0
- package/dist/testing/index.d.ts +27 -0
- package/dist/testing/index.js +28 -0
- package/dist/testing/scenes.d.ts +100 -0
- package/dist/testing/scenes.js +267 -0
- package/dist/types.d.ts +611 -0
- package/dist/types.js +21 -0
- package/dist/web-init.d.ts +68 -0
- package/dist/web-init.js +64 -0
- package/ios/Package.swift +58 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
- package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
- package/package.json +144 -0
- package/react-native.config.js +40 -0
- package/scripts/check-skia-peer.mjs +86 -0
- package/scripts/skia-peer-check.mjs +363 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The ONE component.
|
|
4
|
+
*
|
|
5
|
+
* It draws whatever `content` says, at the doc-space rects `content` carries, and
|
|
6
|
+
* it does nothing else. No gestures, no viewport, no shared values, no `<Canvas>`.
|
|
7
|
+
* It is meant to be mounted inside SOMEBODY ELSE'S `<Group transform={...}>`:
|
|
8
|
+
*
|
|
9
|
+
* <Canvas>
|
|
10
|
+
* <Group transform={worldTransform}>
|
|
11
|
+
* <PdfContentView content={content} />
|
|
12
|
+
* ...the host's own annotation layers...
|
|
13
|
+
* </Group>
|
|
14
|
+
* </Canvas>
|
|
15
|
+
*
|
|
16
|
+
* THE ONE RULE: the transform decides WHEN to rasterize, never WHAT to draw. Every
|
|
17
|
+
* number this component hands to Skia is a plain JS number read off a frozen
|
|
18
|
+
* `PdfContent`, so a pan or a zoom re-records the host's scene without this
|
|
19
|
+
* subtree changing at all.
|
|
20
|
+
*/
|
|
21
|
+
import { Children, isValidElement, useEffect, useMemo } from 'react';
|
|
22
|
+
/*
|
|
23
|
+
* LEAF IMPORTS, NOT THE BARREL — and this is load-bearing, not fussiness.
|
|
24
|
+
*
|
|
25
|
+
* `import {Group, Image, Rect, Paint} from '@shopify/react-native-skia'` is a
|
|
26
|
+
* VALUE import of a package whose barrel is
|
|
27
|
+
* `export * from "./skia"` -> `export {Skia} from "./Skia"` -> on web,
|
|
28
|
+
* `export const Skia = JsiSkApi(global.CanvasKit)`. RN Skia declares no
|
|
29
|
+
* `sideEffects: false` and no `exports` map, so no bundler may drop that
|
|
30
|
+
* evaluation, and it is taken ONCE: reach it before `LoadSkiaWeb` resolves and
|
|
31
|
+
* the api is permanently broken (see `../skia/skia-api.ts`).
|
|
32
|
+
*
|
|
33
|
+
* That one line was the last thing keeping the package ROOT from being safe to
|
|
34
|
+
* evaluate during rock-desktop's SSR pass — `PdfContentView` is on the root
|
|
35
|
+
* barrel, so naming the package named `Skia.web.js`. MEASURED with a Vite build,
|
|
36
|
+
* not inferred: a bundle of this file used to contain
|
|
37
|
+
* `JsiSkApi(global.CanvasKit)` and now does not, while a bundle of the bare
|
|
38
|
+
* specifier still does.
|
|
39
|
+
*
|
|
40
|
+
* The five modules below are the leaves the barrel would have reached anyway.
|
|
41
|
+
* Each is a handful of lines that imports nothing but `react` (the components)
|
|
42
|
+
* or nothing at all (the enums), so this costs no bundle size on either
|
|
43
|
+
* platform, and on web the module instance is IDENTICAL to the barrel's —
|
|
44
|
+
* `lib/module/index.js` -> `renderer/index.js` -> `renderer/components/index.js`
|
|
45
|
+
* is the same file graph. Legal because RN Skia publishes `lib/**` and declares
|
|
46
|
+
* no `exports` map; same standing as boldr-utils' deep import of
|
|
47
|
+
* `@shopify/react-native-skia/lib/module/web/index.js` for `WithSkiaWeb`.
|
|
48
|
+
*
|
|
49
|
+
* `GroupProps` / `SamplingOptions` stay on the bare specifier because
|
|
50
|
+
* `import type` is erased entirely and reaches no module at runtime.
|
|
51
|
+
*/
|
|
52
|
+
import { Group } from '@shopify/react-native-skia/lib/module/renderer/components/Group.js';
|
|
53
|
+
import { Paint } from '@shopify/react-native-skia/lib/module/renderer/components/Paint.js';
|
|
54
|
+
import { Image } from '@shopify/react-native-skia/lib/module/renderer/components/image/Image.js';
|
|
55
|
+
import { Rect } from '@shopify/react-native-skia/lib/module/renderer/components/shapes/Rect.js';
|
|
56
|
+
import { FilterMode, MipmapMode, } from '@shopify/react-native-skia/lib/module/skia/types/Image/Image.js';
|
|
57
|
+
/* ------------------------------------------------------------------ *
|
|
58
|
+
* Sampling
|
|
59
|
+
* ------------------------------------------------------------------ */
|
|
60
|
+
/*
|
|
61
|
+
* One frozen object per variant, created ONCE at module scope.
|
|
62
|
+
*
|
|
63
|
+
* RN Skia's `shallowEq` is REFERENCE equality on every prop, and `prepareUpdate`
|
|
64
|
+
* returns null when the props compare equal. An inline `{filter: FilterMode.Linear}`
|
|
65
|
+
* is a fresh object on every render, so every single render of every single
|
|
66
|
+
* <Image> would be treated as a real prop change and re-enter the node's update
|
|
67
|
+
* path — the exact per-frame cost this package exists to avoid.
|
|
68
|
+
*/
|
|
69
|
+
const SAMPLING_LINEAR = Object.freeze({
|
|
70
|
+
filter: FilterMode.Linear,
|
|
71
|
+
// Explicitly None. See the `sampling` JSDoc: mipmaps are unreachable here.
|
|
72
|
+
mipmap: MipmapMode.None,
|
|
73
|
+
});
|
|
74
|
+
const SAMPLING_NEAREST = Object.freeze({
|
|
75
|
+
filter: FilterMode.Nearest,
|
|
76
|
+
mipmap: MipmapMode.None,
|
|
77
|
+
});
|
|
78
|
+
/** Mitchell (B=C=1/3) — the standard "photographic" cubic, and what Skia's own
|
|
79
|
+
* `MitchellCubicSampling` uses. Frozen locally so we never hand a mutable object
|
|
80
|
+
* owned by the Skia package into a memoised prop. */
|
|
81
|
+
const SAMPLING_CUBIC = Object.freeze({ B: 1 / 3, C: 1 / 3 });
|
|
82
|
+
const SAMPLING = Object.freeze({
|
|
83
|
+
linear: SAMPLING_LINEAR,
|
|
84
|
+
nearest: SAMPLING_NEAREST,
|
|
85
|
+
cubic: SAMPLING_CUBIC,
|
|
86
|
+
});
|
|
87
|
+
const IS_DEV = typeof __DEV__ !== 'undefined'
|
|
88
|
+
? __DEV__ === true
|
|
89
|
+
: typeof process !== 'undefined' &&
|
|
90
|
+
process.env?.['NODE_ENV'] !== 'production';
|
|
91
|
+
/**
|
|
92
|
+
* Is this child RN Skia's `<Paint>`? Identity first, NAME as a fallback.
|
|
93
|
+
*
|
|
94
|
+
* The fallback is the price of the leaf import above, and it is only owed on
|
|
95
|
+
* NATIVE: Metro resolves `@shopify/react-native-skia` through its `react-native`
|
|
96
|
+
* main field to `src/index.ts`, so a host's `<Paint>` comes from `src/renderer/`
|
|
97
|
+
* while this file's comes from `lib/module/renderer/`. Two different function
|
|
98
|
+
* objects for the same component, and `===` misses. On web (and under vitest,
|
|
99
|
+
* whose shim is backed by the same `lib/module` tree) both sides resolve to the
|
|
100
|
+
* one module and identity is exact.
|
|
101
|
+
*
|
|
102
|
+
* Both halves are cheap to be wrong about: this feeds a `__DEV__` console
|
|
103
|
+
* warning about a documented misuse, so a false negative loses a warning and a
|
|
104
|
+
* false positive (a host component of the host's own that happens to be named
|
|
105
|
+
* `Paint`) prints one that names exactly what it saw. Neither changes what is
|
|
106
|
+
* drawn. That is the whole reason a name check is acceptable here and would not
|
|
107
|
+
* be anywhere the render depends on it.
|
|
108
|
+
*/
|
|
109
|
+
function isPaintNode(type) {
|
|
110
|
+
if (type === Paint) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return (typeof type === 'function' && type.name === 'Paint');
|
|
114
|
+
}
|
|
115
|
+
/* ------------------------------------------------------------------ *
|
|
116
|
+
* Component
|
|
117
|
+
* ------------------------------------------------------------------ */
|
|
118
|
+
function rasterImage(raster, sampling) {
|
|
119
|
+
const { docRect } = raster;
|
|
120
|
+
return (_jsx(Image
|
|
121
|
+
// `raster.key` is the cache key: globally unique and stable for as long as
|
|
122
|
+
// the bytes live. Using it verbatim (rather than prefixing by role) means a
|
|
123
|
+
// raster demoted from `detail` to `retiring` keeps its Skia node instead of
|
|
124
|
+
// being destroyed and recreated on the swap commit.
|
|
125
|
+
, { image: raster.image,
|
|
126
|
+
// The raster's docRect IS the rect the bytes were rendered for — never
|
|
127
|
+
// inset. See the PdfRaster invariant.
|
|
128
|
+
x: docRect.x, y: docRect.y, width: docRect.width, height: docRect.height,
|
|
129
|
+
// fit="fill" ON EVERY IMAGE, ALWAYS. RN Skia's <Image> defaults to
|
|
130
|
+
// "contain", which letterboxes whenever the bitmap's aspect ratio differs
|
|
131
|
+
// from the destination rect by even a rounding error — silently shifting
|
|
132
|
+
// the page a fraction of a point and misregistering it against every
|
|
133
|
+
// annotation layer drawn above it. "fill" makes the destination rect
|
|
134
|
+
// authoritative, which is the whole premise of doc-space rects.
|
|
135
|
+
fit: "fill", sampling: sampling }, raster.key));
|
|
136
|
+
}
|
|
137
|
+
export function PdfContentView({ content, zIndex, sampling = 'linear', paperColor = null, opacity, blendMode, children, }) {
|
|
138
|
+
const samplingOptions = SAMPLING[sampling] ?? SAMPLING_LINEAR;
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!IS_DEV) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
Children.forEach(children, child => {
|
|
144
|
+
if (isValidElement(child) && isPaintNode(child.type)) {
|
|
145
|
+
console.warn('[react-native-pdf-canvas] PdfContentView received a <Paint> child. A Paint ' +
|
|
146
|
+
'child draws each node twice (once at the declared paint, once at the ' +
|
|
147
|
+
'inherited one), and paintDeclarations.clear() runs on the first drawing — ' +
|
|
148
|
+
'so it would apply to the base raster and to none of the detail rasters. ' +
|
|
149
|
+
'Pass a ColorFilter / ImageFilter / Shader / PathEffect declaration instead, ' +
|
|
150
|
+
'or wrap PdfContentView in your own <Group>.');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}, [children]);
|
|
154
|
+
// A single flat array so React keys are not namespaced by an outer array index —
|
|
155
|
+
// otherwise reordering pages (a layout change) would remount every image node
|
|
156
|
+
// even though the rasters themselves are unchanged.
|
|
157
|
+
const nodes = useMemo(() => {
|
|
158
|
+
const out = [];
|
|
159
|
+
if (paperColor != null) {
|
|
160
|
+
for (const page of content.pages) {
|
|
161
|
+
const { pageRect } = page;
|
|
162
|
+
out.push(_jsx(Rect, { x: pageRect.x, y: pageRect.y, width: pageRect.width, height: pageRect.height, color: paperColor }, `paper:${page.page}`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const page of content.pages) {
|
|
166
|
+
// BASE first: the whole page at fit scale, in one or more pieces whose union
|
|
167
|
+
// is the page rect. It is never emptied once populated, and it is the entire
|
|
168
|
+
// anti-blank mechanism — everything above it is an optional sharpening pass
|
|
169
|
+
// over a page that is already fully painted. Within the set the order does
|
|
170
|
+
// not matter: the pieces overlap by a couple of device pixels and carry the
|
|
171
|
+
// same bytes there.
|
|
172
|
+
for (const raster of page.base) {
|
|
173
|
+
out.push(rasterImage(raster, samplingOptions));
|
|
174
|
+
}
|
|
175
|
+
// RETIRING before DETAIL. The previous detail set has to draw UNDER its
|
|
176
|
+
// replacement: it exists only to cover the region the new set has not
|
|
177
|
+
// finished painting, and drawing it over would show stale, wrongly-scaled
|
|
178
|
+
// pixels on top of correct ones.
|
|
179
|
+
for (const raster of page.retiring) {
|
|
180
|
+
out.push(rasterImage(raster, samplingOptions));
|
|
181
|
+
}
|
|
182
|
+
for (const raster of page.detail) {
|
|
183
|
+
out.push(rasterImage(raster, samplingOptions));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}, [content, paperColor, samplingOptions]);
|
|
188
|
+
return (_jsxs(Group, { zIndex: zIndex, opacity: opacity, blendMode: blendMode, children: [children, nodes] }));
|
|
189
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { PdfContentView } from './PdfContentView.js';
|
|
2
|
+
export type { PdfContentViewProps, PdfBlendMode } from './PdfContentView.js';
|
|
3
|
+
export { usePdfDocument, openPdfDocument, makeSkImageFromPixels, setDefaultRasterizer, getDefaultRasterizer, } from './usePdfDocument.js';
|
|
4
|
+
export type { PdfDocument, PdfDocumentStatus, PdfPageRender, PdfRenderOptions, UsePdfDocumentOptions, UsePdfDocumentResult, } from './usePdfDocument.js';
|
|
5
|
+
export { usePdfLayer, DEFAULT_PAGE_SELECTION } from './usePdfLayer.js';
|
|
6
|
+
export type { PdfLayer, PdfOnScreenPages, PdfPageSelection, PdfPageWindow, UsePdfLayerOptions, } from './usePdfLayer.js';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening a PDF, and rendering one-off page rasters from it.
|
|
3
|
+
*
|
|
4
|
+
* The hook mirrors RN Skia's `useImage` contract — null until ready, never
|
|
5
|
+
* throws, mount-guarded, keyed on source identity — with ONE deliberate
|
|
6
|
+
* difference: it CLOSES the document on unmount. `useImage` can get away with
|
|
7
|
+
* leaving cleanup to the JSI finaliser because an SkImage is only bytes; a PDF
|
|
8
|
+
* handle is a file descriptor (PDFium's `FPDF_LoadDocument`, Android's
|
|
9
|
+
* `ParcelFileDescriptor`, PDFKit's mapped file). Leaking those is how a document
|
|
10
|
+
* browser dies at the fd limit after a few dozen opens.
|
|
11
|
+
*
|
|
12
|
+
* `renderPage` is what makes this package generically useful rather than shaped
|
|
13
|
+
* around one screen: thumbnail strips, a print preview, a static first-page hero
|
|
14
|
+
* all want a single raster at a chosen size and none of them want an epoch, a
|
|
15
|
+
* cadence policy, or a controller.
|
|
16
|
+
*/
|
|
17
|
+
import type { SkImage } from '@shopify/react-native-skia';
|
|
18
|
+
import { PdfError } from '../types.js';
|
|
19
|
+
import type { DocRect, DocSize, PageGeometry, PageRasterizer, PdfSource, PixelSize, RasterPixels, RasterizerCapabilities, RasterizerHandle } from '../types.js';
|
|
20
|
+
/** Install the rasterizer used when `usePdfDocument` / `openPdfDocument` are not
|
|
21
|
+
* given one explicitly. Returns the previous value so a test can restore it. */
|
|
22
|
+
export declare function setDefaultRasterizer(rasterizer: PageRasterizer | null): PageRasterizer | null;
|
|
23
|
+
export declare function getDefaultRasterizer(): PageRasterizer | null;
|
|
24
|
+
/** A single raster produced by `PdfDocument.renderPage`. */
|
|
25
|
+
export interface PdfPageRender {
|
|
26
|
+
readonly page: number;
|
|
27
|
+
readonly image: SkImage;
|
|
28
|
+
/** Exactly the doc-space rect the bytes were rendered for. Never inset. */
|
|
29
|
+
readonly docRect: DocRect;
|
|
30
|
+
/** Device pixels per PDF point. */
|
|
31
|
+
readonly scale: number;
|
|
32
|
+
readonly pixels: PixelSize;
|
|
33
|
+
readonly bytes: number;
|
|
34
|
+
/**
|
|
35
|
+
* Drop the GPU/CPU image. EXPLICIT because the caller owns this raster — it is
|
|
36
|
+
* not in the layer cache and nothing else will ever free it.
|
|
37
|
+
*/
|
|
38
|
+
release(): void;
|
|
39
|
+
}
|
|
40
|
+
export interface PdfRenderOptions {
|
|
41
|
+
/** Doc-space rect to render. Defaults to the whole page at the origin. */
|
|
42
|
+
docRect?: DocRect;
|
|
43
|
+
/** Device pixels per PDF point. Ignored when `widthPx` is given. */
|
|
44
|
+
scale?: number;
|
|
45
|
+
/** Target width in device pixels; derives `scale` from `docRect.width`. */
|
|
46
|
+
widthPx?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Draw PDF-embedded annotations into the raster.
|
|
49
|
+
*
|
|
50
|
+
* Defaults to TRUE here — a file raster (thumbnail, print, hero) is expected to
|
|
51
|
+
* look like the document does in any other viewer. The layer path passes false,
|
|
52
|
+
* because the Skia scene draws the app's own annotations and they would
|
|
53
|
+
* double-draw. Silently downgraded to false if the backend cannot do it.
|
|
54
|
+
*/
|
|
55
|
+
annotations?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Not in the assignment's option list, but `RasterRequest.background` is
|
|
58
|
+
* required and defaulting it invisibly is exactly the "opaque white under a
|
|
59
|
+
* transparent PDF" bug that `paperColor` defaults to null to avoid. White is
|
|
60
|
+
* the right default for a file raster; transparent is available for callers
|
|
61
|
+
* compositing onto their own surface.
|
|
62
|
+
*/
|
|
63
|
+
background?: 'white' | 'transparent';
|
|
64
|
+
signal?: AbortSignal;
|
|
65
|
+
}
|
|
66
|
+
export interface PdfDocument {
|
|
67
|
+
readonly pageCount: number;
|
|
68
|
+
/** Identifier of the backend that produced this document, for diagnostics. */
|
|
69
|
+
readonly rasterizerId: string;
|
|
70
|
+
readonly capabilities: RasterizerCapabilities;
|
|
71
|
+
/** Cheap: does not load a page. */
|
|
72
|
+
pageGeometry(index: number): PageGeometry;
|
|
73
|
+
/** Intrinsic sizes of every page, in the order a `PdfPageLayout` expects. */
|
|
74
|
+
pageSizes(): DocSize[];
|
|
75
|
+
renderPage(index: number, options?: PdfRenderOptions): Promise<PdfPageRender>;
|
|
76
|
+
/** True once `close()` has run. Every further call rejects with `cancelled`. */
|
|
77
|
+
readonly closed: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Release the underlying file handle. `usePdfDocument` owns this for documents
|
|
80
|
+
* it opened — do not call it on a hook-owned document. Public because
|
|
81
|
+
* `openPdfDocument` is usable outside React.
|
|
82
|
+
*/
|
|
83
|
+
close(): void;
|
|
84
|
+
/**
|
|
85
|
+
* The open backend handle. Passed to `createPdfController`; not otherwise part
|
|
86
|
+
* of the supported surface.
|
|
87
|
+
*/
|
|
88
|
+
readonly handle: RasterizerHandle;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* ONE conversion for the whole package, delegated to the ingest seam.
|
|
92
|
+
*
|
|
93
|
+
* This used to be a second, simpler implementation of `imageFromPixels`, and the
|
|
94
|
+
* two disagreed on the things that are easy to get wrong and impossible to debug
|
|
95
|
+
* from a screenshot:
|
|
96
|
+
*
|
|
97
|
+
* - `RasterPixels.format` travels with the bytes rather than being assumed,
|
|
98
|
+
* because the backends genuinely differ (PDFium is BGRA unless asked
|
|
99
|
+
* otherwise, Android `ARGB_8888` is RGBA in memory, CGBitmapContext is
|
|
100
|
+
* whatever it was configured as). Getting it wrong swaps red and blue.
|
|
101
|
+
* - `Skia.Data.fromBytes` IGNORES byteOffset/byteLength on native
|
|
102
|
+
* (JsiSkDataFactory.h copies the whole ArrayBuffer), so a `subarray()` view
|
|
103
|
+
* into a shared scratch buffer ingests shifted bytes and shears. Ingest
|
|
104
|
+
* copies to a tight buffer; this function did not.
|
|
105
|
+
* - A `MakeImage` that returns null for BGRA is retried once as RGBA there,
|
|
106
|
+
* rather than losing the page.
|
|
107
|
+
*
|
|
108
|
+
* Kept as an export because it is the documented name for "pixels to SkImage"
|
|
109
|
+
* on the document surface, and `renderPage` below is its main caller.
|
|
110
|
+
*/
|
|
111
|
+
export declare function makeSkImageFromPixels(pixels: RasterPixels): SkImage;
|
|
112
|
+
/**
|
|
113
|
+
* Open a document outside React. The caller owns `close()`.
|
|
114
|
+
*/
|
|
115
|
+
export declare function openPdfDocument(source: PdfSource, rasterizer?: PageRasterizer): Promise<PdfDocument>;
|
|
116
|
+
export type PdfDocumentStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
117
|
+
export interface UsePdfDocumentResult {
|
|
118
|
+
document: PdfDocument | null;
|
|
119
|
+
status: PdfDocumentStatus;
|
|
120
|
+
error: PdfError | null;
|
|
121
|
+
}
|
|
122
|
+
export interface UsePdfDocumentOptions {
|
|
123
|
+
rasterizer?: PageRasterizer;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Open `source` and keep it open for the life of the component.
|
|
127
|
+
*
|
|
128
|
+
* KEYED ON SOURCE IDENTITY, exactly like `useImage`: the effect re-runs when the
|
|
129
|
+
* `source` REFERENCE changes, so an inline `{uri}` object literal reopens the file
|
|
130
|
+
* on every render. Memoise it, or hoist it to a module constant.
|
|
131
|
+
*
|
|
132
|
+
* Never throws. A failure lands in `error` with a `PdfErrorCode` on it.
|
|
133
|
+
*/
|
|
134
|
+
export declare function usePdfDocument(source: PdfSource | null | undefined, options?: UsePdfDocumentOptions): UsePdfDocumentResult;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening a PDF, and rendering one-off page rasters from it.
|
|
3
|
+
*
|
|
4
|
+
* The hook mirrors RN Skia's `useImage` contract — null until ready, never
|
|
5
|
+
* throws, mount-guarded, keyed on source identity — with ONE deliberate
|
|
6
|
+
* difference: it CLOSES the document on unmount. `useImage` can get away with
|
|
7
|
+
* leaving cleanup to the JSI finaliser because an SkImage is only bytes; a PDF
|
|
8
|
+
* handle is a file descriptor (PDFium's `FPDF_LoadDocument`, Android's
|
|
9
|
+
* `ParcelFileDescriptor`, PDFKit's mapped file). Leaking those is how a document
|
|
10
|
+
* browser dies at the fd limit after a few dozen opens.
|
|
11
|
+
*
|
|
12
|
+
* `renderPage` is what makes this package generically useful rather than shaped
|
|
13
|
+
* around one screen: thumbnail strips, a print preview, a static first-page hero
|
|
14
|
+
* all want a single raster at a chosen size and none of them want an epoch, a
|
|
15
|
+
* cadence policy, or a controller.
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
// Through the `./skia` barrel — see the note in `usePdfLayer.ts`.
|
|
19
|
+
import { imageFromPixels } from '../skia/index.js';
|
|
20
|
+
import { PdfError } from '../types.js';
|
|
21
|
+
/* ------------------------------------------------------------------ *
|
|
22
|
+
* Default rasterizer registry
|
|
23
|
+
* ------------------------------------------------------------------ *
|
|
24
|
+
*
|
|
25
|
+
* CONTRACT GAP, flagged rather than papered over: `PageRasterizer` is documented
|
|
26
|
+
* as "injected per document, defaulting to the platform module", but the package
|
|
27
|
+
* entry points are platform-agnostic one-line re-exports (see index.native.ts),
|
|
28
|
+
* so there is no entry-point seam to hang a platform default on, and importing a
|
|
29
|
+
* guessed module path from here would couple this file to a file it does not own.
|
|
30
|
+
*
|
|
31
|
+
* Instead the default is a settable module-level slot. The platform module (or
|
|
32
|
+
* the app, or a test) installs it once; `usePdfDocument` falls back to it when no
|
|
33
|
+
* rasterizer is passed. If a platform module later wants to register itself on
|
|
34
|
+
* import, it calls `setDefaultRasterizer` at module scope and nothing here
|
|
35
|
+
* changes.
|
|
36
|
+
*/
|
|
37
|
+
let defaultRasterizer = null;
|
|
38
|
+
/** Install the rasterizer used when `usePdfDocument` / `openPdfDocument` are not
|
|
39
|
+
* given one explicitly. Returns the previous value so a test can restore it. */
|
|
40
|
+
export function setDefaultRasterizer(rasterizer) {
|
|
41
|
+
const previous = defaultRasterizer;
|
|
42
|
+
defaultRasterizer = rasterizer;
|
|
43
|
+
return previous;
|
|
44
|
+
}
|
|
45
|
+
export function getDefaultRasterizer() {
|
|
46
|
+
return defaultRasterizer;
|
|
47
|
+
}
|
|
48
|
+
/* ------------------------------------------------------------------ *
|
|
49
|
+
* Pixels -> SkImage
|
|
50
|
+
* ------------------------------------------------------------------ */
|
|
51
|
+
/**
|
|
52
|
+
* ONE conversion for the whole package, delegated to the ingest seam.
|
|
53
|
+
*
|
|
54
|
+
* This used to be a second, simpler implementation of `imageFromPixels`, and the
|
|
55
|
+
* two disagreed on the things that are easy to get wrong and impossible to debug
|
|
56
|
+
* from a screenshot:
|
|
57
|
+
*
|
|
58
|
+
* - `RasterPixels.format` travels with the bytes rather than being assumed,
|
|
59
|
+
* because the backends genuinely differ (PDFium is BGRA unless asked
|
|
60
|
+
* otherwise, Android `ARGB_8888` is RGBA in memory, CGBitmapContext is
|
|
61
|
+
* whatever it was configured as). Getting it wrong swaps red and blue.
|
|
62
|
+
* - `Skia.Data.fromBytes` IGNORES byteOffset/byteLength on native
|
|
63
|
+
* (JsiSkDataFactory.h copies the whole ArrayBuffer), so a `subarray()` view
|
|
64
|
+
* into a shared scratch buffer ingests shifted bytes and shears. Ingest
|
|
65
|
+
* copies to a tight buffer; this function did not.
|
|
66
|
+
* - A `MakeImage` that returns null for BGRA is retried once as RGBA there,
|
|
67
|
+
* rather than losing the page.
|
|
68
|
+
*
|
|
69
|
+
* Kept as an export because it is the documented name for "pixels to SkImage"
|
|
70
|
+
* on the document surface, and `renderPage` below is its main caller.
|
|
71
|
+
*/
|
|
72
|
+
export function makeSkImageFromPixels(pixels) {
|
|
73
|
+
return imageFromPixels(pixels);
|
|
74
|
+
}
|
|
75
|
+
/* ------------------------------------------------------------------ *
|
|
76
|
+
* openPdfDocument
|
|
77
|
+
* ------------------------------------------------------------------ */
|
|
78
|
+
/**
|
|
79
|
+
* Read the abort flag THROUGH A CALL, on both sides of the render.
|
|
80
|
+
*
|
|
81
|
+
* `if (signal.aborted === true)` narrows `signal.aborted` to `false | undefined`
|
|
82
|
+
* for the rest of the function, and TypeScript keeps that narrowing across an
|
|
83
|
+
* `await` — it has no way to know that an AbortController on the other side of
|
|
84
|
+
* the boundary can flip the flag while the render is in flight. The post-render
|
|
85
|
+
* check would therefore be reported as dead code (TS2367) while being the one
|
|
86
|
+
* that actually matters: it is what stops a raster landing for a viewport, or a
|
|
87
|
+
* component, that is already gone. A function call produces no narrowing, so both
|
|
88
|
+
* checks stay honest.
|
|
89
|
+
*/
|
|
90
|
+
function isAborted(signal) {
|
|
91
|
+
return signal?.aborted === true;
|
|
92
|
+
}
|
|
93
|
+
function toPdfError(cause, fallback) {
|
|
94
|
+
if (cause instanceof PdfError) {
|
|
95
|
+
return cause;
|
|
96
|
+
}
|
|
97
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
98
|
+
return new PdfError('backend-failure', `${fallback}: ${message}`, { cause });
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Open a document outside React. The caller owns `close()`.
|
|
102
|
+
*/
|
|
103
|
+
export async function openPdfDocument(source, rasterizer) {
|
|
104
|
+
const backend = rasterizer ?? defaultRasterizer;
|
|
105
|
+
if (backend === null || backend === undefined) {
|
|
106
|
+
throw new PdfError('unsupported', 'No PageRasterizer available. Pass one explicitly, or install a platform default with setDefaultRasterizer().');
|
|
107
|
+
}
|
|
108
|
+
let handle;
|
|
109
|
+
try {
|
|
110
|
+
handle = await backend.open(source);
|
|
111
|
+
}
|
|
112
|
+
catch (cause) {
|
|
113
|
+
throw toPdfError(cause, 'Failed to open PDF');
|
|
114
|
+
}
|
|
115
|
+
let closed = false;
|
|
116
|
+
const doc = {
|
|
117
|
+
handle,
|
|
118
|
+
pageCount: handle.pageCount,
|
|
119
|
+
rasterizerId: backend.id,
|
|
120
|
+
capabilities: backend.capabilities,
|
|
121
|
+
get closed() {
|
|
122
|
+
return closed;
|
|
123
|
+
},
|
|
124
|
+
pageGeometry(index) {
|
|
125
|
+
return handle.pageGeometry(index);
|
|
126
|
+
},
|
|
127
|
+
pageSizes() {
|
|
128
|
+
const sizes = [];
|
|
129
|
+
for (let i = 0; i < handle.pageCount; i++) {
|
|
130
|
+
const geometry = handle.pageGeometry(i);
|
|
131
|
+
sizes.push({ width: geometry.width, height: geometry.height });
|
|
132
|
+
}
|
|
133
|
+
return sizes;
|
|
134
|
+
},
|
|
135
|
+
async renderPage(index, options) {
|
|
136
|
+
if (closed) {
|
|
137
|
+
throw new PdfError('cancelled', 'renderPage called on a closed PdfDocument');
|
|
138
|
+
}
|
|
139
|
+
if (!Number.isInteger(index) || index < 0 || index >= handle.pageCount) {
|
|
140
|
+
// No 'invalid-argument' code exists in PdfErrorCode; 'backend-failure' is
|
|
141
|
+
// the closest honest bucket. Flagged in the report.
|
|
142
|
+
throw new PdfError('backend-failure', `page index ${index} is out of range (pageCount ${handle.pageCount})`);
|
|
143
|
+
}
|
|
144
|
+
if (isAborted(options?.signal)) {
|
|
145
|
+
throw new PdfError('cancelled', 'renderPage aborted before it started');
|
|
146
|
+
}
|
|
147
|
+
const geometry = handle.pageGeometry(index);
|
|
148
|
+
const docRect = options?.docRect ?? {
|
|
149
|
+
x: 0,
|
|
150
|
+
y: 0,
|
|
151
|
+
width: geometry.width,
|
|
152
|
+
height: geometry.height,
|
|
153
|
+
};
|
|
154
|
+
if (!(docRect.width > 0) || !(docRect.height > 0)) {
|
|
155
|
+
throw new PdfError('backend-failure', `renderPage needs a positive docRect, got ${docRect.width}x${docRect.height}`);
|
|
156
|
+
}
|
|
157
|
+
const widthPx = options?.widthPx;
|
|
158
|
+
const scale = widthPx !== undefined && widthPx > 0
|
|
159
|
+
? widthPx / docRect.width
|
|
160
|
+
: (options?.scale ?? 1);
|
|
161
|
+
if (!(scale > 0) || !Number.isFinite(scale)) {
|
|
162
|
+
throw new PdfError('backend-failure', `renderPage needs a positive scale, got ${scale}`);
|
|
163
|
+
}
|
|
164
|
+
let pixels;
|
|
165
|
+
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);
|
|
176
|
+
}
|
|
177
|
+
catch (cause) {
|
|
178
|
+
throw toPdfError(cause, `Failed to render page ${index}`);
|
|
179
|
+
}
|
|
180
|
+
// The handle may have been closed while the render was in flight; the image
|
|
181
|
+
// itself is fine (the bytes are already copied out) but the caller almost
|
|
182
|
+
// certainly no longer wants it.
|
|
183
|
+
if (isAborted(options?.signal)) {
|
|
184
|
+
throw new PdfError('cancelled', 'renderPage aborted');
|
|
185
|
+
}
|
|
186
|
+
const image = makeSkImageFromPixels(pixels);
|
|
187
|
+
let released = false;
|
|
188
|
+
return {
|
|
189
|
+
page: index,
|
|
190
|
+
image,
|
|
191
|
+
docRect,
|
|
192
|
+
scale,
|
|
193
|
+
pixels: { width: pixels.width, height: pixels.height },
|
|
194
|
+
bytes: pixels.rowBytes * pixels.height,
|
|
195
|
+
release() {
|
|
196
|
+
if (released) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
released = true;
|
|
200
|
+
image.dispose();
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
close() {
|
|
205
|
+
if (closed) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
closed = true;
|
|
209
|
+
handle.close();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
return doc;
|
|
213
|
+
}
|
|
214
|
+
const IDLE = Object.freeze({
|
|
215
|
+
document: null,
|
|
216
|
+
status: 'idle',
|
|
217
|
+
error: null,
|
|
218
|
+
});
|
|
219
|
+
const LOADING = Object.freeze({
|
|
220
|
+
document: null,
|
|
221
|
+
status: 'loading',
|
|
222
|
+
error: null,
|
|
223
|
+
});
|
|
224
|
+
/**
|
|
225
|
+
* Open `source` and keep it open for the life of the component.
|
|
226
|
+
*
|
|
227
|
+
* KEYED ON SOURCE IDENTITY, exactly like `useImage`: the effect re-runs when the
|
|
228
|
+
* `source` REFERENCE changes, so an inline `{uri}` object literal reopens the file
|
|
229
|
+
* on every render. Memoise it, or hoist it to a module constant.
|
|
230
|
+
*
|
|
231
|
+
* Never throws. A failure lands in `error` with a `PdfErrorCode` on it.
|
|
232
|
+
*/
|
|
233
|
+
export function usePdfDocument(source, options) {
|
|
234
|
+
const rasterizer = options?.rasterizer;
|
|
235
|
+
const [state, setState] = useState(IDLE);
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (source === null || source === undefined) {
|
|
238
|
+
setState(IDLE);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Mount guard: `cancelled` covers both unmount and a source change landing
|
|
242
|
+
// before the open resolves. Without it a slow open on a discarded source
|
|
243
|
+
// overwrites the state of the current one.
|
|
244
|
+
let cancelled = false;
|
|
245
|
+
let opened = null;
|
|
246
|
+
setState(LOADING);
|
|
247
|
+
openPdfDocument(source, rasterizer).then(document => {
|
|
248
|
+
if (cancelled) {
|
|
249
|
+
// We own this handle and nobody will ever see it — close it here or the
|
|
250
|
+
// fd leaks for the lifetime of the process.
|
|
251
|
+
document.close();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
opened = document;
|
|
255
|
+
setState({ document, status: 'ready', error: null });
|
|
256
|
+
}, (cause) => {
|
|
257
|
+
if (cancelled) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
setState({
|
|
261
|
+
document: null,
|
|
262
|
+
status: 'error',
|
|
263
|
+
error: toPdfError(cause, 'Failed to open PDF'),
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
return () => {
|
|
267
|
+
cancelled = true;
|
|
268
|
+
// THE DIFFERENCE FROM useImage: a PDF handle is a file descriptor, not just
|
|
269
|
+
// bytes, so unmount has to close it rather than leaving it to the finaliser.
|
|
270
|
+
opened?.close();
|
|
271
|
+
opened = null;
|
|
272
|
+
};
|
|
273
|
+
}, [source, rasterizer]);
|
|
274
|
+
return state;
|
|
275
|
+
}
|