@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,514 @@
|
|
|
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 Incremental update writer.
|
|
8
|
+
*
|
|
9
|
+
* `appendIncremental(pdfBytes, updates)` produces a new PDF that
|
|
10
|
+
* carries the original bytes verbatim followed by an "incremental
|
|
11
|
+
* update" section per ISO 32000-2:2020 §7.5.6:
|
|
12
|
+
*
|
|
13
|
+
* originalBytes ‖ newObjects ‖ xref ‖ trailer (with /Prev) ‖ %%EOF
|
|
14
|
+
*
|
|
15
|
+
* Strategy:
|
|
16
|
+
*
|
|
17
|
+
* 1. Locate the previous `startxref` offset in `pdfBytes` so the new
|
|
18
|
+
* trailer can carry `/Prev N`.
|
|
19
|
+
* 2. Locate the previous trailer dict (via `parseTrailerDict`) to
|
|
20
|
+
* carry forward `/Root`, `/Info`, `/Size`, `/ID` unless overridden.
|
|
21
|
+
* 3. Emit each `update` indirect (same shape as `pdfWriter.writeDocument`
|
|
22
|
+
* items: `{ num, gen, value }`) starting at the end of the original
|
|
23
|
+
* bytes; record byte offsets.
|
|
24
|
+
* 4. Emit a fresh classical xref table covering only the updated
|
|
25
|
+
* object numbers (one subsection per contiguous run). Object 0 is
|
|
26
|
+
* only emitted in the section starting at 0; otherwise we emit only
|
|
27
|
+
* the modified runs (per §7.5.6 — incremental xref sections do not
|
|
28
|
+
* need to cover object 0).
|
|
29
|
+
* 5. Emit a trailer with `/Prev = previousXrefOffset`, updated
|
|
30
|
+
* `/Size = max(prev.Size, newMaxNum + 1)`, and carry-over
|
|
31
|
+
* `/Root` / `/Info` / `/ID` if any.
|
|
32
|
+
*
|
|
33
|
+
* The PDF reader (`pdfDocument.readDocument`) already follows the
|
|
34
|
+
* `/Prev` chain — the new entries win over older ones automatically.
|
|
35
|
+
*
|
|
36
|
+
* Section form: the form of the update follows the form of the
|
|
37
|
+
* base's NEWEST section — the one `startxref` designates.
|
|
38
|
+
*
|
|
39
|
+
* - Classical table there → steps 4-5 above, byte-identical to the
|
|
40
|
+
* writer's original, table-only output (only the newest trailer is typed).
|
|
41
|
+
* - `/Type /XRef` stream there → the update ends with an uncompressed
|
|
42
|
+
* cross-reference stream (`pdfXref.buildXrefStream`) carrying `/W`,
|
|
43
|
+
* `/Index`, `/Size`, `/Prev`, `/Root` (+ `/Info`, `/ID`). `/Root`,
|
|
44
|
+
* `/Info`, `/ID`, `/Size` default to the newest-first merge of every
|
|
45
|
+
* section's dict, as `readDocument` types it (a linearized file's
|
|
46
|
+
* main stream lacks `/Root`; the first-page one carries it).
|
|
47
|
+
* - Hybrid-reference base (a classical trailer with `/XRefStm`, anywhere
|
|
48
|
+
* in the chain) → refused, `pdf/incremental/hybrid-base` (two
|
|
49
|
+
* conforming readers may resolve different objects in such a file).
|
|
50
|
+
* - Neither form at `startxref` → refused,
|
|
51
|
+
* `pdf/incremental/unsupported-base`.
|
|
52
|
+
*
|
|
53
|
+
* Constraints / scope:
|
|
54
|
+
*
|
|
55
|
+
* - No ObjStm grouping (see Item #7b).
|
|
56
|
+
* - Does not patch existing objects in-place — only appends.
|
|
57
|
+
*
|
|
58
|
+
* @module pdf/document/incrementalWriter
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Module factory — worker-safe, self-contained.
|
|
63
|
+
*/
|
|
64
|
+
import { pdfErrors } from '../errors.js';
|
|
65
|
+
import { pdfSerializer } from '../syntax/serializer.js';
|
|
66
|
+
import { pdfTokenizer } from '../syntax/tokenizer.js';
|
|
67
|
+
import { pdfParser } from '../syntax/parser.js';
|
|
68
|
+
import { pdfXref } from '../syntax/xref.js';
|
|
69
|
+
import { pdfTrailer } from '../syntax/trailer.js';
|
|
70
|
+
|
|
71
|
+
export const pdfIncrementalWriter = {
|
|
72
|
+
name: 'pdfIncrementalWriter',
|
|
73
|
+
dependencies: [
|
|
74
|
+
'pdfErrors',
|
|
75
|
+
'pdfSerializer',
|
|
76
|
+
'pdfTokenizer',
|
|
77
|
+
'pdfParser',
|
|
78
|
+
'pdfXref',
|
|
79
|
+
'pdfTrailer'
|
|
80
|
+
],
|
|
81
|
+
deps: [pdfErrors, pdfSerializer, pdfTokenizer, pdfParser, pdfXref, pdfTrailer],
|
|
82
|
+
factory(errors, serializerMod, tokenizerMod, parserMod, xrefMod, trailerMod) {
|
|
83
|
+
const { RenderError, ParseError } = errors;
|
|
84
|
+
const serializeIndirect = serializerMod.serializeIndirect;
|
|
85
|
+
const serializeObject = serializerMod.serializeObject;
|
|
86
|
+
const locateStartXref = xrefMod.locateStartXref;
|
|
87
|
+
const readStartXref = xrefMod.readStartXref;
|
|
88
|
+
const parseXrefTable = xrefMod.parseXrefTable;
|
|
89
|
+
const parseTrailerDict = xrefMod.parseTrailerDict;
|
|
90
|
+
const readXrefStreamDict = xrefMod.readXrefStreamDict;
|
|
91
|
+
const buildXrefStream = xrefMod.buildXrefStream;
|
|
92
|
+
const typeTrailer = trailerMod.typeTrailer;
|
|
93
|
+
|
|
94
|
+
// Trailer / xref-stream dict keys that describe ONE section only
|
|
95
|
+
// (§7.5.5, §7.5.8.2) — same set as pdfDocument's trailer merge.
|
|
96
|
+
const SECTION_LOCAL_KEYS = new Set([
|
|
97
|
+
'Prev', 'XRefStm', 'Type', 'W', 'Index', 'Length',
|
|
98
|
+
'Filter', 'DecodeParms', 'F', 'FFilter', 'FDecodeParms', 'DL'
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const te = new TextEncoder();
|
|
102
|
+
|
|
103
|
+
function pad10(n) { return String(n).padStart(10, '0'); }
|
|
104
|
+
|
|
105
|
+
function hexLit(bytes) {
|
|
106
|
+
const H = '0123456789ABCDEF';
|
|
107
|
+
let s = '<';
|
|
108
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
109
|
+
s += H[bytes[i] >> 4] + H[bytes[i] & 0xF];
|
|
110
|
+
}
|
|
111
|
+
return s + '>';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function concat(arrays) {
|
|
115
|
+
let n = 0;
|
|
116
|
+
for (const a of arrays) n += a.length;
|
|
117
|
+
const out = new Uint8Array(n);
|
|
118
|
+
let o = 0;
|
|
119
|
+
for (const a of arrays) { out.set(a, o); o += a.length; }
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildXrefSections(offsets, gens) {
|
|
124
|
+
// offsets : Map<num, byteOffset>
|
|
125
|
+
// gens : Map<num, gen>
|
|
126
|
+
// Always start with the free-list head (object 0).
|
|
127
|
+
const nums = Array.from(offsets.keys()).sort((a, b) => a - b);
|
|
128
|
+
const sections = [];
|
|
129
|
+
|
|
130
|
+
// Object 0 subsection: emit just the head (1 entry).
|
|
131
|
+
sections.push({ first: 0, count: 1, entries: ['0000000000 65535 f \n'] });
|
|
132
|
+
|
|
133
|
+
// Contiguous runs of updated nums.
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < nums.length) {
|
|
136
|
+
let j = i;
|
|
137
|
+
while (j + 1 < nums.length && nums[j + 1] === nums[j] + 1) j++;
|
|
138
|
+
const entries = [];
|
|
139
|
+
for (let k = i; k <= j; k++) {
|
|
140
|
+
const n = nums[k];
|
|
141
|
+
const g = gens.get(n) | 0;
|
|
142
|
+
entries.push(`${pad10(offsets.get(n))} ${String(g).padStart(5, '0')} n \n`);
|
|
143
|
+
}
|
|
144
|
+
sections.push({ first: nums[i], count: (j - i + 1), entries });
|
|
145
|
+
i = j + 1;
|
|
146
|
+
}
|
|
147
|
+
let s = 'xref\n';
|
|
148
|
+
for (const sec of sections) {
|
|
149
|
+
s += `${sec.first} ${sec.count}\n`;
|
|
150
|
+
for (const e of sec.entries) s += e;
|
|
151
|
+
}
|
|
152
|
+
return te.encode(s);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildTrailer(opts) {
|
|
156
|
+
const parts = [`trailer\n<< /Size ${opts.size}`];
|
|
157
|
+
parts.push(` /Root ${opts.root.num} ${opts.root.gen | 0} R`);
|
|
158
|
+
if (opts.info) parts.push(` /Info ${opts.info.num} ${opts.info.gen | 0} R`);
|
|
159
|
+
if (opts.id) {
|
|
160
|
+
parts.push(' /ID [');
|
|
161
|
+
parts.push(hexLit(opts.id[0]));
|
|
162
|
+
parts.push(hexLit(opts.id[1]));
|
|
163
|
+
parts.push(']');
|
|
164
|
+
}
|
|
165
|
+
// An update over an encrypted document repeats its /Encrypt
|
|
166
|
+
// (ISO 32000-2 §7.5.6). An indirect reference is the usual
|
|
167
|
+
// form; a direct dictionary is serialised like an object body
|
|
168
|
+
// (its strings are binary, so it is spliced in as bytes).
|
|
169
|
+
let directEncrypt = null;
|
|
170
|
+
if (opts.encrypt) {
|
|
171
|
+
if (Number.isInteger(opts.encrypt.num)) {
|
|
172
|
+
parts.push(` /Encrypt ${opts.encrypt.num} ${opts.encrypt.gen | 0} R`);
|
|
173
|
+
} else {
|
|
174
|
+
directEncrypt = serializeObject(opts.encrypt);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const tail = ` /Prev ${opts.prev} >>\n`
|
|
178
|
+
+ `startxref\n${opts.xrefOffset}\n%%EOF\n`;
|
|
179
|
+
if (directEncrypt) {
|
|
180
|
+
parts.push(' /Encrypt ');
|
|
181
|
+
return concat([te.encode(parts.join('')), directEncrypt, te.encode(tail)]);
|
|
182
|
+
}
|
|
183
|
+
parts.push(tail);
|
|
184
|
+
return te.encode(parts.join(''));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Skip the PDF white-space run (§7.2.3) starting at `at`. */
|
|
188
|
+
function skipWhitespace(bytes, at) {
|
|
189
|
+
let p = at < 0 ? 0 : at;
|
|
190
|
+
while (p < bytes.length) {
|
|
191
|
+
const b = bytes[p];
|
|
192
|
+
if (b === 0x00 || b === 0x09 || b === 0x0A
|
|
193
|
+
|| b === 0x0C || b === 0x0D || b === 0x20) { p++; continue; }
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
return p;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** True when the bytes at `at` (after white space) spell `xref`. */
|
|
200
|
+
function startsXrefTable(bytes, at) {
|
|
201
|
+
const p = skipWhitespace(bytes, at);
|
|
202
|
+
return bytes[p] === 0x78 && bytes[p + 1] === 0x72
|
|
203
|
+
&& bytes[p + 2] === 0x65 && bytes[p + 3] === 0x66;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Walk the cross-reference chain from `at` (newest first) and
|
|
208
|
+
* return each section's form and trailer dict. Tolerant: the walk
|
|
209
|
+
* stops at the first section that does not parse, so an older
|
|
210
|
+
* damaged section never changes what the newest one yields.
|
|
211
|
+
*/
|
|
212
|
+
function walkSections(pdfBytes, at) {
|
|
213
|
+
const out = [];
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
let cursor = at;
|
|
216
|
+
for (let safety = 0; safety < 32 && cursor >= 0; safety++) {
|
|
217
|
+
if (seen.has(cursor)) break;
|
|
218
|
+
seen.add(cursor);
|
|
219
|
+
let kind, dict;
|
|
220
|
+
try {
|
|
221
|
+
if (startsXrefTable(pdfBytes, cursor)) {
|
|
222
|
+
const section = parseXrefTable(pdfBytes, cursor);
|
|
223
|
+
dict = parseTrailerDict(pdfBytes, section.end).dict;
|
|
224
|
+
kind = 'table';
|
|
225
|
+
} else {
|
|
226
|
+
dict = readXrefStreamDict(pdfBytes, cursor).dict;
|
|
227
|
+
kind = 'stream';
|
|
228
|
+
}
|
|
229
|
+
} catch (_) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
out.push({ at: cursor, kind, dict });
|
|
233
|
+
const prev = dict && dict.type === 'dict' && dict.entries.Prev;
|
|
234
|
+
if (prev && prev.type === 'int' && prev.value >= 0
|
|
235
|
+
&& prev.value !== cursor) {
|
|
236
|
+
cursor = prev.value;
|
|
237
|
+
} else break;
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Newest-first merge of the section dicts (same rule as
|
|
244
|
+
* `pdfDocument.readDocument`): each document key comes
|
|
245
|
+
* from the newest section that carries it; section-local keys are
|
|
246
|
+
* never merged.
|
|
247
|
+
*/
|
|
248
|
+
function mergeSectionDicts(sections) {
|
|
249
|
+
const entries = {};
|
|
250
|
+
for (const s of sections) {
|
|
251
|
+
if (!s.dict || s.dict.type !== 'dict') continue;
|
|
252
|
+
for (const k of Object.keys(s.dict.entries)) {
|
|
253
|
+
if (SECTION_LOCAL_KEYS.has(k)) continue;
|
|
254
|
+
if (!(k in entries)) entries[k] = s.dict.entries[k];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { type: 'dict', entries };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Hybrid-reference file (§7.5.8.4): a classical trailer carrying
|
|
262
|
+
* `/XRefStm`. Refused: two conforming readers may resolve different objects in such a
|
|
263
|
+
* file, so an update over it could read differently per viewer.
|
|
264
|
+
*/
|
|
265
|
+
function refuseHybrid(sections) {
|
|
266
|
+
for (const s of sections) {
|
|
267
|
+
const stm = s.kind === 'table' && s.dict && s.dict.type === 'dict'
|
|
268
|
+
&& s.dict.entries.XRefStm;
|
|
269
|
+
if (stm) {
|
|
270
|
+
throw new RenderError('pdf/incremental/hybrid-base',
|
|
271
|
+
'appendIncremental refuses a hybrid-reference base: the '
|
|
272
|
+
+ `classical xref section at offset ${s.at} carries /XRefStm `
|
|
273
|
+
+ '(a companion cross-reference stream); updating it could '
|
|
274
|
+
+ 'resolve differently in table-only and stream-aware readers',
|
|
275
|
+
{ context: { offset: s.at, xrefStm: stm.value } });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function readPrevTrailer(pdfBytes) {
|
|
281
|
+
// Mirror of pdfDocument.readDocument's xref-locating logic.
|
|
282
|
+
const sxAt = locateStartXref(pdfBytes);
|
|
283
|
+
if (sxAt < 0) {
|
|
284
|
+
throw new ParseError('pdf/incremental/no-startxref',
|
|
285
|
+
'pdfBytes has no startxref — not a valid PDF');
|
|
286
|
+
}
|
|
287
|
+
const prevXrefOffset = readStartXref(pdfBytes, sxAt);
|
|
288
|
+
let prev = { xrefOffset: prevXrefOffset, trailer: null, form: 'table' };
|
|
289
|
+
if (startsXrefTable(pdfBytes, prevXrefOffset)) {
|
|
290
|
+
// Classical base: the newest trailer alone is typed, exactly
|
|
291
|
+
// as the original table-only writer did — this path's output
|
|
292
|
+
// is byte-identical.
|
|
293
|
+
try {
|
|
294
|
+
const section = parseXrefTable(pdfBytes, prevXrefOffset);
|
|
295
|
+
const { dict } = parseTrailerDict(pdfBytes, section.end);
|
|
296
|
+
prev.trailer = typeTrailer(dict);
|
|
297
|
+
} catch (_) {
|
|
298
|
+
// unreadable — leave trailer null (opts.root required).
|
|
299
|
+
}
|
|
300
|
+
refuseHybrid(walkSections(pdfBytes, prevXrefOffset));
|
|
301
|
+
return prev;
|
|
302
|
+
}
|
|
303
|
+
// Stream base: the newest section must be a
|
|
304
|
+
// /Type /XRef stream; anything else is not a base this writer
|
|
305
|
+
// can extend.
|
|
306
|
+
try {
|
|
307
|
+
readXrefStreamDict(pdfBytes, prevXrefOffset);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
throw new ParseError('pdf/incremental/unsupported-base',
|
|
310
|
+
'appendIncremental cannot extend this base: startxref '
|
|
311
|
+
+ `designates offset ${prevXrefOffset}, which starts neither a `
|
|
312
|
+
+ 'classical xref table nor a /Type /XRef cross-reference stream',
|
|
313
|
+
{ context: { offset: prevXrefOffset, cause: e && e.code } });
|
|
314
|
+
}
|
|
315
|
+
const sections = walkSections(pdfBytes, prevXrefOffset);
|
|
316
|
+
refuseHybrid(sections);
|
|
317
|
+
prev.form = 'stream';
|
|
318
|
+
try {
|
|
319
|
+
prev.trailer = typeTrailer(mergeSectionDicts(sections));
|
|
320
|
+
} catch (_) {
|
|
321
|
+
// no /Root or /Size anywhere in the chain — opts.root required.
|
|
322
|
+
}
|
|
323
|
+
return prev;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function maxNumOf(updates) {
|
|
327
|
+
let m = 0;
|
|
328
|
+
for (const it of updates) {
|
|
329
|
+
if (it && Number.isFinite(it.num) && it.num > m) m = it.num;
|
|
330
|
+
}
|
|
331
|
+
return m;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* readBaseTrailer(pdfBytes) — the trailer an update over `pdfBytes`
|
|
336
|
+
* starts from, without writing anything.
|
|
337
|
+
*
|
|
338
|
+
* Walks the whole cross-reference chain from `startxref` (newest
|
|
339
|
+
* first) and types the newest-first MERGE of every section's dict
|
|
340
|
+
* (the rule `pdfDocument.readDocument` applies) — for a
|
|
341
|
+
* classical base too, so a caller allocating object numbers sees
|
|
342
|
+
* the same `/Size` a reader sees. The base is vetted exactly as
|
|
343
|
+
* `appendIncremental` vets it: a hybrid-reference base throws
|
|
344
|
+
* `pdf/incremental/hybrid-base`, a `startxref` designating neither
|
|
345
|
+
* form throws `pdf/incremental/unsupported-base`.
|
|
346
|
+
*
|
|
347
|
+
* Returns `{ form: 'table' | 'stream', xrefOffset, trailer }` where
|
|
348
|
+
* `trailer` is the typed merged trailer (`{ size, root, info?, id?,
|
|
349
|
+
* … }`, see `pdfTrailer.typeTrailer`) or `null` when no section
|
|
350
|
+
* supplies a usable `/Size` + `/Root`.
|
|
351
|
+
*/
|
|
352
|
+
function readBaseTrailer(pdfBytes) {
|
|
353
|
+
if (!(pdfBytes instanceof Uint8Array)) {
|
|
354
|
+
throw new RenderError('pdf/incremental/bad-input',
|
|
355
|
+
'readBaseTrailer expects a Uint8Array');
|
|
356
|
+
}
|
|
357
|
+
const prev = readPrevTrailer(pdfBytes);
|
|
358
|
+
let trailer = null;
|
|
359
|
+
try {
|
|
360
|
+
trailer = typeTrailer(mergeSectionDicts(
|
|
361
|
+
walkSections(pdfBytes, prev.xrefOffset)));
|
|
362
|
+
} catch (_) {
|
|
363
|
+
// no /Root or /Size anywhere in the chain.
|
|
364
|
+
}
|
|
365
|
+
return { form: prev.form, xrefOffset: prev.xrefOffset, trailer };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* appendIncremental(pdfBytes, opts) — append an incremental
|
|
370
|
+
* update.
|
|
371
|
+
*
|
|
372
|
+
* opts:
|
|
373
|
+
* updates : Array<{ num, gen?, value }> — required
|
|
374
|
+
* root : { num, gen } | undefined — defaults to prev /Root
|
|
375
|
+
* info : { num, gen } | undefined — defaults to prev /Info
|
|
376
|
+
* id : [Uint8Array, Uint8Array] — defaults to prev /ID
|
|
377
|
+
* size : number | undefined — defaults to max(prev.Size, newMax+1)
|
|
378
|
+
* encrypt : { num, gen } | undefined — the base's /Encrypt, repeated
|
|
379
|
+
* in the update's trailer (or cross-reference stream dict)
|
|
380
|
+
* right after /ID; required for a conforming update over
|
|
381
|
+
* an encrypted document (ISO 32000-2 §7.5.6). Never
|
|
382
|
+
* defaulted from the base; absent → output unchanged. A
|
|
383
|
+
* direct dictionary (typed `{ type: 'dict' }`) is written
|
|
384
|
+
* verbatim in a classical trailer; a cross-reference
|
|
385
|
+
* stream update refuses it (`pdf/xref/bad-stream-section`).
|
|
386
|
+
*
|
|
387
|
+
* Over a stream base the cross-reference stream object takes number
|
|
388
|
+
* max(size, prev.Size, newMax+1) and the written /Size is one more.
|
|
389
|
+
*
|
|
390
|
+
* Returns Uint8Array.
|
|
391
|
+
*/
|
|
392
|
+
function appendIncremental(pdfBytes, opts) {
|
|
393
|
+
return appendSection(pdfBytes, opts).bytes;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* appendIncrementalWithOffsets(pdfBytes, opts) — exactly
|
|
398
|
+
* `appendIncremental` (same options, same validation, byte-identical
|
|
399
|
+
* output), also reporting where each appended object landed.
|
|
400
|
+
*
|
|
401
|
+
* Returns `{ bytes, offsets, xrefOffset }`: `offsets` is a
|
|
402
|
+
* `Map<num, byteOffset>` giving the absolute offset of each update's
|
|
403
|
+
* `num gen obj` header in `bytes`; `xrefOffset` is where the update's
|
|
404
|
+
* cross-reference section (table or stream object) starts — the
|
|
405
|
+
* offset the new `startxref` records.
|
|
406
|
+
*/
|
|
407
|
+
function appendIncrementalWithOffsets(pdfBytes, opts) {
|
|
408
|
+
return appendSection(pdfBytes, opts);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Shared body of `appendIncremental` / `appendIncrementalWithOffsets`. */
|
|
412
|
+
function appendSection(pdfBytes, opts) {
|
|
413
|
+
if (!(pdfBytes instanceof Uint8Array)) {
|
|
414
|
+
throw new RenderError('pdf/incremental/bad-input',
|
|
415
|
+
'appendIncremental expects (Uint8Array, opts)');
|
|
416
|
+
}
|
|
417
|
+
if (!opts || !Array.isArray(opts.updates)) {
|
|
418
|
+
throw new RenderError('pdf/incremental/no-updates',
|
|
419
|
+
'opts.updates must be an array');
|
|
420
|
+
}
|
|
421
|
+
const updates = opts.updates.slice().sort((a, b) => a.num - b.num);
|
|
422
|
+
for (const it of updates) {
|
|
423
|
+
if (!it || !Number.isFinite(it.num) || it.num < 1) {
|
|
424
|
+
throw new RenderError('pdf/incremental/bad-update',
|
|
425
|
+
'each update needs num >= 1',
|
|
426
|
+
{ context: { item: it } });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const prev = readPrevTrailer(pdfBytes);
|
|
431
|
+
const root = opts.root
|
|
432
|
+
|| (prev.trailer && prev.trailer.root)
|
|
433
|
+
|| null;
|
|
434
|
+
if (!root || !Number.isFinite(root.num)) {
|
|
435
|
+
throw new RenderError('pdf/incremental/no-root',
|
|
436
|
+
'opts.root required when previous trailer cannot be read');
|
|
437
|
+
}
|
|
438
|
+
const info = opts.info
|
|
439
|
+
|| (prev.trailer && prev.trailer.info)
|
|
440
|
+
|| null;
|
|
441
|
+
const id = opts.id
|
|
442
|
+
|| (prev.trailer && prev.trailer.id)
|
|
443
|
+
|| null;
|
|
444
|
+
const prevSize = (prev.trailer && Number.isFinite(prev.trailer.size))
|
|
445
|
+
? prev.trailer.size : 0;
|
|
446
|
+
|
|
447
|
+
// Emit body: starting cursor = end of original bytes.
|
|
448
|
+
// PDF spec recommends a leading newline before incremental
|
|
449
|
+
// section if the previous file did not end with one. We
|
|
450
|
+
// always insert one to be safe.
|
|
451
|
+
const parts = [pdfBytes];
|
|
452
|
+
let cursor = pdfBytes.length;
|
|
453
|
+
const offsets = new Map();
|
|
454
|
+
const gens = new Map();
|
|
455
|
+
|
|
456
|
+
// Ensure separator newline.
|
|
457
|
+
const sep = te.encode('\n');
|
|
458
|
+
parts.push(sep);
|
|
459
|
+
cursor += sep.length;
|
|
460
|
+
|
|
461
|
+
for (const { num, gen, value } of updates) {
|
|
462
|
+
offsets.set(num, cursor);
|
|
463
|
+
gens.set(num, gen | 0);
|
|
464
|
+
const bytes = serializeIndirect(num, gen | 0, value);
|
|
465
|
+
parts.push(bytes);
|
|
466
|
+
cursor += bytes.length;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const xrefOffset = cursor;
|
|
470
|
+
const newMax = maxNumOf(updates);
|
|
471
|
+
const size = (opts.size | 0) || Math.max(prevSize, newMax + 1);
|
|
472
|
+
|
|
473
|
+
if (prev.form === 'stream') {
|
|
474
|
+
// The base's newest section is a cross-reference
|
|
475
|
+
// stream, so the update is one too (§7.5.8) — its own
|
|
476
|
+
// object takes the first number past every other one.
|
|
477
|
+
const xrefNum = Math.max(size, prevSize, newMax + 1);
|
|
478
|
+
const entries = [];
|
|
479
|
+
for (const [num, offset] of offsets) {
|
|
480
|
+
entries.push({ num, offset, gen: gens.get(num) });
|
|
481
|
+
}
|
|
482
|
+
parts.push(buildXrefStream({
|
|
483
|
+
num: xrefNum,
|
|
484
|
+
offset: xrefOffset,
|
|
485
|
+
entries,
|
|
486
|
+
size: xrefNum + 1,
|
|
487
|
+
prev: prev.xrefOffset,
|
|
488
|
+
root,
|
|
489
|
+
info,
|
|
490
|
+
id,
|
|
491
|
+
encrypt: opts.encrypt
|
|
492
|
+
}));
|
|
493
|
+
parts.push(te.encode(`startxref\n${xrefOffset}\n%%EOF\n`));
|
|
494
|
+
return { bytes: concat(parts), offsets, xrefOffset };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
parts.push(buildXrefSections(offsets, gens));
|
|
498
|
+
|
|
499
|
+
parts.push(buildTrailer({
|
|
500
|
+
size,
|
|
501
|
+
root,
|
|
502
|
+
info,
|
|
503
|
+
id,
|
|
504
|
+
encrypt: opts.encrypt,
|
|
505
|
+
prev: prev.xrefOffset,
|
|
506
|
+
xrefOffset
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
return { bytes: concat(parts), offsets, xrefOffset };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return { appendIncremental, appendIncrementalWithOffsets, readBaseTrailer };
|
|
513
|
+
}
|
|
514
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
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 Page typing per ISO 32000-2:2020 §7.7.3.3.
|
|
8
|
+
*
|
|
9
|
+
* A Page dict (`/Type /Page`) carries `/Parent`, `/MediaBox`,
|
|
10
|
+
* `/CropBox`, `/Resources`, `/Contents`, `/Rotate`, optional
|
|
11
|
+
* `/Annots`, `/Group`, `/Thumb`, `/Tabs`, `/StructParents`, `/Metadata`,
|
|
12
|
+
* etc.
|
|
13
|
+
*
|
|
14
|
+
* `/Contents` may be a single stream ref, an array of stream refs, or
|
|
15
|
+
* — rarely — an inline stream. The typed record exposes it as an
|
|
16
|
+
* array of refs (possibly empty).
|
|
17
|
+
*
|
|
18
|
+
* `/Resources` is inheritable from the parent page tree node (§7.7.3.4)
|
|
19
|
+
* — resolution is left to the document layer.
|
|
20
|
+
*
|
|
21
|
+
* @module pdf/document/page
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Module factory — worker-safe, self-contained.
|
|
26
|
+
*/
|
|
27
|
+
import { pdfErrors } from '../errors.js';
|
|
28
|
+
import { pdfParser } from '../syntax/parser.js';
|
|
29
|
+
|
|
30
|
+
export const pdfPage = {
|
|
31
|
+
name: 'pdfPage',
|
|
32
|
+
dependencies: ['pdfErrors', 'pdfParser'],
|
|
33
|
+
deps: [pdfErrors, pdfParser],
|
|
34
|
+
factory(errors, parserMod) {
|
|
35
|
+
const { ParseError } = errors;
|
|
36
|
+
const isType = (parserMod && parserMod.isType)
|
|
37
|
+
|| ((v, kind) => !!(v && v.type === kind));
|
|
38
|
+
|
|
39
|
+
const KNOWN = new Set([
|
|
40
|
+
'Type', 'Parent', 'LastModified', 'Resources', 'MediaBox',
|
|
41
|
+
'CropBox', 'BleedBox', 'TrimBox', 'ArtBox', 'BoxColorInfo',
|
|
42
|
+
'Contents', 'Rotate', 'Group', 'Thumb', 'B', 'Dur', 'Trans',
|
|
43
|
+
'Annots', 'AA', 'Metadata', 'PieceInfo', 'StructParents',
|
|
44
|
+
'ID', 'PZ', 'SeparationInfo', 'Tabs', 'TemplateInstantiated',
|
|
45
|
+
'PresSteps', 'UserUnit', 'VP', 'AF', 'OutputIntents',
|
|
46
|
+
'DPart', 'AssociatedFiles'
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function toBox(v) {
|
|
50
|
+
if (!v || v.type !== 'array' || v.items.length !== 4) return null;
|
|
51
|
+
const r = new Array(4);
|
|
52
|
+
for (let i = 0; i < 4; i++) {
|
|
53
|
+
const it = v.items[i];
|
|
54
|
+
if (!it || (it.type !== 'int' && it.type !== 'real')) return null;
|
|
55
|
+
r[i] = it.value;
|
|
56
|
+
}
|
|
57
|
+
return r;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toRotate(v) {
|
|
61
|
+
if (!v || (v.type !== 'int' && v.type !== 'real')) return 0;
|
|
62
|
+
const n = v.value | 0;
|
|
63
|
+
const m = ((n % 360) + 360) % 360;
|
|
64
|
+
if (m % 90 !== 0) return 0;
|
|
65
|
+
return m;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toContentsRefs(v) {
|
|
69
|
+
if (!v) return [];
|
|
70
|
+
if (v.type === 'ref') return [{ num: v.num, gen: v.gen }];
|
|
71
|
+
if (v.type === 'array') {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const it of v.items) {
|
|
74
|
+
if (it.type === 'ref') out.push({ num: it.num, gen: it.gen });
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toAnnots(v) {
|
|
82
|
+
if (!v) return [];
|
|
83
|
+
if (v.type === 'ref') return [v];
|
|
84
|
+
if (v.type === 'array') return v.items.slice();
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function typePage(dict) {
|
|
89
|
+
if (!isType(dict, 'dict')) {
|
|
90
|
+
throw new ParseError('pdf/page/not-dict',
|
|
91
|
+
'Page must be a dictionary',
|
|
92
|
+
{ context: { type: dict && dict.type } });
|
|
93
|
+
}
|
|
94
|
+
const e = dict.entries;
|
|
95
|
+
if (e.Type && (e.Type.type !== 'name' || e.Type.value !== 'Page')) {
|
|
96
|
+
throw new ParseError('pdf/page/bad-type',
|
|
97
|
+
'/Type must be /Page when present',
|
|
98
|
+
{ context: { actual: e.Type.value } });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const out = {
|
|
102
|
+
parent: e.Parent && e.Parent.type === 'ref'
|
|
103
|
+
? { num: e.Parent.num, gen: e.Parent.gen } : null,
|
|
104
|
+
mediaBox: toBox(e.MediaBox),
|
|
105
|
+
resources: e.Resources || null,
|
|
106
|
+
contents: toContentsRefs(e.Contents),
|
|
107
|
+
rotate: toRotate(e.Rotate),
|
|
108
|
+
annots: toAnnots(e.Annots),
|
|
109
|
+
raw: dict,
|
|
110
|
+
_extras: {}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (e.CropBox) out.cropBox = toBox(e.CropBox);
|
|
114
|
+
if (e.BleedBox) out.bleedBox = toBox(e.BleedBox);
|
|
115
|
+
if (e.TrimBox) out.trimBox = toBox(e.TrimBox);
|
|
116
|
+
if (e.ArtBox) out.artBox = toBox(e.ArtBox);
|
|
117
|
+
if (e.UserUnit && e.UserUnit.type === 'real') out.userUnit = e.UserUnit.value;
|
|
118
|
+
if (e.UserUnit && e.UserUnit.type === 'int') out.userUnit = e.UserUnit.value;
|
|
119
|
+
if (e.Tabs && e.Tabs.type === 'name') out.tabs = e.Tabs.value;
|
|
120
|
+
if (e.Metadata) out.metadata = e.Metadata;
|
|
121
|
+
if (e.Group) out.group = e.Group;
|
|
122
|
+
|
|
123
|
+
for (const k of Object.keys(e)) {
|
|
124
|
+
if (!KNOWN.has(k)) out._extras[k] = e[k];
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { typePage };
|
|
130
|
+
}
|
|
131
|
+
};
|