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.
Files changed (206) hide show
  1. package/.commitlintrc.cjs +25 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  4. package/.github/workflows/docs.yaml +93 -0
  5. package/.github/workflows/prepare-release.yaml +79 -0
  6. package/.github/workflows/release.yaml +80 -0
  7. package/.github/workflows/test.yaml +35 -0
  8. package/.husky/commit-msg +1 -0
  9. package/.husky/pre-commit +1 -0
  10. package/.prettierignore +4 -0
  11. package/.prettierrc +4 -0
  12. package/CONTRIBUTING.md +109 -0
  13. package/EXAMPLES.md +1515 -0
  14. package/LICENSE +21 -0
  15. package/README.md +285 -0
  16. package/examples/001-create-pdf.ts +112 -0
  17. package/examples/002-create-encrypted-pdf.ts +121 -0
  18. package/examples/003-sign-pdf.ts +347 -0
  19. package/examples/004-incremental-update.ts +206 -0
  20. package/examples/005-modify-acroform.ts +374 -0
  21. package/examples/006-tokeniser-example.ts +131 -0
  22. package/examples/007-decoder-example.ts +197 -0
  23. package/package.json +72 -0
  24. package/packages/pdf-lite/README.md +3 -0
  25. package/packages/pdf-lite/package.json +68 -0
  26. package/packages/pdf-lite/scripts/create-encryption-tests.sh +41 -0
  27. package/packages/pdf-lite/scripts/gen-signing-keys.sh +290 -0
  28. package/packages/pdf-lite/scripts/generate-all-signing-keys.sh +70 -0
  29. package/packages/pdf-lite/src/core/decoder.ts +454 -0
  30. package/packages/pdf-lite/src/core/generators.ts +128 -0
  31. package/packages/pdf-lite/src/core/incremental-parser.ts +221 -0
  32. package/packages/pdf-lite/src/core/index.ts +2 -0
  33. package/packages/pdf-lite/src/core/objects/pdf-array.ts +54 -0
  34. package/packages/pdf-lite/src/core/objects/pdf-boolean.ts +19 -0
  35. package/packages/pdf-lite/src/core/objects/pdf-comment.ts +50 -0
  36. package/packages/pdf-lite/src/core/objects/pdf-date.ts +74 -0
  37. package/packages/pdf-lite/src/core/objects/pdf-dictionary.ts +171 -0
  38. package/packages/pdf-lite/src/core/objects/pdf-hexadecimal.ts +54 -0
  39. package/packages/pdf-lite/src/core/objects/pdf-indirect-object.ts +137 -0
  40. package/packages/pdf-lite/src/core/objects/pdf-name.ts +19 -0
  41. package/packages/pdf-lite/src/core/objects/pdf-null.ts +15 -0
  42. package/packages/pdf-lite/src/core/objects/pdf-number.ts +98 -0
  43. package/packages/pdf-lite/src/core/objects/pdf-object-reference.ts +30 -0
  44. package/packages/pdf-lite/src/core/objects/pdf-object.ts +107 -0
  45. package/packages/pdf-lite/src/core/objects/pdf-start-xref.ts +39 -0
  46. package/packages/pdf-lite/src/core/objects/pdf-stream.ts +687 -0
  47. package/packages/pdf-lite/src/core/objects/pdf-string.ts +38 -0
  48. package/packages/pdf-lite/src/core/objects/pdf-trailer.ts +57 -0
  49. package/packages/pdf-lite/src/core/objects/pdf-xref-table.ts +264 -0
  50. package/packages/pdf-lite/src/core/parser.ts +22 -0
  51. package/packages/pdf-lite/src/core/ref.ts +102 -0
  52. package/packages/pdf-lite/src/core/serializer.ts +68 -0
  53. package/packages/pdf-lite/src/core/streams/object-stream.ts +20 -0
  54. package/packages/pdf-lite/src/core/tokeniser.ts +687 -0
  55. package/packages/pdf-lite/src/core/tokens/boolean-token.ts +20 -0
  56. package/packages/pdf-lite/src/core/tokens/byte-offset-token.ts +20 -0
  57. package/packages/pdf-lite/src/core/tokens/comment-token.ts +32 -0
  58. package/packages/pdf-lite/src/core/tokens/end-array-token.ts +10 -0
  59. package/packages/pdf-lite/src/core/tokens/end-dictionary-token.ts +10 -0
  60. package/packages/pdf-lite/src/core/tokens/end-object-token.ts +10 -0
  61. package/packages/pdf-lite/src/core/tokens/end-stream-token.ts +11 -0
  62. package/packages/pdf-lite/src/core/tokens/hexadecimal-token.ts +22 -0
  63. package/packages/pdf-lite/src/core/tokens/name-token.ts +19 -0
  64. package/packages/pdf-lite/src/core/tokens/null-token.ts +9 -0
  65. package/packages/pdf-lite/src/core/tokens/number-token.ts +164 -0
  66. package/packages/pdf-lite/src/core/tokens/object-reference-token.ts +24 -0
  67. package/packages/pdf-lite/src/core/tokens/start-array-token.ts +10 -0
  68. package/packages/pdf-lite/src/core/tokens/start-dictionary-token.ts +10 -0
  69. package/packages/pdf-lite/src/core/tokens/start-object-token.ts +28 -0
  70. package/packages/pdf-lite/src/core/tokens/start-stream-token.ts +52 -0
  71. package/packages/pdf-lite/src/core/tokens/start-xref-token.ts +10 -0
  72. package/packages/pdf-lite/src/core/tokens/stream-chunk-token.ts +8 -0
  73. package/packages/pdf-lite/src/core/tokens/string-token.ts +17 -0
  74. package/packages/pdf-lite/src/core/tokens/token.ts +43 -0
  75. package/packages/pdf-lite/src/core/tokens/trailer-token.ts +12 -0
  76. package/packages/pdf-lite/src/core/tokens/whitespace-token.ts +43 -0
  77. package/packages/pdf-lite/src/core/tokens/xref-table-entry-token.ts +65 -0
  78. package/packages/pdf-lite/src/core/tokens/xref-table-section-start-token.ts +31 -0
  79. package/packages/pdf-lite/src/core/tokens/xref-table-start-token.ts +13 -0
  80. package/packages/pdf-lite/src/crypto/ciphers/aes128.ts +63 -0
  81. package/packages/pdf-lite/src/crypto/ciphers/aes256.ts +50 -0
  82. package/packages/pdf-lite/src/crypto/ciphers/rc4.ts +82 -0
  83. package/packages/pdf-lite/src/crypto/constants.ts +10 -0
  84. package/packages/pdf-lite/src/crypto/key-derivation/key-derivation-aes256.ts +213 -0
  85. package/packages/pdf-lite/src/crypto/key-derivation/key-derivation.ts +122 -0
  86. package/packages/pdf-lite/src/crypto/key-gen/key-gen-aes256.ts +79 -0
  87. package/packages/pdf-lite/src/crypto/key-gen/key-gen-rc4-128.ts +190 -0
  88. package/packages/pdf-lite/src/crypto/key-gen/key-gen-rc4-40.ts +129 -0
  89. package/packages/pdf-lite/src/crypto/types.ts +6 -0
  90. package/packages/pdf-lite/src/crypto/utils.ts +81 -0
  91. package/packages/pdf-lite/src/filters/ascii85.ts +128 -0
  92. package/packages/pdf-lite/src/filters/asciihex.ts +55 -0
  93. package/packages/pdf-lite/src/filters/flate.ts +39 -0
  94. package/packages/pdf-lite/src/filters/lzw.ts +144 -0
  95. package/packages/pdf-lite/src/filters/pass-through.ts +37 -0
  96. package/packages/pdf-lite/src/filters/runlength.ts +92 -0
  97. package/packages/pdf-lite/src/filters/types.ts +21 -0
  98. package/packages/pdf-lite/src/index.ts +4 -0
  99. package/packages/pdf-lite/src/pdf/errors.ts +5 -0
  100. package/packages/pdf-lite/src/pdf/index.ts +4 -0
  101. package/packages/pdf-lite/src/pdf/pdf-document.ts +924 -0
  102. package/packages/pdf-lite/src/pdf/pdf-reader.ts +57 -0
  103. package/packages/pdf-lite/src/pdf/pdf-revision.ts +234 -0
  104. package/packages/pdf-lite/src/pdf/pdf-xref-lookup.ts +527 -0
  105. package/packages/pdf-lite/src/security/crypt-filters/aesv2.ts +58 -0
  106. package/packages/pdf-lite/src/security/crypt-filters/aesv3.ts +56 -0
  107. package/packages/pdf-lite/src/security/crypt-filters/base.ts +140 -0
  108. package/packages/pdf-lite/src/security/crypt-filters/identity.ts +40 -0
  109. package/packages/pdf-lite/src/security/crypt-filters/v2.ts +59 -0
  110. package/packages/pdf-lite/src/security/handlers/base.ts +625 -0
  111. package/packages/pdf-lite/src/security/handlers/pubSec.ts +413 -0
  112. package/packages/pdf-lite/src/security/handlers/utils.ts +304 -0
  113. package/packages/pdf-lite/src/security/handlers/v1.ts +225 -0
  114. package/packages/pdf-lite/src/security/handlers/v2.ts +128 -0
  115. package/packages/pdf-lite/src/security/handlers/v4.ts +379 -0
  116. package/packages/pdf-lite/src/security/handlers/v5.ts +298 -0
  117. package/packages/pdf-lite/src/security/types.ts +158 -0
  118. package/packages/pdf-lite/src/signing/document-security-store.ts +224 -0
  119. package/packages/pdf-lite/src/signing/index.ts +3 -0
  120. package/packages/pdf-lite/src/signing/signatures/adbe-pkcs7-detached.ts +154 -0
  121. package/packages/pdf-lite/src/signing/signatures/adbe-pkcs7-sha1.ts +161 -0
  122. package/packages/pdf-lite/src/signing/signatures/adbe-x509-rsa-sha1.ts +106 -0
  123. package/packages/pdf-lite/src/signing/signatures/base.ts +229 -0
  124. package/packages/pdf-lite/src/signing/signatures/etsi-cades-detached.ts +229 -0
  125. package/packages/pdf-lite/src/signing/signatures/etsi-rfc3161.ts +92 -0
  126. package/packages/pdf-lite/src/signing/signatures/index.ts +6 -0
  127. package/packages/pdf-lite/src/signing/signer.ts +120 -0
  128. package/packages/pdf-lite/src/signing/types.ts +86 -0
  129. package/packages/pdf-lite/src/signing/utils.ts +71 -0
  130. package/packages/pdf-lite/src/types.ts +44 -0
  131. package/packages/pdf-lite/src/utils/IterableReadableStream.ts +30 -0
  132. package/packages/pdf-lite/src/utils/algos.ts +446 -0
  133. package/packages/pdf-lite/src/utils/assert.ts +42 -0
  134. package/packages/pdf-lite/src/utils/bytesToHex.ts +18 -0
  135. package/packages/pdf-lite/src/utils/bytesToHexBytes.ts +27 -0
  136. package/packages/pdf-lite/src/utils/bytesToString.ts +17 -0
  137. package/packages/pdf-lite/src/utils/concatUint8Arrays.ts +26 -0
  138. package/packages/pdf-lite/src/utils/escapeString.ts +49 -0
  139. package/packages/pdf-lite/src/utils/hexBytesToBytes.ts +22 -0
  140. package/packages/pdf-lite/src/utils/hexBytesToString.ts +21 -0
  141. package/packages/pdf-lite/src/utils/hexToBytes.ts +18 -0
  142. package/packages/pdf-lite/src/utils/padBytes.ts +25 -0
  143. package/packages/pdf-lite/src/utils/predictors.ts +332 -0
  144. package/packages/pdf-lite/src/utils/replaceInBuffer.ts +56 -0
  145. package/packages/pdf-lite/src/utils/stringToBytes.ts +22 -0
  146. package/packages/pdf-lite/src/utils/stringToHexBytes.ts +23 -0
  147. package/packages/pdf-lite/src/utils/unescapeString.ts +123 -0
  148. package/packages/pdf-lite/test/acceptance/__snapshots__/versions.node.test.ts.snap +60766 -0
  149. package/packages/pdf-lite/test/acceptance/fixtures/1.3/basic.pdf +0 -0
  150. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-aes-128.pdf +0 -0
  151. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-aes-256.pdf +0 -0
  152. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-rc4-128.pdf +0 -0
  153. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic-rc4-40.pdf +0 -0
  154. package/packages/pdf-lite/test/acceptance/fixtures/1.4/basic.pdf +0 -0
  155. package/packages/pdf-lite/test/acceptance/fixtures/1.5/basic.pdf +0 -0
  156. package/packages/pdf-lite/test/acceptance/fixtures/1.6/basic.pdf +0 -0
  157. package/packages/pdf-lite/test/acceptance/fixtures/1.7/basic.pdf +0 -0
  158. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-aes-128.pdf +43 -0
  159. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-aes-256.pdf +43 -0
  160. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-rc4-128.pdf +43 -0
  161. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic-rc4-40.pdf +44 -0
  162. package/packages/pdf-lite/test/acceptance/fixtures/2.0/basic.pdf +79 -0
  163. package/packages/pdf-lite/test/acceptance/versions.node.test.ts +41 -0
  164. package/packages/pdf-lite/test/unit/__snapshots__/decoder.node.test.ts.snap +86947 -0
  165. package/packages/pdf-lite/test/unit/__snapshots__/tokeniser.node.test.ts.snap +131829 -0
  166. package/packages/pdf-lite/test/unit/ciphers.test.ts +61 -0
  167. package/packages/pdf-lite/test/unit/decoder.node.test.ts +21 -0
  168. package/packages/pdf-lite/test/unit/decoder.test.ts +567 -0
  169. package/packages/pdf-lite/test/unit/filters.test.ts +67 -0
  170. package/packages/pdf-lite/test/unit/fixtures/basic.pdf +0 -0
  171. package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-aes-128.pdf +0 -0
  172. package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-aes-256.pdf +0 -0
  173. package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-rc4-128.pdf +0 -0
  174. package/packages/pdf-lite/test/unit/fixtures/encrypted_v1/basic-rc4-40.pdf +43 -0
  175. package/packages/pdf-lite/test/unit/fixtures/protectedAdobeLivecycle.pdf +0 -0
  176. package/packages/pdf-lite/test/unit/fixtures/rsa-2048/index.ts +187 -0
  177. package/packages/pdf-lite/test/unit/fixtures/template.pdf +0 -0
  178. package/packages/pdf-lite/test/unit/incremental-update.test.ts +0 -0
  179. package/packages/pdf-lite/test/unit/objects.test.ts +0 -0
  180. package/packages/pdf-lite/test/unit/pdf-document-signing.test.ts +0 -0
  181. package/packages/pdf-lite/test/unit/pdf-revision.test.ts +195 -0
  182. package/packages/pdf-lite/test/unit/pdf.browser.test.ts +0 -0
  183. package/packages/pdf-lite/test/unit/predictors.test.ts +226 -0
  184. package/packages/pdf-lite/test/unit/ref.test.ts +158 -0
  185. package/packages/pdf-lite/test/unit/security-handlers.test.ts +645 -0
  186. package/packages/pdf-lite/test/unit/serializer.test.ts +81 -0
  187. package/packages/pdf-lite/test/unit/signature-objects.test.ts +814 -0
  188. package/packages/pdf-lite/test/unit/string-escaping.test.ts +84 -0
  189. package/packages/pdf-lite/test/unit/tokeniser.node.test.ts +38 -0
  190. package/packages/pdf-lite/test/unit/tokeniser.test.ts +1213 -0
  191. package/packages/pdf-lite/test/unit/utils.test.ts +248 -0
  192. package/packages/pdf-lite/test/unit/xref-lookup.test.ts +72 -0
  193. package/packages/pdf-lite/tsconfig.json +4 -0
  194. package/packages/pdf-lite/tsconfig.prod.json +8 -0
  195. package/packages/pdf-lite/typedoc.json +14 -0
  196. package/packages/pdf-lite/vitest.config.ts +43 -0
  197. package/pnpm-workspace.yaml +2 -0
  198. package/renovate.json +34 -0
  199. package/scripts/build-examples.ts +30 -0
  200. package/scripts/bump-version.sh +56 -0
  201. package/scripts/gen-html-docs.sh +21 -0
  202. package/scripts/gen-md-docs.sh +15 -0
  203. package/scripts/prepare-release.sh +33 -0
  204. package/tsconfig.json +22 -0
  205. package/tsconfig.prod.json +12 -0
  206. 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
+ }