@awacloud/pdf 0.0.0-stage → 1.0.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/CHANGELOG.md +609 -0
- package/LICENSE +661 -0
- package/NOTICE +77 -0
- package/README.md +363 -2
- package/dist/build/index.js +21 -0
- package/dist/build/pdf-full-rw.js +10972 -0
- package/dist/build/pdf-full-rw.meta.json +105 -0
- package/dist/build/pdf-full-rw.min.js +53 -0
- package/dist/build/pdf-full.js +6078 -0
- package/dist/build/pdf-full.meta.json +90 -0
- package/dist/build/pdf-full.min.js +32 -0
- package/dist/build/pdf-large-rw.js +10367 -0
- package/dist/build/pdf-large-rw.meta.json +99 -0
- package/dist/build/pdf-large-rw.min.js +53 -0
- package/dist/build/pdf-large.js +5473 -0
- package/dist/build/pdf-large.meta.json +84 -0
- package/dist/build/pdf-large.min.js +32 -0
- package/dist/build/pdf-legacy-rw.js +12402 -0
- package/dist/build/pdf-legacy-rw.meta.json +110 -0
- package/dist/build/pdf-legacy-rw.min.js +53 -0
- package/dist/build/pdf-legacy.js +7508 -0
- package/dist/build/pdf-legacy.meta.json +95 -0
- package/dist/build/pdf-legacy.min.js +32 -0
- package/dist/build/pdf-rw.js +7578 -0
- package/dist/build/pdf-rw.meta.json +77 -0
- package/dist/build/pdf-rw.min.js +53 -0
- package/dist/build/pdf.js +2684 -0
- package/dist/build/pdf.meta.json +62 -0
- package/dist/build/pdf.min.js +32 -0
- package/dist/standalone/pdf-full-rw.js +16798 -0
- package/dist/standalone/pdf-full-rw.meta.json +78 -0
- package/dist/standalone/pdf-full-rw.min.js +56 -0
- package/dist/standalone/pdf-full.js +11904 -0
- package/dist/standalone/pdf-full.meta.json +63 -0
- package/dist/standalone/pdf-full.min.js +35 -0
- package/dist/standalone/pdf-large-rw.js +16193 -0
- package/dist/standalone/pdf-large-rw.meta.json +72 -0
- package/dist/standalone/pdf-large-rw.min.js +56 -0
- package/dist/standalone/pdf-large.js +11299 -0
- package/dist/standalone/pdf-large.meta.json +57 -0
- package/dist/standalone/pdf-large.min.js +35 -0
- package/dist/standalone/pdf-legacy-rw.js +18228 -0
- package/dist/standalone/pdf-legacy-rw.meta.json +83 -0
- package/dist/standalone/pdf-legacy-rw.min.js +56 -0
- package/dist/standalone/pdf-legacy.js +13334 -0
- package/dist/standalone/pdf-legacy.meta.json +68 -0
- package/dist/standalone/pdf-legacy.min.js +35 -0
- package/dist/standalone/pdf-rw.js +13404 -0
- package/dist/standalone/pdf-rw.meta.json +50 -0
- package/dist/standalone/pdf-rw.min.js +56 -0
- package/dist/standalone/pdf.js +8510 -0
- package/dist/standalone/pdf.meta.json +35 -0
- package/dist/standalone/pdf.min.js +35 -0
- package/docs/README.md +53 -0
- package/docs/api/README.md +38 -0
- package/docs/api/_shared/README.md +91 -0
- package/docs/api/action/README.md +29 -0
- package/docs/api/action/action.md +81 -0
- package/docs/api/action/goTo.md +66 -0
- package/docs/api/action/launch.md +58 -0
- package/docs/api/action/named.md +55 -0
- package/docs/api/action/uri.md +54 -0
- package/docs/api/annot/README.md +53 -0
- package/docs/api/annot/annot.md +114 -0
- package/docs/api/annot/fileAttach.md +53 -0
- package/docs/api/annot/freeText.md +68 -0
- package/docs/api/annot/ink.md +69 -0
- package/docs/api/annot/link.md +74 -0
- package/docs/api/annot/markup.md +83 -0
- package/docs/api/annot/popup.md +52 -0
- package/docs/api/annot/projection.md +56 -0
- package/docs/api/annot/redact.md +67 -0
- package/docs/api/annot/square.md +87 -0
- package/docs/api/annot/stamp.md +54 -0
- package/docs/api/annot/text.md +69 -0
- package/docs/api/annot/widget.md +69 -0
- package/docs/api/associatedFiles/README.md +9 -0
- package/docs/api/associatedFiles/associatedFiles.md +78 -0
- package/docs/api/bundles/README.md +68 -0
- package/docs/api/bundles/dist-matrix.md +165 -0
- package/docs/api/bundles/pdf-full.md +148 -0
- package/docs/api/bundles/pdf-large.md +144 -0
- package/docs/api/bundles/pdf-legacy.md +169 -0
- package/docs/api/content/README.md +29 -0
- package/docs/api/content/color.md +99 -0
- package/docs/api/content/graphics.md +114 -0
- package/docs/api/content/images.md +124 -0
- package/docs/api/content/ops.md +100 -0
- package/docs/api/content/stream.md +107 -0
- package/docs/api/content/text.md +98 -0
- package/docs/api/crypto/README.md +29 -0
- package/docs/api/crypto/aesGcm.md +72 -0
- package/docs/api/crypto/permissions.md +79 -0
- package/docs/api/crypto/security.md +98 -0
- package/docs/api/crypto/standardV4.md +104 -0
- package/docs/api/crypto/standardV5.md +84 -0
- package/docs/api/crypto/standardV6.md +93 -0
- package/docs/api/destination/README.md +9 -0
- package/docs/api/destination/destination.md +79 -0
- package/docs/api/document/README.md +29 -0
- package/docs/api/document/builder.md +281 -0
- package/docs/api/document/catalog.md +98 -0
- package/docs/api/document/document.md +187 -0
- package/docs/api/document/encryptedWriter.md +149 -0
- package/docs/api/document/incrementalWriter.md +148 -0
- package/docs/api/document/page.md +99 -0
- package/docs/api/document/pages.md +82 -0
- package/docs/api/document/resources.md +102 -0
- package/docs/api/document/writer.md +157 -0
- package/docs/api/document/xrefStreamWriter.md +122 -0
- package/docs/api/embedded/README.md +13 -0
- package/docs/api/embedded/collection.md +80 -0
- package/docs/api/embedded/embeddedFile.md +86 -0
- package/docs/api/embedded/fileSpec.md +87 -0
- package/docs/api/errors.md +110 -0
- package/docs/api/extra/3d-richmedia.md +76 -0
- package/docs/api/extra/README.md +99 -0
- package/docs/api/extra/annot-extended.md +71 -0
- package/docs/api/extra/associated-files.md +70 -0
- package/docs/api/extra/ccitt-fax-decoder.md +74 -0
- package/docs/api/extra/color-spaces-extended.md +72 -0
- package/docs/api/extra/content-ops-extended.md +82 -0
- package/docs/api/extra/document-parts.md +69 -0
- package/docs/api/extra/embedded-files-portfolio.md +87 -0
- package/docs/api/extra/font-cid-typed.md +77 -0
- package/docs/api/extra/font-color-tagging.md +76 -0
- package/docs/api/extra/form-actions-extended.md +75 -0
- package/docs/api/extra/info-dict-deprecated.md +72 -0
- package/docs/api/extra/jbig2-read.md +80 -0
- package/docs/api/extra/legacy-deprecated-annots.md +89 -0
- package/docs/api/extra/legacy-deprecated-filters.md +78 -0
- package/docs/api/extra/legacy-rc4-read.md +74 -0
- package/docs/api/extra/legacy-xfa-read.md +65 -0
- package/docs/api/extra/linearization-write.md +71 -0
- package/docs/api/extra/misc.md +93 -0
- package/docs/api/extra/optional-content-extended.md +83 -0
- package/docs/api/extra/pdf-a-output-intent.md +65 -0
- package/docs/api/extra/pdf-sandbox.md +76 -0
- package/docs/api/extra/pdf-ua-tagged.md +63 -0
- package/docs/api/extra/pdf-x-prepress.md +65 -0
- package/docs/api/extra/redaction-iso32005.md +65 -0
- package/docs/api/extra/shading-typed.md +73 -0
- package/docs/api/extra/sig-aes-gcm.md +69 -0
- package/docs/api/extra/sig-pades.md +103 -0
- package/docs/api/extra/tagged-pdf-typed.md +78 -0
- package/docs/api/extra/transparency-typed.md +74 -0
- package/docs/api/extra/well-tagged-pdf.md +61 -0
- package/docs/api/extra/xmp-extended.md +65 -0
- package/docs/api/font/README.md +25 -0
- package/docs/api/font/embed.md +157 -0
- package/docs/api/font/encoding.md +95 -0
- package/docs/api/font/font.md +97 -0
- package/docs/api/font/type3.md +89 -0
- package/docs/api/form/README.md +35 -0
- package/docs/api/form/acroform.md +88 -0
- package/docs/api/form/appearance.md +87 -0
- package/docs/api/form/button.md +97 -0
- package/docs/api/form/choice.md +96 -0
- package/docs/api/form/fieldTree.md +93 -0
- package/docs/api/form/signature.md +90 -0
- package/docs/api/form/text.md +88 -0
- package/docs/api/linearization/README.md +11 -0
- package/docs/api/linearization/linearization.md +81 -0
- package/docs/api/main.md +116 -0
- package/docs/api/metadata/README.md +10 -0
- package/docs/api/metadata/info.md +70 -0
- package/docs/api/metadata/xmp.md +62 -0
- package/docs/api/ocg/README.md +23 -0
- package/docs/api/ocg/config.md +95 -0
- package/docs/api/ocg/ocg.md +77 -0
- package/docs/api/outline/README.md +11 -0
- package/docs/api/outline/outline.md +107 -0
- package/docs/api/pdf.md +152 -0
- package/docs/api/prepress/README.md +10 -0
- package/docs/api/prepress/outputIntent.md +79 -0
- package/docs/api/prepress/pageBoundary.md +75 -0
- package/docs/api/sig/README.md +32 -0
- package/docs/api/sig/byteRange.md +120 -0
- package/docs/api/sig/certChain.md +84 -0
- package/docs/api/sig/dss.md +111 -0
- package/docs/api/sig/oids.md +76 -0
- package/docs/api/sig/sha1.md +72 -0
- package/docs/api/sig/sign.md +317 -0
- package/docs/api/sig/signature.md +178 -0
- package/docs/api/sig/timestamp.md +84 -0
- package/docs/api/syntax/README.md +29 -0
- package/docs/api/syntax/crossRefStream.md +115 -0
- package/docs/api/syntax/filters/README.md +50 -0
- package/docs/api/syntax/filters/ascii85.md +76 -0
- package/docs/api/syntax/filters/asciiHex.md +73 -0
- package/docs/api/syntax/filters/dispatch.md +125 -0
- package/docs/api/syntax/filters/flate.md +134 -0
- package/docs/api/syntax/filters/runLength.md +78 -0
- package/docs/api/syntax/objStream.md +88 -0
- package/docs/api/syntax/parser-obj.md +97 -0
- package/docs/api/syntax/parser.md +151 -0
- package/docs/api/syntax/serializer.md +109 -0
- package/docs/api/syntax/tokenizer.md +104 -0
- package/docs/api/syntax/trailer.md +85 -0
- package/docs/api/syntax/xref.md +139 -0
- package/docs/api/tagged/README.md +25 -0
- package/docs/api/tagged/classMap.md +67 -0
- package/docs/api/tagged/markedContent.md +62 -0
- package/docs/api/tagged/parentTree.md +67 -0
- package/docs/api/tagged/roleMap.md +67 -0
- package/docs/api/tagged/structElement.md +76 -0
- package/docs/api/tagged/structTree.md +75 -0
- package/docs/guide/coverage.md +113 -0
- package/docs/guide/crypto.md +121 -0
- package/docs/guide/extending.md +76 -0
- package/docs/guide/getting-started.md +75 -0
- package/docs/guide/legacy-1.7.md +42 -0
- package/docs/guide/pades-integration.md +579 -0
- package/docs/guide/read-pdf.md +89 -0
- package/package.json +97 -4
- package/src/_shared/index.js +179 -0
- package/src/action/action.js +119 -0
- package/src/action/goTo.js +89 -0
- package/src/action/launch.js +61 -0
- package/src/action/named.js +54 -0
- package/src/action/uri.js +51 -0
- package/src/annot/annot.js +212 -0
- package/src/annot/fileAttach.js +55 -0
- package/src/annot/freeText.js +82 -0
- package/src/annot/ink.js +77 -0
- package/src/annot/link.js +77 -0
- package/src/annot/markup.js +91 -0
- package/src/annot/popup.js +53 -0
- package/src/annot/projection.js +52 -0
- package/src/annot/redact.js +87 -0
- package/src/annot/square.js +132 -0
- package/src/annot/stamp.js +48 -0
- package/src/annot/text.js +54 -0
- package/src/annot/widget.js +61 -0
- package/src/associatedFiles/associatedFiles.js +86 -0
- package/src/bundles/pdf-full.js +91 -0
- package/src/bundles/pdf-large.js +81 -0
- package/src/bundles/pdf-legacy.js +107 -0
- package/src/content/color.js +114 -0
- package/src/content/graphics.js +192 -0
- package/src/content/images.js +160 -0
- package/src/content/ops.js +137 -0
- package/src/content/stream.js +154 -0
- package/src/content/text.js +125 -0
- package/src/crypto/aesGcm.js +123 -0
- package/src/crypto/permissions.js +112 -0
- package/src/crypto/security.js +327 -0
- package/src/crypto/standardV4.js +443 -0
- package/src/crypto/standardV5.js +306 -0
- package/src/crypto/standardV6.js +334 -0
- package/src/destination/destination.js +183 -0
- package/src/document/builder.js +618 -0
- package/src/document/catalog.js +100 -0
- package/src/document/document.js +472 -0
- package/src/document/encryptedWriter.js +554 -0
- package/src/document/incrementalWriter.js +514 -0
- package/src/document/page.js +131 -0
- package/src/document/pages.js +103 -0
- package/src/document/resources.js +146 -0
- package/src/document/writer.js +211 -0
- package/src/document/xrefStreamWriter.js +353 -0
- package/src/embedded/collection.js +102 -0
- package/src/embedded/embeddedFile.js +99 -0
- package/src/embedded/fileSpec.js +137 -0
- package/src/errors.js +78 -0
- package/src/extra/3d-richmedia.js +171 -0
- package/src/extra/annot-extended.js +200 -0
- package/src/extra/associated-files.js +131 -0
- package/src/extra/ccitt-fax-decoder.js +776 -0
- package/src/extra/color-spaces-extended.js +196 -0
- package/src/extra/content-ops-extended.js +153 -0
- package/src/extra/document-parts.js +149 -0
- package/src/extra/embedded-files-portfolio.js +234 -0
- package/src/extra/font-cid-typed.js +185 -0
- package/src/extra/font-color-tagging.js +144 -0
- package/src/extra/form-actions-extended.js +196 -0
- package/src/extra/info-dict-deprecated.js +137 -0
- package/src/extra/jbig2-read.js +169 -0
- package/src/extra/legacy-deprecated-annots.js +198 -0
- package/src/extra/legacy-deprecated-filters.js +167 -0
- package/src/extra/legacy-rc4-read.js +235 -0
- package/src/extra/legacy-xfa-read.js +104 -0
- package/src/extra/linearization-write.js +97 -0
- package/src/extra/misc.js +217 -0
- package/src/extra/optional-content-extended.js +142 -0
- package/src/extra/pdf-a-output-intent.js +112 -0
- package/src/extra/pdf-sandbox.js +88 -0
- package/src/extra/pdf-ua-tagged.js +116 -0
- package/src/extra/pdf-x-prepress.js +114 -0
- package/src/extra/redaction-iso32005.js +136 -0
- package/src/extra/shading-typed.js +222 -0
- package/src/extra/sig-aes-gcm.js +135 -0
- package/src/extra/sig-pades.js +242 -0
- package/src/extra/tagged-pdf-typed.js +203 -0
- package/src/extra/transparency-typed.js +135 -0
- package/src/extra/well-tagged-pdf.js +138 -0
- package/src/extra/xmp-extended.js +190 -0
- package/src/font/embed.js +480 -0
- package/src/font/encoding.js +92 -0
- package/src/font/font.js +101 -0
- package/src/font/type3.js +75 -0
- package/src/form/acroform.js +94 -0
- package/src/form/appearance.js +90 -0
- package/src/form/button.js +105 -0
- package/src/form/choice.js +152 -0
- package/src/form/fieldTree.js +120 -0
- package/src/form/signature.js +100 -0
- package/src/form/text.js +101 -0
- package/src/linearization/linearization.js +107 -0
- package/src/main.js +411 -0
- package/src/metadata/info.js +87 -0
- package/src/metadata/xmp.js +62 -0
- package/src/ocg/config.js +156 -0
- package/src/ocg/ocg.js +124 -0
- package/src/outline/outline.js +157 -0
- package/src/pdf.js +133 -0
- package/src/prepress/outputIntent.js +118 -0
- package/src/prepress/pageBoundary.js +108 -0
- package/src/sig/byteRange.js +306 -0
- package/src/sig/certChain.js +247 -0
- package/src/sig/dss.js +317 -0
- package/src/sig/oids.js +157 -0
- package/src/sig/sha1.js +142 -0
- package/src/sig/sign.js +1899 -0
- package/src/sig/signature.js +1441 -0
- package/src/sig/timestamp.js +236 -0
- package/src/syntax/crossRefStream.js +133 -0
- package/src/syntax/filters/ascii85.js +122 -0
- package/src/syntax/filters/asciiHex.js +83 -0
- package/src/syntax/filters/dispatch.js +176 -0
- package/src/syntax/filters/flate.js +316 -0
- package/src/syntax/filters/runLength.js +96 -0
- package/src/syntax/objStream.js +99 -0
- package/src/syntax/parser-obj.js +52 -0
- package/src/syntax/parser.js +321 -0
- package/src/syntax/serializer.js +221 -0
- package/src/syntax/tokenizer.js +290 -0
- package/src/syntax/trailer.js +76 -0
- package/src/syntax/xref.js +341 -0
- package/src/tagged/classMap.js +81 -0
- package/src/tagged/markedContent.js +123 -0
- package/src/tagged/parentTree.js +126 -0
- package/src/tagged/roleMap.js +107 -0
- package/src/tagged/structElement.js +138 -0
- package/src/tagged/structTree.js +94 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// Copyright (c) 2026 AwaCloud SAS
|
|
2
|
+
// Author: Matthieu Bouilloux
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
4
|
+
// Dual-licensed; see the NOTICE file for licensing and any additional terms.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @fileoverview Xref-stream + ObjStm writer.
|
|
8
|
+
*
|
|
9
|
+
* Provides an alternative to `pdfWriter.writeDocument` that emits the
|
|
10
|
+
* cross-reference as a `/Type /XRef` stream object (PDF 1.5+ /
|
|
11
|
+
* ISO 32000-2:2020 §7.5.8), optionally grouping non-stream indirects
|
|
12
|
+
* into `/Type /ObjStm` compressed object streams (§7.5.7).
|
|
13
|
+
*
|
|
14
|
+
* Wire format produced:
|
|
15
|
+
*
|
|
16
|
+
* %PDF-2.0\n
|
|
17
|
+
* %binary marker\n
|
|
18
|
+
* <serialized indirects, including any ObjStm wrappers>
|
|
19
|
+
* <xref-stream object>
|
|
20
|
+
* startxref\n<offset>\n
|
|
21
|
+
* %%EOF\n
|
|
22
|
+
*
|
|
23
|
+
* The xref stream carries fields (type, field2, field3) with widths W
|
|
24
|
+
* chosen to fit the largest values:
|
|
25
|
+
*
|
|
26
|
+
* type=0 free → (0, 0, 0)
|
|
27
|
+
* type=1 uncompressed indirect → (1, byteOffset, gen)
|
|
28
|
+
* type=2 compressed in ObjStm → (2, objStmNum, indexInStm)
|
|
29
|
+
*
|
|
30
|
+
* Options:
|
|
31
|
+
*
|
|
32
|
+
* writeXrefStreamDocument({
|
|
33
|
+
* indirects, // [{ num, gen, value }]
|
|
34
|
+
* root, info, id, // same semantics as pdfWriter
|
|
35
|
+
* version, // default "2.0"
|
|
36
|
+
* useObjStm: false, // when true, group non-stream non-encrypt
|
|
37
|
+
* // indirects into ObjStm wrappers.
|
|
38
|
+
* objStmCapacity: 64 // max objects per ObjStm
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* @module pdf/document/xrefStreamWriter
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Module factory — worker-safe, self-contained.
|
|
46
|
+
*/
|
|
47
|
+
import { pdfErrors } from '../errors.js';
|
|
48
|
+
import { pdfSerializer } from '../syntax/serializer.js';
|
|
49
|
+
import { pdfFlate } from '../syntax/filters/flate.js';
|
|
50
|
+
|
|
51
|
+
export const pdfXrefStreamWriter = {
|
|
52
|
+
name: 'pdfXrefStreamWriter',
|
|
53
|
+
dependencies: ['pdfErrors', 'pdfSerializer', 'pdfFlate'],
|
|
54
|
+
deps: [pdfErrors, pdfSerializer, pdfFlate],
|
|
55
|
+
factory(errors, serializerMod, flateMod) {
|
|
56
|
+
const { RenderError } = errors;
|
|
57
|
+
const serializeIndirect = serializerMod.serializeIndirect;
|
|
58
|
+
const serializeObject = serializerMod.serializeObject;
|
|
59
|
+
const flateEncode = flateMod && flateMod.encode;
|
|
60
|
+
|
|
61
|
+
const te = new TextEncoder();
|
|
62
|
+
|
|
63
|
+
function concat(arrays) {
|
|
64
|
+
let n = 0;
|
|
65
|
+
for (const a of arrays) n += a.length;
|
|
66
|
+
const out = new Uint8Array(n);
|
|
67
|
+
let o = 0;
|
|
68
|
+
for (const a of arrays) { out.set(a, o); o += a.length; }
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hexLit(bytes) {
|
|
73
|
+
const H = '0123456789ABCDEF';
|
|
74
|
+
let s = '<';
|
|
75
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
76
|
+
s += H[bytes[i] >> 4] + H[bytes[i] & 0xF];
|
|
77
|
+
}
|
|
78
|
+
return s + '>';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function byteWidth(n) {
|
|
82
|
+
if (n <= 0) return 1;
|
|
83
|
+
let w = 0;
|
|
84
|
+
while (n > 0) { n = n >>> 8; w++; }
|
|
85
|
+
return w;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeBE(buf, off, value, w) {
|
|
89
|
+
for (let i = w - 1; i >= 0; i--) {
|
|
90
|
+
buf[off + i] = value & 0xff;
|
|
91
|
+
value = (value / 256) | 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateIndirects(list) {
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const it of list) {
|
|
98
|
+
if (!it || !Number.isFinite(it.num) || it.num < 1) {
|
|
99
|
+
throw new RenderError('pdf/xrefstm-writer/bad-indirect',
|
|
100
|
+
'each indirect requires num >= 1');
|
|
101
|
+
}
|
|
102
|
+
if (seen.has(it.num)) {
|
|
103
|
+
throw new RenderError('pdf/xrefstm-writer/duplicate-num',
|
|
104
|
+
'duplicate object number',
|
|
105
|
+
{ context: { num: it.num } });
|
|
106
|
+
}
|
|
107
|
+
seen.add(it.num);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isCompressible(value) {
|
|
112
|
+
// ObjStm cannot carry stream objects; gen must be 0; and an
|
|
113
|
+
// object cannot be the xref-stream itself (we emit that
|
|
114
|
+
// last so it's not in the indirect list).
|
|
115
|
+
if (!value || typeof value.type !== 'string') return false;
|
|
116
|
+
if (value.type === 'stream') return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildObjStm(members, objNum) {
|
|
121
|
+
// members: [{ num, gen, value }]
|
|
122
|
+
// Per §7.5.7 the body is the concatenation of object
|
|
123
|
+
// headers (pairs "objNum offset") followed by serialized
|
|
124
|
+
// object values (no "N M obj … endobj" wrapper around each
|
|
125
|
+
// member — just the object value itself).
|
|
126
|
+
const bodyParts = [];
|
|
127
|
+
const offsets = new Array(members.length);
|
|
128
|
+
|
|
129
|
+
// First serialize all object values to compute offsets.
|
|
130
|
+
let cursor = 0;
|
|
131
|
+
for (let i = 0; i < members.length; i++) {
|
|
132
|
+
const m = members[i];
|
|
133
|
+
const bytes = serializeObject(m.value);
|
|
134
|
+
offsets[i] = cursor;
|
|
135
|
+
bodyParts.push(bytes);
|
|
136
|
+
bodyParts.push(te.encode('\n'));
|
|
137
|
+
cursor += bytes.length + 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let headerStr = '';
|
|
141
|
+
for (let i = 0; i < members.length; i++) {
|
|
142
|
+
if (i > 0) headerStr += ' ';
|
|
143
|
+
headerStr += `${members[i].num} ${offsets[i]}`;
|
|
144
|
+
}
|
|
145
|
+
headerStr += '\n';
|
|
146
|
+
const headerBytes = te.encode(headerStr);
|
|
147
|
+
|
|
148
|
+
const payload = concat([headerBytes, ...bodyParts]);
|
|
149
|
+
// Compress with flate.
|
|
150
|
+
const compressed = flateEncode(payload);
|
|
151
|
+
const dict = {
|
|
152
|
+
type: 'dict',
|
|
153
|
+
entries: {
|
|
154
|
+
Type: { type: 'name', value: 'ObjStm' },
|
|
155
|
+
N: { type: 'int', value: members.length },
|
|
156
|
+
First: { type: 'int', value: headerBytes.length },
|
|
157
|
+
Filter: { type: 'name', value: 'FlateDecode' }
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
num: objNum,
|
|
162
|
+
gen: 0,
|
|
163
|
+
value: { type: 'stream', dict, raw: compressed }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildXrefStreamIndirect(opts) {
|
|
168
|
+
const {
|
|
169
|
+
num, size, root, info, id,
|
|
170
|
+
w, indexPairs, payload
|
|
171
|
+
} = opts;
|
|
172
|
+
const entries = {
|
|
173
|
+
Type: { type: 'name', value: 'XRef' },
|
|
174
|
+
Size: { type: 'int', value: size },
|
|
175
|
+
W: { type: 'array', items: [
|
|
176
|
+
{ type: 'int', value: w[0] },
|
|
177
|
+
{ type: 'int', value: w[1] },
|
|
178
|
+
{ type: 'int', value: w[2] }
|
|
179
|
+
] },
|
|
180
|
+
Root: { type: 'ref', num: root.num, gen: root.gen | 0 },
|
|
181
|
+
Filter: { type: 'name', value: 'FlateDecode' }
|
|
182
|
+
};
|
|
183
|
+
if (info) entries.Info = { type: 'ref', num: info.num, gen: info.gen | 0 };
|
|
184
|
+
if (id) {
|
|
185
|
+
entries.ID = { type: 'array', items: [
|
|
186
|
+
{ type: 'string', value: id[0], syntax: 'hex' },
|
|
187
|
+
{ type: 'string', value: id[1], syntax: 'hex' }
|
|
188
|
+
] };
|
|
189
|
+
}
|
|
190
|
+
// Always emit /Index — covers the contiguous run.
|
|
191
|
+
entries.Index = { type: 'array', items: [] };
|
|
192
|
+
for (const [first, count] of indexPairs) {
|
|
193
|
+
entries.Index.items.push({ type: 'int', value: first });
|
|
194
|
+
entries.Index.items.push({ type: 'int', value: count });
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
num, gen: 0,
|
|
198
|
+
value: { type: 'stream', dict: { type: 'dict', entries }, raw: payload }
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function writeXrefStreamDocument(opts) {
|
|
203
|
+
if (!opts || !Array.isArray(opts.indirects)) {
|
|
204
|
+
throw new RenderError('pdf/xrefstm-writer/bad-input',
|
|
205
|
+
'writeXrefStreamDocument expects { indirects: [...] }');
|
|
206
|
+
}
|
|
207
|
+
if (!opts.root || !Number.isFinite(opts.root.num)) {
|
|
208
|
+
throw new RenderError('pdf/xrefstm-writer/no-root',
|
|
209
|
+
'opts.root required');
|
|
210
|
+
}
|
|
211
|
+
if (typeof flateEncode !== 'function') {
|
|
212
|
+
throw new RenderError('pdf/xrefstm-writer/no-flate',
|
|
213
|
+
'pdfFlate.encode is required to emit xref-streams');
|
|
214
|
+
}
|
|
215
|
+
const version = opts.version || '2.0';
|
|
216
|
+
if (!/^\d\.\d$/.test(version)) {
|
|
217
|
+
throw new RenderError('pdf/xrefstm-writer/bad-version',
|
|
218
|
+
'version must look like "x.y"');
|
|
219
|
+
}
|
|
220
|
+
const indirects = opts.indirects.slice().sort((a, b) => a.num - b.num);
|
|
221
|
+
validateIndirects(indirects);
|
|
222
|
+
|
|
223
|
+
// Compute fresh object number for the xref-stream itself.
|
|
224
|
+
const maxInputNum = indirects.length
|
|
225
|
+
? indirects[indirects.length - 1].num : 0;
|
|
226
|
+
let nextNum = maxInputNum + 1;
|
|
227
|
+
|
|
228
|
+
// Build ObjStm groupings (optional) and the final indirect
|
|
229
|
+
// list. We track xref entries in a Map<num, {type, f2, f3}>.
|
|
230
|
+
const xrefEntries = new Map();
|
|
231
|
+
// Always include object 0 (free head).
|
|
232
|
+
xrefEntries.set(0, { type: 0, f2: 0, f3: 65535 });
|
|
233
|
+
|
|
234
|
+
const emittedIndirects = [];
|
|
235
|
+
const useObjStm = !!opts.useObjStm;
|
|
236
|
+
const cap = (opts.objStmCapacity | 0) || 64;
|
|
237
|
+
|
|
238
|
+
if (useObjStm) {
|
|
239
|
+
const compressible = [];
|
|
240
|
+
const passthrough = [];
|
|
241
|
+
for (const it of indirects) {
|
|
242
|
+
if (isCompressible(it.value) && (it.gen | 0) === 0) {
|
|
243
|
+
compressible.push(it);
|
|
244
|
+
} else {
|
|
245
|
+
passthrough.push(it);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Group compressible into ObjStms.
|
|
249
|
+
for (let i = 0; i < compressible.length; i += cap) {
|
|
250
|
+
const chunk = compressible.slice(i, i + cap);
|
|
251
|
+
const objStmNum = nextNum++;
|
|
252
|
+
const indirect = buildObjStm(chunk, objStmNum);
|
|
253
|
+
emittedIndirects.push(indirect);
|
|
254
|
+
for (let k = 0; k < chunk.length; k++) {
|
|
255
|
+
xrefEntries.set(chunk[k].num, {
|
|
256
|
+
type: 2, f2: objStmNum, f3: k
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const it of passthrough) emittedIndirects.push(it);
|
|
261
|
+
} else {
|
|
262
|
+
for (const it of indirects) emittedIndirects.push(it);
|
|
263
|
+
}
|
|
264
|
+
emittedIndirects.sort((a, b) => a.num - b.num);
|
|
265
|
+
|
|
266
|
+
// Emit header + binary marker.
|
|
267
|
+
const parts = [];
|
|
268
|
+
let cursor = 0;
|
|
269
|
+
function push(u8) { parts.push(u8); cursor += u8.length; }
|
|
270
|
+
push(te.encode(`%PDF-${version}\n`));
|
|
271
|
+
push(new Uint8Array([0x25, 0xE2, 0xE3, 0xCF, 0xD3, 0x0A]));
|
|
272
|
+
|
|
273
|
+
for (const it of emittedIndirects) {
|
|
274
|
+
xrefEntries.set(it.num, {
|
|
275
|
+
type: 1, f2: cursor, f3: it.gen | 0
|
|
276
|
+
});
|
|
277
|
+
push(serializeIndirect(it.num, it.gen | 0, it.value));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Append the xref-stream indirect.
|
|
281
|
+
const xrefNum = nextNum;
|
|
282
|
+
// The xref-stream itself MUST appear in its own xref table
|
|
283
|
+
// (per §7.5.8 the xref-stream's xref entry has type=1 with
|
|
284
|
+
// the offset where the xref-stream begins).
|
|
285
|
+
const xrefOffset = cursor;
|
|
286
|
+
xrefEntries.set(xrefNum, { type: 1, f2: xrefOffset, f3: 0 });
|
|
287
|
+
|
|
288
|
+
// Build /Index pairs from sorted nums, contiguous runs.
|
|
289
|
+
const allNums = Array.from(xrefEntries.keys()).sort((a, b) => a - b);
|
|
290
|
+
const indexPairs = [];
|
|
291
|
+
let i = 0;
|
|
292
|
+
while (i < allNums.length) {
|
|
293
|
+
let j = i;
|
|
294
|
+
while (j + 1 < allNums.length
|
|
295
|
+
&& allNums[j + 1] === allNums[j] + 1) j++;
|
|
296
|
+
indexPairs.push([allNums[i], j - i + 1]);
|
|
297
|
+
i = j + 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Pick W widths from observed max values.
|
|
301
|
+
let maxType = 0, maxF2 = 0, maxF3 = 0;
|
|
302
|
+
for (const e of xrefEntries.values()) {
|
|
303
|
+
if (e.type > maxType) maxType = e.type;
|
|
304
|
+
if (e.f2 > maxF2) maxF2 = e.f2;
|
|
305
|
+
if (e.f3 > maxF3) maxF3 = e.f3;
|
|
306
|
+
}
|
|
307
|
+
const w = [
|
|
308
|
+
Math.max(1, byteWidth(maxType)),
|
|
309
|
+
Math.max(1, byteWidth(maxF2)),
|
|
310
|
+
Math.max(1, byteWidth(maxF3))
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
// Build the raw payload.
|
|
314
|
+
const recordSize = w[0] + w[1] + w[2];
|
|
315
|
+
const payload = new Uint8Array(allNums.length * recordSize);
|
|
316
|
+
let off = 0;
|
|
317
|
+
for (const n of allNums) {
|
|
318
|
+
const e = xrefEntries.get(n);
|
|
319
|
+
writeBE(payload, off, e.type, w[0]);
|
|
320
|
+
writeBE(payload, off + w[0], e.f2, w[1]);
|
|
321
|
+
writeBE(payload, off + w[0] + w[1], e.f3, w[2]);
|
|
322
|
+
off += recordSize;
|
|
323
|
+
}
|
|
324
|
+
const compressed = flateEncode(payload);
|
|
325
|
+
|
|
326
|
+
const size = xrefNum + 1;
|
|
327
|
+
const xrefIndirect = buildXrefStreamIndirect({
|
|
328
|
+
num: xrefNum,
|
|
329
|
+
size,
|
|
330
|
+
root: opts.root,
|
|
331
|
+
info: opts.info,
|
|
332
|
+
id: opts.id,
|
|
333
|
+
w,
|
|
334
|
+
indexPairs,
|
|
335
|
+
payload: compressed
|
|
336
|
+
});
|
|
337
|
+
push(serializeIndirect(xrefIndirect.num, xrefIndirect.gen,
|
|
338
|
+
xrefIndirect.value));
|
|
339
|
+
|
|
340
|
+
// Trailer is the xref-stream object itself; we just need
|
|
341
|
+
// `startxref` + `%%EOF`.
|
|
342
|
+
push(te.encode(`startxref\n${xrefOffset}\n%%EOF\n`));
|
|
343
|
+
|
|
344
|
+
// ID note: when /ID is requested and we have no trailer to
|
|
345
|
+
// host it, /ID has been embedded in the xref-stream dict.
|
|
346
|
+
// (intentional per §7.5.8.)
|
|
347
|
+
void hexLit; // suppress unused; reserved for future trailer paths
|
|
348
|
+
return concat(parts);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { writeXrefStreamDocument };
|
|
352
|
+
}
|
|
353
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Copyright (c) 2026 AwaCloud SAS
|
|
2
|
+
// Author: Matthieu Bouilloux
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
4
|
+
// Dual-licensed; see the NOTICE file for licensing and any additional terms.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @fileoverview Collection (PDF Portfolio) typing per ISO 32000-2:2020
|
|
8
|
+
* §7.11.6.
|
|
9
|
+
*
|
|
10
|
+
* A Collection dict (`/Type /Collection`) lives on the Catalog and
|
|
11
|
+
* describes how to display the embedded files attached to a "PDF
|
|
12
|
+
* Portfolio". Entries:
|
|
13
|
+
* /Schema — dict of collection field definitions.
|
|
14
|
+
* /D — name of the embedded file shown initially.
|
|
15
|
+
* /View — display mode (D/T/H — Details, Tile, Hidden, custom).
|
|
16
|
+
* /Sort — sort dictionary.
|
|
17
|
+
* /Navigator — navigator extension dict.
|
|
18
|
+
*
|
|
19
|
+
* @module pdf/embedded/collection
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Module factory.
|
|
24
|
+
*/
|
|
25
|
+
import { pdfErrors } from '../errors.js';
|
|
26
|
+
import { pdfParser } from '../syntax/parser.js';
|
|
27
|
+
|
|
28
|
+
export const pdfCollection = {
|
|
29
|
+
name: 'pdfCollection',
|
|
30
|
+
dependencies: ['pdfErrors', 'pdfParser'],
|
|
31
|
+
deps: [pdfErrors, pdfParser],
|
|
32
|
+
factory(errors, parser) {
|
|
33
|
+
const { ParseError } = errors;
|
|
34
|
+
const { isType } = parser;
|
|
35
|
+
|
|
36
|
+
const KNOWN = new Set(['Type', 'Schema', 'D', 'View', 'Sort', 'Navigator', 'Folders', 'Colors', 'Split']);
|
|
37
|
+
const VIEW_MODES = new Set(['D', 'T', 'H', 'C']);
|
|
38
|
+
|
|
39
|
+
function typeCollection(dict) {
|
|
40
|
+
if (!isType(dict, 'dict')) {
|
|
41
|
+
throw new ParseError('pdf/collection/not-dict',
|
|
42
|
+
'Collection must be a dictionary',
|
|
43
|
+
{ context: { type: dict && dict.type } });
|
|
44
|
+
}
|
|
45
|
+
const e = dict.entries;
|
|
46
|
+
if (e.Type && (e.Type.type !== 'name' || e.Type.value !== 'Collection')) {
|
|
47
|
+
throw new ParseError('pdf/collection/bad-type',
|
|
48
|
+
'/Type must be /Collection',
|
|
49
|
+
{ context: { actual: e.Type.value } });
|
|
50
|
+
}
|
|
51
|
+
const out = { raw: dict, _extras: {} };
|
|
52
|
+
|
|
53
|
+
if (e.Schema) {
|
|
54
|
+
if (e.Schema.type !== 'dict') {
|
|
55
|
+
throw new ParseError('pdf/collection/bad-schema',
|
|
56
|
+
'/Schema must be a dictionary',
|
|
57
|
+
{ context: { type: e.Schema.type } });
|
|
58
|
+
}
|
|
59
|
+
out.schema = e.Schema;
|
|
60
|
+
}
|
|
61
|
+
if (e.D) {
|
|
62
|
+
if (e.D.type !== 'string') {
|
|
63
|
+
throw new ParseError('pdf/collection/bad-d',
|
|
64
|
+
'/D must be a string',
|
|
65
|
+
{ context: { type: e.D.type } });
|
|
66
|
+
}
|
|
67
|
+
out.initialDoc = e.D.value;
|
|
68
|
+
}
|
|
69
|
+
if (e.View) {
|
|
70
|
+
if (e.View.type !== 'name' || !VIEW_MODES.has(e.View.value)) {
|
|
71
|
+
throw new ParseError('pdf/collection/bad-view',
|
|
72
|
+
'/View must be one of D, T, H, C',
|
|
73
|
+
{ context: { actual: e.View.value } });
|
|
74
|
+
}
|
|
75
|
+
out.view = e.View.value;
|
|
76
|
+
}
|
|
77
|
+
if (e.Sort) {
|
|
78
|
+
if (e.Sort.type !== 'dict') {
|
|
79
|
+
throw new ParseError('pdf/collection/bad-sort',
|
|
80
|
+
'/Sort must be a dictionary',
|
|
81
|
+
{ context: { type: e.Sort.type } });
|
|
82
|
+
}
|
|
83
|
+
out.sort = e.Sort;
|
|
84
|
+
}
|
|
85
|
+
if (e.Navigator) {
|
|
86
|
+
if (e.Navigator.type !== 'dict' && e.Navigator.type !== 'ref') {
|
|
87
|
+
throw new ParseError('pdf/collection/bad-navigator',
|
|
88
|
+
'/Navigator must be a dict or ref',
|
|
89
|
+
{ context: { type: e.Navigator.type } });
|
|
90
|
+
}
|
|
91
|
+
out.navigator = e.Navigator;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const k of Object.keys(e)) {
|
|
95
|
+
if (!KNOWN.has(k)) out._extras[k] = e[k];
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { typeCollection };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Copyright (c) 2026 AwaCloud SAS
|
|
2
|
+
// Author: Matthieu Bouilloux
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
4
|
+
// Dual-licensed; see the NOTICE file for licensing and any additional terms.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @fileoverview Embedded File stream typing per ISO 32000-2:2020 §7.11.4.
|
|
8
|
+
*
|
|
9
|
+
* An embedded file stream has `/Type /EmbeddedFile`, an optional
|
|
10
|
+
* `/Subtype` (MIME type encoded as a name), and an optional `/Params`
|
|
11
|
+
* sub-dictionary holding `/Size`, `/CreationDate`, `/ModDate`, and
|
|
12
|
+
* `/CheckSum` (a 16-byte MD5 hash as a hex/byte string).
|
|
13
|
+
*
|
|
14
|
+
* @module pdf/embedded/embeddedFile
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Module factory.
|
|
19
|
+
*/
|
|
20
|
+
import { pdfErrors } from '../errors.js';
|
|
21
|
+
import { pdfParser } from '../syntax/parser.js';
|
|
22
|
+
|
|
23
|
+
export const pdfEmbeddedFile = {
|
|
24
|
+
name: 'pdfEmbeddedFile',
|
|
25
|
+
dependencies: ['pdfErrors', 'pdfParser'],
|
|
26
|
+
deps: [pdfErrors, pdfParser],
|
|
27
|
+
factory(errors, parser) {
|
|
28
|
+
const { ParseError } = errors;
|
|
29
|
+
const { isType } = parser;
|
|
30
|
+
|
|
31
|
+
const KNOWN_PARAMS = new Set(['Size', 'CreationDate', 'ModDate', 'CheckSum']);
|
|
32
|
+
|
|
33
|
+
function typeEmbeddedFile(stream) {
|
|
34
|
+
if (!isType(stream, 'stream')) {
|
|
35
|
+
throw new ParseError('pdf/embedded/not-stream',
|
|
36
|
+
'embedded file must be a stream',
|
|
37
|
+
{ context: { type: stream && stream.type } });
|
|
38
|
+
}
|
|
39
|
+
if (!stream.dict || stream.dict.type !== 'dict') {
|
|
40
|
+
throw new ParseError('pdf/embedded/no-dict',
|
|
41
|
+
'embedded file stream missing its dict');
|
|
42
|
+
}
|
|
43
|
+
const e = stream.dict.entries;
|
|
44
|
+
if (e.Type && (e.Type.type !== 'name' || e.Type.value !== 'EmbeddedFile')) {
|
|
45
|
+
throw new ParseError('pdf/embedded/bad-type',
|
|
46
|
+
'/Type must be /EmbeddedFile',
|
|
47
|
+
{ context: { actual: e.Type.value } });
|
|
48
|
+
}
|
|
49
|
+
const out = { bytes: stream.raw, raw: stream, _extras: {} };
|
|
50
|
+
if (e.Subtype) {
|
|
51
|
+
if (e.Subtype.type !== 'name') {
|
|
52
|
+
throw new ParseError('pdf/embedded/bad-subtype',
|
|
53
|
+
'/Subtype must be a name',
|
|
54
|
+
{ context: { type: e.Subtype.type } });
|
|
55
|
+
}
|
|
56
|
+
out.subtype = e.Subtype.value;
|
|
57
|
+
}
|
|
58
|
+
if (e.Params) {
|
|
59
|
+
if (e.Params.type !== 'dict') {
|
|
60
|
+
throw new ParseError('pdf/embedded/bad-params',
|
|
61
|
+
'/Params must be a dictionary',
|
|
62
|
+
{ context: { type: e.Params.type } });
|
|
63
|
+
}
|
|
64
|
+
out.params = typeParams(e.Params);
|
|
65
|
+
}
|
|
66
|
+
for (const k of Object.keys(e)) {
|
|
67
|
+
if (k !== 'Type' && k !== 'Subtype' && k !== 'Params'
|
|
68
|
+
&& k !== 'Length' && k !== 'Filter' && k !== 'DecodeParms'
|
|
69
|
+
&& k !== 'DL') {
|
|
70
|
+
out._extras[k] = e[k];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function typeParams(dict) {
|
|
77
|
+
const e = dict.entries;
|
|
78
|
+
const out = { raw: dict, _extras: {} };
|
|
79
|
+
if (e.Size && (e.Size.type === 'int' || e.Size.type === 'real')) {
|
|
80
|
+
out.size = e.Size.value | 0;
|
|
81
|
+
}
|
|
82
|
+
if (e.CreationDate && e.CreationDate.type === 'string') {
|
|
83
|
+
out.creationDate = e.CreationDate.value;
|
|
84
|
+
}
|
|
85
|
+
if (e.ModDate && e.ModDate.type === 'string') {
|
|
86
|
+
out.modDate = e.ModDate.value;
|
|
87
|
+
}
|
|
88
|
+
if (e.CheckSum && e.CheckSum.type === 'string') {
|
|
89
|
+
out.checkSum = e.CheckSum.value;
|
|
90
|
+
}
|
|
91
|
+
for (const k of Object.keys(e)) {
|
|
92
|
+
if (!KNOWN_PARAMS.has(k)) out._extras[k] = e[k];
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { typeEmbeddedFile };
|
|
98
|
+
}
|
|
99
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Copyright (c) 2026 AwaCloud SAS
|
|
2
|
+
// Author: Matthieu Bouilloux
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
4
|
+
// Dual-licensed; see the NOTICE file for licensing and any additional terms.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @fileoverview File Specification typing per ISO 32000-2:2020 §7.11.
|
|
8
|
+
*
|
|
9
|
+
* A File Specification (`/Type /Filespec` or legacy `/Type /F`) refers
|
|
10
|
+
* to an external or embedded file. Entries:
|
|
11
|
+
* /FS — file system (URL or absent for local).
|
|
12
|
+
* /F /UF /DOS /Mac /Unix — file path forms (UF preferred since 1.7).
|
|
13
|
+
* /ID — array of two byte-strings (uniqueness).
|
|
14
|
+
* /V — boolean, volatile.
|
|
15
|
+
* /EF — dict of embedded file streams (F, UF, DOS, …).
|
|
16
|
+
* /RF — related files (dict of arrays).
|
|
17
|
+
* /Desc — description string.
|
|
18
|
+
* /CI — collection item dict.
|
|
19
|
+
* /AFRelationship — name; PDF 2.0 Associated Files relationship.
|
|
20
|
+
*
|
|
21
|
+
* @module pdf/embedded/fileSpec
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Module factory.
|
|
26
|
+
*/
|
|
27
|
+
import { pdfErrors } from '../errors.js';
|
|
28
|
+
import { pdfParser } from '../syntax/parser.js';
|
|
29
|
+
|
|
30
|
+
export const pdfFileSpec = {
|
|
31
|
+
name: 'pdfFileSpec',
|
|
32
|
+
dependencies: ['pdfErrors', 'pdfParser'],
|
|
33
|
+
deps: [pdfErrors, pdfParser],
|
|
34
|
+
factory(errors, parser) {
|
|
35
|
+
const { ParseError } = errors;
|
|
36
|
+
const { isType } = parser;
|
|
37
|
+
|
|
38
|
+
const KNOWN = new Set([
|
|
39
|
+
'Type', 'FS', 'F', 'UF', 'DOS', 'Mac', 'Unix', 'ID', 'V',
|
|
40
|
+
'EF', 'RF', 'Desc', 'CI', 'AFRelationship', 'Thumb'
|
|
41
|
+
]);
|
|
42
|
+
const REL = new Set([
|
|
43
|
+
'Source', 'Data', 'Alternative', 'Supplement',
|
|
44
|
+
'EncryptedPayload', 'FormData', 'Schema', 'Unspecified'
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
function typeFileSpec(dict) {
|
|
48
|
+
if (!isType(dict, 'dict')) {
|
|
49
|
+
throw new ParseError('pdf/filespec/not-dict',
|
|
50
|
+
'File spec must be a dictionary',
|
|
51
|
+
{ context: { type: dict && dict.type } });
|
|
52
|
+
}
|
|
53
|
+
const e = dict.entries;
|
|
54
|
+
if (e.Type && e.Type.type === 'name'
|
|
55
|
+
&& e.Type.value !== 'Filespec' && e.Type.value !== 'F') {
|
|
56
|
+
throw new ParseError('pdf/filespec/bad-type',
|
|
57
|
+
'/Type must be /Filespec or /F when present',
|
|
58
|
+
{ context: { actual: e.Type.value } });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const out = { raw: dict, _extras: {} };
|
|
62
|
+
|
|
63
|
+
if (e.FS) {
|
|
64
|
+
if (e.FS.type !== 'name') {
|
|
65
|
+
throw new ParseError('pdf/filespec/bad-fs',
|
|
66
|
+
'/FS must be a name', { context: { type: e.FS.type } });
|
|
67
|
+
}
|
|
68
|
+
out.fs = e.FS.value;
|
|
69
|
+
}
|
|
70
|
+
for (const k of ['F', 'UF', 'DOS', 'Mac', 'Unix']) {
|
|
71
|
+
if (e[k]) {
|
|
72
|
+
if (e[k].type !== 'string') {
|
|
73
|
+
throw new ParseError('pdf/filespec/bad-path',
|
|
74
|
+
'/' + k + ' must be a string',
|
|
75
|
+
{ context: { type: e[k].type } });
|
|
76
|
+
}
|
|
77
|
+
out[k.toLowerCase() === 'f' ? 'f' : k.toLowerCase()] = e[k].value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (e.ID) {
|
|
81
|
+
if (e.ID.type !== 'array') {
|
|
82
|
+
throw new ParseError('pdf/filespec/bad-id',
|
|
83
|
+
'/ID must be an array', { context: { type: e.ID.type } });
|
|
84
|
+
}
|
|
85
|
+
out.id = e.ID.items.filter((x) => x.type === 'string').map((x) => x.value);
|
|
86
|
+
}
|
|
87
|
+
if (e.V && e.V.type === 'bool') out.volatile = e.V.value;
|
|
88
|
+
if (e.EF) {
|
|
89
|
+
if (e.EF.type !== 'dict') {
|
|
90
|
+
throw new ParseError('pdf/filespec/bad-ef',
|
|
91
|
+
'/EF must be a dictionary',
|
|
92
|
+
{ context: { type: e.EF.type } });
|
|
93
|
+
}
|
|
94
|
+
out.embedded = e.EF.entries;
|
|
95
|
+
}
|
|
96
|
+
if (e.RF) {
|
|
97
|
+
if (e.RF.type !== 'dict') {
|
|
98
|
+
throw new ParseError('pdf/filespec/bad-rf',
|
|
99
|
+
'/RF must be a dictionary',
|
|
100
|
+
{ context: { type: e.RF.type } });
|
|
101
|
+
}
|
|
102
|
+
out.related = e.RF.entries;
|
|
103
|
+
}
|
|
104
|
+
if (e.Desc) {
|
|
105
|
+
if (e.Desc.type !== 'string') {
|
|
106
|
+
throw new ParseError('pdf/filespec/bad-desc',
|
|
107
|
+
'/Desc must be a string',
|
|
108
|
+
{ context: { type: e.Desc.type } });
|
|
109
|
+
}
|
|
110
|
+
out.desc = e.Desc.value;
|
|
111
|
+
}
|
|
112
|
+
if (e.CI) out.ci = e.CI;
|
|
113
|
+
if (e.AFRelationship) {
|
|
114
|
+
if (e.AFRelationship.type !== 'name') {
|
|
115
|
+
throw new ParseError('pdf/filespec/bad-afrel',
|
|
116
|
+
'/AFRelationship must be a name',
|
|
117
|
+
{ context: { type: e.AFRelationship.type } });
|
|
118
|
+
}
|
|
119
|
+
const v = e.AFRelationship.value;
|
|
120
|
+
out.afRelationship = v;
|
|
121
|
+
out.afRelationshipStandard = REL.has(v);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!out.f && !out.uf && !out.dos && !out.mac && !out.unix && !out.embedded) {
|
|
125
|
+
throw new ParseError('pdf/filespec/empty',
|
|
126
|
+
'File spec has no path or embedded file');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const k of Object.keys(e)) {
|
|
130
|
+
if (!KNOWN.has(k)) out._extras[k] = e[k];
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { typeFileSpec };
|
|
136
|
+
}
|
|
137
|
+
};
|