@strav/pdf 0.4.17 → 0.4.18

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.
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Cross-reference resolution (spec §7.5). Locates the trailing `startxref`,
3
+ * then walks the section chain — classic `xref` tables and `/Type /XRef`
4
+ * streams, plus the hybrid `/XRefStm` pointer — following `/Prev` to older
5
+ * sections. Newest section wins on conflict (first-seen during the walk).
6
+ */
7
+
8
+ import {
9
+ type PdfObject,
10
+ type PdfDictionary,
11
+ isNum,
12
+ isArr,
13
+ isDict,
14
+ isRef,
15
+ isStream,
16
+ } from '../objects/types.ts'
17
+ import { PdfParseError } from '../util/errors.ts'
18
+ import { decodeStream } from '../streams/decode.ts'
19
+ import { Lexer, latin1 } from './lexer.ts'
20
+ import { ObjectParser } from './object_parser.ts'
21
+
22
+ /** Uncompressed object: byte offset. Compressed: container objstm + index. */
23
+ export type XrefEntry =
24
+ | { type: 'n'; offset: number; gen: number }
25
+ | { type: 'c'; streamObj: number; index: number }
26
+
27
+ export interface XrefTable {
28
+ entries: Map<number, XrefEntry>
29
+ trailer: PdfDictionary
30
+ }
31
+
32
+ /** Scan the tail for the last `startxref` and return its offset value. */
33
+ export function findStartXref(buf: Uint8Array): number {
34
+ const needle = 'startxref'
35
+ const from = Math.max(0, buf.length - 2048)
36
+ for (let p = buf.length - needle.length; p >= from; p--) {
37
+ if (buf[p] === 0x73 && latin1(buf, p, p + needle.length) === needle) {
38
+ const lex = new Lexer(buf, p + needle.length)
39
+ const t = lex.next()
40
+ if (t.type === 'num') return t.value
41
+ }
42
+ }
43
+ throw new PdfParseError('No startxref found')
44
+ }
45
+
46
+ export function parseXref(buf: Uint8Array): XrefTable {
47
+ const entries = new Map<number, XrefEntry>()
48
+ let trailer: PdfDictionary | undefined
49
+ const visited = new Set<number>()
50
+ const queue: number[] = [findStartXref(buf)]
51
+
52
+ while (queue.length) {
53
+ const off = queue.shift()!
54
+ if (off < 0 || off >= buf.length || visited.has(off)) continue
55
+ visited.add(off)
56
+
57
+ const lex = new Lexer(buf, off)
58
+ const t = lex.peek()
59
+ let sectionTrailer: PdfDictionary
60
+
61
+ if (t.type === 'kw' && t.value === 'xref') {
62
+ sectionTrailer = parseClassic(buf, off, entries)
63
+ } else {
64
+ sectionTrailer = parseXrefStream(buf, off, entries)
65
+ }
66
+ if (!trailer) trailer = sectionTrailer
67
+
68
+ // Hybrid: an /XRefStm points at a parallel xref stream for this section.
69
+ const xrefStm = sectionTrailer.entries.get('XRefStm')
70
+ if (xrefStm && isNum(xrefStm)) queue.push(xrefStm.value)
71
+ const prev = sectionTrailer.entries.get('Prev')
72
+ if (prev && isNum(prev)) queue.push(prev.value)
73
+ }
74
+
75
+ if (!trailer) throw new PdfParseError('No trailer dictionary')
76
+ return { entries, trailer }
77
+ }
78
+
79
+ function setIfAbsent(map: Map<number, XrefEntry>, n: number, e: XrefEntry): void {
80
+ if (!map.has(n)) map.set(n, e)
81
+ }
82
+
83
+ function parseClassic(
84
+ buf: Uint8Array,
85
+ off: number,
86
+ entries: Map<number, XrefEntry>,
87
+ ): PdfDictionary {
88
+ const lex = new Lexer(buf, off)
89
+ lex.next() // 'xref'
90
+ for (;;) {
91
+ const a = lex.next()
92
+ if (a.type === 'kw' && a.value === 'trailer') break
93
+ if (a.type === 'eof') throw new PdfParseError('Unterminated xref table')
94
+ if (a.type !== 'num') throw new PdfParseError('Malformed xref subsection header')
95
+ const count = lex.next()
96
+ if (count.type !== 'num') throw new PdfParseError('Malformed xref subsection header')
97
+ const start = a.value
98
+ for (let i = 0; i < count.value; i++) {
99
+ const offTok = lex.next()
100
+ const genTok = lex.next()
101
+ const kind = lex.next()
102
+ if (offTok.type !== 'num' || genTok.type !== 'num' || kind.type !== 'kw') {
103
+ throw new PdfParseError('Malformed xref entry')
104
+ }
105
+ if (kind.value === 'n') {
106
+ setIfAbsent(entries, start + i, {
107
+ type: 'n',
108
+ offset: offTok.value,
109
+ gen: genTok.value,
110
+ })
111
+ }
112
+ }
113
+ }
114
+ // trailer << … >>
115
+ const parser = new ObjectParser(new Lexer(buf, lex.pos))
116
+ const tr = parser.parseObject()
117
+ if (!isDict(tr)) throw new PdfParseError('Trailer is not a dictionary')
118
+ return tr
119
+ }
120
+
121
+ function parseXrefStream(
122
+ buf: Uint8Array,
123
+ off: number,
124
+ entries: Map<number, XrefEntry>,
125
+ ): PdfDictionary {
126
+ const parser = new ObjectParser(new Lexer(buf, off))
127
+ const { value } = parser.parseIndirectAt(off)
128
+ if (!isStream(value)) throw new PdfParseError('Expected an xref stream object')
129
+ const d = value.dict
130
+ const data = decodeStream(d, value.data, (o) => o)
131
+
132
+ const wObj = d.entries.get('W')
133
+ if (!wObj || !isArr(wObj)) throw new PdfParseError('Xref stream missing /W')
134
+ const W = wObj.items.map((x) => (isNum(x) ? x.value : 0))
135
+ const [w0, w1, w2] = [W[0] ?? 0, W[1] ?? 0, W[2] ?? 0]
136
+ const recLen = w0 + w1 + w2
137
+
138
+ const sizeObj = d.entries.get('Size')
139
+ const size = sizeObj && isNum(sizeObj) ? sizeObj.value : 0
140
+ const indexObj = d.entries.get('Index')
141
+ const index: number[] =
142
+ indexObj && isArr(indexObj)
143
+ ? indexObj.items.map((x) => (isNum(x) ? x.value : 0))
144
+ : [0, size]
145
+
146
+ const readField = (p: number, w: number, dflt: number): number => {
147
+ if (w === 0) return dflt
148
+ let v = 0
149
+ for (let k = 0; k < w; k++) v = v * 256 + data[p + k]!
150
+ return v
151
+ }
152
+
153
+ let pos = 0
154
+ for (let s = 0; s + 1 < index.length; s += 2) {
155
+ const start = index[s]!
156
+ const cnt = index[s + 1]!
157
+ for (let i = 0; i < cnt && pos + recLen <= data.length; i++) {
158
+ const objNum = start + i
159
+ const type = readField(pos, w0, 1)
160
+ const f2 = readField(pos + w0, w1, 0)
161
+ const f3 = readField(pos + w0 + w1, w2, 0)
162
+ pos += recLen
163
+ if (type === 1) {
164
+ setIfAbsent(entries, objNum, { type: 'n', offset: f2, gen: f3 })
165
+ } else if (type === 2) {
166
+ setIfAbsent(entries, objNum, { type: 'c', streamObj: f2, index: f3 })
167
+ }
168
+ }
169
+ }
170
+ return d
171
+ }
172
+
173
+ /**
174
+ * Last-resort recovery: scan the whole buffer for `N G obj` headers and build
175
+ * an xref table from scratch (latest occurrence wins). Used when the real
176
+ * xref is missing or corrupt.
177
+ */
178
+ export function bruteForceXref(buf: Uint8Array): XrefTable {
179
+ const entries = new Map<number, XrefEntry>()
180
+ const re = /(\d+)\s+(\d+)\s+obj\b/g
181
+ const text = latin1(buf, 0, buf.length)
182
+ let m: RegExpExecArray | null
183
+ while ((m = re.exec(text))) {
184
+ const n = Number(m[1])
185
+ const g = Number(m[2])
186
+ entries.set(n, { type: 'n', offset: m.index, gen: g })
187
+ }
188
+ // Locate a trailer dict, else synthesize from a /Root /Catalog scan.
189
+ let trailer: PdfDictionary | undefined
190
+ const tIdx = text.lastIndexOf('trailer')
191
+ if (tIdx >= 0) {
192
+ try {
193
+ const tr = new ObjectParser(new Lexer(buf, tIdx + 7)).parseObject()
194
+ if (isDict(tr)) trailer = tr
195
+ } catch {
196
+ /* fall through */
197
+ }
198
+ }
199
+ if (!trailer || !trailer.entries.has('Root')) {
200
+ trailer = synthesizeTrailer(buf, entries)
201
+ }
202
+ return { entries, trailer }
203
+ }
204
+
205
+ function synthesizeTrailer(
206
+ buf: Uint8Array,
207
+ entries: Map<number, XrefEntry>,
208
+ ): PdfDictionary {
209
+ for (const [n, e] of entries) {
210
+ if (e.type !== 'n') continue
211
+ try {
212
+ const { value } = new ObjectParser(new Lexer(buf, e.offset)).parseIndirectAt(e.offset)
213
+ const d = isStream(value) ? value.dict : value
214
+ if (isDict(d)) {
215
+ const ty = d.entries.get('Type')
216
+ if (ty && 'value' in ty && ty.value === 'Catalog') {
217
+ const tr: PdfDictionary = { kind: 'dict', entries: new Map() }
218
+ tr.entries.set('Root', { kind: 'ref', num: n, gen: e.gen })
219
+ return tr
220
+ }
221
+ }
222
+ } catch {
223
+ /* skip unparseable objects */
224
+ }
225
+ }
226
+ throw new PdfParseError('Could not recover a document catalog')
227
+ }
228
+
229
+ export { isRef }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Stream filter dispatch (read side, spec §7.4). Resolves a stream's
3
+ * `/Filter` + `/DecodeParms` (name or parallel arrays, possibly indirect) and
4
+ * applies each decode filter in order. Image filters (DCT/JPX/CCITT/JBIG2) are
5
+ * terminal and returned unchanged — text extraction never needs their pixels.
6
+ */
7
+
8
+ import type { PdfDictionary, PdfObject } from '../objects/types.ts'
9
+ import { isArr, isDict, isName, isNum } from '../objects/types.ts'
10
+ import { flateDecode, type PredictorParams } from './flate.ts'
11
+ import { lzwDecode } from './lzw.ts'
12
+ import { ascii85Decode } from './ascii85.ts'
13
+ import { asciiHexDecode } from './ascii_hex.ts'
14
+ import { runLengthDecode } from './runlength.ts'
15
+
16
+ /** Filters whose output is binary image data, not byte-stream content. */
17
+ const IMAGE_FILTERS = new Set(['DCTDecode', 'JPXDecode', 'CCITTFaxDecode', 'JBIG2Decode'])
18
+
19
+ export type Resolve = (o: PdfObject | undefined) => PdfObject | undefined
20
+
21
+ function dictGet(d: PdfDictionary, key: string, resolve: Resolve): PdfObject | undefined {
22
+ return resolve(d.entries.get(key))
23
+ }
24
+
25
+ function asList(o: PdfObject | undefined, resolve: Resolve): (PdfObject | undefined)[] {
26
+ if (!o) return []
27
+ if (isArr(o)) return o.items.map((x) => resolve(x))
28
+ return [o]
29
+ }
30
+
31
+ function predictorParams(o: PdfObject | undefined, resolve: Resolve): PredictorParams &
32
+ { earlyChange?: number } {
33
+ if (!o || !isDict(o)) return {}
34
+ const n = (k: string): number | undefined => {
35
+ const v = resolve(o.entries.get(k))
36
+ return v && isNum(v) ? v.value : undefined
37
+ }
38
+ return {
39
+ predictor: n('Predictor'),
40
+ colors: n('Colors'),
41
+ bitsPerComponent: n('BitsPerComponent'),
42
+ columns: n('Columns'),
43
+ earlyChange: n('EarlyChange'),
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Decode the on-disk bytes of a stream into its logical content. Returns the
49
+ * (possibly partially) decoded bytes; stops at the first image filter.
50
+ */
51
+ export function decodeStream(
52
+ dict: PdfDictionary,
53
+ data: Uint8Array,
54
+ resolve: Resolve = (o) => o,
55
+ ): Uint8Array {
56
+ const filters = asList(
57
+ dictGet(dict, 'Filter', resolve) ?? dictGet(dict, 'F', resolve),
58
+ resolve,
59
+ )
60
+ if (filters.length === 0) return data
61
+
62
+ const parmsRaw = dictGet(dict, 'DecodeParms', resolve) ?? dictGet(dict, 'DP', resolve)
63
+ const parmsList = asList(parmsRaw, resolve)
64
+
65
+ let out = data
66
+ for (let i = 0; i < filters.length; i++) {
67
+ const f = filters[i]
68
+ if (!f || !isName(f)) continue
69
+ const parms = predictorParams(parmsList[i], resolve)
70
+ switch (f.value) {
71
+ case 'FlateDecode':
72
+ case 'Fl':
73
+ out = flateDecode(out, parms)
74
+ break
75
+ case 'LZWDecode':
76
+ case 'LZW':
77
+ out = lzwDecode(out, parms)
78
+ break
79
+ case 'ASCII85Decode':
80
+ case 'A85':
81
+ out = ascii85Decode(out)
82
+ break
83
+ case 'ASCIIHexDecode':
84
+ case 'AHx':
85
+ out = asciiHexDecode(out)
86
+ break
87
+ case 'RunLengthDecode':
88
+ case 'RL':
89
+ out = runLengthDecode(out)
90
+ break
91
+ default:
92
+ if (IMAGE_FILTERS.has(f.value)) return out // terminal: leave encoded
93
+ // Unknown filter — return what we have rather than corrupt further.
94
+ return out
95
+ }
96
+ }
97
+ return out
98
+ }
@@ -1,17 +1,107 @@
1
1
  /**
2
2
  * FlateDecode (spec §7.2). Node/Bun build only — `node:zlib` at level 9 for
3
3
  * deterministic output (level affects the byte sequence). No browser fallback
4
- * (project decision); no PNG/TIFF predictor in v1.
4
+ * (project decision).
5
+ *
6
+ * Encoding never applies a predictor. Decoding (read side, M13) supports the
7
+ * PNG (10–15) and TIFF (2) predictors via /DecodeParms (spec §7.4.4.4), which
8
+ * real-world PDFs commonly use for xref and image streams.
5
9
  */
6
10
 
7
11
  import { deflateSync, inflateSync } from 'node:zlib'
8
12
 
13
+ /** Predictor parameters from a stream's /DecodeParms (spec §7.4.4.4). */
14
+ export interface PredictorParams {
15
+ /** 1 = none, 2 = TIFF, 10–15 = PNG (the exact PNG type is per-row). */
16
+ predictor?: number
17
+ /** Samples per pixel. Default 1. */
18
+ colors?: number
19
+ /** Bits per component. Default 8. */
20
+ bitsPerComponent?: number
21
+ /** Samples per row. Default 1. */
22
+ columns?: number
23
+ }
24
+
9
25
  /** Deflate (zlib) encode at level 9. */
10
26
  export function flateEncode(data: Uint8Array): Uint8Array {
11
27
  return new Uint8Array(deflateSync(data, { level: 9 }))
12
28
  }
13
29
 
14
- /** Inflate — not used in production output; provided for round-trip tests. */
15
- export function flateDecode(data: Uint8Array): Uint8Array {
16
- return new Uint8Array(inflateSync(data))
30
+ /**
31
+ * Inflate, then reverse the predictor if one is configured. Falls back to a
32
+ * raw-deflate retry (`-15` window) for the malformed-zlib-header streams some
33
+ * producers emit.
34
+ */
35
+ export function flateDecode(data: Uint8Array, params?: PredictorParams): Uint8Array {
36
+ let out: Uint8Array
37
+ try {
38
+ out = new Uint8Array(inflateSync(data))
39
+ } catch {
40
+ out = new Uint8Array(inflateSync(data, { finishFlush: 2 /* Z_SYNC_FLUSH */ }))
41
+ }
42
+ return params && (params.predictor ?? 1) > 1 ? unpredict(out, params) : out
43
+ }
44
+
45
+ /** Reverse a PNG/TIFF predictor (spec §7.4.4.4). Exported for other filters. */
46
+ export function unpredict(data: Uint8Array, params: PredictorParams): Uint8Array {
47
+ const predictor = params.predictor ?? 1
48
+ if (predictor <= 1) return data
49
+
50
+ const colors = params.colors ?? 1
51
+ const bpc = params.bitsPerComponent ?? 8
52
+ const columns = params.columns ?? 1
53
+ const bpp = Math.ceil((colors * bpc) / 8) // bytes per pixel (≥1)
54
+ const rowBytes = Math.ceil((colors * bpc * columns) / 8)
55
+
56
+ if (predictor === 2) {
57
+ // TIFF predictor 2: horizontal differencing, per-component.
58
+ if (bpc !== 8) return data // sub-byte TIFF predictor: rare, left as-is
59
+ const out = data.slice()
60
+ for (let r = 0; r + rowBytes <= out.length; r += rowBytes) {
61
+ for (let i = bpp; i < rowBytes; i++) {
62
+ out[r + i] = (out[r + i]! + out[r + i - bpp]!) & 0xff
63
+ }
64
+ }
65
+ return out
66
+ }
67
+
68
+ // PNG predictors: each row is prefixed by a 1-byte filter type.
69
+ const rows = Math.floor(data.length / (rowBytes + 1))
70
+ const out = new Uint8Array(rows * rowBytes)
71
+ const prev = new Uint8Array(rowBytes)
72
+ let src = 0
73
+ let dst = 0
74
+ for (let r = 0; r < rows; r++) {
75
+ const type = data[src++]!
76
+ const row = data.subarray(src, src + rowBytes)
77
+ src += rowBytes
78
+ for (let i = 0; i < rowBytes; i++) {
79
+ const a = i >= bpp ? out[dst + i - bpp]! : 0 // left
80
+ const b = prev[i]! // up
81
+ const c = i >= bpp ? prev[i - bpp]! : 0 // upper-left
82
+ let v = row[i]!
83
+ switch (type) {
84
+ case 0: break // None
85
+ case 1: v = (v + a) & 0xff; break // Sub
86
+ case 2: v = (v + b) & 0xff; break // Up
87
+ case 3: v = (v + ((a + b) >> 1)) & 0xff; break // Average
88
+ case 4: v = (v + paeth(a, b, c)) & 0xff; break // Paeth
89
+ default: break
90
+ }
91
+ out[dst + i] = v
92
+ }
93
+ prev.set(out.subarray(dst, dst + rowBytes))
94
+ dst += rowBytes
95
+ }
96
+ return out
97
+ }
98
+
99
+ function paeth(a: number, b: number, c: number): number {
100
+ const p = a + b - c
101
+ const pa = Math.abs(p - a)
102
+ const pb = Math.abs(p - b)
103
+ const pc = Math.abs(p - c)
104
+ if (pa <= pb && pa <= pc) return a
105
+ if (pb <= pc) return b
106
+ return c
17
107
  }
@@ -4,6 +4,11 @@ export {
4
4
  MIN_FILTER_BYTES,
5
5
  } from './stream.ts'
6
6
  export type { FilterName, MakeStreamOptions } from './stream.ts'
7
- export { flateEncode, flateDecode } from './flate.ts'
7
+ export { flateEncode, flateDecode, unpredict } from './flate.ts'
8
+ export type { PredictorParams } from './flate.ts'
8
9
  export { ascii85Encode, ascii85Decode } from './ascii85.ts'
9
10
  export { asciiHexEncode, asciiHexDecode } from './ascii_hex.ts'
11
+ export { lzwDecode } from './lzw.ts'
12
+ export { runLengthDecode } from './runlength.ts'
13
+ export { decodeStream } from './decode.ts'
14
+ export type { Resolve } from './decode.ts'
@@ -0,0 +1,74 @@
1
+ /**
2
+ * LZWDecode (spec §7.4.4). Variable-width codes 9–12 bits, MSB-first. Code 256
3
+ * = clear table, 257 = EOD. `earlyChange` (default 1) bumps the code width one
4
+ * code early, matching Adobe's encoder. A predictor may follow (spec §7.4.4.4).
5
+ *
6
+ * Decode-only — the writer never emits LZW.
7
+ */
8
+
9
+ import { unpredict, type PredictorParams } from './flate.ts'
10
+
11
+ const CLEAR = 256
12
+ const EOD = 257
13
+
14
+ export function lzwDecode(
15
+ data: Uint8Array,
16
+ params?: PredictorParams & { earlyChange?: number },
17
+ ): Uint8Array {
18
+ const earlyChange = params?.earlyChange ?? 1
19
+ const out: number[] = []
20
+
21
+ let bitBuf = 0
22
+ let bitCnt = 0
23
+ let pos = 0
24
+ const next = (width: number): number => {
25
+ while (bitCnt < width) {
26
+ if (pos >= data.length) return EOD
27
+ bitBuf = (bitBuf << 8) | data[pos++]!
28
+ bitCnt += 8
29
+ }
30
+ bitCnt -= width
31
+ return (bitBuf >> bitCnt) & ((1 << width) - 1)
32
+ }
33
+
34
+ let dict: number[][] = []
35
+ let width = 9
36
+ const reset = () => {
37
+ dict = []
38
+ for (let i = 0; i < 256; i++) dict[i] = [i]
39
+ dict[CLEAR] = []
40
+ dict[EOD] = []
41
+ width = 9
42
+ }
43
+ reset()
44
+
45
+ let prev: number[] | null = null
46
+ for (;;) {
47
+ const code = next(width)
48
+ if (code === EOD) break
49
+ if (code === CLEAR) {
50
+ reset()
51
+ prev = null
52
+ continue
53
+ }
54
+
55
+ let entry: number[]
56
+ if (dict[code]) {
57
+ entry = dict[code]!
58
+ } else if (code === dict.length && prev) {
59
+ entry = [...prev, prev[0]!]
60
+ } else {
61
+ break // corrupt stream — stop gracefully
62
+ }
63
+ for (const b of entry) out.push(b)
64
+
65
+ if (prev) {
66
+ dict.push([...prev, entry[0]!])
67
+ if (dict.length + earlyChange >= 1 << width && width < 12) width++
68
+ }
69
+ prev = entry
70
+ }
71
+
72
+ const bytes = Uint8Array.from(out)
73
+ return params && (params.predictor ?? 1) > 1 ? unpredict(bytes, params) : bytes
74
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * RunLengthDecode (spec §7.4.5). Length byte L:
3
+ * 0–127 → copy the next L+1 bytes literally
4
+ * 129–255→ repeat the next single byte 257−L times
5
+ * 128 → EOD
6
+ *
7
+ * Decode-only — the writer never emits RunLength.
8
+ */
9
+
10
+ export function runLengthDecode(data: Uint8Array): Uint8Array {
11
+ const out: number[] = []
12
+ let i = 0
13
+ while (i < data.length) {
14
+ const len = data[i++]!
15
+ if (len === 128) break // EOD
16
+ if (len < 128) {
17
+ for (let k = 0; k <= len && i < data.length; k++) out.push(data[i++]!)
18
+ } else {
19
+ if (i >= data.length) break
20
+ const b = data[i++]!
21
+ for (let k = 0; k < 257 - len; k++) out.push(b)
22
+ }
23
+ }
24
+ return Uint8Array.from(out)
25
+ }
@@ -23,6 +23,9 @@ export type PdfGenErrorCode =
23
23
  | 'PDF_TEXT_STATE'
24
24
  | 'PDF_TEXT_ENCODING'
25
25
  | 'PDF_NO_FONT'
26
+ | 'PDF_PARSE'
27
+ | 'PDF_ENCRYPTED'
28
+ | 'PDF_UNSUPPORTED_DECODE'
26
29
 
27
30
  export class PdfGenError extends Error {
28
31
  readonly code: PdfGenErrorCode
@@ -59,3 +62,20 @@ export class InvalidImageError extends PdfGenError {
59
62
  super('PDF_INVALID_IMAGE', message)
60
63
  }
61
64
  }
65
+
66
+ /** Thrown when an existing PDF cannot be parsed (read side, M13). */
67
+ export class PdfParseError extends PdfGenError {
68
+ constructor(message: string) {
69
+ super('PDF_PARSE', message)
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Thrown when a PDF is encrypted in a way M13 does not support: a non-empty
75
+ * user password, or a non-standard / unsupported security handler.
76
+ */
77
+ export class EncryptedPdfError extends PdfGenError {
78
+ constructor(message: string) {
79
+ super('PDF_ENCRYPTED', message)
80
+ }
81
+ }