@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.
- package/README.md +25 -7
- package/package.json +5 -3
- package/src/index.ts +10 -0
- package/src/reader/cmap_parser.ts +173 -0
- package/src/reader/decrypt.ts +226 -0
- package/src/reader/document.ts +246 -0
- package/src/reader/encodings.ts +73 -0
- package/src/reader/extract.ts +152 -0
- package/src/reader/fonts.ts +259 -0
- package/src/reader/index.ts +27 -0
- package/src/reader/layout.ts +106 -0
- package/src/reader/lexer.ts +270 -0
- package/src/reader/object_parser.ts +203 -0
- package/src/reader/objstm.ts +44 -0
- package/src/reader/text_interpreter.ts +327 -0
- package/src/reader/xref.ts +229 -0
- package/src/streams/decode.ts +98 -0
- package/src/streams/flate.ts +94 -4
- package/src/streams/index.ts +6 -1
- package/src/streams/lzw.ts +74 -0
- package/src/streams/runlength.ts +25 -0
- package/src/util/errors.ts +20 -0
|
@@ -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
|
+
}
|
package/src/streams/flate.ts
CHANGED
|
@@ -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)
|
|
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
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
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
|
}
|
package/src/streams/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/util/errors.ts
CHANGED
|
@@ -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
|
+
}
|