pdf-lite 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/.commitlintrc.cjs +25 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/workflows/docs.yaml +93 -0
- package/.github/workflows/prepare-release.yaml +79 -0
- package/.github/workflows/release.yaml +80 -0
- package/.github/workflows/test.yaml +35 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +4 -0
- package/CONTRIBUTING.md +109 -0
- package/EXAMPLES.md +1515 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/examples/001-create-pdf.ts +112 -0
- package/examples/002-create-encrypted-pdf.ts +121 -0
- package/examples/003-sign-pdf.ts +347 -0
- package/examples/004-incremental-update.ts +206 -0
- package/examples/005-modify-acroform.ts +374 -0
- package/examples/006-tokeniser-example.ts +131 -0
- package/examples/007-decoder-example.ts +197 -0
- package/package.json +72 -0
- package/packages/pdf-lite/README.md +3 -0
- package/packages/pdf-lite/package.json +68 -0
- package/packages/pdf-lite/scripts/create-encryption-tests.sh +41 -0
- package/packages/pdf-lite/scripts/gen-signing-keys.sh +290 -0
- package/packages/pdf-lite/scripts/generate-all-signing-keys.sh +70 -0
- package/packages/pdf-lite/src/core/decoder.ts +454 -0
- package/packages/pdf-lite/src/core/generators.ts +128 -0
- package/packages/pdf-lite/src/core/incremental-parser.ts +221 -0
- package/packages/pdf-lite/src/core/index.ts +2 -0
- package/packages/pdf-lite/src/core/objects/pdf-array.ts +54 -0
- package/packages/pdf-lite/src/core/objects/pdf-boolean.ts +19 -0
- package/packages/pdf-lite/src/core/objects/pdf-comment.ts +50 -0
- package/packages/pdf-lite/src/core/objects/pdf-date.ts +74 -0
- package/packages/pdf-lite/src/core/objects/pdf-dictionary.ts +171 -0
- package/packages/pdf-lite/src/core/objects/pdf-hexadecimal.ts +54 -0
- package/packages/pdf-lite/src/core/objects/pdf-indirect-object.ts +137 -0
- package/packages/pdf-lite/src/core/objects/pdf-name.ts +19 -0
- package/packages/pdf-lite/src/core/objects/pdf-null.ts +15 -0
- package/packages/pdf-lite/src/core/objects/pdf-number.ts +98 -0
- package/packages/pdf-lite/src/core/objects/pdf-object-reference.ts +30 -0
- package/packages/pdf-lite/src/core/objects/pdf-object.ts +107 -0
- package/packages/pdf-lite/src/core/objects/pdf-start-xref.ts +39 -0
- package/packages/pdf-lite/src/core/objects/pdf-stream.ts +687 -0
- package/packages/pdf-lite/src/core/objects/pdf-string.ts +38 -0
- package/packages/pdf-lite/src/core/objects/pdf-trailer.ts +57 -0
- package/packages/pdf-lite/src/core/objects/pdf-xref-table.ts +264 -0
- package/packages/pdf-lite/src/core/parser.ts +22 -0
- package/packages/pdf-lite/src/core/ref.ts +102 -0
- package/packages/pdf-lite/src/core/serializer.ts +68 -0
- package/packages/pdf-lite/src/core/streams/object-stream.ts +20 -0
- package/packages/pdf-lite/src/core/tokeniser.ts +687 -0
- package/packages/pdf-lite/src/core/tokens/boolean-token.ts +20 -0
- package/packages/pdf-lite/src/core/tokens/byte-offset-token.ts +20 -0
- package/packages/pdf-lite/src/core/tokens/comment-token.ts +32 -0
- package/packages/pdf-lite/src/core/tokens/end-array-token.ts +10 -0
- package/packages/pdf-lite/src/core/tokens/end-dictionary-token.ts +10 -0
- package/packages/pdf-lite/src/core/tokens/end-object-token.ts +10 -0
- package/packages/pdf-lite/src/core/tokens/end-stream-token.ts +11 -0
- package/packages/pdf-lite/src/core/tokens/hexadecimal-token.ts +22 -0
- package/packages/pdf-lite/src/core/tokens/name-token.ts +19 -0
- package/packages/pdf-lite/src/core/tokens/null-token.ts +9 -0
- package/packages/pdf-lite/src/core/tokens/number-token.ts +164 -0
- package/packages/pdf-lite/src/core/tokens/object-reference-token.ts +24 -0
- package/packages/pdf-lite/src/core/tokens/start-array-token.ts +10 -0
- package/packages/pdf-lite/src/core/tokens/start-dictionary-token.ts +10 -0
- package/packages/pdf-lite/src/core/tokens/start-object-token.ts +28 -0
- package/packages/pdf-lite/src/core/tokens/start-stream-token.ts +52 -0
- package/packages/pdf-lite/src/core/tokens/start-xref-token.ts +10 -0
- package/packages/pdf-lite/src/core/tokens/stream-chunk-token.ts +8 -0
- package/packages/pdf-lite/src/core/tokens/string-token.ts +17 -0
- package/packages/pdf-lite/src/core/tokens/token.ts +43 -0
- package/packages/pdf-lite/src/core/tokens/trailer-token.ts +12 -0
- package/packages/pdf-lite/src/core/tokens/whitespace-token.ts +43 -0
- package/packages/pdf-lite/src/core/tokens/xref-table-entry-token.ts +65 -0
- package/packages/pdf-lite/src/core/tokens/xref-table-section-start-token.ts +31 -0
- package/packages/pdf-lite/src/core/tokens/xref-table-start-token.ts +13 -0
- package/packages/pdf-lite/src/crypto/ciphers/aes128.ts +63 -0
- package/packages/pdf-lite/src/crypto/ciphers/aes256.ts +50 -0
- package/packages/pdf-lite/src/crypto/ciphers/rc4.ts +82 -0
- package/packages/pdf-lite/src/crypto/constants.ts +10 -0
- package/packages/pdf-lite/src/crypto/key-derivation/key-derivation-aes256.ts +213 -0
- package/packages/pdf-lite/src/crypto/key-derivation/key-derivation.ts +122 -0
- package/packages/pdf-lite/src/crypto/key-gen/key-gen-aes256.ts +79 -0
- package/packages/pdf-lite/src/crypto/key-gen/key-gen-rc4-128.ts +190 -0
- package/packages/pdf-lite/src/crypto/key-gen/key-gen-rc4-40.ts +129 -0
- package/packages/pdf-lite/src/crypto/types.ts +6 -0
- package/packages/pdf-lite/src/crypto/utils.ts +81 -0
- package/packages/pdf-lite/src/filters/ascii85.ts +128 -0
- package/packages/pdf-lite/src/filters/asciihex.ts +55 -0
- package/packages/pdf-lite/src/filters/flate.ts +39 -0
- package/packages/pdf-lite/src/filters/lzw.ts +144 -0
- package/packages/pdf-lite/src/filters/pass-through.ts +37 -0
- package/packages/pdf-lite/src/filters/runlength.ts +92 -0
- package/packages/pdf-lite/src/filters/types.ts +21 -0
- package/packages/pdf-lite/src/index.ts +4 -0
- package/packages/pdf-lite/src/pdf/errors.ts +5 -0
- package/packages/pdf-lite/src/pdf/index.ts +4 -0
- package/packages/pdf-lite/src/pdf/pdf-document.ts +924 -0
- package/packages/pdf-lite/src/pdf/pdf-reader.ts +57 -0
- package/packages/pdf-lite/src/pdf/pdf-revision.ts +234 -0
- package/packages/pdf-lite/src/pdf/pdf-xref-lookup.ts +527 -0
- package/packages/pdf-lite/src/security/crypt-filters/aesv2.ts +58 -0
- package/packages/pdf-lite/src/security/crypt-filters/aesv3.ts +56 -0
- package/packages/pdf-lite/src/security/crypt-filters/base.ts +140 -0
- package/packages/pdf-lite/src/security/crypt-filters/identity.ts +40 -0
- package/packages/pdf-lite/src/security/crypt-filters/v2.ts +59 -0
- package/packages/pdf-lite/src/security/handlers/base.ts +625 -0
- package/packages/pdf-lite/src/security/handlers/pubSec.ts +413 -0
- package/packages/pdf-lite/src/security/handlers/utils.ts +304 -0
- package/packages/pdf-lite/src/security/handlers/v1.ts +225 -0
- package/packages/pdf-lite/src/security/handlers/v2.ts +128 -0
- package/packages/pdf-lite/src/security/handlers/v4.ts +379 -0
- package/packages/pdf-lite/src/security/handlers/v5.ts +298 -0
- package/packages/pdf-lite/src/security/types.ts +158 -0
- package/packages/pdf-lite/src/signing/document-security-store.ts +224 -0
- package/packages/pdf-lite/src/signing/index.ts +3 -0
- package/packages/pdf-lite/src/signing/signatures/adbe-pkcs7-detached.ts +154 -0
- package/packages/pdf-lite/src/signing/signatures/adbe-pkcs7-sha1.ts +161 -0
- package/packages/pdf-lite/src/signing/signatures/adbe-x509-rsa-sha1.ts +106 -0
- package/packages/pdf-lite/src/signing/signatures/base.ts +229 -0
- package/packages/pdf-lite/src/signing/signatures/etsi-cades-detached.ts +229 -0
- package/packages/pdf-lite/src/signing/signatures/etsi-rfc3161.ts +92 -0
- package/packages/pdf-lite/src/signing/signatures/index.ts +6 -0
- package/packages/pdf-lite/src/signing/signer.ts +120 -0
- package/packages/pdf-lite/src/signing/types.ts +86 -0
- package/packages/pdf-lite/src/signing/utils.ts +71 -0
- package/packages/pdf-lite/src/types.ts +44 -0
- package/packages/pdf-lite/src/utils/IterableReadableStream.ts +30 -0
- package/packages/pdf-lite/src/utils/algos.ts +446 -0
- package/packages/pdf-lite/src/utils/assert.ts +42 -0
- package/packages/pdf-lite/src/utils/bytesToHex.ts +18 -0
- package/packages/pdf-lite/src/utils/bytesToHexBytes.ts +27 -0
- package/packages/pdf-lite/src/utils/bytesToString.ts +17 -0
- package/packages/pdf-lite/src/utils/concatUint8Arrays.ts +26 -0
- package/packages/pdf-lite/src/utils/escapeString.ts +49 -0
- package/packages/pdf-lite/src/utils/hexBytesToBytes.ts +22 -0
- package/packages/pdf-lite/src/utils/hexBytesToString.ts +21 -0
- package/packages/pdf-lite/src/utils/hexToBytes.ts +18 -0
- package/packages/pdf-lite/src/utils/padBytes.ts +25 -0
- package/packages/pdf-lite/src/utils/predictors.ts +332 -0
- package/packages/pdf-lite/src/utils/replaceInBuffer.ts +56 -0
- package/packages/pdf-lite/src/utils/stringToBytes.ts +22 -0
- package/packages/pdf-lite/src/utils/stringToHexBytes.ts +23 -0
- package/packages/pdf-lite/src/utils/unescapeString.ts +123 -0
- package/packages/pdf-lite/test/acceptance/__snapshots__/versions.node.test.ts.snap +60766 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.3/basic.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-aes-128.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-aes-256.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-rc4-128.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-rc4-40.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.5/basic.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.6/basic.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/1.7/basic.pdf +0 -0
- package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-aes-128.pdf +43 -0
- package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-aes-256.pdf +43 -0
- package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-rc4-128.pdf +43 -0
- package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-rc4-40.pdf +44 -0
- package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic.pdf +79 -0
- package/packages/pdf-lite/test/acceptance/versions.node.test.ts +41 -0
- package/packages/pdf-lite/test/unit/__snapshots__/decoder.node.test.ts.snap +86947 -0
- package/packages/pdf-lite/test/unit/__snapshots__/tokeniser.node.test.ts.snap +131829 -0
- package/packages/pdf-lite/test/unit/ciphers.test.ts +61 -0
- package/packages/pdf-lite/test/unit/decoder.node.test.ts +21 -0
- package/packages/pdf-lite/test/unit/decoder.test.ts +567 -0
- package/packages/pdf-lite/test/unit/filters.test.ts +67 -0
- package/packages/pdf-lite/test/unit/fixtures/basic.pdf +0 -0
- package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-aes-128.pdf +0 -0
- package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-aes-256.pdf +0 -0
- package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-rc4-128.pdf +0 -0
- package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-rc4-40.pdf +43 -0
- package/packages/pdf-lite/test/unit/fixtures/protectedAdobeLivecycle.pdf +0 -0
- package/packages/pdf-lite/test/unit/fixtures/rsa-2048/index.ts +187 -0
- package/packages/pdf-lite/test/unit/fixtures/template.pdf +0 -0
- package/packages/pdf-lite/test/unit/incremental-update.test.ts +0 -0
- package/packages/pdf-lite/test/unit/objects.test.ts +0 -0
- package/packages/pdf-lite/test/unit/pdf-document-signing.test.ts +0 -0
- package/packages/pdf-lite/test/unit/pdf-revision.test.ts +195 -0
- package/packages/pdf-lite/test/unit/pdf.browser.test.ts +0 -0
- package/packages/pdf-lite/test/unit/predictors.test.ts +226 -0
- package/packages/pdf-lite/test/unit/ref.test.ts +158 -0
- package/packages/pdf-lite/test/unit/security-handlers.test.ts +645 -0
- package/packages/pdf-lite/test/unit/serializer.test.ts +81 -0
- package/packages/pdf-lite/test/unit/signature-objects.test.ts +814 -0
- package/packages/pdf-lite/test/unit/string-escaping.test.ts +84 -0
- package/packages/pdf-lite/test/unit/tokeniser.node.test.ts +38 -0
- package/packages/pdf-lite/test/unit/tokeniser.test.ts +1213 -0
- package/packages/pdf-lite/test/unit/utils.test.ts +248 -0
- package/packages/pdf-lite/test/unit/xref-lookup.test.ts +72 -0
- package/packages/pdf-lite/tsconfig.json +4 -0
- package/packages/pdf-lite/tsconfig.prod.json +8 -0
- package/packages/pdf-lite/typedoc.json +14 -0
- package/packages/pdf-lite/vitest.config.ts +43 -0
- package/pnpm-workspace.yaml +2 -0
- package/renovate.json +34 -0
- package/scripts/build-examples.ts +30 -0
- package/scripts/bump-version.sh +56 -0
- package/scripts/gen-html-docs.sh +21 -0
- package/scripts/gen-md-docs.sh +15 -0
- package/scripts/prepare-release.sh +33 -0
- package/tsconfig.json +22 -0
- package/tsconfig.prod.json +12 -0
- package/typedoc.json +34 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
import { PdfObject } from '../core/objects/pdf-object'
|
|
2
|
+
import {
|
|
3
|
+
PdfSecurityHandler,
|
|
4
|
+
PdfStandardSecurityHandler,
|
|
5
|
+
} from '../security/handlers/base'
|
|
6
|
+
import { createFromDictionary } from '../security/handlers/utils'
|
|
7
|
+
import { PdfIndirectObject } from '../core/objects/pdf-indirect-object'
|
|
8
|
+
import { PdfComment } from '../core/objects/pdf-comment'
|
|
9
|
+
import { PdfToken } from '../core/tokens/token'
|
|
10
|
+
import { PdfWhitespaceToken } from '../core/tokens/whitespace-token'
|
|
11
|
+
import {
|
|
12
|
+
PdfObjStream,
|
|
13
|
+
PdfStream,
|
|
14
|
+
PdfXRefStreamCompressedEntry,
|
|
15
|
+
} from '../core/objects/pdf-stream'
|
|
16
|
+
import { PdfDictionary } from '../core/objects/pdf-dictionary'
|
|
17
|
+
import { PdfObjectReference } from '../core/objects/pdf-object-reference'
|
|
18
|
+
import { PdfXrefLookup } from './pdf-xref-lookup'
|
|
19
|
+
import { PdfTokenSerializer } from '../core/serializer'
|
|
20
|
+
import { PdfRevision } from './pdf-revision'
|
|
21
|
+
import { PdfV5SecurityHandler } from '../security/handlers/v5'
|
|
22
|
+
import { PdfEncryptionDictionaryObject } from '../security/types'
|
|
23
|
+
import { PdfByteOffsetToken } from '../core/tokens/byte-offset-token'
|
|
24
|
+
import { PdfNumberToken } from '../core/tokens/number-token'
|
|
25
|
+
import { PdfXRefTableEntryToken } from '../core/tokens/xref-table-entry-token'
|
|
26
|
+
import { Ref } from '../core/ref'
|
|
27
|
+
import { PdfStartXRef } from '../core/objects/pdf-start-xref'
|
|
28
|
+
import { PdfTrailerEntries } from '../core/objects/pdf-trailer'
|
|
29
|
+
import { FoundCompressedObjectError } from './errors'
|
|
30
|
+
import { PdfDocumentSecurityStoreObject } from '../signing/document-security-store'
|
|
31
|
+
import { ByteArray } from '../types'
|
|
32
|
+
import { PdfReader } from './pdf-reader'
|
|
33
|
+
import { PdfSigner } from '../signing/signer'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Represents a PDF document with support for reading, writing, and modifying PDF files.
|
|
37
|
+
* Handles document structure, revisions, encryption, and digital signatures.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* // Create a new document
|
|
42
|
+
* const document = new PdfDocument()
|
|
43
|
+
*
|
|
44
|
+
* // Read from bytes
|
|
45
|
+
* const document = await PdfDocument.fromBytes(fileBytes)
|
|
46
|
+
*
|
|
47
|
+
* // Add objects and commit
|
|
48
|
+
* document.add(pdfObject)
|
|
49
|
+
* await document.commit()
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export class PdfDocument extends PdfObject {
|
|
53
|
+
/** PDF version comment header */
|
|
54
|
+
header: PdfComment = PdfComment.versionComment('1.7')
|
|
55
|
+
/** List of document revisions for incremental updates */
|
|
56
|
+
revisions: PdfRevision[]
|
|
57
|
+
/** Signer instance for digital signature operations */
|
|
58
|
+
signer: PdfSigner
|
|
59
|
+
/** Security handler for encryption/decryption operations */
|
|
60
|
+
securityHandler?: PdfSecurityHandler
|
|
61
|
+
|
|
62
|
+
private hasEncryptionDictionary?: boolean = false
|
|
63
|
+
private toBeCommitted: PdfObject[] = []
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new PDF document instance.
|
|
67
|
+
*
|
|
68
|
+
* @param options - Configuration options for the document
|
|
69
|
+
* @param options.revisions - Pre-existing revisions for the document
|
|
70
|
+
* @param options.version - PDF version string (e.g., '1.7', '2.0') or version comment
|
|
71
|
+
* @param options.password - User password for encryption
|
|
72
|
+
* @param options.ownerPassword - Owner password for encryption
|
|
73
|
+
* @param options.securityHandler - Custom security handler for encryption
|
|
74
|
+
* @param options.signer - Custom signer for digital signatures
|
|
75
|
+
*/
|
|
76
|
+
constructor(options?: {
|
|
77
|
+
revisions?: PdfRevision[]
|
|
78
|
+
version?: string | PdfComment
|
|
79
|
+
password?: string
|
|
80
|
+
ownerPassword?: string
|
|
81
|
+
securityHandler?: PdfSecurityHandler
|
|
82
|
+
signer?: PdfSigner
|
|
83
|
+
}) {
|
|
84
|
+
super()
|
|
85
|
+
|
|
86
|
+
this.revisions = options?.revisions ?? [new PdfRevision()]
|
|
87
|
+
|
|
88
|
+
if (options?.version instanceof PdfComment) {
|
|
89
|
+
this.header = options.version
|
|
90
|
+
} else {
|
|
91
|
+
this.setVersion(options?.version ?? '2.0')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.securityHandler =
|
|
95
|
+
options?.securityHandler ?? this.getSecurityHandler()
|
|
96
|
+
|
|
97
|
+
if (options?.password) {
|
|
98
|
+
this.setPassword(options.password)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options?.ownerPassword) {
|
|
102
|
+
this.setOwnerPassword(options.ownerPassword)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.signer = options?.signer ?? new PdfSigner()
|
|
106
|
+
|
|
107
|
+
this.linkRevisions()
|
|
108
|
+
this.calculateOffsets()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Creates a PdfDocument from an array of PDF objects.
|
|
113
|
+
* Parses objects into revisions based on EOF comments.
|
|
114
|
+
*
|
|
115
|
+
* @param objects - Array of PDF objects to construct the document from
|
|
116
|
+
* @returns A new PdfDocument instance
|
|
117
|
+
*/
|
|
118
|
+
static fromObjects(objects: PdfObject[]): PdfDocument {
|
|
119
|
+
let header: PdfComment | undefined
|
|
120
|
+
const revisions: PdfRevision[] = []
|
|
121
|
+
let currentObjects: PdfObject[] = []
|
|
122
|
+
|
|
123
|
+
for (const obj of objects) {
|
|
124
|
+
if (obj instanceof PdfComment && obj.isVersionComment()) {
|
|
125
|
+
header = obj
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
currentObjects.push(obj)
|
|
130
|
+
if (obj instanceof PdfComment && obj.isEOFComment()) {
|
|
131
|
+
revisions.push(new PdfRevision({ objects: currentObjects }))
|
|
132
|
+
currentObjects = []
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (currentObjects.length > 0) {
|
|
137
|
+
revisions.push(new PdfRevision({ objects: currentObjects }))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return new PdfDocument({ revisions, version: header })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Starts a new revision for incremental updates.
|
|
145
|
+
* Creates a new revision linked to the previous one.
|
|
146
|
+
*
|
|
147
|
+
* @returns The document instance for method chaining
|
|
148
|
+
*/
|
|
149
|
+
startNewRevision(): PdfDocument {
|
|
150
|
+
const newRevision = new PdfRevision({ prev: this.latestRevision })
|
|
151
|
+
this.revisions.push(newRevision)
|
|
152
|
+
|
|
153
|
+
const lastStartXRef = this.objects.findLast(
|
|
154
|
+
(x) => x instanceof PdfStartXRef,
|
|
155
|
+
)
|
|
156
|
+
if (lastStartXRef) {
|
|
157
|
+
newRevision.xref.offset = lastStartXRef.offset.ref
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return this
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Adds objects to the document's latest revision.
|
|
165
|
+
* Automatically starts a new revision if the current one is locked.
|
|
166
|
+
*
|
|
167
|
+
* @param objects - PDF objects to add to the document
|
|
168
|
+
*/
|
|
169
|
+
add(...objects: PdfObject[]): void {
|
|
170
|
+
if (this.latestRevision.locked) {
|
|
171
|
+
this.startNewRevision()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const obj of objects) {
|
|
175
|
+
this.toBeCommitted.push(obj)
|
|
176
|
+
this.latestRevision.addObject(obj)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Gets the latest (most recent) revision of the document.
|
|
182
|
+
*
|
|
183
|
+
* @returns The latest PdfRevision
|
|
184
|
+
* @throws Error if the revision for the last StartXRef cannot be found
|
|
185
|
+
*/
|
|
186
|
+
get latestRevision(): PdfRevision {
|
|
187
|
+
const lastStartXRef = this.objects.findLast(
|
|
188
|
+
(x) => x instanceof PdfStartXRef,
|
|
189
|
+
)
|
|
190
|
+
if (!lastStartXRef) {
|
|
191
|
+
return this.revisions[this.revisions.length - 1]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const revision =
|
|
195
|
+
this.revisions.find(
|
|
196
|
+
(rev) => rev.xref.offset === lastStartXRef.offset.ref,
|
|
197
|
+
) ??
|
|
198
|
+
this.revisions.find((rev) =>
|
|
199
|
+
rev.xref.offset.equals(lastStartXRef.offset.value),
|
|
200
|
+
)
|
|
201
|
+
if (!revision) {
|
|
202
|
+
throw new Error('Cannot find revision for last StartXRef')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return revision
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Gets the cross-reference lookup table for the latest revision.
|
|
210
|
+
*
|
|
211
|
+
* @returns The PdfXrefLookup for the latest revision
|
|
212
|
+
*/
|
|
213
|
+
get xrefLookup(): PdfXrefLookup {
|
|
214
|
+
return this.latestRevision.xref
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Gets the trailer dictionary from the cross-reference lookup.
|
|
219
|
+
*
|
|
220
|
+
* @returns The trailer dictionary containing document metadata references
|
|
221
|
+
*/
|
|
222
|
+
get trailerDict(): PdfDictionary<PdfTrailerEntries> {
|
|
223
|
+
return this.xrefLookup.trailerDict
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Gets all objects across all revisions in the document.
|
|
228
|
+
*
|
|
229
|
+
* @returns A readonly array of all PDF objects
|
|
230
|
+
*/
|
|
231
|
+
get objects(): ReadonlyArray<PdfObject> {
|
|
232
|
+
return this.revisions.flatMap((rev) => rev.objects)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Gets the encryption dictionary from the document if present.
|
|
237
|
+
*
|
|
238
|
+
* @returns The encryption dictionary object or undefined if not encrypted
|
|
239
|
+
* @throws Error if the encryption dictionary reference points to a non-dictionary object
|
|
240
|
+
*/
|
|
241
|
+
get encryptionDictionary(): PdfEncryptionDictionaryObject | undefined {
|
|
242
|
+
const encryptionDictionaryRef = this.trailerDict
|
|
243
|
+
.get('Encrypt')
|
|
244
|
+
?.as(PdfObjectReference)
|
|
245
|
+
|
|
246
|
+
if (!encryptionDictionaryRef) {
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const encryptionDictObject = this.findUncompressedObject(
|
|
251
|
+
encryptionDictionaryRef,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if (!(encryptionDictObject?.content instanceof PdfDictionary)) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Encryption dictionary object ${encryptionDictionaryRef.objectNumber} ${encryptionDictionaryRef.generationNumber} is not a dictionary, it is a ${encryptionDictObject?.content.objectType}`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
encryptionDictObject.encryptable = false
|
|
261
|
+
return encryptionDictObject as PdfEncryptionDictionaryObject
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Gets the document catalog (root) dictionary.
|
|
266
|
+
*
|
|
267
|
+
* @returns The root dictionary or undefined if not found
|
|
268
|
+
* @throws Error if the Root reference points to a non-dictionary object
|
|
269
|
+
*/
|
|
270
|
+
get rootDictionary(): PdfDictionary | undefined {
|
|
271
|
+
const rootRef = this.trailerDict.get('Root')?.as(PdfObjectReference)
|
|
272
|
+
|
|
273
|
+
if (!rootRef) {
|
|
274
|
+
return undefined
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const rootObject = this.findUncompressedObject(rootRef)
|
|
278
|
+
|
|
279
|
+
if (!(rootObject?.content instanceof PdfDictionary)) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Root object ${rootRef.objectNumber} ${rootRef.generationNumber} is not a dictionary, it is a ${rootObject?.content.objectType}`,
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return rootObject.content
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Gets the reference to the metadata stream from the document catalog.
|
|
290
|
+
*
|
|
291
|
+
* @returns The metadata stream reference or undefined if not present
|
|
292
|
+
*/
|
|
293
|
+
get metadataStreamReference(): PdfObjectReference | undefined {
|
|
294
|
+
const root = this.rootDictionary
|
|
295
|
+
if (!root) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const metadataRef = root.get('Metadata')?.as(PdfObjectReference)
|
|
300
|
+
|
|
301
|
+
if (!metadataRef) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return metadataRef
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private getSecurityHandler(): PdfSecurityHandler | undefined {
|
|
309
|
+
const encryptionDictionaryRef = this.trailerDict
|
|
310
|
+
.get('Encrypt')
|
|
311
|
+
?.as(PdfObjectReference)
|
|
312
|
+
|
|
313
|
+
if (!encryptionDictionaryRef) {
|
|
314
|
+
return undefined
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const encryptionDictObject = this.findUncompressedObject(
|
|
318
|
+
encryptionDictionaryRef,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if (!(encryptionDictObject?.content instanceof PdfDictionary)) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Encryption dictionary object ${encryptionDictionaryRef.objectNumber} ${encryptionDictionaryRef.generationNumber} is not a dictionary, it is a ${encryptionDictObject?.content.objectType}`,
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.hasEncryptionDictionary = true
|
|
328
|
+
return createFromDictionary(encryptionDictObject.content, {
|
|
329
|
+
documentId: this.trailerDict.get('ID'),
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private initSecurityHandler(options: {
|
|
334
|
+
password?: string
|
|
335
|
+
ownerPassword?: string
|
|
336
|
+
}): void {
|
|
337
|
+
if (this.securityHandler instanceof PdfStandardSecurityHandler) {
|
|
338
|
+
const documentId = this.trailerDict.get('ID')
|
|
339
|
+
options.password &&
|
|
340
|
+
this.securityHandler.setPassword(options.password)
|
|
341
|
+
options.ownerPassword &&
|
|
342
|
+
this.securityHandler.setOwnerPassword(options.ownerPassword)
|
|
343
|
+
documentId && this.securityHandler.setDocumentId(documentId)
|
|
344
|
+
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this.securityHandler = new PdfV5SecurityHandler({
|
|
349
|
+
password: options.password,
|
|
350
|
+
ownerPassword: options.ownerPassword,
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Sets the user password for document encryption.
|
|
356
|
+
*
|
|
357
|
+
* @param password - The user password to set
|
|
358
|
+
* @throws Error if the security handler doesn't support password setting
|
|
359
|
+
*/
|
|
360
|
+
setPassword(password: string): void {
|
|
361
|
+
if (this.securityHandler instanceof PdfStandardSecurityHandler) {
|
|
362
|
+
this.securityHandler.setPassword(password)
|
|
363
|
+
} else if (!this.securityHandler) {
|
|
364
|
+
this.initSecurityHandler({ password })
|
|
365
|
+
} else {
|
|
366
|
+
throw new Error(
|
|
367
|
+
'Setting password is only supported for Standard Security Handler',
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Sets the owner password for document encryption.
|
|
374
|
+
*
|
|
375
|
+
* @param ownerPassword - The owner password to set
|
|
376
|
+
* @throws Error if the security handler doesn't support password setting
|
|
377
|
+
*/
|
|
378
|
+
setOwnerPassword(ownerPassword: string): void {
|
|
379
|
+
if (this.securityHandler instanceof PdfStandardSecurityHandler) {
|
|
380
|
+
this.securityHandler.setOwnerPassword(ownerPassword)
|
|
381
|
+
} else if (!this.securityHandler) {
|
|
382
|
+
this.initSecurityHandler({ ownerPassword })
|
|
383
|
+
} else {
|
|
384
|
+
throw new Error(
|
|
385
|
+
'Setting ownerPassword is only supported for Standard Security Handler',
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks if a PDF object exists in the document.
|
|
392
|
+
*
|
|
393
|
+
* @param obj - The PDF object to check
|
|
394
|
+
* @returns True if the object exists in the document
|
|
395
|
+
*/
|
|
396
|
+
hasObject(obj: PdfObject): boolean {
|
|
397
|
+
return this.objects.includes(obj)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private isObjectEncryptable(obj: PdfIndirectObject): boolean {
|
|
401
|
+
if (!this.securityHandler) {
|
|
402
|
+
return false
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!obj.isEncryptable()) {
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (obj.matchesReference(this.encryptionDictionary?.reference)) {
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (
|
|
414
|
+
!this.securityHandler.encryptMetadata &&
|
|
415
|
+
obj.matchesReference(this.metadataStreamReference)
|
|
416
|
+
) {
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return true
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Decrypts all encrypted objects in the document.
|
|
425
|
+
* Removes the security handler and encryption dictionary after decryption.
|
|
426
|
+
*/
|
|
427
|
+
async decrypt(): Promise<void> {
|
|
428
|
+
if (!this.securityHandler) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
for (const object of this.objects) {
|
|
433
|
+
if (!(object instanceof PdfIndirectObject)) {
|
|
434
|
+
continue
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!this.isObjectEncryptable(object)) {
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
await this.securityHandler.decryptObject(object)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.securityHandler = undefined
|
|
445
|
+
this.hasEncryptionDictionary = false
|
|
446
|
+
|
|
447
|
+
const encryptionDict = this.encryptionDictionary
|
|
448
|
+
|
|
449
|
+
if (encryptionDict) {
|
|
450
|
+
await this.deleteObject(encryptionDict)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await this.update()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Encrypts all objects in the document using the security handler.
|
|
458
|
+
* Creates and adds an encryption dictionary to all revisions.
|
|
459
|
+
*/
|
|
460
|
+
async encrypt(): Promise<void> {
|
|
461
|
+
this.initSecurityHandler({})
|
|
462
|
+
|
|
463
|
+
await this.securityHandler!.write()
|
|
464
|
+
|
|
465
|
+
for (const object of this.objects) {
|
|
466
|
+
if (!(object instanceof PdfIndirectObject)) {
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!this.isObjectEncryptable(object)) {
|
|
471
|
+
continue
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
await this.securityHandler!.encryptObject(object)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const encryptionDictObject = new PdfIndirectObject({
|
|
478
|
+
content: this.securityHandler!.dict,
|
|
479
|
+
encryptable: false,
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
for (const revision of this.revisions) {
|
|
483
|
+
revision.xref.trailerDict.set(
|
|
484
|
+
'Encrypt',
|
|
485
|
+
encryptionDictObject.reference,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if (!revision.xref.trailerDict.get('ID')) {
|
|
489
|
+
revision.xref.trailerDict.set(
|
|
490
|
+
'ID',
|
|
491
|
+
this.securityHandler!.getDocumentId(),
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await this.commit(encryptionDictObject)
|
|
497
|
+
this.hasEncryptionDictionary = true
|
|
498
|
+
|
|
499
|
+
await this.update()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Finds a compressed object by its object number within an object stream.
|
|
504
|
+
*
|
|
505
|
+
* @param options - Object identifier with objectNumber and optional generationNumber
|
|
506
|
+
* @returns The found indirect object or undefined if not found
|
|
507
|
+
* @throws Error if the object cannot be found in the expected object stream
|
|
508
|
+
*/
|
|
509
|
+
async findCompressedObject(
|
|
510
|
+
options:
|
|
511
|
+
| {
|
|
512
|
+
objectNumber: number
|
|
513
|
+
generationNumber?: number
|
|
514
|
+
}
|
|
515
|
+
| PdfObjectReference,
|
|
516
|
+
): Promise<PdfIndirectObject | undefined> {
|
|
517
|
+
const xrefEntry = this.xrefLookup.getObject(options.objectNumber)
|
|
518
|
+
|
|
519
|
+
if (!(xrefEntry instanceof PdfXRefStreamCompressedEntry)) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
'Cannot find object inside object stream via PdfDocument.findObject',
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const objectStreamIndirect = this.findUncompressedObject({
|
|
526
|
+
objectNumber: xrefEntry.objectStreamNumber.value,
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
if (!objectStreamIndirect) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`Cannot find object stream ${xrefEntry.objectStreamNumber.value} for object ${options.objectNumber}`,
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (
|
|
536
|
+
this.securityHandler &&
|
|
537
|
+
this.isObjectEncryptable(objectStreamIndirect)
|
|
538
|
+
) {
|
|
539
|
+
await this.securityHandler.decryptObject(objectStreamIndirect)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const objectStream = objectStreamIndirect.content
|
|
543
|
+
.as(PdfStream)
|
|
544
|
+
.parseAs(PdfObjStream)
|
|
545
|
+
|
|
546
|
+
const decompressedObject = objectStream.getObject({
|
|
547
|
+
objectNumber: options.objectNumber,
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
return decompressedObject
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Finds an uncompressed indirect object by its object number.
|
|
555
|
+
*
|
|
556
|
+
* @param options - Object identifier with objectNumber and optional generationNumber
|
|
557
|
+
* @returns The found indirect object or undefined if not found
|
|
558
|
+
* @throws FoundCompressedObjectError if the object is compressed (in an object stream)
|
|
559
|
+
*/
|
|
560
|
+
findUncompressedObject(
|
|
561
|
+
options:
|
|
562
|
+
| {
|
|
563
|
+
objectNumber: number
|
|
564
|
+
generationNumber?: number
|
|
565
|
+
}
|
|
566
|
+
| PdfObjectReference,
|
|
567
|
+
): PdfIndirectObject | undefined {
|
|
568
|
+
const xrefEntry = this.xrefLookup.getObject(options.objectNumber)
|
|
569
|
+
|
|
570
|
+
if (xrefEntry instanceof PdfXRefStreamCompressedEntry) {
|
|
571
|
+
throw new FoundCompressedObjectError(
|
|
572
|
+
`TODO: Cannot find object ${options.objectNumber} inside object stream via PdfDocument.findObject`,
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (
|
|
577
|
+
!xrefEntry ||
|
|
578
|
+
(options.generationNumber !== undefined &&
|
|
579
|
+
xrefEntry.generationNumber.value !== options.generationNumber)
|
|
580
|
+
) {
|
|
581
|
+
return undefined
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return this.objects.find(
|
|
585
|
+
(obj) =>
|
|
586
|
+
obj instanceof PdfIndirectObject &&
|
|
587
|
+
obj.objectNumber === options.objectNumber &&
|
|
588
|
+
(options.generationNumber === undefined ||
|
|
589
|
+
obj.generationNumber === options.generationNumber) &&
|
|
590
|
+
obj.offset.equals(xrefEntry.byteOffset.ref),
|
|
591
|
+
) as PdfIndirectObject | undefined
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Reads and optionally decrypts an object by its object number.
|
|
596
|
+
* Handles both compressed and uncompressed objects.
|
|
597
|
+
*
|
|
598
|
+
* @param options - Object lookup options
|
|
599
|
+
* @param options.objectNumber - The object number to find
|
|
600
|
+
* @param options.generationNumber - Optional generation number filter
|
|
601
|
+
* @param options.allowUnindexed - If true, searches unindexed objects as fallback
|
|
602
|
+
* @returns A cloned and decrypted copy of the object, or undefined if not found
|
|
603
|
+
*/
|
|
604
|
+
async readObject(options: {
|
|
605
|
+
objectNumber: number
|
|
606
|
+
generationNumber?: number
|
|
607
|
+
allowUnindexed?: boolean
|
|
608
|
+
}): Promise<PdfIndirectObject | undefined> {
|
|
609
|
+
let foundObject: PdfIndirectObject | undefined
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
foundObject = this.findUncompressedObject(options)
|
|
613
|
+
} catch (e) {
|
|
614
|
+
if (e instanceof FoundCompressedObjectError) {
|
|
615
|
+
foundObject = await this.findCompressedObject(options)
|
|
616
|
+
} else {
|
|
617
|
+
throw e
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!foundObject && options.allowUnindexed) {
|
|
622
|
+
foundObject = this.objects.find(
|
|
623
|
+
(obj) =>
|
|
624
|
+
obj instanceof PdfIndirectObject &&
|
|
625
|
+
obj.objectNumber === options.objectNumber &&
|
|
626
|
+
(options.generationNumber === undefined ||
|
|
627
|
+
obj.generationNumber === options.generationNumber),
|
|
628
|
+
) as PdfIndirectObject | undefined
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (!foundObject) {
|
|
632
|
+
return undefined
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (this.securityHandler && this.isObjectEncryptable(foundObject)) {
|
|
636
|
+
foundObject = foundObject.clone()
|
|
637
|
+
|
|
638
|
+
await this.securityHandler.decryptObject(foundObject)
|
|
639
|
+
} else if (this.isIncremental()) {
|
|
640
|
+
foundObject = foundObject.clone() // Clone to prevent modifications in locked revisions
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return foundObject
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Deletes an object from all revisions in the document.
|
|
647
|
+
*
|
|
648
|
+
* @param obj - The PDF object to delete
|
|
649
|
+
*/
|
|
650
|
+
async deleteObject(obj: PdfObject | undefined): Promise<void> {
|
|
651
|
+
if (!obj) return
|
|
652
|
+
|
|
653
|
+
for (const revision of this.revisions) {
|
|
654
|
+
revision.deleteObject(obj)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
await this.update()
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Sets the PDF version for the document.
|
|
662
|
+
*
|
|
663
|
+
* @param version - The PDF version string (e.g., '1.7', '2.0')
|
|
664
|
+
* @throws Error if attempting to change version after objects have been added in incremental mode
|
|
665
|
+
*/
|
|
666
|
+
setVersion(version: string): void {
|
|
667
|
+
if (this.revisions[0].locked) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
'Cannot change PDF version in incremental mode after objects have been added',
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
this.header = PdfComment.versionComment(version)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Sets whether the document should use incremental updates.
|
|
678
|
+
* When true, locks all existing revisions to preserve original content.
|
|
679
|
+
*
|
|
680
|
+
* @param value - True to enable incremental mode, false to disable
|
|
681
|
+
*/
|
|
682
|
+
setIncremental(value: boolean): void {
|
|
683
|
+
for (const revision of this.revisions) {
|
|
684
|
+
revision.locked = value
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Checks if the document is in incremental mode.
|
|
690
|
+
*
|
|
691
|
+
* @returns True if all revisions are locked for incremental updates
|
|
692
|
+
*/
|
|
693
|
+
isIncremental(): boolean {
|
|
694
|
+
return this.latestRevision.locked
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Commits pending objects to the document.
|
|
699
|
+
* Adds objects, applies encryption if configured, and updates the document structure.
|
|
700
|
+
*
|
|
701
|
+
* @param newObjects - Additional objects to add before committing
|
|
702
|
+
*/
|
|
703
|
+
async commit(...newObjects: PdfObject[]): Promise<void> {
|
|
704
|
+
this.add(...newObjects)
|
|
705
|
+
|
|
706
|
+
const queue = this.toBeCommitted.slice()
|
|
707
|
+
this.toBeCommitted = []
|
|
708
|
+
|
|
709
|
+
for (const newObject of queue) {
|
|
710
|
+
if (
|
|
711
|
+
this.securityHandler &&
|
|
712
|
+
newObject instanceof PdfIndirectObject &&
|
|
713
|
+
this.isObjectEncryptable(newObject)
|
|
714
|
+
) {
|
|
715
|
+
await this.securityHandler.write()
|
|
716
|
+
|
|
717
|
+
if (!this.hasEncryptionDictionary) {
|
|
718
|
+
const encryptionDictObject = new PdfIndirectObject({
|
|
719
|
+
content: this.securityHandler!.dict,
|
|
720
|
+
encryptable: false,
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
this.latestRevision.addObject(encryptionDictObject)
|
|
724
|
+
this.trailerDict.set(
|
|
725
|
+
'Encrypt',
|
|
726
|
+
encryptionDictObject.reference,
|
|
727
|
+
)
|
|
728
|
+
this.hasEncryptionDictionary = true
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
await this.securityHandler.encryptObject(newObject)
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
await this.update()
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Sets the Document Security Store (DSS) for the document.
|
|
740
|
+
* Used for long-term validation of digital signatures.
|
|
741
|
+
*
|
|
742
|
+
* @param dss - The Document Security Store object to set
|
|
743
|
+
* @throws Error if the document has no root dictionary
|
|
744
|
+
*/
|
|
745
|
+
async setDocumentSecurityStore(
|
|
746
|
+
dss: PdfDocumentSecurityStoreObject,
|
|
747
|
+
): Promise<void> {
|
|
748
|
+
let rootDictionary = this.rootDictionary
|
|
749
|
+
if (!rootDictionary) {
|
|
750
|
+
throw new Error('Cannot set DSS - document has no root dictionary')
|
|
751
|
+
}
|
|
752
|
+
rootDictionary.set('DSS', dss.reference)
|
|
753
|
+
|
|
754
|
+
if (!this.hasObject(dss)) {
|
|
755
|
+
await this.commit(dss)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Returns tokens paired with their source objects.
|
|
761
|
+
* Useful for debugging and analysis of document structure.
|
|
762
|
+
*
|
|
763
|
+
* @returns Array of token-object pairs
|
|
764
|
+
*/
|
|
765
|
+
tokensWithObjects(): {
|
|
766
|
+
token: PdfToken
|
|
767
|
+
object: PdfObject | undefined
|
|
768
|
+
}[] {
|
|
769
|
+
const documentTokens: {
|
|
770
|
+
token: PdfToken
|
|
771
|
+
object: PdfObject | undefined
|
|
772
|
+
}[] = this.objects.flatMap((obj) => {
|
|
773
|
+
const tokens = obj.toTokens()
|
|
774
|
+
if (
|
|
775
|
+
tokens.length > 0 &&
|
|
776
|
+
!(tokens[tokens.length - 1] instanceof PdfWhitespaceToken)
|
|
777
|
+
) {
|
|
778
|
+
tokens.push(PdfWhitespaceToken.NEWLINE)
|
|
779
|
+
}
|
|
780
|
+
return tokens.map((token) => ({ token, object: obj }))
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
const headerTokens = this.header
|
|
784
|
+
.toTokens()
|
|
785
|
+
.map((token) => ({ token, object: this.header }))
|
|
786
|
+
|
|
787
|
+
documentTokens.unshift(...headerTokens)
|
|
788
|
+
|
|
789
|
+
return documentTokens
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
protected tokenize(): PdfToken[] {
|
|
793
|
+
return this.tokensWithObjects().map(({ token }) => token)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private linkRevisions(): void {
|
|
797
|
+
const xrefLookups = this.revisions.map((rev) => rev.xref)
|
|
798
|
+
const indirectObjects = this.objects.filter(
|
|
799
|
+
(x) => x instanceof PdfIndirectObject,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
for (const revision of this.revisions) {
|
|
803
|
+
revision.xref.linkPrev(xrefLookups)
|
|
804
|
+
revision.xref.linkIndirectObjects(indirectObjects)
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private linkOffsets(): void {
|
|
809
|
+
const refMap = new Map<
|
|
810
|
+
number,
|
|
811
|
+
{
|
|
812
|
+
main?: Ref<number>
|
|
813
|
+
others?: Set<Ref<number>>
|
|
814
|
+
}
|
|
815
|
+
>()
|
|
816
|
+
|
|
817
|
+
const tokens = this.toTokens()
|
|
818
|
+
|
|
819
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
820
|
+
const token = tokens[i]
|
|
821
|
+
let main: Ref<number> | undefined
|
|
822
|
+
let other: Ref<number> | undefined
|
|
823
|
+
|
|
824
|
+
if (token instanceof PdfByteOffsetToken) {
|
|
825
|
+
main = token.value
|
|
826
|
+
} else if (token instanceof PdfXRefTableEntryToken) {
|
|
827
|
+
other = token.offset.ref
|
|
828
|
+
} else if (token instanceof PdfNumberToken && token.isByteToken) {
|
|
829
|
+
other = token.ref
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (!other && !main) {
|
|
833
|
+
continue
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const id = (main ?? other)!.resolve()
|
|
837
|
+
if (!refMap.has(id)) {
|
|
838
|
+
refMap.set(id, { main: main, others: new Set<Ref<number>>() })
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (main) refMap.get(id)!.main = main
|
|
842
|
+
if (other) refMap.get(id)!.others!.add(other)
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
for (const [, { main, others }] of refMap) {
|
|
846
|
+
if (!main) continue
|
|
847
|
+
|
|
848
|
+
for (const other of others ?? []) {
|
|
849
|
+
other.update(main)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private calculateOffsets(): void {
|
|
855
|
+
const serializer = new PdfTokenSerializer()
|
|
856
|
+
serializer.feed(...this.toTokens())
|
|
857
|
+
serializer.calculateOffsets()
|
|
858
|
+
this.linkOffsets()
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private updateRevisions(): void {
|
|
862
|
+
let modified = false
|
|
863
|
+
this.revisions.forEach((rev, i) => {
|
|
864
|
+
if (rev.isModified()) {
|
|
865
|
+
modified = true
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (modified) {
|
|
869
|
+
rev.update()
|
|
870
|
+
}
|
|
871
|
+
})
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private async update(): Promise<void> {
|
|
875
|
+
this.calculateOffsets()
|
|
876
|
+
this.updateRevisions()
|
|
877
|
+
await this.signer?.sign(this)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Serializes the document to a byte array.
|
|
882
|
+
*
|
|
883
|
+
* @returns The PDF document as a Uint8Array
|
|
884
|
+
*/
|
|
885
|
+
toBytes(): ByteArray {
|
|
886
|
+
this.calculateOffsets()
|
|
887
|
+
this.updateRevisions()
|
|
888
|
+
const serializer = new PdfTokenSerializer()
|
|
889
|
+
serializer.feed(...this.toTokens())
|
|
890
|
+
return serializer.toBytes()
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Creates a deep copy of the document.
|
|
895
|
+
*
|
|
896
|
+
* @returns A cloned PdfDocument instance
|
|
897
|
+
*/
|
|
898
|
+
clone(): this {
|
|
899
|
+
const clonedRevisions = this.revisions.map((rev) => rev.clone())
|
|
900
|
+
return new PdfDocument({
|
|
901
|
+
revisions: clonedRevisions,
|
|
902
|
+
version: this.header.clone(),
|
|
903
|
+
securityHandler: this.securityHandler,
|
|
904
|
+
}) as this
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Creates a PdfDocument from a byte stream.
|
|
909
|
+
*
|
|
910
|
+
* @param input - Async or sync iterable of byte arrays
|
|
911
|
+
* @returns A promise that resolves to the parsed PdfDocument
|
|
912
|
+
*/
|
|
913
|
+
static fromBytes(
|
|
914
|
+
input: AsyncIterable<ByteArray> | Iterable<ByteArray>,
|
|
915
|
+
): Promise<PdfDocument> {
|
|
916
|
+
return PdfReader.fromBytes(input)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
isModified(): boolean {
|
|
920
|
+
return (
|
|
921
|
+
super.isModified() || this.revisions.some((rev) => rev.isModified())
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
}
|