@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.
Files changed (161) hide show
  1. package/PdfCanvas.podspec +80 -0
  2. package/README.md +1391 -0
  3. package/android/build.gradle +175 -0
  4. package/android/consumer-rules.pro +3 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +2 -0
  8. package/android/gradlew +248 -0
  9. package/android/gradlew.bat +98 -0
  10. package/android/settings.gradle +34 -0
  11. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
  12. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
  13. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
  14. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
  15. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
  16. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
  17. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
  18. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
  19. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
  20. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
  21. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
  22. package/android/src/jsi/cpp/CMakeLists.txt +33 -0
  23. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
  24. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
  25. package/android/src/main/AndroidManifest.xml +7 -0
  26. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
  27. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
  28. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
  29. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
  30. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
  31. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
  32. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
  33. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
  34. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
  35. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
  36. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
  37. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
  38. package/android/tools/compile-gate.sh +289 -0
  39. package/dist/cache.d.ts +161 -0
  40. package/dist/cache.js +389 -0
  41. package/dist/clock.d.ts +21 -0
  42. package/dist/clock.js +31 -0
  43. package/dist/controller.d.ts +131 -0
  44. package/dist/controller.js +1002 -0
  45. package/dist/geometry.d.ts +96 -0
  46. package/dist/geometry.js +248 -0
  47. package/dist/index.d.ts +52 -0
  48. package/dist/index.js +50 -0
  49. package/dist/index.native.d.ts +21 -0
  50. package/dist/index.native.js +23 -0
  51. package/dist/index.web.d.ts +24 -0
  52. package/dist/index.web.js +26 -0
  53. package/dist/layout.d.ts +78 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/peer-check.d.ts +83 -0
  56. package/dist/peer-check.js +183 -0
  57. package/dist/planner.d.ts +35 -0
  58. package/dist/planner.js +530 -0
  59. package/dist/policy.d.ts +14 -0
  60. package/dist/policy.js +168 -0
  61. package/dist/rasterizer/android.d.ts +121 -0
  62. package/dist/rasterizer/android.js +126 -0
  63. package/dist/rasterizer/buffer.d.ts +18 -0
  64. package/dist/rasterizer/buffer.js +18 -0
  65. package/dist/rasterizer/fake.d.ts +92 -0
  66. package/dist/rasterizer/fake.js +433 -0
  67. package/dist/rasterizer/index.d.ts +38 -0
  68. package/dist/rasterizer/index.js +69 -0
  69. package/dist/rasterizer/index.native.d.ts +19 -0
  70. package/dist/rasterizer/index.native.js +80 -0
  71. package/dist/rasterizer/index.web.d.ts +63 -0
  72. package/dist/rasterizer/index.web.js +75 -0
  73. package/dist/rasterizer/ingest.d.ts +180 -0
  74. package/dist/rasterizer/ingest.js +307 -0
  75. package/dist/rasterizer/ios.d.ts +84 -0
  76. package/dist/rasterizer/ios.js +155 -0
  77. package/dist/rasterizer/native-bridge.d.ts +261 -0
  78. package/dist/rasterizer/native-bridge.js +413 -0
  79. package/dist/rasterizer/web/client.d.ts +84 -0
  80. package/dist/rasterizer/web/client.js +359 -0
  81. package/dist/rasterizer/web/engine.d.ts +69 -0
  82. package/dist/rasterizer/web/engine.js +308 -0
  83. package/dist/rasterizer/web/index.d.ts +26 -0
  84. package/dist/rasterizer/web/index.js +21 -0
  85. package/dist/rasterizer/web/pdfium.d.ts +213 -0
  86. package/dist/rasterizer/web/pdfium.js +170 -0
  87. package/dist/rasterizer/web/protocol.d.ts +138 -0
  88. package/dist/rasterizer/web/protocol.js +37 -0
  89. package/dist/rasterizer/web/session.d.ts +106 -0
  90. package/dist/rasterizer/web/session.js +413 -0
  91. package/dist/rasterizer/web/worker.d.ts +44 -0
  92. package/dist/rasterizer/web/worker.js +76 -0
  93. package/dist/react/PdfContentView.d.ts +98 -0
  94. package/dist/react/PdfContentView.js +189 -0
  95. package/dist/react/index.d.ts +6 -0
  96. package/dist/react/index.js +3 -0
  97. package/dist/react/usePdfDocument.d.ts +134 -0
  98. package/dist/react/usePdfDocument.js +275 -0
  99. package/dist/react/usePdfLayer.d.ts +94 -0
  100. package/dist/react/usePdfLayer.js +388 -0
  101. package/dist/skia/index.d.ts +26 -0
  102. package/dist/skia/index.js +25 -0
  103. package/dist/skia/index.native.d.ts +14 -0
  104. package/dist/skia/index.native.js +16 -0
  105. package/dist/skia/index.web.d.ts +12 -0
  106. package/dist/skia/index.web.js +14 -0
  107. package/dist/skia/resolve.native.d.ts +18 -0
  108. package/dist/skia/resolve.native.js +22 -0
  109. package/dist/skia/resolve.web.d.ts +41 -0
  110. package/dist/skia/resolve.web.js +62 -0
  111. package/dist/skia/skia-api.d.ts +94 -0
  112. package/dist/skia/skia-api.js +113 -0
  113. package/dist/testing/index.d.ts +27 -0
  114. package/dist/testing/index.js +28 -0
  115. package/dist/testing/scenes.d.ts +100 -0
  116. package/dist/testing/scenes.js +267 -0
  117. package/dist/types.d.ts +611 -0
  118. package/dist/types.js +21 -0
  119. package/dist/web-init.d.ts +68 -0
  120. package/dist/web-init.js +64 -0
  121. package/ios/Package.swift +58 -0
  122. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
  123. package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
  124. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
  125. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
  126. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
  127. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
  128. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
  129. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
  130. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
  131. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
  132. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
  133. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
  134. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
  135. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
  136. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
  137. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
  138. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
  139. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
  140. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
  141. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
  142. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
  143. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
  144. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
  145. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
  146. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
  147. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
  148. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
  149. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
  150. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
  151. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
  152. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
  153. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
  154. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
  155. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
  156. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
  157. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
  158. package/package.json +144 -0
  159. package/react-native.config.js +40 -0
  160. package/scripts/check-skia-peer.mjs +86 -0
  161. 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
+ }