reamkit 1.6.0 → 1.7.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/dist/esm/core/drawingml/chart-serializer.d.ts +2 -0
- package/dist/esm/core/drawingml/chart-serializer.js +53 -0
- package/dist/esm/excel/print-model.js +6 -3
- package/dist/esm/excel/xlsx-writer.js +81 -17
- package/dist/esm/pdf-reader/content.d.ts +39 -1
- package/dist/esm/pdf-reader/content.js +132 -4
- package/dist/esm/pdf-reader/crypto.d.ts +7 -0
- package/dist/esm/pdf-reader/crypto.js +609 -0
- package/dist/esm/pdf-reader/decrypt.d.ts +5 -0
- package/dist/esm/pdf-reader/decrypt.js +199 -0
- package/dist/esm/pdf-reader/document.d.ts +7 -0
- package/dist/esm/pdf-reader/document.js +209 -26
- package/dist/esm/pdf-reader/flow-build.d.ts +16 -1
- package/dist/esm/pdf-reader/flow-build.js +102 -3
- package/dist/esm/pdf-reader/image-decode.d.ts +15 -0
- package/dist/esm/pdf-reader/image-decode.js +442 -0
- package/dist/esm/pdf-reader/images.d.ts +16 -0
- package/dist/esm/pdf-reader/images.js +90 -0
- package/dist/esm/pdf-reader/layout.d.ts +2 -2
- package/dist/esm/pdf-reader/layout.js +48 -14
- package/dist/esm/pdf-reader/png-encode.d.ts +2 -0
- package/dist/esm/pdf-reader/png-encode.js +98 -0
- package/dist/esm/pdf-reader/predictor.d.ts +7 -0
- package/dist/esm/pdf-reader/predictor.js +61 -0
- package/dist/esm/pdf-reader/reader.js +11 -4
- package/dist/esm/pdf-reader/tagged.d.ts +2 -2
- package/dist/esm/pdf-reader/tagged.js +60 -7
- package/dist/esm/pdf-reader/text.js +48 -1
- package/dist/esm/pdf-reader/vector.d.ts +12 -0
- package/dist/esm/pdf-reader/vector.js +54 -0
- package/dist/esm/word/docx-writer.js +96 -7
- package/dist/esm/word/omml-serializer.d.ts +2 -0
- package/dist/esm/word/omml-serializer.js +48 -0
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PdfFile } from './document.js';
|
|
2
|
+
import { PdfStream } from '../pdf/objects.js';
|
|
3
|
+
export type DecodedImage = {
|
|
4
|
+
readonly ok: true;
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
readonly format: 'png' | 'jpeg' | 'jpeg2000';
|
|
7
|
+
readonly widthPx: number;
|
|
8
|
+
readonly heightPx: number;
|
|
9
|
+
readonly degraded?: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly severity: 'dropped' | 'degraded';
|
|
13
|
+
readonly detail: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function decodePdfImage(file: PdfFile, stream: PdfStream): DecodedImage;
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { PDF_NULL, PdfHexString, PdfName, PdfStream } from "../pdf/objects.js";
|
|
2
|
+
import { reversePredictor } from "./predictor.js";
|
|
3
|
+
import { encodePng } from "./png-encode.js";
|
|
4
|
+
import { unzlibSync } from "fflate";
|
|
5
|
+
//#region src/pdf-reader/image-decode.ts
|
|
6
|
+
var MAX_PIXELS = 4e7;
|
|
7
|
+
function decodePdfImage(file, stream) {
|
|
8
|
+
const d = stream.dict;
|
|
9
|
+
const width = intOf(file.get(d, "Width")) || intOf(file.get(d, "W"));
|
|
10
|
+
const height = intOf(file.get(d, "Height")) || intOf(file.get(d, "H"));
|
|
11
|
+
if (width <= 0 || height <= 0) return fail("dropped", "image with no dimensions");
|
|
12
|
+
if (width * height > MAX_PIXELS) return fail("dropped", "image too large to decode");
|
|
13
|
+
if (boolOf(d.get("ImageMask")) || boolOf(d.get("IM"))) return fail("dropped", "stencil image mask not reconstructed");
|
|
14
|
+
const filters = filterNames(file, d);
|
|
15
|
+
const last = filters[filters.length - 1];
|
|
16
|
+
if (last === "DCTDecode" || last === "DCT") {
|
|
17
|
+
const degraded = hasSMask(file, d) ? "image transparency dropped (JPEG carries no alpha)" : void 0;
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
bytes: applyChainExceptLast(filters, stream.data),
|
|
21
|
+
format: "jpeg",
|
|
22
|
+
widthPx: width,
|
|
23
|
+
heightPx: height,
|
|
24
|
+
...degraded ? { degraded } : {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (last === "JPXDecode") return {
|
|
28
|
+
ok: true,
|
|
29
|
+
bytes: applyChainExceptLast(filters, stream.data),
|
|
30
|
+
format: "jpeg2000",
|
|
31
|
+
widthPx: width,
|
|
32
|
+
heightPx: height,
|
|
33
|
+
degraded: "JPEG 2000 image — limited viewer support"
|
|
34
|
+
};
|
|
35
|
+
if (last === "CCITTFaxDecode" || last === "CCF" || last === "JBIG2Decode") return fail("dropped", "fax-encoded (CCITT/JBIG2) image not decoded");
|
|
36
|
+
if (last === "LZWDecode" || last === "LZW") return fail("dropped", "LZW-encoded image not decoded");
|
|
37
|
+
const decoded = decodeToSamples(file, stream, filters, width, height);
|
|
38
|
+
if (typeof decoded === "string") return fail("dropped", decoded);
|
|
39
|
+
const alpha = decodeSMask(file, d, width, height);
|
|
40
|
+
const { color, samples } = alpha ? combineAlpha(decoded, alpha) : decoded;
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
bytes: encodePng(width, height, color, samples),
|
|
44
|
+
format: "png",
|
|
45
|
+
widthPx: width,
|
|
46
|
+
heightPx: height
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function decodeToSamples(file, stream, filters, width, height) {
|
|
50
|
+
const d = stream.dict;
|
|
51
|
+
const raw = decodeChain(file, stream, filters);
|
|
52
|
+
if (!raw) return "undecodable image stream";
|
|
53
|
+
const cs = resolveColorSpace(file, d.get("ColorSpace") ?? d.get("CS"));
|
|
54
|
+
if (!cs) return "unsupported image colour space";
|
|
55
|
+
const bpc = intOf(file.get(d, "BitsPerComponent")) || intOf(file.get(d, "BPC")) || 8;
|
|
56
|
+
const decodeArr = decodeArrayOf(file, d);
|
|
57
|
+
return toColor(cs, unpackSamples(raw, width, height, cs.components, bpc), width * height, bpc, decodeArr);
|
|
58
|
+
}
|
|
59
|
+
function resolveColorSpace(file, csVal) {
|
|
60
|
+
const cs = file.resolve(csVal ?? PDF_NULL);
|
|
61
|
+
if (cs instanceof PdfName) return namedColorSpace(cs.value);
|
|
62
|
+
if (!Array.isArray(cs) || cs.length === 0) return void 0;
|
|
63
|
+
const head = file.resolve(cs[0]);
|
|
64
|
+
const tag = head instanceof PdfName ? head.value : "";
|
|
65
|
+
if (tag === "ICCBased") {
|
|
66
|
+
const profile = file.resolve(cs[1]);
|
|
67
|
+
if (profile instanceof PdfStream) {
|
|
68
|
+
const n = intOf(file.get(profile.dict, "N"));
|
|
69
|
+
if (n === 1) return {
|
|
70
|
+
kind: "gray",
|
|
71
|
+
components: 1
|
|
72
|
+
};
|
|
73
|
+
if (n === 3) return {
|
|
74
|
+
kind: "rgb",
|
|
75
|
+
components: 3
|
|
76
|
+
};
|
|
77
|
+
if (n === 4) return {
|
|
78
|
+
kind: "cmyk",
|
|
79
|
+
components: 4
|
|
80
|
+
};
|
|
81
|
+
const alt = resolveColorSpace(file, profile.dict.get("Alternate"));
|
|
82
|
+
if (alt) return alt;
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (tag === "CalGray" || tag === "G") return {
|
|
87
|
+
kind: "gray",
|
|
88
|
+
components: 1
|
|
89
|
+
};
|
|
90
|
+
if (tag === "CalRGB" || tag === "RGB") return {
|
|
91
|
+
kind: "rgb",
|
|
92
|
+
components: 3
|
|
93
|
+
};
|
|
94
|
+
if (tag === "Indexed" || tag === "I") {
|
|
95
|
+
const base = resolveColorSpace(file, cs[1]);
|
|
96
|
+
const hival = intOf(file.resolve(cs[2] ?? PDF_NULL));
|
|
97
|
+
const lookup = valueToBytes(file, cs[3]);
|
|
98
|
+
if (!base || base.kind === "indexed" || !lookup) return void 0;
|
|
99
|
+
return {
|
|
100
|
+
kind: "indexed",
|
|
101
|
+
components: 1,
|
|
102
|
+
base,
|
|
103
|
+
hival,
|
|
104
|
+
lookup
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function namedColorSpace(name) {
|
|
109
|
+
if (name === "DeviceGray" || name === "G" || name === "CalGray") return {
|
|
110
|
+
kind: "gray",
|
|
111
|
+
components: 1
|
|
112
|
+
};
|
|
113
|
+
if (name === "DeviceRGB" || name === "RGB" || name === "CalRGB") return {
|
|
114
|
+
kind: "rgb",
|
|
115
|
+
components: 3
|
|
116
|
+
};
|
|
117
|
+
if (name === "DeviceCMYK" || name === "CMYK") return {
|
|
118
|
+
kind: "cmyk",
|
|
119
|
+
components: 4
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function toColor(cs, s, px, bpc, decode) {
|
|
123
|
+
const maxv = (1 << Math.min(bpc, 15)) * (bpc >= 16 ? 2 : 1) - 1;
|
|
124
|
+
const c01 = (v, comp) => {
|
|
125
|
+
const t = maxv > 0 ? v / maxv : 0;
|
|
126
|
+
if (decode && decode.length >= (comp + 1) * 2) {
|
|
127
|
+
const dmin = decode[comp * 2];
|
|
128
|
+
const dmax = decode[comp * 2 + 1];
|
|
129
|
+
return clamp01(dmin + t * (dmax - dmin));
|
|
130
|
+
}
|
|
131
|
+
return t;
|
|
132
|
+
};
|
|
133
|
+
const to8 = (v, comp) => Math.round(c01(v, comp) * 255);
|
|
134
|
+
if (cs.kind === "gray") {
|
|
135
|
+
const out = new Uint8Array(px);
|
|
136
|
+
for (let i = 0; i < px; i++) out[i] = to8(s[i], 0);
|
|
137
|
+
return {
|
|
138
|
+
color: "gray",
|
|
139
|
+
samples: out
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (cs.kind === "rgb") {
|
|
143
|
+
const out = new Uint8Array(px * 3);
|
|
144
|
+
for (let i = 0; i < px; i++) {
|
|
145
|
+
out[i * 3] = to8(s[i * 3], 0);
|
|
146
|
+
out[i * 3 + 1] = to8(s[i * 3 + 1], 1);
|
|
147
|
+
out[i * 3 + 2] = to8(s[i * 3 + 2], 2);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
color: "rgb",
|
|
151
|
+
samples: out
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (cs.kind === "cmyk") {
|
|
155
|
+
const out = new Uint8Array(px * 3);
|
|
156
|
+
for (let i = 0; i < px; i++) {
|
|
157
|
+
const c = c01(s[i * 4], 0);
|
|
158
|
+
const m = c01(s[i * 4 + 1], 1);
|
|
159
|
+
const y = c01(s[i * 4 + 2], 2);
|
|
160
|
+
const k = c01(s[i * 4 + 3], 3);
|
|
161
|
+
out[i * 3] = Math.round(255 * (1 - c) * (1 - k));
|
|
162
|
+
out[i * 3 + 1] = Math.round(255 * (1 - m) * (1 - k));
|
|
163
|
+
out[i * 3 + 2] = Math.round(255 * (1 - y) * (1 - k));
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
color: "rgb",
|
|
167
|
+
samples: out
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const base = cs.base;
|
|
171
|
+
const lookup = cs.lookup;
|
|
172
|
+
const hival = cs.hival ?? 255;
|
|
173
|
+
const bn = base.components;
|
|
174
|
+
if (base.kind === "gray") {
|
|
175
|
+
const out = new Uint8Array(px);
|
|
176
|
+
for (let i = 0; i < px; i++) out[i] = lookup[Math.min(s[i], hival) * bn] ?? 0;
|
|
177
|
+
return {
|
|
178
|
+
color: "gray",
|
|
179
|
+
samples: out
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const out = new Uint8Array(px * 3);
|
|
183
|
+
for (let i = 0; i < px; i++) {
|
|
184
|
+
const [r, g, b] = paletteRgb(base, lookup, Math.min(s[i], hival) * bn);
|
|
185
|
+
out[i * 3] = r;
|
|
186
|
+
out[i * 3 + 1] = g;
|
|
187
|
+
out[i * 3 + 2] = b;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
color: "rgb",
|
|
191
|
+
samples: out
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function paletteRgb(base, lookup, off) {
|
|
195
|
+
if (base.kind === "rgb") return [
|
|
196
|
+
lookup[off] ?? 0,
|
|
197
|
+
lookup[off + 1] ?? 0,
|
|
198
|
+
lookup[off + 2] ?? 0
|
|
199
|
+
];
|
|
200
|
+
if (base.kind === "cmyk") {
|
|
201
|
+
const c = (lookup[off] ?? 0) / 255;
|
|
202
|
+
const m = (lookup[off + 1] ?? 0) / 255;
|
|
203
|
+
const y = (lookup[off + 2] ?? 0) / 255;
|
|
204
|
+
const k = (lookup[off + 3] ?? 0) / 255;
|
|
205
|
+
return [
|
|
206
|
+
Math.round(255 * (1 - c) * (1 - k)),
|
|
207
|
+
Math.round(255 * (1 - m) * (1 - k)),
|
|
208
|
+
Math.round(255 * (1 - y) * (1 - k))
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
const g = lookup[off] ?? 0;
|
|
212
|
+
return [
|
|
213
|
+
g,
|
|
214
|
+
g,
|
|
215
|
+
g
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
function unpackSamples(raw, width, height, ncomp, bpc) {
|
|
219
|
+
const perRow = width * ncomp;
|
|
220
|
+
const out = new Uint16Array(perRow * height);
|
|
221
|
+
const rowBytes = Math.ceil(perRow * bpc / 8);
|
|
222
|
+
for (let y = 0; y < height; y++) {
|
|
223
|
+
const rowOff = y * rowBytes;
|
|
224
|
+
const base = y * perRow;
|
|
225
|
+
if (bpc === 8) for (let i = 0; i < perRow; i++) out[base + i] = raw[rowOff + i] ?? 0;
|
|
226
|
+
else if (bpc === 16) for (let i = 0; i < perRow; i++) out[base + i] = (raw[rowOff + 2 * i] ?? 0) << 8 | (raw[rowOff + 2 * i + 1] ?? 0);
|
|
227
|
+
else {
|
|
228
|
+
const mask = (1 << bpc) - 1;
|
|
229
|
+
let bit = 0;
|
|
230
|
+
for (let i = 0; i < perRow; i++) {
|
|
231
|
+
const bytePos = rowOff + (bit >> 3);
|
|
232
|
+
const shift = 8 - bpc - (bit & 7);
|
|
233
|
+
out[base + i] = ((raw[bytePos] ?? 0) >> shift & mask) >>> 0;
|
|
234
|
+
bit += bpc;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
function decodeSMask(file, d, width, height) {
|
|
241
|
+
const sm = file.resolve(d.get("SMask") ?? PDF_NULL);
|
|
242
|
+
if (!(sm instanceof PdfStream)) return void 0;
|
|
243
|
+
const sw = intOf(file.get(sm.dict, "Width"));
|
|
244
|
+
const sh = intOf(file.get(sm.dict, "Height"));
|
|
245
|
+
if (sw <= 0 || sh <= 0) return void 0;
|
|
246
|
+
const decoded = decodeToSamples(file, sm, filterNames(file, sm.dict), sw, sh);
|
|
247
|
+
if (typeof decoded === "string") return void 0;
|
|
248
|
+
const ch = decoded.color === "rgb" ? 3 : 1;
|
|
249
|
+
const gray = new Uint8Array(sw * sh);
|
|
250
|
+
for (let i = 0; i < sw * sh; i++) gray[i] = decoded.samples[i * ch];
|
|
251
|
+
if (sw === width && sh === height) return { data: gray };
|
|
252
|
+
const out = new Uint8Array(width * height);
|
|
253
|
+
for (let y = 0; y < height; y++) {
|
|
254
|
+
const sy = Math.min(sh - 1, Math.floor(y * sh / height));
|
|
255
|
+
for (let x = 0; x < width; x++) {
|
|
256
|
+
const sx = Math.min(sw - 1, Math.floor(x * sw / width));
|
|
257
|
+
out[y * width + x] = gray[sy * sw + sx];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { data: out };
|
|
261
|
+
}
|
|
262
|
+
function combineAlpha(color, alpha) {
|
|
263
|
+
const px = alpha.data.length;
|
|
264
|
+
if (color.color === "gray") {
|
|
265
|
+
const out = new Uint8Array(px * 2);
|
|
266
|
+
for (let i = 0; i < px; i++) {
|
|
267
|
+
out[i * 2] = color.samples[i];
|
|
268
|
+
out[i * 2 + 1] = alpha.data[i];
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
color: "gray-alpha",
|
|
272
|
+
samples: out
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const out = new Uint8Array(px * 4);
|
|
276
|
+
for (let i = 0; i < px; i++) {
|
|
277
|
+
out[i * 4] = color.samples[i * 3];
|
|
278
|
+
out[i * 4 + 1] = color.samples[i * 3 + 1];
|
|
279
|
+
out[i * 4 + 2] = color.samples[i * 3 + 2];
|
|
280
|
+
out[i * 4 + 3] = alpha.data[i];
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
color: "rgba",
|
|
284
|
+
samples: out
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function filterNames(file, d) {
|
|
288
|
+
const f = file.resolve(d.get("Filter") ?? PDF_NULL);
|
|
289
|
+
const arr = Array.isArray(f) ? f : [f];
|
|
290
|
+
const out = [];
|
|
291
|
+
for (const x of arr) {
|
|
292
|
+
const r = file.resolve(x);
|
|
293
|
+
if (r instanceof PdfName) out.push(r.value);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
function decodeChain(file, stream, filters) {
|
|
298
|
+
let data = stream.data;
|
|
299
|
+
let flate = false;
|
|
300
|
+
for (const f of filters) if (f === "FlateDecode" || f === "Fl") try {
|
|
301
|
+
data = unzlibSync(data);
|
|
302
|
+
flate = true;
|
|
303
|
+
} catch {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
else if (f === "RunLengthDecode" || f === "RL") data = runLengthDecode(data);
|
|
307
|
+
else if (f === "ASCII85Decode" || f === "A85") data = ascii85Decode(data);
|
|
308
|
+
else if (f === "ASCIIHexDecode" || f === "AHx") data = asciiHexDecode(data);
|
|
309
|
+
else return;
|
|
310
|
+
return flate ? applyPredictor(file, stream.dict, data) : data;
|
|
311
|
+
}
|
|
312
|
+
function applyChainExceptLast(filters, raw) {
|
|
313
|
+
let data = raw;
|
|
314
|
+
for (let i = 0; i < filters.length - 1; i++) {
|
|
315
|
+
const f = filters[i];
|
|
316
|
+
if (f === "FlateDecode" || f === "Fl") try {
|
|
317
|
+
data = unzlibSync(data);
|
|
318
|
+
} catch {}
|
|
319
|
+
else if (f === "RunLengthDecode" || f === "RL") data = runLengthDecode(data);
|
|
320
|
+
else if (f === "ASCII85Decode" || f === "A85") data = ascii85Decode(data);
|
|
321
|
+
else if (f === "ASCIIHexDecode" || f === "AHx") data = asciiHexDecode(data);
|
|
322
|
+
}
|
|
323
|
+
return data;
|
|
324
|
+
}
|
|
325
|
+
function applyPredictor(file, d, data) {
|
|
326
|
+
const parms = decodeParmsOf(file, d);
|
|
327
|
+
if (!parms) return data;
|
|
328
|
+
const predictor = intOf(file.get(parms, "Predictor"));
|
|
329
|
+
if (predictor < 2) return data;
|
|
330
|
+
return reversePredictor(data, {
|
|
331
|
+
predictor,
|
|
332
|
+
colors: intOf(file.get(parms, "Colors")) || 1,
|
|
333
|
+
bitsPerComponent: intOf(file.get(parms, "BitsPerComponent")) || 8,
|
|
334
|
+
columns: intOf(file.get(parms, "Columns")) || 1
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function runLengthDecode(data) {
|
|
338
|
+
const out = [];
|
|
339
|
+
let i = 0;
|
|
340
|
+
while (i < data.length) {
|
|
341
|
+
const len = data[i++];
|
|
342
|
+
if (len === 128) break;
|
|
343
|
+
if (len < 128) for (let j = 0; j <= len && i < data.length; j++) out.push(data[i++]);
|
|
344
|
+
else {
|
|
345
|
+
const b = data[i++] ?? 0;
|
|
346
|
+
for (let j = 0; j < 257 - len; j++) out.push(b);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return Uint8Array.from(out);
|
|
350
|
+
}
|
|
351
|
+
function ascii85Decode(data) {
|
|
352
|
+
const out = [];
|
|
353
|
+
let tuple = 0;
|
|
354
|
+
let count = 0;
|
|
355
|
+
for (const c of data) {
|
|
356
|
+
if (c === 126) break;
|
|
357
|
+
if (c <= 32) continue;
|
|
358
|
+
if (c === 122 && count === 0) {
|
|
359
|
+
out.push(0, 0, 0, 0);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (c < 33 || c > 117) continue;
|
|
363
|
+
tuple = tuple * 85 + (c - 33);
|
|
364
|
+
if (++count === 5) {
|
|
365
|
+
out.push(tuple >>> 24 & 255, tuple >>> 16 & 255, tuple >>> 8 & 255, tuple & 255);
|
|
366
|
+
tuple = 0;
|
|
367
|
+
count = 0;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (count > 0) {
|
|
371
|
+
for (let i = count; i < 5; i++) tuple = tuple * 85 + 84;
|
|
372
|
+
for (let i = 0; i < count - 1; i++) out.push(tuple >>> 24 - i * 8 & 255);
|
|
373
|
+
}
|
|
374
|
+
return Uint8Array.from(out);
|
|
375
|
+
}
|
|
376
|
+
function asciiHexDecode(data) {
|
|
377
|
+
const out = [];
|
|
378
|
+
let hi = -1;
|
|
379
|
+
for (const c of data) {
|
|
380
|
+
if (c === 62) break;
|
|
381
|
+
const v = hexVal(c);
|
|
382
|
+
if (v < 0) continue;
|
|
383
|
+
if (hi < 0) hi = v;
|
|
384
|
+
else {
|
|
385
|
+
out.push(hi << 4 | v);
|
|
386
|
+
hi = -1;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (hi >= 0) out.push(hi << 4);
|
|
390
|
+
return Uint8Array.from(out);
|
|
391
|
+
}
|
|
392
|
+
function decodeParmsOf(file, d) {
|
|
393
|
+
const p = file.resolve(d.get("DecodeParms") ?? d.get("DP") ?? PDF_NULL);
|
|
394
|
+
if (p instanceof Map) return p;
|
|
395
|
+
if (Array.isArray(p)) for (const e of p) {
|
|
396
|
+
const r = file.resolve(e);
|
|
397
|
+
if (r instanceof Map) return r;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function decodeArrayOf(file, d) {
|
|
401
|
+
const v = file.resolve(d.get("Decode") ?? d.get("D") ?? PDF_NULL);
|
|
402
|
+
if (!Array.isArray(v)) return void 0;
|
|
403
|
+
const nums = v.map((x) => typeof x === "number" ? x : NaN);
|
|
404
|
+
return nums.some((n) => !Number.isFinite(n)) ? void 0 : nums;
|
|
405
|
+
}
|
|
406
|
+
function hasSMask(file, d) {
|
|
407
|
+
return file.resolve(d.get("SMask") ?? PDF_NULL) instanceof PdfStream;
|
|
408
|
+
}
|
|
409
|
+
function valueToBytes(file, v) {
|
|
410
|
+
const r = file.resolve(v ?? PDF_NULL);
|
|
411
|
+
if (r instanceof PdfHexString) return r.bytes;
|
|
412
|
+
if (typeof r === "string") {
|
|
413
|
+
const out = new Uint8Array(r.length);
|
|
414
|
+
for (let i = 0; i < r.length; i++) out[i] = r.charCodeAt(i) & 255;
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
if (r instanceof PdfStream) return file.streamData(r);
|
|
418
|
+
}
|
|
419
|
+
function intOf(v) {
|
|
420
|
+
return typeof v === "number" ? Math.round(v) : 0;
|
|
421
|
+
}
|
|
422
|
+
function boolOf(v) {
|
|
423
|
+
return v === true;
|
|
424
|
+
}
|
|
425
|
+
function clamp01(x) {
|
|
426
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
427
|
+
}
|
|
428
|
+
function hexVal(c) {
|
|
429
|
+
if (c >= 48 && c <= 57) return c - 48;
|
|
430
|
+
if (c >= 65 && c <= 70) return c - 65 + 10;
|
|
431
|
+
if (c >= 97 && c <= 102) return c - 97 + 10;
|
|
432
|
+
return -1;
|
|
433
|
+
}
|
|
434
|
+
function fail(severity, detail) {
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
severity,
|
|
438
|
+
detail
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
//#endregion
|
|
442
|
+
export { decodePdfImage };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Loss } from '../core/ir/index.js';
|
|
2
|
+
import { PdfFile, PdfPage } from './document.js';
|
|
3
|
+
export interface PdfImage {
|
|
4
|
+
readonly bytes: Uint8Array;
|
|
5
|
+
readonly format: 'png' | 'jpeg' | 'jpeg2000';
|
|
6
|
+
readonly widthPt: number;
|
|
7
|
+
readonly heightPt: number;
|
|
8
|
+
readonly x: number;
|
|
9
|
+
readonly y: number;
|
|
10
|
+
readonly mcid?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface PageImages {
|
|
13
|
+
readonly images: Array<PdfImage>;
|
|
14
|
+
readonly losses: Array<Loss>;
|
|
15
|
+
}
|
|
16
|
+
export declare function collectPageImages(file: PdfFile, page: PdfPage): PageImages;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
|
|
2
|
+
import { FEATURES } from "../core/ir/features.js";
|
|
3
|
+
import { interpretContent, multiply } from "./content.js";
|
|
4
|
+
import { decodePdfImage } from "./image-decode.js";
|
|
5
|
+
//#region src/pdf-reader/images.ts
|
|
6
|
+
var NO_FONTS = /* @__PURE__ */ new Map();
|
|
7
|
+
var MAX_FORM_DEPTH = 12;
|
|
8
|
+
var MAX_IMAGES = 4096;
|
|
9
|
+
function collectPageImages(file, page) {
|
|
10
|
+
const images = [];
|
|
11
|
+
const lossByDetail = /* @__PURE__ */ new Map();
|
|
12
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
13
|
+
const addLoss = (severity, detail) => {
|
|
14
|
+
if (!lossByDetail.has(detail)) lossByDetail.set(detail, {
|
|
15
|
+
severity,
|
|
16
|
+
feature: FEATURES.images,
|
|
17
|
+
detail
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
const walk = (resources, content, baseCtm, depth, inheritedMcid) => {
|
|
21
|
+
const xobjects = resources ? file.get(resources, "XObject") : PDF_NULL;
|
|
22
|
+
const xobjDict = xobjects instanceof Map ? xobjects : void 0;
|
|
23
|
+
for (const placement of interpretContent(content, NO_FONTS, baseCtm).images) {
|
|
24
|
+
if (images.length >= MAX_IMAGES) return;
|
|
25
|
+
const stream = xobjDict ? file.resolve(xobjDict.get(placement.name) ?? PDF_NULL) : PDF_NULL;
|
|
26
|
+
if (!(stream instanceof PdfStream)) continue;
|
|
27
|
+
const subtype = nameOf(file.get(stream.dict, "Subtype"));
|
|
28
|
+
const mcid = placement.mcid ?? inheritedMcid;
|
|
29
|
+
if (subtype === "Image") {
|
|
30
|
+
const decoded = decodePdfImage(file, stream);
|
|
31
|
+
if (decoded.ok) {
|
|
32
|
+
images.push(geometry(placement.ctm, decoded, mcid));
|
|
33
|
+
if (decoded.degraded) addLoss("degraded", decoded.degraded);
|
|
34
|
+
} else addLoss(decoded.severity, decoded.detail);
|
|
35
|
+
} else if (subtype === "Form" && depth < MAX_FORM_DEPTH && !visiting.has(stream)) {
|
|
36
|
+
visiting.add(stream);
|
|
37
|
+
const formRes = file.get(stream.dict, "Resources");
|
|
38
|
+
walk(formRes instanceof Map ? formRes : resources, file.streamData(stream), multiply(matrixOf(file, stream.dict), placement.ctm), depth + 1, mcid);
|
|
39
|
+
visiting.delete(stream);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walk(page.resources, file.pageContent(page), [
|
|
44
|
+
1,
|
|
45
|
+
0,
|
|
46
|
+
0,
|
|
47
|
+
1,
|
|
48
|
+
0,
|
|
49
|
+
0
|
|
50
|
+
], 0, void 0);
|
|
51
|
+
return {
|
|
52
|
+
images,
|
|
53
|
+
losses: [...lossByDetail.values()]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function geometry(ctm, decoded, mcid) {
|
|
57
|
+
return {
|
|
58
|
+
bytes: decoded.bytes,
|
|
59
|
+
format: decoded.format,
|
|
60
|
+
widthPt: Math.hypot(ctm[0], ctm[1]) || 1,
|
|
61
|
+
heightPt: Math.hypot(ctm[2], ctm[3]) || 1,
|
|
62
|
+
x: ctm[4],
|
|
63
|
+
y: ctm[5],
|
|
64
|
+
...mcid !== void 0 ? { mcid } : {}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function matrixOf(file, dict) {
|
|
68
|
+
const m = file.resolve(dict.get("Matrix") ?? PDF_NULL);
|
|
69
|
+
if (Array.isArray(m) && m.length >= 6 && m.every((v) => typeof v === "number")) return [
|
|
70
|
+
m[0],
|
|
71
|
+
m[1],
|
|
72
|
+
m[2],
|
|
73
|
+
m[3],
|
|
74
|
+
m[4],
|
|
75
|
+
m[5]
|
|
76
|
+
];
|
|
77
|
+
return [
|
|
78
|
+
1,
|
|
79
|
+
0,
|
|
80
|
+
0,
|
|
81
|
+
1,
|
|
82
|
+
0,
|
|
83
|
+
0
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
function nameOf(v) {
|
|
87
|
+
return v instanceof PdfName ? v.value : "";
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { collectPageImages };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { FlowDoc } from '../core/ir/flow.js';
|
|
2
1
|
import { PdfFile } from './document.js';
|
|
3
|
-
|
|
2
|
+
import { Reconstruction } from './flow-build.js';
|
|
3
|
+
export declare function reconstructByLayout(file: PdfFile): Reconstruction;
|
|
@@ -1,12 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ResourceStore } from "../core/ir/resources.js";
|
|
2
|
+
import { buildFlowDoc, dedupeLosses, imageBlock, paragraphFromRuns, shapeBlock } from "./flow-build.js";
|
|
3
|
+
import { collectPageImages } from "./images.js";
|
|
2
4
|
import { extractPageText } from "./text.js";
|
|
5
|
+
import { collectPageVectors } from "./vector.js";
|
|
3
6
|
//#region src/pdf-reader/layout.ts
|
|
4
7
|
function reconstructByLayout(file) {
|
|
5
|
-
const pages = file.pages()
|
|
6
|
-
const
|
|
8
|
+
const pages = file.pages();
|
|
9
|
+
const pageLines = pages.map((page) => groupIntoLines(extractPageText(file, page)).filter((l) => l.text.length > 0));
|
|
10
|
+
const medianFont = median(pageLines.flat().map((l) => l.fontSize)) || 12;
|
|
11
|
+
const resources = new ResourceStore();
|
|
12
|
+
const losses = [];
|
|
7
13
|
const body = [];
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
pages.forEach((page, i) => {
|
|
15
|
+
const blocks = [];
|
|
16
|
+
for (const para of groupIntoParagraphs(pageLines[i])) blocks.push({
|
|
17
|
+
top: para.top,
|
|
18
|
+
el: paragraphFromRuns(para.spans, headingLevel(para.fontSize, medianFont))
|
|
19
|
+
});
|
|
20
|
+
const imgs = collectPageImages(file, page);
|
|
21
|
+
losses.push(...imgs.losses);
|
|
22
|
+
for (const img of imgs.images) blocks.push({
|
|
23
|
+
top: img.y + img.heightPt,
|
|
24
|
+
el: imageBlock(img, resources)
|
|
25
|
+
});
|
|
26
|
+
for (const v of collectPageVectors(file, page)) blocks.push({
|
|
27
|
+
top: v.maxY,
|
|
28
|
+
el: shapeBlock(v)
|
|
29
|
+
});
|
|
30
|
+
blocks.sort((a, b) => b.top - a.top);
|
|
31
|
+
for (const block of blocks) body.push(block.el);
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
doc: buildFlowDoc(body, resources),
|
|
35
|
+
losses: dedupeLosses(losses)
|
|
36
|
+
};
|
|
10
37
|
}
|
|
11
38
|
function groupIntoLines(runs) {
|
|
12
39
|
const sorted = [...runs].sort((a, b) => b.y - a.y || a.x - b.x);
|
|
@@ -25,22 +52,28 @@ function groupIntoLines(runs) {
|
|
|
25
52
|
}
|
|
26
53
|
return clusters.map((c) => {
|
|
27
54
|
const ordered = c.runs.sort((a, b) => a.x - b.x);
|
|
55
|
+
const fontSize = c.fontSize || 10;
|
|
56
|
+
const spans = lineSpans(ordered, fontSize);
|
|
28
57
|
return {
|
|
29
58
|
y: c.y,
|
|
30
|
-
fontSize
|
|
31
|
-
text:
|
|
59
|
+
fontSize,
|
|
60
|
+
text: spans.map((s) => s.text).join("").replace(/\s+/g, " ").trim(),
|
|
61
|
+
spans
|
|
32
62
|
};
|
|
33
63
|
});
|
|
34
64
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
65
|
+
function lineSpans(runs, fontSize) {
|
|
66
|
+
const spans = [];
|
|
37
67
|
let prevEnd;
|
|
38
68
|
for (const run of runs) {
|
|
39
|
-
if (prevEnd !== void 0 && run.x - prevEnd > fontSize * .25) text
|
|
40
|
-
|
|
69
|
+
if (prevEnd !== void 0 && run.x - prevEnd > fontSize * .25) spans.push({ text: " " });
|
|
70
|
+
spans.push(run.href !== void 0 ? {
|
|
71
|
+
text: run.text,
|
|
72
|
+
href: run.href
|
|
73
|
+
} : { text: run.text });
|
|
41
74
|
prevEnd = run.x + run.text.length * (run.fontSizePt || fontSize) * .5;
|
|
42
75
|
}
|
|
43
|
-
return
|
|
76
|
+
return spans;
|
|
44
77
|
}
|
|
45
78
|
function groupIntoParagraphs(lines) {
|
|
46
79
|
const groups = [];
|
|
@@ -52,8 +85,9 @@ function groupIntoParagraphs(lines) {
|
|
|
52
85
|
prevY = line.y;
|
|
53
86
|
}
|
|
54
87
|
return groups.map((g) => ({
|
|
55
|
-
|
|
56
|
-
fontSize: Math.max(...g.map((l) => l.fontSize))
|
|
88
|
+
spans: g.flatMap((l, i) => i > 0 ? [{ text: " " }, ...l.spans] : [...l.spans]),
|
|
89
|
+
fontSize: Math.max(...g.map((l) => l.fontSize)),
|
|
90
|
+
top: g[0].y
|
|
57
91
|
}));
|
|
58
92
|
}
|
|
59
93
|
function headingLevel(fontSize, medianFont) {
|