@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.
- package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
- package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
- package/dist/ai-edits/apply.d.ts +49 -3
- package/dist/ai-edits/apply.js +649 -85
- package/dist/ai-edits/blockRange.d.ts +7 -3
- package/dist/ai-edits/blockRange.js +14 -6
- package/dist/ai-edits/headless.d.ts +39 -5
- package/dist/ai-edits/headless.js +76 -17
- package/dist/ai-edits/index.d.ts +6 -6
- package/dist/ai-edits/index.js +3 -3
- package/dist/ai-edits/read.d.ts +4 -0
- package/dist/ai-edits/read.js +30 -6
- package/dist/ai-edits/scoped-reading.js +4 -3
- package/dist/ai-edits/snapshot.d.ts +42 -2
- package/dist/ai-edits/snapshot.js +127 -62
- package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
- package/dist/ai-edits/table-row-column-mutations.js +98 -6
- package/dist/ai-edits/table-targets.d.ts +12 -3
- package/dist/ai-edits/table-targets.js +15 -5
- package/dist/ai-edits/types.d.ts +219 -15
- package/dist/ai-edits/word-diff.d.ts +54 -11
- package/dist/ai-edits/word-diff.js +221 -61
- package/dist/compare/__fixtures__/body-sequence.d.ts +33 -0
- package/dist/compare/__fixtures__/body-sequence.js +67 -0
- package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
- package/dist/compare/__fixtures__/nested-table.js +50 -0
- package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
- package/dist/compare/__fixtures__/numbered-list.js +85 -0
- package/dist/compare/column-alignment.d.ts +27 -0
- package/dist/compare/column-alignment.js +162 -0
- package/dist/compare/compare.d.ts +107 -0
- package/dist/compare/compare.js +406 -0
- package/dist/compare/formatting.d.ts +29 -0
- package/dist/compare/formatting.js +84 -0
- package/dist/compare/plan.d.ts +22 -0
- package/dist/compare/plan.js +1059 -0
- package/dist/compare/reproducible-package.d.ts +17 -0
- package/dist/compare/reproducible-package.js +30 -0
- package/dist/compare/scenario.d.ts +77 -0
- package/dist/compare/scenario.js +263 -0
- package/dist/compare/types.d.ts +282 -0
- package/dist/compare/types.js +32 -0
- package/dist/compare/verification.d.ts +55 -0
- package/dist/compare/verification.js +146 -0
- package/dist/compat/eigenpal.d.ts +9 -6
- package/dist/compat/eigenpal.js +6 -3
- package/dist/controller/layoutPipeline.d.ts +13 -0
- package/dist/controller/layoutPipeline.js +15 -1
- package/dist/display-list/build/buildContext.d.ts +39 -0
- package/dist/display-list/build/buildContext.js +31 -0
- package/dist/display-list/build/buildDisplayList.d.ts +79 -0
- package/dist/display-list/build/buildDisplayList.js +283 -0
- package/dist/display-list/build/colors.d.ts +20 -0
- package/dist/display-list/build/colors.js +136 -0
- package/dist/display-list/build/floatingImages.d.ts +24 -0
- package/dist/display-list/build/floatingImages.js +41 -0
- package/dist/display-list/build/fontTable.d.ts +31 -0
- package/dist/display-list/build/fontTable.js +97 -0
- package/dist/display-list/build/furniture.d.ts +86 -0
- package/dist/display-list/build/furniture.js +98 -0
- package/dist/display-list/build/glyphs.d.ts +55 -0
- package/dist/display-list/build/glyphs.js +103 -0
- package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
- package/dist/display-list/build/headerFooterPrimitives.js +40 -0
- package/dist/display-list/build/imagePrimitives.d.ts +35 -0
- package/dist/display-list/build/imagePrimitives.js +264 -0
- package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
- package/dist/display-list/build/pageBorderPrimitives.js +111 -0
- package/dist/display-list/build/pageFurniture.d.ts +35 -0
- package/dist/display-list/build/pageFurniture.js +141 -0
- package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
- package/dist/display-list/build/paragraphPrimitives.js +819 -0
- package/dist/display-list/build/regions.d.ts +46 -0
- package/dist/display-list/build/regions.js +56 -0
- package/dist/display-list/build/storyPrimitives.d.ts +29 -0
- package/dist/display-list/build/storyPrimitives.js +279 -0
- package/dist/display-list/build/strokes.d.ts +19 -0
- package/dist/display-list/build/strokes.js +78 -0
- package/dist/display-list/build/tablePrimitives.d.ts +38 -0
- package/dist/display-list/build/tablePrimitives.js +409 -0
- package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
- package/dist/display-list/build/textBoxPrimitives.js +129 -0
- package/dist/display-list/build/textDecorations.d.ts +29 -0
- package/dist/display-list/build/textDecorations.js +29 -0
- package/dist/display-list/build/unsupported.d.ts +57 -0
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
- package/dist/display-list/build/watermarkPrimitives.js +128 -0
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
- package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
- package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
- package/dist/display-list/editor/displayListPagePainter.js +63 -0
- package/dist/display-list/editor/pageRenderer.d.ts +24 -0
- package/dist/display-list/editor/pageRenderer.js +36 -0
- package/dist/display-list/primitives.d.ts +95 -0
- package/dist/display-list/primitives.js +127 -0
- package/dist/display-list/types.d.ts +472 -0
- package/dist/display-list/types.js +0 -0
- package/dist/document-operations.d.ts +39 -8
- package/dist/document-operations.js +169 -16
- package/dist/document-stories.d.ts +16 -0
- package/dist/document-stories.js +48 -4
- package/dist/docx/packageParts.d.ts +35 -0
- package/dist/docx/packageParts.js +120 -0
- package/dist/docx/paragraphParser.js +22 -22
- package/dist/docx/paragraphTraversal.js +1 -1
- package/dist/docx/renderedPageBreakNormalization.js +3 -1
- package/dist/docx/revisionIdNormalization.d.ts +13 -0
- package/dist/docx/revisionIdNormalization.js +103 -0
- package/dist/docx/rezip.js +26 -17
- package/dist/docx/selectiveSave.js +2 -2
- package/dist/docx/serializer/commentSerializer.js +6 -1
- package/dist/docx/serializer/paragraphSerializer.js +33 -4
- package/dist/docx/serializer/runSerializer.js +11 -10
- package/dist/docx/serializer/tableSerializer.js +2 -1
- package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
- package/dist/docx/server/createBilingualDocx.js +3 -2
- package/dist/docx/streamingXmlParser.d.ts +13 -1
- package/dist/docx/streamingXmlParser.js +49 -11
- package/dist/docx/unzip.d.ts +1 -2
- package/dist/docx/unzip.js +25 -6
- package/dist/docx/xmlParser.d.ts +3 -1
- package/dist/docx/xmlParser.js +33 -27
- package/dist/export-pdf.d.ts +43 -0
- package/dist/export-pdf.js +98 -0
- package/dist/fonts/headlessMeasure.d.ts +76 -0
- package/dist/fonts/headlessMeasure.js +0 -0
- package/dist/fonts/sfnt/parse.d.ts +51 -0
- package/dist/fonts/sfnt/parse.js +525 -0
- package/dist/fonts/sfnt/subset.d.ts +20 -0
- package/dist/fonts/sfnt/subset.js +350 -0
- package/dist/fonts/sfnt/tables.d.ts +88 -0
- package/dist/fonts/sfnt/tables.js +131 -0
- package/dist/fonts/sfnt/woff.d.ts +14 -0
- package/dist/fonts/sfnt/woff.js +163 -0
- package/dist/generated/text_shaper.js +324 -0
- package/dist/generated/text_shaper_bg.wasm +0 -0
- package/dist/headless-layout.d.ts +75 -0
- package/dist/headless-layout.js +350 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.js +6 -3
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
- package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
- package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
- package/dist/layout-engine/measure/advanceComposition.js +148 -0
- package/dist/layout-engine/measure/cache.d.ts +2 -0
- package/dist/layout-engine/measure/measureContainer.js +13 -0
- package/dist/layout-engine/measure/measureParagraph.js +1 -2
- package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
- package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
- package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
- package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
- package/dist/layout-engine/tableIndentCompatibility.js +15 -0
- package/dist/layout-engine/types.d.ts +8 -0
- package/dist/layout-painter/renderPage.d.ts +29 -1
- package/dist/layout-painter/renderPage.js +47 -19
- package/dist/layout-painter/renderParagraph.d.ts +21 -2
- package/dist/layout-painter/renderParagraph.js +1 -1
- package/dist/layout-painter/renderTable.js +37 -4
- package/dist/managers/editorShortcuts.d.ts +33 -1
- package/dist/managers/editorShortcuts.js +20 -1
- package/dist/markdown/renderRuns.js +7 -2
- package/dist/markdown/renderTable.js +5 -1
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/pdf/contentStream.d.ts +73 -0
- package/dist/pdf/contentStream.js +143 -0
- package/dist/pdf/fonts.d.ts +96 -0
- package/dist/pdf/fonts.js +593 -0
- package/dist/pdf/images.d.ts +28 -0
- package/dist/pdf/images.js +439 -0
- package/dist/pdf/objects.d.ts +93 -0
- package/dist/pdf/objects.js +237 -0
- package/dist/pdf/pageSpace.d.ts +43 -0
- package/dist/pdf/pageSpace.js +57 -0
- package/dist/pdf/paint.d.ts +32 -0
- package/dist/pdf/paint.js +442 -0
- package/dist/pdf/writePdf.d.ts +56 -0
- package/dist/pdf/writePdf.js +345 -0
- package/dist/prosemirror/commands/comments.js +91 -6
- package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
- package/dist/prosemirror/conversion/toProseDoc.js +41 -4
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
- package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
- package/dist/prosemirror/plugins/suggestionMode.js +1 -0
- package/dist/prosemirror/revisionCarriers.js +19 -2
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
- package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
- package/dist/prosemirror/validation.js +55 -27
- package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
- package/dist/prosemirror/zeroWidthAnchors.js +23 -0
- package/dist/redline.js +12 -79
- package/dist/server.d.ts +3 -3
- package/dist/server.js +2 -2
- package/dist/shaping/placeRun.d.ts +51 -0
- package/dist/shaping/placeRun.js +84 -0
- package/dist/shaping/shaper.d.ts +92 -0
- package/dist/shaping/shaper.js +102 -0
- package/dist/types/block-id.d.ts +18 -2
- package/dist/types/block-id.js +49 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/utils/clipboard.js +4 -3
- package/dist/utils/fontResolver.js +183 -0
- package/dist/utils/htmlComments.d.ts +17 -0
- package/dist/utils/htmlComments.js +22 -0
- package/dist/utils/scriptSegments.d.ts +12 -1
- package/dist/utils/scriptSegments.js +17 -1
- package/dist/version-comparison.d.ts +1 -1
- 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 };
|