@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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web default rasterizer: PDFium in a module worker.
|
|
3
|
+
*
|
|
4
|
+
* ------------------------------------------------------------------------
|
|
5
|
+
* WHY THIS NEEDS A CALL AND ANDROID DOES NOT
|
|
6
|
+
* ------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* `index.native.ts` can resolve its backend by itself, because a native module
|
|
9
|
+
* either is in the binary or is not and `NativeModules.PdfCanvas` answers that
|
|
10
|
+
* question with no help from anyone. On web the equivalent question — "how do I
|
|
11
|
+
* construct a worker, and where is `pdfium.wasm`" — has no answer this package
|
|
12
|
+
* can discover. It depends entirely on the host's bundler, its `base`, its asset
|
|
13
|
+
* pipeline and its CSP, and every scheme a library might guess at
|
|
14
|
+
* (`new URL('./worker.js', import.meta.url)`, a CDN default, a same-origin path)
|
|
15
|
+
* fails somewhere in production rather than at build time.
|
|
16
|
+
*
|
|
17
|
+
* So the host says it once, and this module holds the answer:
|
|
18
|
+
*
|
|
19
|
+
* import PdfCanvasWorker from
|
|
20
|
+
* '@reekon-tools/react-native-pdf-canvas/worker?worker';
|
|
21
|
+
* import wasmUrl from '@embedpdf/pdfium/pdfium.wasm?url';
|
|
22
|
+
* import {configureWebPdfium} from
|
|
23
|
+
* '@reekon-tools/react-native-pdf-canvas/rasterizer';
|
|
24
|
+
* import {setDefaultRasterizer} from '@reekon-tools/react-native-pdf-canvas';
|
|
25
|
+
*
|
|
26
|
+
* setDefaultRasterizer(
|
|
27
|
+
* configureWebPdfium({
|
|
28
|
+
* createWorker: () => new PdfCanvasWorker(),
|
|
29
|
+
* wasm: {url: wasmUrl},
|
|
30
|
+
* }),
|
|
31
|
+
* );
|
|
32
|
+
*
|
|
33
|
+
* The second call is separate on purpose: `setDefaultRasterizer` lives on the
|
|
34
|
+
* React side (`react/usePdfDocument.ts`), and reaching it from here would put
|
|
35
|
+
* React into the bundle of anyone who imports the backend registry.
|
|
36
|
+
*
|
|
37
|
+
* Until it is configured, `getDefaultRasterizer()` returns the self-describing
|
|
38
|
+
* placeholder that fails at `open()` — the same shape as before, so the pure
|
|
39
|
+
* core, the Skia ingest path and the fake backend all keep working in a browser
|
|
40
|
+
* and in vitest.
|
|
41
|
+
*/
|
|
42
|
+
import type { WebRasterizerOptions } from './web/index.js';
|
|
43
|
+
import type { PageRasterizer } from '../types.js';
|
|
44
|
+
export * from './index.js';
|
|
45
|
+
export * from './web/index.js';
|
|
46
|
+
/**
|
|
47
|
+
* Install the web backend and return it.
|
|
48
|
+
*
|
|
49
|
+
* IDENTITY IS STABLE PER CONFIGURATION, which is what
|
|
50
|
+
* `getDefaultRasterizer`'s contract requires — a rasterizer identity change is
|
|
51
|
+
* a reason to reopen every document, so the default must not be a fresh object
|
|
52
|
+
* per call. Calling this a second time with different options DOES change the
|
|
53
|
+
* identity, and that is correct: the backend genuinely changed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function configureWebPdfium(options: WebRasterizerOptions): PageRasterizer;
|
|
56
|
+
/** Forget the configuration. Exists for tests; harmless anywhere. */
|
|
57
|
+
export declare function resetWebPdfium(): void;
|
|
58
|
+
/**
|
|
59
|
+
* See the note in `index.native.ts`: `react/usePdfDocument.ts` exports a
|
|
60
|
+
* registry accessor of the same name and a different meaning, so the root index
|
|
61
|
+
* must not `export *` from this module.
|
|
62
|
+
*/
|
|
63
|
+
export declare const getDefaultRasterizer: () => PageRasterizer;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web default rasterizer: PDFium in a module worker.
|
|
3
|
+
*
|
|
4
|
+
* ------------------------------------------------------------------------
|
|
5
|
+
* WHY THIS NEEDS A CALL AND ANDROID DOES NOT
|
|
6
|
+
* ------------------------------------------------------------------------
|
|
7
|
+
*
|
|
8
|
+
* `index.native.ts` can resolve its backend by itself, because a native module
|
|
9
|
+
* either is in the binary or is not and `NativeModules.PdfCanvas` answers that
|
|
10
|
+
* question with no help from anyone. On web the equivalent question — "how do I
|
|
11
|
+
* construct a worker, and where is `pdfium.wasm`" — has no answer this package
|
|
12
|
+
* can discover. It depends entirely on the host's bundler, its `base`, its asset
|
|
13
|
+
* pipeline and its CSP, and every scheme a library might guess at
|
|
14
|
+
* (`new URL('./worker.js', import.meta.url)`, a CDN default, a same-origin path)
|
|
15
|
+
* fails somewhere in production rather than at build time.
|
|
16
|
+
*
|
|
17
|
+
* So the host says it once, and this module holds the answer:
|
|
18
|
+
*
|
|
19
|
+
* import PdfCanvasWorker from
|
|
20
|
+
* '@reekon-tools/react-native-pdf-canvas/worker?worker';
|
|
21
|
+
* import wasmUrl from '@embedpdf/pdfium/pdfium.wasm?url';
|
|
22
|
+
* import {configureWebPdfium} from
|
|
23
|
+
* '@reekon-tools/react-native-pdf-canvas/rasterizer';
|
|
24
|
+
* import {setDefaultRasterizer} from '@reekon-tools/react-native-pdf-canvas';
|
|
25
|
+
*
|
|
26
|
+
* setDefaultRasterizer(
|
|
27
|
+
* configureWebPdfium({
|
|
28
|
+
* createWorker: () => new PdfCanvasWorker(),
|
|
29
|
+
* wasm: {url: wasmUrl},
|
|
30
|
+
* }),
|
|
31
|
+
* );
|
|
32
|
+
*
|
|
33
|
+
* The second call is separate on purpose: `setDefaultRasterizer` lives on the
|
|
34
|
+
* React side (`react/usePdfDocument.ts`), and reaching it from here would put
|
|
35
|
+
* React into the bundle of anyone who imports the backend registry.
|
|
36
|
+
*
|
|
37
|
+
* Until it is configured, `getDefaultRasterizer()` returns the self-describing
|
|
38
|
+
* placeholder that fails at `open()` — the same shape as before, so the pure
|
|
39
|
+
* core, the Skia ingest path and the fake backend all keep working in a browser
|
|
40
|
+
* and in vitest.
|
|
41
|
+
*/
|
|
42
|
+
import { createMissingBackendRasterizer } from './index.js';
|
|
43
|
+
import { createWebRasterizer } from './web/index.js';
|
|
44
|
+
export * from './index.js';
|
|
45
|
+
export * from './web/index.js';
|
|
46
|
+
const MISSING = createMissingBackendRasterizer('missing-web', 'The web PDFium rasterizer has not been configured: call ' +
|
|
47
|
+
'`configureWebPdfium({createWorker, wasm})` once at startup, handing it a ' +
|
|
48
|
+
'factory for a MODULE worker built from ' +
|
|
49
|
+
"'@reekon-tools/react-native-pdf-canvas/worker' and a locator for " +
|
|
50
|
+
"'@embedpdf/pdfium/pdfium.wasm'. This package constructs neither by " +
|
|
51
|
+
'itself, because both depend on your bundler.');
|
|
52
|
+
let configured = null;
|
|
53
|
+
/**
|
|
54
|
+
* Install the web backend and return it.
|
|
55
|
+
*
|
|
56
|
+
* IDENTITY IS STABLE PER CONFIGURATION, which is what
|
|
57
|
+
* `getDefaultRasterizer`'s contract requires — a rasterizer identity change is
|
|
58
|
+
* a reason to reopen every document, so the default must not be a fresh object
|
|
59
|
+
* per call. Calling this a second time with different options DOES change the
|
|
60
|
+
* identity, and that is correct: the backend genuinely changed.
|
|
61
|
+
*/
|
|
62
|
+
export function configureWebPdfium(options) {
|
|
63
|
+
configured = createWebRasterizer(options);
|
|
64
|
+
return configured;
|
|
65
|
+
}
|
|
66
|
+
/** Forget the configuration. Exists for tests; harmless anywhere. */
|
|
67
|
+
export function resetWebPdfium() {
|
|
68
|
+
configured = null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* See the note in `index.native.ts`: `react/usePdfDocument.ts` exports a
|
|
72
|
+
* registry accessor of the same name and a different meaning, so the root index
|
|
73
|
+
* must not `export *` from this module.
|
|
74
|
+
*/
|
|
75
|
+
export const getDefaultRasterizer = () => configured ?? MISSING;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RasterPixels -> SkImage.
|
|
3
|
+
*
|
|
4
|
+
* This is the only module in the package that CALLS the Skia runtime — the
|
|
5
|
+
* planner, the cadence policy and the fake backend are all pure, which is what
|
|
6
|
+
* keeps them testable in vitest with no native modules loaded. The two resolver
|
|
7
|
+
* files under `../skia/` are the only others that name it at all, and neither
|
|
8
|
+
* calls anything: they hand this file a way to find it. All three are reachable
|
|
9
|
+
* only from the `./skia` subpath.
|
|
10
|
+
*
|
|
11
|
+
* ------------------------------------------------------------------------
|
|
12
|
+
* PLATFORM SPLIT — how many copies of the pixels exist once ingest returns.
|
|
13
|
+
* ------------------------------------------------------------------------
|
|
14
|
+
*
|
|
15
|
+
* NATIVE: `Skia.Data.fromBytes` is `SkData::MakeWithCopy(...)`
|
|
16
|
+
* (cpp/api/JsiSkDataFactory.h) — one copy — and `Skia.Image.MakeImage` is
|
|
17
|
+
* `SkImages::RasterFromData(info, data, bytesPerRow)`
|
|
18
|
+
* (cpp/api/JsiSkImageFactory.h), which SHARES that SkData rather than copying
|
|
19
|
+
* it again. Ingest therefore costs exactly ONE copy, and the backend's own
|
|
20
|
+
* buffer is collectable the moment JS drops its reference.
|
|
21
|
+
*
|
|
22
|
+
* WEB: `Skia.Data.fromBytes` only wraps the Uint8Array
|
|
23
|
+
* (skia/web/JsiSkDataFactory.ts -> `new JsiSkData(CanvasKit, bytes)`), and
|
|
24
|
+
* `CanvasKit.MakeImage` is the copy: "we will always make a copy of the pixel
|
|
25
|
+
* data, because of inconsistencies in behavior between GPU and CPU". Also one
|
|
26
|
+
* copy — but into the WASM heap, which grows ONE WAY. Emscripten never returns
|
|
27
|
+
* pages to the OS, so it is PEAK simultaneous raster bytes, not average, that
|
|
28
|
+
* sets the tab's memory floor for the rest of the session.
|
|
29
|
+
*
|
|
30
|
+
* That is why BOTH layers are tiled — base and detail alike — to
|
|
31
|
+
* `RasterPolicy.maxPieceDimension`: a grid of small overlapping pieces holds the
|
|
32
|
+
* same RESIDENT total as one big raster but has a far smaller in-flight peak,
|
|
33
|
+
* and on web the peak is the part that is permanent. It is also why RETIRING is
|
|
34
|
+
* released on the next commit rather than kept "just in case".
|
|
35
|
+
*
|
|
36
|
+
* ------------------------------------------------------------------------
|
|
37
|
+
* WHITE FILL IS THE BACKEND'S JOB, and it is not optional.
|
|
38
|
+
* ------------------------------------------------------------------------
|
|
39
|
+
*
|
|
40
|
+
* Neither PDFium's FPDF_RenderPageBitmap nor Android's PdfRenderer.render
|
|
41
|
+
* clears the destination first, and a freshly allocated RGBA buffer is
|
|
42
|
+
* transparent black. Composited under the app's annotation layers that reads as
|
|
43
|
+
* a BLACK page, not a blank one. Every backend must fill the requested
|
|
44
|
+
* background before drawing. Ingest cannot repair it afterwards: it has no way
|
|
45
|
+
* to tell "the PDF painted black here" from "nothing painted here".
|
|
46
|
+
*
|
|
47
|
+
* ------------------------------------------------------------------------
|
|
48
|
+
* NOTHING HERE MAY NAME `Skia` AT MODULE SCOPE.
|
|
49
|
+
* ------------------------------------------------------------------------
|
|
50
|
+
*
|
|
51
|
+
* This file used to open with
|
|
52
|
+
* `import {AlphaType, ColorType, Skia} from '@shopify/react-native-skia'`, and
|
|
53
|
+
* that single line decided the fate of the whole package on web: it is a VALUE
|
|
54
|
+
* import of the bare specifier, whose barrel evaluates
|
|
55
|
+
* `Skia.web.js` = `JsiSkApi(global.CanvasKit)` — once, permanently, possibly
|
|
56
|
+
* before CanvasKit exists. See `../skia/skia-api.ts` for the full account.
|
|
57
|
+
*
|
|
58
|
+
* Two consequences that must both hold, and are asserted by
|
|
59
|
+
* `__tests__/module-ordering.test.ts`:
|
|
60
|
+
*
|
|
61
|
+
* - `Skia` is reached through `skiaApi()`, called INSIDE `makeImage`. Not
|
|
62
|
+
* stored in a module-level const, not read in `imageFromPixels`'s preamble.
|
|
63
|
+
* - `AlphaType` / `ColorType` come from their own leaf modules under
|
|
64
|
+
* `skia/types/Image/`, not from the barrel. They are plain numeric enum
|
|
65
|
+
* objects with no imports of their own, but reaching them through
|
|
66
|
+
* `@shopify/react-native-skia` would have dragged `Skia.web.js` in exactly as
|
|
67
|
+
* the `Skia` import did — MEASURED by bundling, since nothing about the two
|
|
68
|
+
* import statements looks different.
|
|
69
|
+
*/
|
|
70
|
+
import { ColorType } from '@shopify/react-native-skia/lib/module/skia/types/Image/ColorType.js';
|
|
71
|
+
import { AlphaType } from '@shopify/react-native-skia/lib/module/skia/types/Image/ImageFactory.js';
|
|
72
|
+
import type { SkImage } from '@shopify/react-native-skia';
|
|
73
|
+
import type { AlphaEncoding, PdfRaster, PixelFormat, RasterPixels } from '../types.js';
|
|
74
|
+
import type { RasterIngestMeta } from '../controller.js';
|
|
75
|
+
export declare const BYTES_PER_PIXEL = 4;
|
|
76
|
+
/**
|
|
77
|
+
* The two ColorType members that survive BOTH backends.
|
|
78
|
+
*
|
|
79
|
+
* RN Skia declares 19 ColorType members, and most of them throw on web:
|
|
80
|
+
* JsiSkImageFactory.MakeImage runs the value through
|
|
81
|
+
* `getEnum(CanvasKit, 'ColorType', v)`, which scans CanvasKit's embind enum for
|
|
82
|
+
* a matching numeric value and throws `Enum ColorType does not have value N`
|
|
83
|
+
* when it finds none. CanvasKit exposes only Alpha_8, RGB_565, RGBA_8888,
|
|
84
|
+
* BGRA_8888, RGBA_1010102, RGB_101010x, Gray_8, RGBA_F16 and RGBA_F32 — so
|
|
85
|
+
* Unknown, ARGB_4444, RGB_888x, BGRA_1010102 and every XR/F16Norm member is a
|
|
86
|
+
* declared-but-fatal option. `PixelFormat` deliberately reaches only the two
|
|
87
|
+
* 8888 members, and both are present in CanvasKit with matching numeric values
|
|
88
|
+
* (RN Skia's enum mirrors SkColorType's ordering).
|
|
89
|
+
*/
|
|
90
|
+
export declare const colorTypeFor: (format: PixelFormat) => ColorType;
|
|
91
|
+
/**
|
|
92
|
+
* The backend's declared alpha encoding -> Skia's tag. NOT A CONSTANT.
|
|
93
|
+
*
|
|
94
|
+
* ------------------------------------------------------------------------
|
|
95
|
+
* WHY THIS IS PER-RASTER AND NOT A MODULE-SCOPE `AlphaType.Premul`.
|
|
96
|
+
* ------------------------------------------------------------------------
|
|
97
|
+
*
|
|
98
|
+
* It was one, and it was wrong for two of the three engines this package knows
|
|
99
|
+
* about. MEASURED: an antialiased opaque-white edge over a transparent page
|
|
100
|
+
* reads `[255, 255, 255, 157]` from PDFium and `[255, 255, 255, 160]` from
|
|
101
|
+
* pdf.js — STRAIGHT alpha. Premultiplied would have been `~[157, 157, 157, 157]`.
|
|
102
|
+
* Android's `Bitmap` ARGB_8888 genuinely is premultiplied, so no single tag is
|
|
103
|
+
* right for all of them and `RasterPixels.alpha` carries the answer, exactly as
|
|
104
|
+
* `format` already carries the byte order.
|
|
105
|
+
*
|
|
106
|
+
* Tagging straight bytes Premul does not error — Skia takes the tag at its word
|
|
107
|
+
* and composites the un-scaled channels, so every partially-transparent pixel
|
|
108
|
+
* comes out too BRIGHT (and an impossible value, channel > alpha, is clamped in
|
|
109
|
+
* whatever way the backend feels like). Invisible under `background: 'white'`,
|
|
110
|
+
* where every pixel is opaque and the two encodings are byte-identical.
|
|
111
|
+
*
|
|
112
|
+
* ------------------------------------------------------------------------
|
|
113
|
+
* AND WHY NEITHER OPTION IS `AlphaType.Opaque`.
|
|
114
|
+
* ------------------------------------------------------------------------
|
|
115
|
+
*
|
|
116
|
+
* Skia treats an Opaque-tagged buffer as licence to ignore the alpha channel
|
|
117
|
+
* entirely, so a `background: 'transparent'` raster tagged Opaque paints its
|
|
118
|
+
* cleared area as opaque BLACK rather than letting the canvas beneath show
|
|
119
|
+
* through. That claim is pinned by a pixel test ('alpha type' in
|
|
120
|
+
* composition.test.ts) rather than left as a comment. A fully-opaque raster is a
|
|
121
|
+
* valid buffer under BOTH of the tags below, so nothing is gained by reaching
|
|
122
|
+
* for Opaque even when the backend happens to have filled every pixel.
|
|
123
|
+
*
|
|
124
|
+
* ------------------------------------------------------------------------
|
|
125
|
+
* WEB ENUM TRAP — both of these are safe, and it was checked, not assumed.
|
|
126
|
+
* ------------------------------------------------------------------------
|
|
127
|
+
*
|
|
128
|
+
* Same trap as `colorTypeFor`: web's `MakeImage` runs the value through
|
|
129
|
+
* `getEnum(CanvasKit, 'AlphaType', v)`, which throws when CanvasKit's embind
|
|
130
|
+
* enum has no member with that numeric value. VERIFIED against canvaskit-wasm
|
|
131
|
+
* 0.41.0 (the version pinned across the whole supported RN Skia range):
|
|
132
|
+
* `CanvasKit.AlphaType` exposes exactly `Opaque = 1`, `Premul = 2`,
|
|
133
|
+
* `Unpremul = 3`, and RN Skia declares `Unknown = 0, Opaque = 1, Premul = 2,
|
|
134
|
+
* Unpremul = 3` — so both members reached below have matching numeric values and
|
|
135
|
+
* neither throws. `AlphaType.Unknown` has no CanvasKit counterpart at all and is
|
|
136
|
+
* a declared-but-fatal option on web, which is the other reason this maps a
|
|
137
|
+
* closed two-member union rather than passing an `AlphaType` through.
|
|
138
|
+
*/
|
|
139
|
+
export declare const alphaTypeFor: (alpha: AlphaEncoding) => AlphaType;
|
|
140
|
+
/**
|
|
141
|
+
* Device-memory footprint of a MOUNTED raster, for the cache budget — both
|
|
142
|
+
* copies (see `RESIDENT_COPIES`), not just the decoded pixels.
|
|
143
|
+
*
|
|
144
|
+
* `cacheBudgetBytes` is deliberately UNCHANGED, so its meaning tightens: the same
|
|
145
|
+
* 96 MiB now covers half as many pixels. That is the point — it was always
|
|
146
|
+
* covering half as many, it just did not know.
|
|
147
|
+
*
|
|
148
|
+
* THE ONE PLACE THE FACTOR IS WRITTEN. Every other site derives from this
|
|
149
|
+
* function — including the controller suite's `ingest` stub, which restated
|
|
150
|
+
* `* 4 * 2` by hand and could therefore agree with itself while disagreeing with
|
|
151
|
+
* production. The single independent statement of the number lives in
|
|
152
|
+
* `composition.test.ts` as a literal, which is what stops this from asserting
|
|
153
|
+
* `rasterByteLength === rasterByteLength`.
|
|
154
|
+
*/
|
|
155
|
+
export declare const rasterByteLength: (width: number, height: number) => number;
|
|
156
|
+
/**
|
|
157
|
+
* Turn a backend's pixels into an SkImage, or throw a PdfError.
|
|
158
|
+
*
|
|
159
|
+
* Throws rather than returning null: a missing image at this point is a bug in
|
|
160
|
+
* a backend or an out-of-memory condition, and the caller (the epoch builder)
|
|
161
|
+
* wants it as a diagnostic against a raster key, not as a silently blank tile.
|
|
162
|
+
*/
|
|
163
|
+
export declare function imageFromPixels(pixels: RasterPixels): SkImage;
|
|
164
|
+
/**
|
|
165
|
+
* The controller's `ingest` seam, implemented.
|
|
166
|
+
*
|
|
167
|
+
* `PdfControllerOptions.ingest` is injected precisely so the controller can run
|
|
168
|
+
* under plain node in vitest; this is the real one, and it is drop-in:
|
|
169
|
+
* `createPdfController({..., ingest: ingestRaster})`.
|
|
170
|
+
*
|
|
171
|
+
* `docRect` and `scale` are copied from the META, not re-derived from the
|
|
172
|
+
* pixels. That is THE ONE RULE in miniature — the bytes are whatever the
|
|
173
|
+
* backend produced, but the rect they are drawn at is the rect they were
|
|
174
|
+
* requested for, and rounding the pixel size must never move it.
|
|
175
|
+
*
|
|
176
|
+
* `bytes` is the RESIDENT footprint of the image (see `rasterByteLength`), not
|
|
177
|
+
* the source buffer's length: after ingest the source buffer is collectable and
|
|
178
|
+
* what the cache budget is accounting for is the image — both copies of it.
|
|
179
|
+
*/
|
|
180
|
+
export declare function ingestRaster(pixels: RasterPixels, meta: RasterIngestMeta): PdfRaster;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RasterPixels -> SkImage.
|
|
3
|
+
*
|
|
4
|
+
* This is the only module in the package that CALLS the Skia runtime — the
|
|
5
|
+
* planner, the cadence policy and the fake backend are all pure, which is what
|
|
6
|
+
* keeps them testable in vitest with no native modules loaded. The two resolver
|
|
7
|
+
* files under `../skia/` are the only others that name it at all, and neither
|
|
8
|
+
* calls anything: they hand this file a way to find it. All three are reachable
|
|
9
|
+
* only from the `./skia` subpath.
|
|
10
|
+
*
|
|
11
|
+
* ------------------------------------------------------------------------
|
|
12
|
+
* PLATFORM SPLIT — how many copies of the pixels exist once ingest returns.
|
|
13
|
+
* ------------------------------------------------------------------------
|
|
14
|
+
*
|
|
15
|
+
* NATIVE: `Skia.Data.fromBytes` is `SkData::MakeWithCopy(...)`
|
|
16
|
+
* (cpp/api/JsiSkDataFactory.h) — one copy — and `Skia.Image.MakeImage` is
|
|
17
|
+
* `SkImages::RasterFromData(info, data, bytesPerRow)`
|
|
18
|
+
* (cpp/api/JsiSkImageFactory.h), which SHARES that SkData rather than copying
|
|
19
|
+
* it again. Ingest therefore costs exactly ONE copy, and the backend's own
|
|
20
|
+
* buffer is collectable the moment JS drops its reference.
|
|
21
|
+
*
|
|
22
|
+
* WEB: `Skia.Data.fromBytes` only wraps the Uint8Array
|
|
23
|
+
* (skia/web/JsiSkDataFactory.ts -> `new JsiSkData(CanvasKit, bytes)`), and
|
|
24
|
+
* `CanvasKit.MakeImage` is the copy: "we will always make a copy of the pixel
|
|
25
|
+
* data, because of inconsistencies in behavior between GPU and CPU". Also one
|
|
26
|
+
* copy — but into the WASM heap, which grows ONE WAY. Emscripten never returns
|
|
27
|
+
* pages to the OS, so it is PEAK simultaneous raster bytes, not average, that
|
|
28
|
+
* sets the tab's memory floor for the rest of the session.
|
|
29
|
+
*
|
|
30
|
+
* That is why BOTH layers are tiled — base and detail alike — to
|
|
31
|
+
* `RasterPolicy.maxPieceDimension`: a grid of small overlapping pieces holds the
|
|
32
|
+
* same RESIDENT total as one big raster but has a far smaller in-flight peak,
|
|
33
|
+
* and on web the peak is the part that is permanent. It is also why RETIRING is
|
|
34
|
+
* released on the next commit rather than kept "just in case".
|
|
35
|
+
*
|
|
36
|
+
* ------------------------------------------------------------------------
|
|
37
|
+
* WHITE FILL IS THE BACKEND'S JOB, and it is not optional.
|
|
38
|
+
* ------------------------------------------------------------------------
|
|
39
|
+
*
|
|
40
|
+
* Neither PDFium's FPDF_RenderPageBitmap nor Android's PdfRenderer.render
|
|
41
|
+
* clears the destination first, and a freshly allocated RGBA buffer is
|
|
42
|
+
* transparent black. Composited under the app's annotation layers that reads as
|
|
43
|
+
* a BLACK page, not a blank one. Every backend must fill the requested
|
|
44
|
+
* background before drawing. Ingest cannot repair it afterwards: it has no way
|
|
45
|
+
* to tell "the PDF painted black here" from "nothing painted here".
|
|
46
|
+
*
|
|
47
|
+
* ------------------------------------------------------------------------
|
|
48
|
+
* NOTHING HERE MAY NAME `Skia` AT MODULE SCOPE.
|
|
49
|
+
* ------------------------------------------------------------------------
|
|
50
|
+
*
|
|
51
|
+
* This file used to open with
|
|
52
|
+
* `import {AlphaType, ColorType, Skia} from '@shopify/react-native-skia'`, and
|
|
53
|
+
* that single line decided the fate of the whole package on web: it is a VALUE
|
|
54
|
+
* import of the bare specifier, whose barrel evaluates
|
|
55
|
+
* `Skia.web.js` = `JsiSkApi(global.CanvasKit)` — once, permanently, possibly
|
|
56
|
+
* before CanvasKit exists. See `../skia/skia-api.ts` for the full account.
|
|
57
|
+
*
|
|
58
|
+
* Two consequences that must both hold, and are asserted by
|
|
59
|
+
* `__tests__/module-ordering.test.ts`:
|
|
60
|
+
*
|
|
61
|
+
* - `Skia` is reached through `skiaApi()`, called INSIDE `makeImage`. Not
|
|
62
|
+
* stored in a module-level const, not read in `imageFromPixels`'s preamble.
|
|
63
|
+
* - `AlphaType` / `ColorType` come from their own leaf modules under
|
|
64
|
+
* `skia/types/Image/`, not from the barrel. They are plain numeric enum
|
|
65
|
+
* objects with no imports of their own, but reaching them through
|
|
66
|
+
* `@shopify/react-native-skia` would have dragged `Skia.web.js` in exactly as
|
|
67
|
+
* the `Skia` import did — MEASURED by bundling, since nothing about the two
|
|
68
|
+
* import statements looks different.
|
|
69
|
+
*/
|
|
70
|
+
import { ColorType } from '@shopify/react-native-skia/lib/module/skia/types/Image/ColorType.js';
|
|
71
|
+
import { AlphaType } from '@shopify/react-native-skia/lib/module/skia/types/Image/ImageFactory.js';
|
|
72
|
+
import { isTightBuffer } from './buffer.js';
|
|
73
|
+
import { skiaApi } from '../skia/skia-api.js';
|
|
74
|
+
import { warnIfSkiaBelowFloor } from '../peer-check.js';
|
|
75
|
+
import { PdfError } from '../types.js';
|
|
76
|
+
export const BYTES_PER_PIXEL = 4;
|
|
77
|
+
/**
|
|
78
|
+
* The two ColorType members that survive BOTH backends.
|
|
79
|
+
*
|
|
80
|
+
* RN Skia declares 19 ColorType members, and most of them throw on web:
|
|
81
|
+
* JsiSkImageFactory.MakeImage runs the value through
|
|
82
|
+
* `getEnum(CanvasKit, 'ColorType', v)`, which scans CanvasKit's embind enum for
|
|
83
|
+
* a matching numeric value and throws `Enum ColorType does not have value N`
|
|
84
|
+
* when it finds none. CanvasKit exposes only Alpha_8, RGB_565, RGBA_8888,
|
|
85
|
+
* BGRA_8888, RGBA_1010102, RGB_101010x, Gray_8, RGBA_F16 and RGBA_F32 — so
|
|
86
|
+
* Unknown, ARGB_4444, RGB_888x, BGRA_1010102 and every XR/F16Norm member is a
|
|
87
|
+
* declared-but-fatal option. `PixelFormat` deliberately reaches only the two
|
|
88
|
+
* 8888 members, and both are present in CanvasKit with matching numeric values
|
|
89
|
+
* (RN Skia's enum mirrors SkColorType's ordering).
|
|
90
|
+
*/
|
|
91
|
+
export const colorTypeFor = (format) => format === 'bgra8888' ? ColorType.BGRA_8888 : ColorType.RGBA_8888;
|
|
92
|
+
/**
|
|
93
|
+
* The backend's declared alpha encoding -> Skia's tag. NOT A CONSTANT.
|
|
94
|
+
*
|
|
95
|
+
* ------------------------------------------------------------------------
|
|
96
|
+
* WHY THIS IS PER-RASTER AND NOT A MODULE-SCOPE `AlphaType.Premul`.
|
|
97
|
+
* ------------------------------------------------------------------------
|
|
98
|
+
*
|
|
99
|
+
* It was one, and it was wrong for two of the three engines this package knows
|
|
100
|
+
* about. MEASURED: an antialiased opaque-white edge over a transparent page
|
|
101
|
+
* reads `[255, 255, 255, 157]` from PDFium and `[255, 255, 255, 160]` from
|
|
102
|
+
* pdf.js — STRAIGHT alpha. Premultiplied would have been `~[157, 157, 157, 157]`.
|
|
103
|
+
* Android's `Bitmap` ARGB_8888 genuinely is premultiplied, so no single tag is
|
|
104
|
+
* right for all of them and `RasterPixels.alpha` carries the answer, exactly as
|
|
105
|
+
* `format` already carries the byte order.
|
|
106
|
+
*
|
|
107
|
+
* Tagging straight bytes Premul does not error — Skia takes the tag at its word
|
|
108
|
+
* and composites the un-scaled channels, so every partially-transparent pixel
|
|
109
|
+
* comes out too BRIGHT (and an impossible value, channel > alpha, is clamped in
|
|
110
|
+
* whatever way the backend feels like). Invisible under `background: 'white'`,
|
|
111
|
+
* where every pixel is opaque and the two encodings are byte-identical.
|
|
112
|
+
*
|
|
113
|
+
* ------------------------------------------------------------------------
|
|
114
|
+
* AND WHY NEITHER OPTION IS `AlphaType.Opaque`.
|
|
115
|
+
* ------------------------------------------------------------------------
|
|
116
|
+
*
|
|
117
|
+
* Skia treats an Opaque-tagged buffer as licence to ignore the alpha channel
|
|
118
|
+
* entirely, so a `background: 'transparent'` raster tagged Opaque paints its
|
|
119
|
+
* cleared area as opaque BLACK rather than letting the canvas beneath show
|
|
120
|
+
* through. That claim is pinned by a pixel test ('alpha type' in
|
|
121
|
+
* composition.test.ts) rather than left as a comment. A fully-opaque raster is a
|
|
122
|
+
* valid buffer under BOTH of the tags below, so nothing is gained by reaching
|
|
123
|
+
* for Opaque even when the backend happens to have filled every pixel.
|
|
124
|
+
*
|
|
125
|
+
* ------------------------------------------------------------------------
|
|
126
|
+
* WEB ENUM TRAP — both of these are safe, and it was checked, not assumed.
|
|
127
|
+
* ------------------------------------------------------------------------
|
|
128
|
+
*
|
|
129
|
+
* Same trap as `colorTypeFor`: web's `MakeImage` runs the value through
|
|
130
|
+
* `getEnum(CanvasKit, 'AlphaType', v)`, which throws when CanvasKit's embind
|
|
131
|
+
* enum has no member with that numeric value. VERIFIED against canvaskit-wasm
|
|
132
|
+
* 0.41.0 (the version pinned across the whole supported RN Skia range):
|
|
133
|
+
* `CanvasKit.AlphaType` exposes exactly `Opaque = 1`, `Premul = 2`,
|
|
134
|
+
* `Unpremul = 3`, and RN Skia declares `Unknown = 0, Opaque = 1, Premul = 2,
|
|
135
|
+
* Unpremul = 3` — so both members reached below have matching numeric values and
|
|
136
|
+
* neither throws. `AlphaType.Unknown` has no CanvasKit counterpart at all and is
|
|
137
|
+
* a declared-but-fatal option on web, which is the other reason this maps a
|
|
138
|
+
* closed two-member union rather than passing an `AlphaType` through.
|
|
139
|
+
*/
|
|
140
|
+
export const alphaTypeFor = (alpha) => alpha === 'straight' ? AlphaType.Unpremul : AlphaType.Premul;
|
|
141
|
+
/**
|
|
142
|
+
* How many copies of the pixels a MOUNTED raster keeps resident.
|
|
143
|
+
*
|
|
144
|
+
* TWO, not one, and the cache was booking one:
|
|
145
|
+
*
|
|
146
|
+
* 1. The CPU copy. `Skia.Image.MakeImage` is
|
|
147
|
+
* `SkImages::RasterFromData(info, data, rowBytes)`, which SHARES the SkData
|
|
148
|
+
* rather than copying it — and sharing means RETAINING: the SkData stays
|
|
149
|
+
* alive for the image's entire life, so the bytes `Skia.Data.fromBytes`
|
|
150
|
+
* copied in are resident until the image is disposed.
|
|
151
|
+
* 2. The GPU copy. The first time that raster image is drawn on a GPU canvas,
|
|
152
|
+
* Skia uploads it to a texture in its own resource cache and that texture is
|
|
153
|
+
* BUDGETED — it is not freed when the draw ends, which is the whole point of
|
|
154
|
+
* a resource cache.
|
|
155
|
+
*
|
|
156
|
+
* So a raster in the tree costs ~2x its pixels, and `ingest.ts` used to book
|
|
157
|
+
* `w * h * 4`: the cache believed it was at 26% of a 96 MiB budget when it was at
|
|
158
|
+
* 53%, and eviction was choosing victims on numbers wrong by a factor of two.
|
|
159
|
+
*/
|
|
160
|
+
const RESIDENT_COPIES = 2;
|
|
161
|
+
/**
|
|
162
|
+
* Device-memory footprint of a MOUNTED raster, for the cache budget — both
|
|
163
|
+
* copies (see `RESIDENT_COPIES`), not just the decoded pixels.
|
|
164
|
+
*
|
|
165
|
+
* `cacheBudgetBytes` is deliberately UNCHANGED, so its meaning tightens: the same
|
|
166
|
+
* 96 MiB now covers half as many pixels. That is the point — it was always
|
|
167
|
+
* covering half as many, it just did not know.
|
|
168
|
+
*
|
|
169
|
+
* THE ONE PLACE THE FACTOR IS WRITTEN. Every other site derives from this
|
|
170
|
+
* function — including the controller suite's `ingest` stub, which restated
|
|
171
|
+
* `* 4 * 2` by hand and could therefore agree with itself while disagreeing with
|
|
172
|
+
* production. The single independent statement of the number lives in
|
|
173
|
+
* `composition.test.ts` as a literal, which is what stops this from asserting
|
|
174
|
+
* `rasterByteLength === rasterByteLength`.
|
|
175
|
+
*/
|
|
176
|
+
export const rasterByteLength = (width, height) => width * height * BYTES_PER_PIXEL * RESIDENT_COPIES;
|
|
177
|
+
/**
|
|
178
|
+
* `Skia.Data.fromBytes` on native reads `bytes.buffer` and copies
|
|
179
|
+
* `buffer.data()` for `buffer.size()` bytes — the WHOLE ArrayBuffer, with
|
|
180
|
+
* byteOffset and byteLength IGNORED (JsiSkDataFactory.h). A Uint8Array that is
|
|
181
|
+
* a view into a larger buffer — anything carved out of a shared scratch
|
|
182
|
+
* allocation with `subarray()` — therefore ingests bytes shifted by its
|
|
183
|
+
* byteOffset, and the image shears or shows a neighbouring piece's pixels.
|
|
184
|
+
*
|
|
185
|
+
* Copying to a tight buffer is the fix. It costs nothing in the normal case,
|
|
186
|
+
* where the backend allocated exactly one buffer per raster.
|
|
187
|
+
*/
|
|
188
|
+
function tighten(bytes) {
|
|
189
|
+
return isTightBuffer(bytes)
|
|
190
|
+
? { bytes, owned: false }
|
|
191
|
+
: { bytes: bytes.slice(), owned: true };
|
|
192
|
+
}
|
|
193
|
+
function makeImage(bytes, width, height, rowBytes, format, alpha) {
|
|
194
|
+
// CALL-TIME, once per process and cached inside the accessor. On web this is
|
|
195
|
+
// the line that must not run before CanvasKit is loaded — see the header.
|
|
196
|
+
const Skia = skiaApi();
|
|
197
|
+
const data = Skia.Data.fromBytes(bytes);
|
|
198
|
+
return Skia.Image.MakeImage({
|
|
199
|
+
width,
|
|
200
|
+
height,
|
|
201
|
+
colorType: colorTypeFor(format),
|
|
202
|
+
alphaType: alphaTypeFor(alpha),
|
|
203
|
+
}, data, rowBytes);
|
|
204
|
+
}
|
|
205
|
+
/** In-place B<->R swap. Only ever called on a buffer this module owns. */
|
|
206
|
+
function swapRedBlue(bytes, width, height, rowBytes) {
|
|
207
|
+
for (let y = 0; y < height; y++) {
|
|
208
|
+
const row = y * rowBytes;
|
|
209
|
+
for (let x = 0; x < width; x++) {
|
|
210
|
+
const o = row + x * BYTES_PER_PIXEL;
|
|
211
|
+
// Guarded by the byteLength check in imageFromPixels; `?? 0` is only
|
|
212
|
+
// here to satisfy noUncheckedIndexedAccess on TypedArray indexing.
|
|
213
|
+
const b0 = bytes[o] ?? 0;
|
|
214
|
+
bytes[o] = bytes[o + 2] ?? 0;
|
|
215
|
+
bytes[o + 2] = b0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Turn a backend's pixels into an SkImage, or throw a PdfError.
|
|
221
|
+
*
|
|
222
|
+
* Throws rather than returning null: a missing image at this point is a bug in
|
|
223
|
+
* a backend or an out-of-memory condition, and the caller (the epoch builder)
|
|
224
|
+
* wants it as a diagnostic against a raster key, not as a silently blank tile.
|
|
225
|
+
*/
|
|
226
|
+
export function imageFromPixels(pixels) {
|
|
227
|
+
// First-raster, dev-only, once per process, and it cannot throw. This is the
|
|
228
|
+
// only layer of the skia peer gate that catches a version drift no install
|
|
229
|
+
// ran — see `../peer-check.ts` for why it warns rather than throwing, and why
|
|
230
|
+
// it is not a module-scope side effect.
|
|
231
|
+
warnIfSkiaBelowFloor();
|
|
232
|
+
const { width, height, rowBytes, format, alpha } = pixels;
|
|
233
|
+
if (!Number.isInteger(width) || !Number.isInteger(height)) {
|
|
234
|
+
throw new PdfError('backend-failure', `Raster dimensions must be integers, got ${width}x${height}.`);
|
|
235
|
+
}
|
|
236
|
+
if (width <= 0 || height <= 0) {
|
|
237
|
+
throw new PdfError('backend-failure', `Raster has no pixels: ${width}x${height}.`);
|
|
238
|
+
}
|
|
239
|
+
if (!Number.isInteger(rowBytes) || rowBytes < width * BYTES_PER_PIXEL) {
|
|
240
|
+
throw new PdfError('backend-failure', `rowBytes ${rowBytes} is shorter than one ${width}px row ` +
|
|
241
|
+
`(${width * BYTES_PER_PIXEL} bytes). A wrong rowBytes shears the ` +
|
|
242
|
+
'image rather than failing, so it is rejected here.');
|
|
243
|
+
}
|
|
244
|
+
// Skia's own sizing rule (SkImageInfo::computeByteSize): every row but the
|
|
245
|
+
// last needs its full stride; the last needs only its pixels. Checking with
|
|
246
|
+
// the same formula avoids rejecting a legitimately tightly-packed final row.
|
|
247
|
+
const required = (height - 1) * rowBytes + width * BYTES_PER_PIXEL;
|
|
248
|
+
if (pixels.bytes.byteLength < required) {
|
|
249
|
+
throw new PdfError('backend-failure', `Raster buffer is ${pixels.bytes.byteLength} bytes, needs ${required} ` +
|
|
250
|
+
`for ${width}x${height} at rowBytes ${rowBytes}.`);
|
|
251
|
+
}
|
|
252
|
+
const tight = tighten(pixels.bytes);
|
|
253
|
+
const image = makeImage(tight.bytes, width, height, rowBytes, format, alpha);
|
|
254
|
+
if (image !== null) {
|
|
255
|
+
return image;
|
|
256
|
+
}
|
|
257
|
+
// MakeImage signals "cannot represent this configuration" by returning null
|
|
258
|
+
// on both backends rather than throwing. BGRA is the likelier of the two
|
|
259
|
+
// formats to be refused (Skia's CPU raster accepts it; a given GPU-backed
|
|
260
|
+
// CanvasKit build may not), so before giving up, swizzle once into RGBA —
|
|
261
|
+
// the configuration every backend accepts — and retry. One extra pass over
|
|
262
|
+
// the buffer on a path that would otherwise lose the page.
|
|
263
|
+
if (format === 'bgra8888') {
|
|
264
|
+
const rgba = tight.owned ? tight.bytes : tight.bytes.slice();
|
|
265
|
+
swapRedBlue(rgba, width, height, rowBytes);
|
|
266
|
+
const retried = makeImage(rgba, width, height, rowBytes, 'rgba8888',
|
|
267
|
+
// The swizzle moves BYTES between channels; it does not change whether
|
|
268
|
+
// those channels are scaled by alpha. Carrying the declaration through is
|
|
269
|
+
// what keeps the retry a byte-order fallback and nothing else.
|
|
270
|
+
alpha);
|
|
271
|
+
if (retried !== null) {
|
|
272
|
+
return retried;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
throw new PdfError('backend-failure', `Skia.Image.MakeImage returned null for a ${width}x${height} ${format} ` +
|
|
276
|
+
`${alpha}-alpha raster (rowBytes ${rowBytes}, ${required} bytes). This ` +
|
|
277
|
+
'is usually memory pressure, or a colour type the active Skia backend ' +
|
|
278
|
+
'refuses.');
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The controller's `ingest` seam, implemented.
|
|
282
|
+
*
|
|
283
|
+
* `PdfControllerOptions.ingest` is injected precisely so the controller can run
|
|
284
|
+
* under plain node in vitest; this is the real one, and it is drop-in:
|
|
285
|
+
* `createPdfController({..., ingest: ingestRaster})`.
|
|
286
|
+
*
|
|
287
|
+
* `docRect` and `scale` are copied from the META, not re-derived from the
|
|
288
|
+
* pixels. That is THE ONE RULE in miniature — the bytes are whatever the
|
|
289
|
+
* backend produced, but the rect they are drawn at is the rect they were
|
|
290
|
+
* requested for, and rounding the pixel size must never move it.
|
|
291
|
+
*
|
|
292
|
+
* `bytes` is the RESIDENT footprint of the image (see `rasterByteLength`), not
|
|
293
|
+
* the source buffer's length: after ingest the source buffer is collectable and
|
|
294
|
+
* what the cache budget is accounting for is the image — both copies of it.
|
|
295
|
+
*/
|
|
296
|
+
export function ingestRaster(pixels, meta) {
|
|
297
|
+
return {
|
|
298
|
+
key: meta.key,
|
|
299
|
+
page: meta.page,
|
|
300
|
+
role: meta.role,
|
|
301
|
+
image: imageFromPixels(pixels),
|
|
302
|
+
docRect: meta.docRect,
|
|
303
|
+
scale: meta.scale,
|
|
304
|
+
epoch: meta.epoch,
|
|
305
|
+
bytes: rasterByteLength(pixels.width, pixels.height),
|
|
306
|
+
};
|
|
307
|
+
}
|