@stll/folio-core 0.32.2 → 0.33.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 (222) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
  2. package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
  3. package/dist/ai-edits/apply.d.ts +49 -3
  4. package/dist/ai-edits/apply.js +649 -85
  5. package/dist/ai-edits/blockRange.d.ts +7 -3
  6. package/dist/ai-edits/blockRange.js +14 -6
  7. package/dist/ai-edits/headless.d.ts +39 -5
  8. package/dist/ai-edits/headless.js +76 -17
  9. package/dist/ai-edits/index.d.ts +6 -6
  10. package/dist/ai-edits/index.js +3 -3
  11. package/dist/ai-edits/read.d.ts +4 -0
  12. package/dist/ai-edits/read.js +30 -6
  13. package/dist/ai-edits/scoped-reading.js +4 -3
  14. package/dist/ai-edits/snapshot.d.ts +42 -2
  15. package/dist/ai-edits/snapshot.js +127 -62
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
  17. package/dist/ai-edits/table-row-column-mutations.js +98 -6
  18. package/dist/ai-edits/table-targets.d.ts +12 -3
  19. package/dist/ai-edits/table-targets.js +15 -5
  20. package/dist/ai-edits/types.d.ts +219 -15
  21. package/dist/ai-edits/word-diff.d.ts +54 -11
  22. package/dist/ai-edits/word-diff.js +221 -61
  23. package/dist/compare/__fixtures__/body-sequence.d.ts +33 -0
  24. package/dist/compare/__fixtures__/body-sequence.js +67 -0
  25. package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
  26. package/dist/compare/__fixtures__/nested-table.js +50 -0
  27. package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
  28. package/dist/compare/__fixtures__/numbered-list.js +85 -0
  29. package/dist/compare/column-alignment.d.ts +27 -0
  30. package/dist/compare/column-alignment.js +162 -0
  31. package/dist/compare/compare.d.ts +107 -0
  32. package/dist/compare/compare.js +406 -0
  33. package/dist/compare/formatting.d.ts +29 -0
  34. package/dist/compare/formatting.js +84 -0
  35. package/dist/compare/plan.d.ts +22 -0
  36. package/dist/compare/plan.js +1059 -0
  37. package/dist/compare/reproducible-package.d.ts +17 -0
  38. package/dist/compare/reproducible-package.js +30 -0
  39. package/dist/compare/scenario.d.ts +77 -0
  40. package/dist/compare/scenario.js +263 -0
  41. package/dist/compare/types.d.ts +282 -0
  42. package/dist/compare/types.js +32 -0
  43. package/dist/compare/verification.d.ts +55 -0
  44. package/dist/compare/verification.js +146 -0
  45. package/dist/compat/eigenpal.d.ts +9 -6
  46. package/dist/compat/eigenpal.js +6 -3
  47. package/dist/controller/layoutPipeline.d.ts +13 -0
  48. package/dist/controller/layoutPipeline.js +15 -1
  49. package/dist/display-list/build/buildContext.d.ts +39 -0
  50. package/dist/display-list/build/buildContext.js +31 -0
  51. package/dist/display-list/build/buildDisplayList.d.ts +79 -0
  52. package/dist/display-list/build/buildDisplayList.js +283 -0
  53. package/dist/display-list/build/colors.d.ts +20 -0
  54. package/dist/display-list/build/colors.js +136 -0
  55. package/dist/display-list/build/floatingImages.d.ts +24 -0
  56. package/dist/display-list/build/floatingImages.js +41 -0
  57. package/dist/display-list/build/fontTable.d.ts +31 -0
  58. package/dist/display-list/build/fontTable.js +97 -0
  59. package/dist/display-list/build/furniture.d.ts +86 -0
  60. package/dist/display-list/build/furniture.js +98 -0
  61. package/dist/display-list/build/glyphs.d.ts +55 -0
  62. package/dist/display-list/build/glyphs.js +103 -0
  63. package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
  64. package/dist/display-list/build/headerFooterPrimitives.js +40 -0
  65. package/dist/display-list/build/imagePrimitives.d.ts +35 -0
  66. package/dist/display-list/build/imagePrimitives.js +264 -0
  67. package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
  68. package/dist/display-list/build/pageBorderPrimitives.js +111 -0
  69. package/dist/display-list/build/pageFurniture.d.ts +35 -0
  70. package/dist/display-list/build/pageFurniture.js +141 -0
  71. package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
  72. package/dist/display-list/build/paragraphPrimitives.js +819 -0
  73. package/dist/display-list/build/regions.d.ts +46 -0
  74. package/dist/display-list/build/regions.js +56 -0
  75. package/dist/display-list/build/storyPrimitives.d.ts +29 -0
  76. package/dist/display-list/build/storyPrimitives.js +279 -0
  77. package/dist/display-list/build/strokes.d.ts +19 -0
  78. package/dist/display-list/build/strokes.js +78 -0
  79. package/dist/display-list/build/tablePrimitives.d.ts +38 -0
  80. package/dist/display-list/build/tablePrimitives.js +409 -0
  81. package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
  82. package/dist/display-list/build/textBoxPrimitives.js +129 -0
  83. package/dist/display-list/build/textDecorations.d.ts +29 -0
  84. package/dist/display-list/build/textDecorations.js +29 -0
  85. package/dist/display-list/build/unsupported.d.ts +57 -0
  86. package/dist/display-list/build/unsupported.js +0 -0
  87. package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
  88. package/dist/display-list/build/watermarkPrimitives.js +128 -0
  89. package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
  90. package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
  91. package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
  92. package/dist/display-list/editor/displayListPagePainter.js +63 -0
  93. package/dist/display-list/editor/pageRenderer.d.ts +24 -0
  94. package/dist/display-list/editor/pageRenderer.js +36 -0
  95. package/dist/display-list/primitives.d.ts +95 -0
  96. package/dist/display-list/primitives.js +127 -0
  97. package/dist/display-list/types.d.ts +472 -0
  98. package/dist/display-list/types.js +0 -0
  99. package/dist/document-operations.d.ts +39 -8
  100. package/dist/document-operations.js +169 -16
  101. package/dist/document-stories.d.ts +16 -0
  102. package/dist/document-stories.js +48 -4
  103. package/dist/docx/packageParts.d.ts +35 -0
  104. package/dist/docx/packageParts.js +120 -0
  105. package/dist/docx/paragraphParser.js +22 -22
  106. package/dist/docx/paragraphTraversal.js +1 -1
  107. package/dist/docx/renderedPageBreakNormalization.js +3 -1
  108. package/dist/docx/revisionIdNormalization.d.ts +13 -0
  109. package/dist/docx/revisionIdNormalization.js +103 -0
  110. package/dist/docx/rezip.js +26 -17
  111. package/dist/docx/selectiveSave.js +2 -2
  112. package/dist/docx/serializer/commentSerializer.js +6 -1
  113. package/dist/docx/serializer/paragraphSerializer.js +33 -4
  114. package/dist/docx/serializer/runSerializer.js +11 -10
  115. package/dist/docx/serializer/tableSerializer.js +2 -1
  116. package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
  117. package/dist/docx/server/createBilingualDocx.js +3 -2
  118. package/dist/docx/streamingXmlParser.d.ts +13 -1
  119. package/dist/docx/streamingXmlParser.js +49 -11
  120. package/dist/docx/unzip.d.ts +1 -2
  121. package/dist/docx/unzip.js +25 -6
  122. package/dist/docx/xmlParser.d.ts +3 -1
  123. package/dist/docx/xmlParser.js +33 -27
  124. package/dist/export-pdf.d.ts +43 -0
  125. package/dist/export-pdf.js +98 -0
  126. package/dist/fonts/headlessMeasure.d.ts +76 -0
  127. package/dist/fonts/headlessMeasure.js +0 -0
  128. package/dist/fonts/sfnt/parse.d.ts +51 -0
  129. package/dist/fonts/sfnt/parse.js +525 -0
  130. package/dist/fonts/sfnt/subset.d.ts +20 -0
  131. package/dist/fonts/sfnt/subset.js +350 -0
  132. package/dist/fonts/sfnt/tables.d.ts +88 -0
  133. package/dist/fonts/sfnt/tables.js +131 -0
  134. package/dist/fonts/sfnt/woff.d.ts +14 -0
  135. package/dist/fonts/sfnt/woff.js +163 -0
  136. package/dist/generated/text_shaper.js +324 -0
  137. package/dist/generated/text_shaper_bg.wasm +0 -0
  138. package/dist/headless-layout.d.ts +75 -0
  139. package/dist/headless-layout.js +350 -0
  140. package/dist/index.d.ts +9 -6
  141. package/dist/index.js +6 -3
  142. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  143. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  144. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  145. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  146. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
  147. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
  148. package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
  149. package/dist/layout-engine/measure/advanceComposition.js +148 -0
  150. package/dist/layout-engine/measure/cache.d.ts +2 -0
  151. package/dist/layout-engine/measure/measureContainer.js +13 -0
  152. package/dist/layout-engine/measure/measureParagraph.js +1 -2
  153. package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
  154. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
  155. package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
  156. package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
  157. package/dist/layout-engine/tableIndentCompatibility.js +15 -0
  158. package/dist/layout-engine/types.d.ts +8 -0
  159. package/dist/layout-painter/renderPage.d.ts +29 -1
  160. package/dist/layout-painter/renderPage.js +47 -19
  161. package/dist/layout-painter/renderParagraph.d.ts +21 -2
  162. package/dist/layout-painter/renderParagraph.js +1 -1
  163. package/dist/layout-painter/renderTable.js +37 -4
  164. package/dist/managers/editorShortcuts.d.ts +33 -1
  165. package/dist/managers/editorShortcuts.js +20 -1
  166. package/dist/markdown/renderRuns.js +7 -2
  167. package/dist/markdown/renderTable.js +5 -1
  168. package/dist/model.d.ts +3 -3
  169. package/dist/model.js +2 -2
  170. package/dist/pdf/contentStream.d.ts +73 -0
  171. package/dist/pdf/contentStream.js +143 -0
  172. package/dist/pdf/fonts.d.ts +96 -0
  173. package/dist/pdf/fonts.js +593 -0
  174. package/dist/pdf/images.d.ts +28 -0
  175. package/dist/pdf/images.js +439 -0
  176. package/dist/pdf/objects.d.ts +93 -0
  177. package/dist/pdf/objects.js +237 -0
  178. package/dist/pdf/pageSpace.d.ts +43 -0
  179. package/dist/pdf/pageSpace.js +57 -0
  180. package/dist/pdf/paint.d.ts +32 -0
  181. package/dist/pdf/paint.js +442 -0
  182. package/dist/pdf/writePdf.d.ts +56 -0
  183. package/dist/pdf/writePdf.js +345 -0
  184. package/dist/prosemirror/commands/comments.js +91 -6
  185. package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
  186. package/dist/prosemirror/conversion/toProseDoc.js +41 -4
  187. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
  188. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
  189. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
  190. package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
  191. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
  192. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
  193. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
  194. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
  195. package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
  196. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
  197. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
  198. package/dist/prosemirror/plugins/suggestionMode.js +1 -0
  199. package/dist/prosemirror/revisionCarriers.js +19 -2
  200. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
  201. package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
  202. package/dist/prosemirror/validation.js +55 -27
  203. package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
  204. package/dist/prosemirror/zeroWidthAnchors.js +23 -0
  205. package/dist/redline.js +12 -79
  206. package/dist/server.d.ts +3 -3
  207. package/dist/server.js +2 -2
  208. package/dist/shaping/placeRun.d.ts +51 -0
  209. package/dist/shaping/placeRun.js +84 -0
  210. package/dist/shaping/shaper.d.ts +92 -0
  211. package/dist/shaping/shaper.js +102 -0
  212. package/dist/types/block-id.d.ts +18 -2
  213. package/dist/types/block-id.js +49 -5
  214. package/dist/types/content.d.ts +2 -2
  215. package/dist/utils/clipboard.js +4 -3
  216. package/dist/utils/fontResolver.js +183 -0
  217. package/dist/utils/htmlComments.d.ts +17 -0
  218. package/dist/utils/htmlComments.js +22 -0
  219. package/dist/utils/scriptSegments.d.ts +12 -1
  220. package/dist/utils/scriptSegments.js +17 -1
  221. package/dist/version-comparison.d.ts +1 -1
  222. package/package.json +7 -3
@@ -0,0 +1,439 @@
1
+ import { pdfArray, pdfHexString, pdfName, pdfNumber } from "./objects.js";
2
+ import { Result, TaggedError } from "better-result";
3
+ import { inflateSync } from "node:zlib";
4
+ //#region src/pdf/images.ts
5
+ /**
6
+ * Image decoding for PDF image XObjects.
7
+ *
8
+ * JPEG bytes go into the file untouched under `/DCTDecode`: re-encoding a
9
+ * lossy image to paint it is a fidelity loss with nothing to gain. PNG is
10
+ * decoded only as far as PDF needs, which is less far than a rasterizer
11
+ * would go: the filters are reversed and the samples handed over as they are,
12
+ * so a 4-bit palette image stays 4-bit and an alpha channel becomes an
13
+ * `/SMask` rather than being composited against a guess at the backdrop.
14
+ *
15
+ * A form this module cannot represent exactly (16-bit samples, interlacing)
16
+ * is an error, never an approximation: painting the wrong pixels silently is
17
+ * worse than refusing the page.
18
+ */
19
+ var PdfImageError = class extends TaggedError("PdfImageError") {};
20
+ const PNG_SIGNATURE = [
21
+ 137,
22
+ 80,
23
+ 78,
24
+ 71,
25
+ 13,
26
+ 10,
27
+ 26,
28
+ 10
29
+ ];
30
+ const PNG_COLOR_TYPE = {
31
+ greyscale: 0,
32
+ rgb: 2,
33
+ palette: 3,
34
+ greyscaleAlpha: 4,
35
+ rgba: 6
36
+ };
37
+ const CHANNELS_PER_COLOR_TYPE = {
38
+ [PNG_COLOR_TYPE.greyscale]: 1,
39
+ [PNG_COLOR_TYPE.rgb]: 3,
40
+ [PNG_COLOR_TYPE.palette]: 1,
41
+ [PNG_COLOR_TYPE.greyscaleAlpha]: 2,
42
+ [PNG_COLOR_TYPE.rgba]: 4
43
+ };
44
+ const FULLY_OPAQUE = 255;
45
+ const EIGHT_BIT = 8;
46
+ const toHex = (bytes) => {
47
+ let out = "";
48
+ for (const byte of bytes) out += byte.toString(16).padStart(2, "0").toUpperCase();
49
+ return out;
50
+ };
51
+ const readPngChunks = (bytes) => {
52
+ for (let index = 0; index < PNG_SIGNATURE.length; index += 1) if (bytes[index] !== PNG_SIGNATURE[index]) return Result.err(new PdfImageError({ message: "not a PNG: bad signature" }));
53
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
54
+ let header = null;
55
+ let palette = null;
56
+ let transparency = null;
57
+ const idat = [];
58
+ let cursor = PNG_SIGNATURE.length;
59
+ const CHUNK_HEADER_BYTES = 8;
60
+ const CHUNK_CRC_BYTES = 4;
61
+ while (cursor + CHUNK_HEADER_BYTES <= bytes.length) {
62
+ const length = view.getUint32(cursor);
63
+ const type = String.fromCharCode(bytes[cursor + 4] ?? 0, bytes[cursor + 5] ?? 0, bytes[cursor + 6] ?? 0, bytes[cursor + 7] ?? 0);
64
+ const dataStart = cursor + CHUNK_HEADER_BYTES;
65
+ const dataEnd = dataStart + length;
66
+ if (dataEnd > bytes.length) return Result.err(new PdfImageError({ message: `PNG chunk ${type} runs past the end` }));
67
+ const data = bytes.subarray(dataStart, dataEnd);
68
+ if (type === "IHDR") header = data;
69
+ else if (type === "PLTE") palette = data;
70
+ else if (type === "tRNS") transparency = data;
71
+ else if (type === "IDAT") idat.push(data);
72
+ else if (type === "IEND") break;
73
+ cursor = dataEnd + CHUNK_CRC_BYTES;
74
+ }
75
+ if (header === null || header.length < 13) return Result.err(new PdfImageError({ message: "PNG has no IHDR" }));
76
+ if (idat.length === 0) return Result.err(new PdfImageError({ message: "PNG has no IDAT" }));
77
+ const totalPixelBytes = idat.reduce((sum, chunk) => sum + chunk.length, 0);
78
+ const pixels = new Uint8Array(totalPixelBytes);
79
+ let pixelCursor = 0;
80
+ for (const chunk of idat) {
81
+ pixels.set(chunk, pixelCursor);
82
+ pixelCursor += chunk.length;
83
+ }
84
+ return Result.ok({
85
+ header,
86
+ palette,
87
+ transparency,
88
+ pixels
89
+ });
90
+ };
91
+ const paeth = (left, above, upperLeft) => {
92
+ const estimate = left + above - upperLeft;
93
+ const distanceLeft = Math.abs(estimate - left);
94
+ const distanceAbove = Math.abs(estimate - above);
95
+ const distanceUpperLeft = Math.abs(estimate - upperLeft);
96
+ if (distanceLeft <= distanceAbove && distanceLeft <= distanceUpperLeft) return left;
97
+ return distanceAbove <= distanceUpperLeft ? above : upperLeft;
98
+ };
99
+ /**
100
+ * Reverses the five PNG filter types in place over a copy, leaving the
101
+ * scanlines byte-aligned exactly as PDF expects them.
102
+ */
103
+ const unfilterPng = ({ filtered, bytesPerRow, bytesPerPixel, height }) => {
104
+ const expected = height * (bytesPerRow + 1);
105
+ if (filtered.length < expected) return Result.err(new PdfImageError({ message: `PNG pixel data is ${String(filtered.length)} bytes, expected ${String(expected)}` }));
106
+ const out = new Uint8Array(height * bytesPerRow);
107
+ for (let row = 0; row < height; row += 1) {
108
+ const filterType = filtered[row * (bytesPerRow + 1)] ?? 0;
109
+ const source = row * (bytesPerRow + 1) + 1;
110
+ const target = row * bytesPerRow;
111
+ const previous = target - bytesPerRow;
112
+ for (let index = 0; index < bytesPerRow; index += 1) {
113
+ const raw = filtered[source + index] ?? 0;
114
+ const left = index >= bytesPerPixel ? out[target + index - bytesPerPixel] ?? 0 : 0;
115
+ const above = row > 0 ? out[previous + index] ?? 0 : 0;
116
+ const upperLeft = row > 0 && index >= bytesPerPixel ? out[previous + index - bytesPerPixel] ?? 0 : 0;
117
+ let value;
118
+ switch (filterType) {
119
+ case 0:
120
+ value = raw;
121
+ break;
122
+ case 1:
123
+ value = raw + left;
124
+ break;
125
+ case 2:
126
+ value = raw + above;
127
+ break;
128
+ case 3:
129
+ value = raw + (left + above >> 1);
130
+ break;
131
+ case 4:
132
+ value = raw + paeth(left, above, upperLeft);
133
+ break;
134
+ default: return Result.err(new PdfImageError({ message: `unknown PNG filter type ${String(filterType)}` }));
135
+ }
136
+ out[target + index] = value & 255;
137
+ }
138
+ }
139
+ return Result.ok(out);
140
+ };
141
+ /** One byte per pixel index, for sub-byte palette and greyscale depths. */
142
+ const expandIndices = ({ rows, bytesPerRow, width, height, bitDepth }) => {
143
+ if (bitDepth === EIGHT_BIT) return rows;
144
+ const out = new Uint8Array(width * height);
145
+ const perByte = EIGHT_BIT / bitDepth;
146
+ const mask = (1 << bitDepth) - 1;
147
+ for (let row = 0; row < height; row += 1) for (let column = 0; column < width; column += 1) {
148
+ const byte = rows[row * bytesPerRow + Math.floor(column / perByte)] ?? 0;
149
+ const shift = EIGHT_BIT - bitDepth * (column % perByte + 1);
150
+ out[row * width + column] = byte >> shift & mask;
151
+ }
152
+ return out;
153
+ };
154
+ const decodePng = (bytes) => {
155
+ const chunks = readPngChunks(bytes);
156
+ if (chunks.isErr()) return Result.err(chunks.error);
157
+ const { header, palette, transparency, pixels } = chunks.value;
158
+ const headerView = new DataView(header.buffer, header.byteOffset, header.byteLength);
159
+ const width = headerView.getUint32(0);
160
+ const height = headerView.getUint32(4);
161
+ const bitDepth = header[8] ?? 0;
162
+ const colorType = header[9] ?? 0;
163
+ const interlace = header[12] ?? 0;
164
+ if (width === 0 || height === 0) return Result.err(new PdfImageError({ message: "PNG has a zero dimension" }));
165
+ if (interlace !== 0) return Result.err(new PdfImageError({ message: "interlaced PNG is not supported" }));
166
+ const channels = CHANNELS_PER_COLOR_TYPE[colorType];
167
+ if (channels === void 0) return Result.err(new PdfImageError({ message: `unknown PNG colour type ${String(colorType)}` }));
168
+ if (!(colorType === PNG_COLOR_TYPE.greyscale || colorType === PNG_COLOR_TYPE.palette ? [
169
+ 1,
170
+ 2,
171
+ 4,
172
+ EIGHT_BIT
173
+ ].includes(bitDepth) : bitDepth === EIGHT_BIT)) return Result.err(new PdfImageError({ message: `PNG bit depth ${String(bitDepth)} with colour type ${String(colorType)} is not supported` }));
174
+ const bitsPerPixel = bitDepth * channels;
175
+ const bytesPerRow = Math.ceil(bitsPerPixel * width / EIGHT_BIT);
176
+ const bytesPerPixel = Math.max(1, Math.ceil(bitsPerPixel / EIGHT_BIT));
177
+ const inflatedBytesExpected = height * (bytesPerRow + 1);
178
+ const inflated = Result.try({
179
+ try: () => inflateSync(pixels, { maxOutputLength: inflatedBytesExpected }),
180
+ catch: (cause) => new PdfImageError({ message: `PNG IDAT does not inflate to the ${String(inflatedBytesExpected)} bytes its IHDR declares: ${cause instanceof Error ? cause.message : String(cause)}` })
181
+ });
182
+ if (inflated.isErr()) return Result.err(inflated.error);
183
+ const filtered = new Uint8Array(inflated.value.buffer, inflated.value.byteOffset, inflated.value.byteLength);
184
+ const unfiltered = unfilterPng({
185
+ filtered,
186
+ bytesPerRow,
187
+ bytesPerPixel,
188
+ height
189
+ });
190
+ if (unfiltered.isErr()) return Result.err(unfiltered.error);
191
+ const rows = unfiltered.value;
192
+ const pixelCount = width * height;
193
+ switch (colorType) {
194
+ case PNG_COLOR_TYPE.greyscale:
195
+ case PNG_COLOR_TYPE.rgb: {
196
+ const isRgb = colorType === PNG_COLOR_TYPE.rgb;
197
+ let alpha;
198
+ if (transparency !== null) {
199
+ const built = buildColorKeyAlpha({
200
+ rows,
201
+ bitDepth,
202
+ bytesPerRow,
203
+ width,
204
+ height,
205
+ components: isRgb ? 3 : 1,
206
+ key: isRgb ? [
207
+ transparency[1] ?? 0,
208
+ transparency[3] ?? 0,
209
+ transparency[5] ?? 0
210
+ ] : [transparency[1] ?? 0]
211
+ });
212
+ if (built.isErr()) return Result.err(built.error);
213
+ alpha = built.value;
214
+ }
215
+ return Result.ok({
216
+ widthPx: width,
217
+ heightPx: height,
218
+ bitsPerComponent: isRgb ? EIGHT_BIT : bitDepth,
219
+ colorSpace: pdfName(isRgb ? "DeviceRGB" : "DeviceGray"),
220
+ encoding: "raw",
221
+ data: rows,
222
+ ...alpha === void 0 ? {} : { alpha }
223
+ });
224
+ }
225
+ case PNG_COLOR_TYPE.palette: {
226
+ if (palette === null) return Result.err(new PdfImageError({ message: "palette PNG has no PLTE chunk" }));
227
+ const entries = Math.floor(palette.length / 3);
228
+ const colorSpace = pdfArray([
229
+ pdfName("Indexed"),
230
+ pdfName("DeviceRGB"),
231
+ pdfNumber(entries - 1),
232
+ pdfHexString(toHex(palette))
233
+ ]);
234
+ let alpha;
235
+ if (transparency !== null) {
236
+ const indices = expandIndices({
237
+ rows,
238
+ bytesPerRow,
239
+ width,
240
+ height,
241
+ bitDepth
242
+ });
243
+ const data = new Uint8Array(pixelCount);
244
+ for (let index = 0; index < pixelCount; index += 1) data[index] = transparency[indices[index] ?? 0] ?? FULLY_OPAQUE;
245
+ alpha = {
246
+ data,
247
+ bitsPerComponent: EIGHT_BIT
248
+ };
249
+ }
250
+ return Result.ok({
251
+ widthPx: width,
252
+ heightPx: height,
253
+ bitsPerComponent: bitDepth,
254
+ colorSpace,
255
+ encoding: "raw",
256
+ data: rows,
257
+ ...alpha === void 0 ? {} : { alpha }
258
+ });
259
+ }
260
+ case PNG_COLOR_TYPE.greyscaleAlpha:
261
+ case PNG_COLOR_TYPE.rgba: {
262
+ const colorComponents = colorType === PNG_COLOR_TYPE.rgba ? 3 : 1;
263
+ const color = new Uint8Array(pixelCount * colorComponents);
264
+ const alphaData = new Uint8Array(pixelCount);
265
+ for (let index = 0; index < pixelCount; index += 1) {
266
+ const source = index * (colorComponents + 1);
267
+ for (let component = 0; component < colorComponents; component += 1) color[index * colorComponents + component] = rows[source + component] ?? 0;
268
+ alphaData[index] = rows[source + colorComponents] ?? FULLY_OPAQUE;
269
+ }
270
+ return Result.ok({
271
+ widthPx: width,
272
+ heightPx: height,
273
+ bitsPerComponent: EIGHT_BIT,
274
+ colorSpace: pdfName(colorComponents === 3 ? "DeviceRGB" : "DeviceGray"),
275
+ encoding: "raw",
276
+ data: color,
277
+ alpha: {
278
+ data: alphaData,
279
+ bitsPerComponent: EIGHT_BIT
280
+ }
281
+ });
282
+ }
283
+ default: return Result.err(new PdfImageError({ message: `unknown PNG colour type ${String(colorType)}` }));
284
+ }
285
+ };
286
+ /**
287
+ * `tRNS` on a truecolour or greyscale PNG names one fully transparent sample
288
+ * value. PDF has no colour-key transparency for image XObjects, so it becomes
289
+ * a soft mask.
290
+ */
291
+ const buildColorKeyAlpha = ({ rows, bitDepth, bytesPerRow, width, height, components, key }) => {
292
+ if (bitDepth !== EIGHT_BIT) return Result.err(new PdfImageError({ message: `tRNS on a ${String(bitDepth)}-bit non-palette PNG is not supported` }));
293
+ const data = new Uint8Array(width * height);
294
+ for (let row = 0; row < height; row += 1) for (let column = 0; column < width; column += 1) {
295
+ const source = row * bytesPerRow + column * components;
296
+ let matches = true;
297
+ for (let component = 0; component < components; component += 1) if ((rows[source + component] ?? 0) !== (key[component] ?? 0)) {
298
+ matches = false;
299
+ break;
300
+ }
301
+ data[row * width + column] = matches ? 0 : FULLY_OPAQUE;
302
+ }
303
+ return Result.ok({
304
+ data,
305
+ bitsPerComponent: EIGHT_BIT
306
+ });
307
+ };
308
+ const JPEG_COMPONENTS_TO_COLOR_SPACE = {
309
+ 1: "DeviceGray",
310
+ 3: "DeviceRGB",
311
+ 4: "DeviceCMYK"
312
+ };
313
+ const CMYK_INVERTED_DECODE = [
314
+ 1,
315
+ 0,
316
+ 1,
317
+ 0,
318
+ 1,
319
+ 0,
320
+ 1,
321
+ 0
322
+ ];
323
+ /**
324
+ * The frame headers of the 0xC0..0xCF marker block, minus DHT (0xC4) and DAC
325
+ * (0xCC), which are tables rather than frames. `/DCTDecode` is defined over
326
+ * the Huffman-coded sequential and progressive modes only; a lossless,
327
+ * arithmetic-coded or hierarchical frame passed through unchanged is a stream
328
+ * no viewer can decode, so it is refused by name. Reading geometry is as far
329
+ * as this decoder goes either way: it never transcodes a frame it accepts.
330
+ */
331
+ const JPEG_FRAME_MODES = {
332
+ 192: {
333
+ name: "baseline DCT",
334
+ dctDecodable: true
335
+ },
336
+ 193: {
337
+ name: "extended sequential DCT",
338
+ dctDecodable: true
339
+ },
340
+ 194: {
341
+ name: "progressive DCT",
342
+ dctDecodable: true
343
+ },
344
+ 195: {
345
+ name: "lossless",
346
+ dctDecodable: false
347
+ },
348
+ 197: {
349
+ name: "differential sequential DCT",
350
+ dctDecodable: false
351
+ },
352
+ 198: {
353
+ name: "differential progressive DCT",
354
+ dctDecodable: false
355
+ },
356
+ 199: {
357
+ name: "differential lossless",
358
+ dctDecodable: false
359
+ },
360
+ 200: {
361
+ name: "reserved JPG extension",
362
+ dctDecodable: false
363
+ },
364
+ 201: {
365
+ name: "arithmetic extended sequential DCT",
366
+ dctDecodable: false
367
+ },
368
+ 202: {
369
+ name: "arithmetic progressive DCT",
370
+ dctDecodable: false
371
+ },
372
+ 203: {
373
+ name: "arithmetic lossless",
374
+ dctDecodable: false
375
+ },
376
+ 205: {
377
+ name: "differential arithmetic sequential DCT",
378
+ dctDecodable: false
379
+ },
380
+ 206: {
381
+ name: "differential arithmetic progressive DCT",
382
+ dctDecodable: false
383
+ },
384
+ 207: {
385
+ name: "differential arithmetic lossless",
386
+ dctDecodable: false
387
+ }
388
+ };
389
+ const decodeJpeg = (bytes) => {
390
+ const MARKER_PREFIX = 255;
391
+ const START_OF_IMAGE = 216;
392
+ const START_OF_SCAN = 218;
393
+ const APP14 = 238;
394
+ if (bytes[0] !== MARKER_PREFIX || bytes[1] !== START_OF_IMAGE) return Result.err(new PdfImageError({ message: "not a JPEG: bad SOI marker" }));
395
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
396
+ let cursor = 2;
397
+ let adobe = false;
398
+ while (cursor + 4 <= bytes.length) {
399
+ if (bytes[cursor] !== MARKER_PREFIX) return Result.err(new PdfImageError({ message: "JPEG marker segment is out of step" }));
400
+ const marker = bytes[cursor + 1] ?? 0;
401
+ if (marker === START_OF_SCAN) break;
402
+ const length = view.getUint16(cursor + 2);
403
+ const segment = cursor + 4;
404
+ const frame = JPEG_FRAME_MODES[marker];
405
+ if (marker === APP14 && length >= 12) adobe = String.fromCharCode(...bytes.subarray(segment, segment + 5)) === "Adobe";
406
+ if (frame !== void 0) {
407
+ if (!frame.dctDecodable) return Result.err(new PdfImageError({ message: `JPEG ${frame.name} frame (marker 0xff${marker.toString(16)}) has no /DCTDecode equivalent` }));
408
+ const height = view.getUint16(segment + 1);
409
+ const width = view.getUint16(segment + 3);
410
+ const components = bytes[segment + 5] ?? 0;
411
+ const colorSpace = JPEG_COMPONENTS_TO_COLOR_SPACE[components];
412
+ if (colorSpace === void 0) return Result.err(new PdfImageError({ message: `JPEG with ${String(components)} components is not supported` }));
413
+ const inverted = components === 4 && adobe;
414
+ return Result.ok({
415
+ widthPx: width,
416
+ heightPx: height,
417
+ bitsPerComponent: EIGHT_BIT,
418
+ colorSpace: pdfName(colorSpace),
419
+ encoding: "jpeg",
420
+ data: bytes,
421
+ ...inverted ? { decode: CMYK_INVERTED_DECODE } : {}
422
+ });
423
+ }
424
+ cursor += 2 + length;
425
+ }
426
+ return Result.err(new PdfImageError({ message: "JPEG has no frame header" }));
427
+ };
428
+ const decodeImage = (source) => {
429
+ switch (source.format) {
430
+ case "png": return decodePng(source.bytes);
431
+ case "jpeg": return decodeJpeg(source.bytes);
432
+ default: {
433
+ const unreachable = source.format;
434
+ return Result.err(new PdfImageError({ message: `unsupported image format ${String(unreachable)}` }));
435
+ }
436
+ }
437
+ };
438
+ //#endregion
439
+ export { PdfImageError, decodeImage };
@@ -0,0 +1,93 @@
1
+ //#region src/pdf/objects.d.ts
2
+ /**
3
+ * The PDF object model: values, serialization, the cross-reference table and
4
+ * the trailer.
5
+ *
6
+ * Everything a backend writes into a PDF file passes through here, which is
7
+ * what makes byte-for-byte determinism enforceable rather than aspirational:
8
+ * numbers go through one formatter, dictionaries carry their entries as an
9
+ * ordered list (never an object or a `Map` whose iteration order depends on
10
+ * how it was built), and object numbers come from one allocator in call
11
+ * order.
12
+ */
13
+ /** The one number formatter. Every number in the file is emitted through it. */
14
+ declare const formatNumber: (value: number) => string;
15
+ type PdfRef = {
16
+ readonly kind: "ref";
17
+ readonly id: number;
18
+ };
19
+ type PdfDictEntry = readonly [string, PdfValue];
20
+ type PdfDict = {
21
+ readonly kind: "dict";
22
+ readonly entries: readonly PdfDictEntry[];
23
+ };
24
+ type PdfStream = {
25
+ readonly kind: "stream";
26
+ readonly dict: PdfDict;
27
+ readonly data: Uint8Array;
28
+ };
29
+ type PdfValue = {
30
+ readonly kind: "null";
31
+ } | {
32
+ readonly kind: "bool";
33
+ readonly value: boolean;
34
+ } | {
35
+ readonly kind: "number";
36
+ readonly value: number;
37
+ } | {
38
+ readonly kind: "name";
39
+ readonly value: string;
40
+ } |
41
+ /** A byte string: every code unit must be < 256 (URIs, dates, CMap names). */
42
+ {
43
+ readonly kind: "asciiString";
44
+ readonly value: string;
45
+ } |
46
+ /** A text string, emitted as UTF-16BE with a BOM so any script survives. */
47
+ {
48
+ readonly kind: "textString";
49
+ readonly value: string;
50
+ } |
51
+ /** Pre-formatted hex digits, for values that are already bytes (`/ID`). */
52
+ {
53
+ readonly kind: "hexString";
54
+ readonly value: string;
55
+ } | {
56
+ readonly kind: "array";
57
+ readonly items: readonly PdfValue[];
58
+ } | PdfDict | PdfStream | PdfRef;
59
+ declare const pdfNull: PdfValue;
60
+ declare const pdfBool: (value: boolean) => PdfValue;
61
+ declare const pdfNumber: (value: number) => PdfValue;
62
+ declare const pdfName: (value: string) => PdfValue;
63
+ declare const pdfAsciiString: (value: string) => PdfValue;
64
+ declare const pdfTextString: (value: string) => PdfValue;
65
+ declare const pdfHexString: (value: string) => PdfValue;
66
+ declare const pdfArray: (items: readonly PdfValue[]) => PdfValue;
67
+ declare const pdfNumberArray: (values: readonly number[]) => PdfValue;
68
+ /**
69
+ * Entries keep the order they are written in, and an `undefined` value drops
70
+ * the key entirely: an optional entry is either present with a value or
71
+ * absent, never `/Key null`.
72
+ */
73
+ declare const pdfDict: (entries: readonly (readonly [string, PdfValue | undefined])[]) => PdfDict;
74
+ declare const pdfStream: (dict: PdfDict, data: Uint8Array) => PdfStream;
75
+ declare const deflateForPdf: (data: Uint8Array) => Uint8Array;
76
+ /** A `/FlateDecode` stream: the one place raw bytes become a compressed one. */
77
+ declare const pdfFlateStream: (entries: readonly (readonly [string, PdfValue | undefined])[], data: Uint8Array) => PdfStream;
78
+ type PdfDocumentOptions = {
79
+ readonly rootRef: PdfRef;
80
+ readonly infoRef: PdfRef;
81
+ /** Two copies of this hex digest become `/ID`. */
82
+ readonly idHex: (bodyBytes: Uint8Array) => string;
83
+ };
84
+ type PdfDocument = {
85
+ /** Reserves an object number; the value may be supplied later. */
86
+ readonly allocate: () => PdfRef;
87
+ readonly define: (ref: PdfRef, value: PdfValue) => void;
88
+ readonly add: (value: PdfValue) => PdfRef;
89
+ readonly serialize: (options: PdfDocumentOptions) => Uint8Array;
90
+ };
91
+ declare const createPdfDocument: () => PdfDocument;
92
+ //#endregion
93
+ export { PdfDict, PdfDictEntry, PdfDocument, PdfDocumentOptions, PdfRef, PdfStream, PdfValue, createPdfDocument, deflateForPdf, formatNumber, pdfArray, pdfAsciiString, pdfBool, pdfDict, pdfFlateStream, pdfHexString, pdfName, pdfNull, pdfNumber, pdfNumberArray, pdfStream, pdfTextString };