reamkit 1.24.0 → 1.25.1
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 +22 -11
- package/dist/esm/core/converter/facade.d.ts +3 -3
- package/dist/esm/core/converter/facade.js +12 -0
- package/dist/esm/core/converter/ream.d.ts +48 -4
- package/dist/esm/core/converter/ream.js +25 -4
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +17 -0
- package/dist/esm/core/outline.d.ts +17 -0
- package/dist/esm/core/outline.js +30 -0
- package/dist/esm/core/style-cascade/resolver.js +1 -0
- package/dist/esm/core/style-cascade/types.d.ts +3 -1
- package/dist/esm/excel/sheet-to-flow.d.ts +10 -0
- package/dist/esm/excel/sheet-to-flow.js +14 -1
- package/dist/esm/html/html-writer.js +3 -2
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/layout/page-doc.js +1 -1
- package/dist/esm/layout/styled-layout.js +41 -15
- package/dist/esm/markdown/markdown-writer.d.ts +41 -0
- package/dist/esm/markdown/markdown-writer.js +733 -0
- package/dist/esm/pdf/styled-page-emitter.js +20 -1
- package/dist/esm/pdf-reader/annots.d.ts +24 -0
- package/dist/esm/pdf-reader/annots.js +126 -0
- package/dist/esm/pdf-reader/content.d.ts +131 -5
- package/dist/esm/pdf-reader/content.js +169 -12
- package/dist/esm/pdf-reader/display.d.ts +56 -0
- package/dist/esm/pdf-reader/display.js +162 -0
- package/dist/esm/pdf-reader/document.d.ts +36 -1
- package/dist/esm/pdf-reader/document.js +92 -25
- package/dist/esm/pdf-reader/embedded-fonts.d.ts +31 -0
- package/dist/esm/pdf-reader/embedded-fonts.js +94 -0
- package/dist/esm/pdf-reader/flow-build.d.ts +61 -6
- package/dist/esm/pdf-reader/flow-build.js +128 -22
- package/dist/esm/pdf-reader/font.js +185 -4
- package/dist/esm/pdf-reader/image-decode.js +55 -4
- package/dist/esm/pdf-reader/images.d.ts +6 -0
- package/dist/esm/pdf-reader/images.js +25 -5
- package/dist/esm/pdf-reader/jpeg.d.ts +18 -0
- package/dist/esm/pdf-reader/jpeg.js +419 -0
- package/dist/esm/pdf-reader/layout.d.ts +1 -1
- package/dist/esm/pdf-reader/layout.js +221 -32
- package/dist/esm/pdf-reader/pattern-tint.d.ts +17 -0
- package/dist/esm/pdf-reader/pattern-tint.js +181 -0
- package/dist/esm/pdf-reader/reader.d.ts +9 -1
- package/dist/esm/pdf-reader/reader.js +23 -7
- package/dist/esm/pdf-reader/shading.d.ts +14 -0
- package/dist/esm/pdf-reader/shading.js +27 -1
- package/dist/esm/pdf-reader/tagged.js +156 -17
- package/dist/esm/pdf-reader/text.d.ts +13 -1
- package/dist/esm/pdf-reader/text.js +70 -3
- package/dist/esm/pdf-reader/vector.d.ts +25 -1
- package/dist/esm/pdf-reader/vector.js +168 -12
- package/dist/esm/pptx/slide-parser.js +5 -0
- package/dist/esm/word/docx-writer.js +79 -17
- package/dist/esm/word/drawing-parser.js +7 -1
- package/package.json +1 -1
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { PDF_NULL, PdfHexString, PdfName } from "../pdf/objects.js";
|
|
2
2
|
import { Lexer } from "./lexer.js";
|
|
3
3
|
//#region src/pdf-reader/content.ts
|
|
4
|
+
/**
|
|
5
|
+
* A painted path emitted by the path-painting operators (§8.5.3), captured in
|
|
6
|
+
* page space (y-up). The optional fields record only what the paint mode set:
|
|
7
|
+
* a fill colour/gradient (EP10/EP16c) and/or a stroke colour + width (EP11),
|
|
8
|
+
* plus the enclosing structure id.
|
|
9
|
+
*/
|
|
10
|
+
/** A path's page-space bounding box, or `undefined` when it names no point. */
|
|
11
|
+
function pathBox(segs) {
|
|
12
|
+
let minX = Infinity;
|
|
13
|
+
let minY = Infinity;
|
|
14
|
+
let maxX = -Infinity;
|
|
15
|
+
let maxY = -Infinity;
|
|
16
|
+
const add = (x, y) => {
|
|
17
|
+
minX = Math.min(minX, x);
|
|
18
|
+
minY = Math.min(minY, y);
|
|
19
|
+
maxX = Math.max(maxX, x);
|
|
20
|
+
maxY = Math.max(maxY, y);
|
|
21
|
+
};
|
|
22
|
+
for (const seg of segs) if (seg.op === "move" || seg.op === "line") add(seg.x, seg.y);
|
|
23
|
+
else if (seg.op === "cubic") {
|
|
24
|
+
add(seg.x1, seg.y1);
|
|
25
|
+
add(seg.x2, seg.y2);
|
|
26
|
+
add(seg.x, seg.y);
|
|
27
|
+
}
|
|
28
|
+
return Number.isFinite(minX) ? {
|
|
29
|
+
minX,
|
|
30
|
+
minY,
|
|
31
|
+
maxX,
|
|
32
|
+
maxY
|
|
33
|
+
} : void 0;
|
|
34
|
+
}
|
|
35
|
+
/** The area of a box, for choosing the smaller of two clip regions. */
|
|
36
|
+
var area = (b) => Math.max(0, b.maxX - b.minX) * Math.max(0, b.maxY - b.minY);
|
|
4
37
|
/** The identity {@link Matrix} (no transform). */
|
|
5
38
|
var IDENTITY = [
|
|
6
39
|
1,
|
|
@@ -36,12 +69,15 @@ var FALLBACK_FONT = {
|
|
|
36
69
|
decode: (codes) => codes.map((c) => String.fromCharCode(c)).join(""),
|
|
37
70
|
width: () => 500
|
|
38
71
|
};
|
|
72
|
+
/** Below this a baseline is upright: a rounded matrix is not a turned one. */
|
|
73
|
+
var UPRIGHT_TOLERANCE_DEG = .5;
|
|
39
74
|
function initialState() {
|
|
40
75
|
return {
|
|
41
76
|
ctm: IDENTITY,
|
|
42
77
|
fontKey: "",
|
|
43
78
|
font: FALLBACK_FONT,
|
|
44
79
|
fontSize: 0,
|
|
80
|
+
renderMode: 0,
|
|
45
81
|
charSpacing: 0,
|
|
46
82
|
wordSpacing: 0,
|
|
47
83
|
hScale: 1,
|
|
@@ -50,7 +86,10 @@ function initialState() {
|
|
|
50
86
|
fillColor: "000000",
|
|
51
87
|
strokeColor: "000000",
|
|
52
88
|
lineWidth: 1,
|
|
53
|
-
fillGradient: void 0
|
|
89
|
+
fillGradient: void 0,
|
|
90
|
+
fillPattern: void 0,
|
|
91
|
+
fillAlpha: 1,
|
|
92
|
+
clip: void 0
|
|
54
93
|
};
|
|
55
94
|
}
|
|
56
95
|
/**
|
|
@@ -65,12 +104,14 @@ function initialState() {
|
|
|
65
104
|
* an unmapped key falls back to Latin-1 with a half-em advance.
|
|
66
105
|
* @param initialCtm The starting CTM mapping user space to page space.
|
|
67
106
|
* @param shadings Shading patterns by name, selected by `scn`/`sc` (EP16c).
|
|
107
|
+
* @param alphas Constant fill alphas by `/ExtGState` name, selected by `gs`.
|
|
68
108
|
* @returns The extracted text runs, image placements and vector paths.
|
|
69
109
|
*/
|
|
70
|
-
function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map()) {
|
|
110
|
+
function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__PURE__ */ new Map(), alphas = /* @__PURE__ */ new Map()) {
|
|
71
111
|
const runs = [];
|
|
72
112
|
const images = [];
|
|
73
113
|
const vectors = [];
|
|
114
|
+
const glyphs = [];
|
|
74
115
|
const lexer = new Lexer(bytes);
|
|
75
116
|
const stack = [];
|
|
76
117
|
let state = initialState();
|
|
@@ -78,6 +119,8 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
78
119
|
let tm = IDENTITY;
|
|
79
120
|
let tlm = IDENTITY;
|
|
80
121
|
let path = [];
|
|
122
|
+
let pendingClip = false;
|
|
123
|
+
let paintOrder = 0;
|
|
81
124
|
let operands = [];
|
|
82
125
|
const mcStack = [];
|
|
83
126
|
const toPage = (x, y) => [x * state.ctm[0] + y * state.ctm[2] + state.ctm[4], x * state.ctm[1] + y * state.ctm[3] + state.ctm[5]];
|
|
@@ -127,8 +170,12 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
127
170
|
if (path.length >= 2 && (fill || stroke)) {
|
|
128
171
|
const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
|
|
129
172
|
vectors.push({
|
|
173
|
+
order: paintOrder++,
|
|
130
174
|
segs: path,
|
|
175
|
+
...state.clip ? { clip: state.clip } : {},
|
|
176
|
+
...fill && state.fillPattern !== void 0 ? { patternName: state.fillPattern } : {},
|
|
131
177
|
...fill ? { fillHex: state.fillColor } : {},
|
|
178
|
+
...fill && state.fillAlpha < 1 ? { alpha: state.fillAlpha } : {},
|
|
132
179
|
...fill && state.fillGradient ? { gradient: state.fillGradient } : {},
|
|
133
180
|
...stroke ? {
|
|
134
181
|
strokeHex: state.strokeColor,
|
|
@@ -137,6 +184,17 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
137
184
|
...mcid !== void 0 ? { mcid } : {}
|
|
138
185
|
});
|
|
139
186
|
}
|
|
187
|
+
if (pendingClip) {
|
|
188
|
+
pendingClip = false;
|
|
189
|
+
const box = pathBox(path);
|
|
190
|
+
if (box) {
|
|
191
|
+
const next = {
|
|
192
|
+
segs: path,
|
|
193
|
+
...box
|
|
194
|
+
};
|
|
195
|
+
state.clip = state.clip && area(state.clip) <= area(next) ? state.clip : next;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
140
198
|
path = [];
|
|
141
199
|
};
|
|
142
200
|
const num = (i) => {
|
|
@@ -144,6 +202,26 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
144
202
|
return typeof v === "number" ? v : 0;
|
|
145
203
|
};
|
|
146
204
|
const advanceGlyph = (code) => {
|
|
205
|
+
const type3 = state.font.type3;
|
|
206
|
+
if (type3) {
|
|
207
|
+
const stream = type3.proc(code);
|
|
208
|
+
if (stream) {
|
|
209
|
+
const scale = [
|
|
210
|
+
state.fontSize * state.hScale,
|
|
211
|
+
0,
|
|
212
|
+
0,
|
|
213
|
+
state.fontSize,
|
|
214
|
+
0,
|
|
215
|
+
state.rise
|
|
216
|
+
];
|
|
217
|
+
glyphs.push({
|
|
218
|
+
stream,
|
|
219
|
+
resources: type3.resources,
|
|
220
|
+
ctm: multiply(type3.matrix, multiply(scale, multiply(tm, state.ctm))),
|
|
221
|
+
order: paintOrder++
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
147
225
|
const w0 = state.font.width(code) / 1e3;
|
|
148
226
|
const isSpace = state.font.bytesPerCode === 1 && code === 32;
|
|
149
227
|
tm = multiply(translation((w0 * state.fontSize + state.charSpacing + (isSpace ? state.wordSpacing : 0)) * state.hScale, 0), tm);
|
|
@@ -153,28 +231,41 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
153
231
|
for (const code of codes) advanceGlyph(code);
|
|
154
232
|
return state.font.decode(codes);
|
|
155
233
|
};
|
|
156
|
-
const emitAt = (origin,
|
|
234
|
+
const emitAt = (origin, shown, end) => {
|
|
235
|
+
const text = logicalOrder(shown);
|
|
157
236
|
if (text.length === 0) return;
|
|
158
237
|
const scaleY = Math.hypot(origin[2], origin[3]) || 1;
|
|
159
238
|
const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
|
|
239
|
+
const angle = Math.atan2(origin[1], origin[0]) * 180 / Math.PI;
|
|
160
240
|
runs.push({
|
|
161
241
|
text,
|
|
162
242
|
x: origin[4],
|
|
163
243
|
y: origin[5],
|
|
244
|
+
endX: end[4],
|
|
245
|
+
endY: end[5],
|
|
246
|
+
...Math.abs(angle) > UPRIGHT_TOLERANCE_DEG ? { angleDeg: angle } : {},
|
|
164
247
|
fontSizePt: state.fontSize * scaleY,
|
|
165
248
|
fontKey: state.fontKey,
|
|
249
|
+
...state.font.name !== void 0 ? { fontName: state.font.name } : {},
|
|
250
|
+
...state.font.type3 ? { type3: true } : {},
|
|
251
|
+
...state.renderMode === 3 || state.renderMode === 7 ? { invisible: true } : {},
|
|
252
|
+
...state.fillPattern !== void 0 ? { fillPatternName: state.fillPattern } : {},
|
|
253
|
+
...strokesText(state.renderMode) ? {
|
|
254
|
+
outlineHex: state.strokeColor,
|
|
255
|
+
outlineWidthPt: ctmLineWidth()
|
|
256
|
+
} : {},
|
|
257
|
+
...state.font.bold ? { bold: true } : {},
|
|
258
|
+
...state.font.italic ? { italic: true } : {},
|
|
259
|
+
colorHex: state.fillColor,
|
|
166
260
|
...mcid !== void 0 ? { mcid } : {}
|
|
167
261
|
});
|
|
168
262
|
};
|
|
169
263
|
const showString = (operand) => {
|
|
170
|
-
emitAt(multiply(tm, state.ctm), consume(operand));
|
|
264
|
+
emitAt(multiply(tm, state.ctm), consume(operand), multiply(tm, state.ctm));
|
|
171
265
|
};
|
|
172
266
|
const showArray = (arr) => {
|
|
173
|
-
const origin = multiply(tm, state.ctm);
|
|
174
|
-
let text = "";
|
|
175
267
|
for (const el of arr) if (typeof el === "number") tm = multiply(translation(-el / 1e3 * state.fontSize * state.hScale, 0), tm);
|
|
176
|
-
else if (typeof el === "string" || el instanceof PdfHexString)
|
|
177
|
-
emitAt(origin, text);
|
|
268
|
+
else if (typeof el === "string" || el instanceof PdfHexString) emitAt(multiply(tm, state.ctm), consume(el), multiply(tm, state.ctm));
|
|
178
269
|
};
|
|
179
270
|
const exec = (op) => {
|
|
180
271
|
switch (op) {
|
|
@@ -222,6 +313,9 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
222
313
|
case "Tc":
|
|
223
314
|
state.charSpacing = num(0);
|
|
224
315
|
break;
|
|
316
|
+
case "Tr":
|
|
317
|
+
state.renderMode = num(0);
|
|
318
|
+
break;
|
|
225
319
|
case "Tw":
|
|
226
320
|
state.wordSpacing = num(0);
|
|
227
321
|
break;
|
|
@@ -267,6 +361,7 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
267
361
|
if (nm instanceof PdfName) {
|
|
268
362
|
const mcid = mcStack.length > 0 ? mcStack[mcStack.length - 1] : void 0;
|
|
269
363
|
images.push({
|
|
364
|
+
order: paintOrder++,
|
|
270
365
|
name: nm.value,
|
|
271
366
|
ctm: state.ctm,
|
|
272
367
|
...mcid !== void 0 ? { mcid } : {}
|
|
@@ -277,19 +372,24 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
277
372
|
case "rg":
|
|
278
373
|
state.fillColor = rgbHex(num(0), num(1), num(2));
|
|
279
374
|
state.fillGradient = void 0;
|
|
375
|
+
state.fillPattern = void 0;
|
|
280
376
|
break;
|
|
281
377
|
case "g":
|
|
282
378
|
state.fillColor = grayHex(num(0));
|
|
283
379
|
state.fillGradient = void 0;
|
|
380
|
+
state.fillPattern = void 0;
|
|
284
381
|
break;
|
|
285
382
|
case "k":
|
|
286
383
|
state.fillColor = cmykHex(num(0), num(1), num(2), num(3));
|
|
287
384
|
state.fillGradient = void 0;
|
|
385
|
+
state.fillPattern = void 0;
|
|
288
386
|
break;
|
|
289
387
|
case "scn":
|
|
290
388
|
case "sc": {
|
|
291
389
|
const last = operands[operands.length - 1];
|
|
292
|
-
|
|
390
|
+
const named = last instanceof PdfName ? last.value : void 0;
|
|
391
|
+
state.fillGradient = named !== void 0 ? shadings.get(named) : void 0;
|
|
392
|
+
state.fillPattern = named !== void 0 && !state.fillGradient ? named : void 0;
|
|
293
393
|
break;
|
|
294
394
|
}
|
|
295
395
|
case "RG":
|
|
@@ -343,8 +443,15 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
343
443
|
case "n":
|
|
344
444
|
paintPath(false, false);
|
|
345
445
|
break;
|
|
446
|
+
case "gs": {
|
|
447
|
+
const nm = operands[operands.length - 1];
|
|
448
|
+
if (nm instanceof PdfName) state.fillAlpha = alphas.get(nm.value) ?? 1;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
346
451
|
case "W":
|
|
347
|
-
case "W*":
|
|
452
|
+
case "W*":
|
|
453
|
+
pendingClip = true;
|
|
454
|
+
break;
|
|
348
455
|
default: break;
|
|
349
456
|
}
|
|
350
457
|
};
|
|
@@ -384,9 +491,59 @@ function interpretContent(bytes, fonts, initialCtm = IDENTITY, shadings = /* @__
|
|
|
384
491
|
return {
|
|
385
492
|
texts: runs,
|
|
386
493
|
images,
|
|
387
|
-
vectors
|
|
494
|
+
vectors,
|
|
495
|
+
glyphs
|
|
388
496
|
};
|
|
389
497
|
}
|
|
498
|
+
/** §9.3.6 — the rendering modes that put a line round the glyphs. */
|
|
499
|
+
function strokesText(mode) {
|
|
500
|
+
return mode === 1 || mode === 2 || mode === 5 || mode === 6;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* §9.4 — a show operator paints its glyphs along the baseline, left to right,
|
|
504
|
+
* whatever the script. For Arabic or Hebrew that means the string a PDF holds
|
|
505
|
+
* is in VISUAL order: the first character of the word is the last one shown.
|
|
506
|
+
*
|
|
507
|
+
* Every reader turns this back into logical order before handing it on, because
|
|
508
|
+
* everything downstream — a search, a markdown file, a layout engine that does
|
|
509
|
+
* its own bidi — takes logical order and reverses it again for display.
|
|
510
|
+
* ArabicCIDTrueType.pdf came out mirrored for exactly that reason: the reader
|
|
511
|
+
* passed visual order through and the layout reversed it a second time.
|
|
512
|
+
*
|
|
513
|
+
* A run is reversed only when it is wholly right-to-left. Anything mixed — a
|
|
514
|
+
* number inside an Arabic sentence runs left to right — needs the full bidi
|
|
515
|
+
* algorithm, and guessing at it would be worse than leaving it alone.
|
|
516
|
+
*/
|
|
517
|
+
function logicalOrder(text) {
|
|
518
|
+
return isRightToLeft(text) ? [...text].reverse().join("") : text;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Whether a string is wholly right-to-left: at least one letter of an RTL
|
|
522
|
+
* script and nothing of any other, spaces and joiners aside.
|
|
523
|
+
*
|
|
524
|
+
* Anything mixed — a number inside an Arabic sentence runs left to right —
|
|
525
|
+
* needs the full bidi algorithm, and guessing at it would be worse than
|
|
526
|
+
* leaving it alone.
|
|
527
|
+
*
|
|
528
|
+
* @param text The string to judge.
|
|
529
|
+
* @returns Whether it is one run of right-to-left script.
|
|
530
|
+
*/
|
|
531
|
+
function isRightToLeft(text) {
|
|
532
|
+
let rtl = false;
|
|
533
|
+
for (const ch of text) {
|
|
534
|
+
const cp = ch.codePointAt(0);
|
|
535
|
+
if (isRtl(cp)) {
|
|
536
|
+
rtl = true;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (cp !== 32 && cp !== 10 && cp !== 8204 && cp !== 8205) return false;
|
|
540
|
+
}
|
|
541
|
+
return rtl;
|
|
542
|
+
}
|
|
543
|
+
/** The right-to-left blocks: Hebrew, Arabic, Syriac, Thaana, and the forms. */
|
|
544
|
+
function isRtl(cp) {
|
|
545
|
+
return cp >= 1424 && cp <= 1535 || cp >= 1536 && cp <= 1791 || cp >= 1792 && cp <= 1871 || cp >= 1920 && cp <= 1983 || cp >= 2208 && cp <= 2303 || cp >= 64285 && cp <= 65023 || cp >= 65136 && cp <= 65279;
|
|
546
|
+
}
|
|
390
547
|
function clamp255(v) {
|
|
391
548
|
return Math.max(0, Math.min(255, Math.round(v * 255)));
|
|
392
549
|
}
|
|
@@ -474,4 +631,4 @@ function skipInlineImage(lexer) {
|
|
|
474
631
|
lexer.pos = ei < 0 ? lexer.length : ei + 2;
|
|
475
632
|
}
|
|
476
633
|
//#endregion
|
|
477
|
-
export { IDENTITY, interpretContent, multiply };
|
|
634
|
+
export { IDENTITY, interpretContent, isRightToLeft, multiply };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TextRun } from './content.js';
|
|
2
|
+
import { PdfImage } from './images.js';
|
|
3
|
+
import { PdfPage } from './document.js';
|
|
4
|
+
import { PdfVector } from './vector.js';
|
|
5
|
+
/** A page's shown geometry: how big it is, and how to place a mark on it. */
|
|
6
|
+
export interface Display {
|
|
7
|
+
/** The shown page's width in points — the `/MediaBox`'s, swapped on a quarter turn. */
|
|
8
|
+
readonly width: number;
|
|
9
|
+
/** The shown page's height in points. */
|
|
10
|
+
readonly height: number;
|
|
11
|
+
/** How far every mark turns with the page, degrees counter-clockwise. */
|
|
12
|
+
readonly turnDeg: number;
|
|
13
|
+
/** Map a `/MediaBox`-space point into the shown page's own y-up frame. */
|
|
14
|
+
readonly place: (x: number, y: number) => {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The {@link Display} a page's `/MediaBox` and `/Rotate` describe.
|
|
21
|
+
*
|
|
22
|
+
* `/Rotate` turns the page CLOCKWISE when shown, so the content turns with it —
|
|
23
|
+
* counter-clockwise by the same amount as seen from the content's own frame,
|
|
24
|
+
* which is what `turnDeg` states.
|
|
25
|
+
*
|
|
26
|
+
* @param page The page whose shown geometry is wanted.
|
|
27
|
+
* @returns Its size, its turn, and the map onto it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function displayOf(page: PdfPage): Display;
|
|
30
|
+
/**
|
|
31
|
+
* The same runs, placed on the shown page.
|
|
32
|
+
*
|
|
33
|
+
* @param runs The runs as the content stream drew them.
|
|
34
|
+
* @param d The page's shown geometry.
|
|
35
|
+
* @returns The runs in the shown page's frame, each carrying the page's turn.
|
|
36
|
+
*/
|
|
37
|
+
export declare function placeRuns(runs: ReadonlyArray<TextRun>, d: Display): Array<TextRun>;
|
|
38
|
+
/**
|
|
39
|
+
* The same pictures, placed on the shown page. A quarter turn swaps a picture's
|
|
40
|
+
* width and height and leaves it standing on its side, which the caller carries
|
|
41
|
+
* as the picture's own rotation.
|
|
42
|
+
*
|
|
43
|
+
* @param images The pictures as the content stream placed them.
|
|
44
|
+
* @param d The page's shown geometry.
|
|
45
|
+
* @returns The pictures in the shown page's frame.
|
|
46
|
+
*/
|
|
47
|
+
export declare function placeImages(images: ReadonlyArray<PdfImage>, d: Display): Array<PdfImage>;
|
|
48
|
+
/**
|
|
49
|
+
* The same paths, placed on the shown page. A path is geometry, so every point
|
|
50
|
+
* of it moves and the bounding box is taken again from what comes out.
|
|
51
|
+
*
|
|
52
|
+
* @param vectors The paths as the content stream painted them.
|
|
53
|
+
* @param d The page's shown geometry.
|
|
54
|
+
* @returns The paths in the shown page's frame.
|
|
55
|
+
*/
|
|
56
|
+
export declare function placeVectors(vectors: ReadonlyArray<PdfVector>, d: Display): Array<PdfVector>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//#region src/pdf-reader/display.ts
|
|
2
|
+
/**
|
|
3
|
+
* The {@link Display} a page's `/MediaBox` and `/Rotate` describe.
|
|
4
|
+
*
|
|
5
|
+
* `/Rotate` turns the page CLOCKWISE when shown, so the content turns with it —
|
|
6
|
+
* counter-clockwise by the same amount as seen from the content's own frame,
|
|
7
|
+
* which is what `turnDeg` states.
|
|
8
|
+
*
|
|
9
|
+
* @param page The page whose shown geometry is wanted.
|
|
10
|
+
* @returns Its size, its turn, and the map onto it.
|
|
11
|
+
*/
|
|
12
|
+
function displayOf(page) {
|
|
13
|
+
const [x0, y0, x1, y1] = page.mediaBox;
|
|
14
|
+
const left = Math.min(x0, x1);
|
|
15
|
+
const bottom = Math.min(y0, y1);
|
|
16
|
+
const w = Math.abs(x1 - x0);
|
|
17
|
+
const h = Math.abs(y1 - y0);
|
|
18
|
+
const quarter = page.rotate === 90 || page.rotate === 270;
|
|
19
|
+
const place = (x, y) => {
|
|
20
|
+
const mx = x - left;
|
|
21
|
+
const my = y - bottom;
|
|
22
|
+
switch (page.rotate) {
|
|
23
|
+
case 90: return {
|
|
24
|
+
x: my,
|
|
25
|
+
y: w - mx
|
|
26
|
+
};
|
|
27
|
+
case 180: return {
|
|
28
|
+
x: w - mx,
|
|
29
|
+
y: h - my
|
|
30
|
+
};
|
|
31
|
+
case 270: return {
|
|
32
|
+
x: h - my,
|
|
33
|
+
y: mx
|
|
34
|
+
};
|
|
35
|
+
default: return {
|
|
36
|
+
x: mx,
|
|
37
|
+
y: my
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
width: quarter ? h : w,
|
|
43
|
+
height: quarter ? w : h,
|
|
44
|
+
turnDeg: page.rotate === 90 ? -90 : page.rotate === 180 ? 180 : page.rotate === 270 ? 90 : 0,
|
|
45
|
+
place
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The same runs, placed on the shown page.
|
|
50
|
+
*
|
|
51
|
+
* @param runs The runs as the content stream drew them.
|
|
52
|
+
* @param d The page's shown geometry.
|
|
53
|
+
* @returns The runs in the shown page's frame, each carrying the page's turn.
|
|
54
|
+
*/
|
|
55
|
+
function placeRuns(runs, d) {
|
|
56
|
+
return runs.map((r) => {
|
|
57
|
+
const origin = d.place(r.x, r.y);
|
|
58
|
+
const end = d.place(r.endX, r.endY);
|
|
59
|
+
const angle = ((r.angleDeg ?? 0) + d.turnDeg) % 360 + 0;
|
|
60
|
+
const { angleDeg: _was, ...rest } = r;
|
|
61
|
+
return {
|
|
62
|
+
...rest,
|
|
63
|
+
x: origin.x,
|
|
64
|
+
y: origin.y,
|
|
65
|
+
endX: end.x,
|
|
66
|
+
endY: end.y,
|
|
67
|
+
...Math.abs(angle) > .5 ? { angleDeg: angle } : {}
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The same pictures, placed on the shown page. A quarter turn swaps a picture's
|
|
73
|
+
* width and height and leaves it standing on its side, which the caller carries
|
|
74
|
+
* as the picture's own rotation.
|
|
75
|
+
*
|
|
76
|
+
* @param images The pictures as the content stream placed them.
|
|
77
|
+
* @param d The page's shown geometry.
|
|
78
|
+
* @returns The pictures in the shown page's frame.
|
|
79
|
+
*/
|
|
80
|
+
function placeImages(images, d) {
|
|
81
|
+
return images.map((img) => {
|
|
82
|
+
const corners = [
|
|
83
|
+
d.place(img.x, img.y),
|
|
84
|
+
d.place(img.x + img.widthPt, img.y),
|
|
85
|
+
d.place(img.x + img.widthPt, img.y + img.heightPt),
|
|
86
|
+
d.place(img.x, img.y + img.heightPt)
|
|
87
|
+
];
|
|
88
|
+
const xs = corners.map((c) => c.x);
|
|
89
|
+
const ys = corners.map((c) => c.y);
|
|
90
|
+
const minX = Math.min(...xs);
|
|
91
|
+
const minY = Math.min(...ys);
|
|
92
|
+
return {
|
|
93
|
+
...img,
|
|
94
|
+
x: minX,
|
|
95
|
+
y: minY,
|
|
96
|
+
widthPt: Math.max(...xs) - minX,
|
|
97
|
+
heightPt: Math.max(...ys) - minY
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The same paths, placed on the shown page. A path is geometry, so every point
|
|
103
|
+
* of it moves and the bounding box is taken again from what comes out.
|
|
104
|
+
*
|
|
105
|
+
* @param vectors The paths as the content stream painted them.
|
|
106
|
+
* @param d The page's shown geometry.
|
|
107
|
+
* @returns The paths in the shown page's frame.
|
|
108
|
+
*/
|
|
109
|
+
function placeVectors(vectors, d) {
|
|
110
|
+
return vectors.map((v) => {
|
|
111
|
+
const segs = v.segs.map((s) => {
|
|
112
|
+
switch (s.op) {
|
|
113
|
+
case "move":
|
|
114
|
+
case "line": {
|
|
115
|
+
const p = d.place(s.x, s.y);
|
|
116
|
+
return {
|
|
117
|
+
op: s.op,
|
|
118
|
+
x: p.x,
|
|
119
|
+
y: p.y
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
case "cubic": {
|
|
123
|
+
const c1 = d.place(s.x1, s.y1);
|
|
124
|
+
const c2 = d.place(s.x2, s.y2);
|
|
125
|
+
const p = d.place(s.x, s.y);
|
|
126
|
+
return {
|
|
127
|
+
op: "cubic",
|
|
128
|
+
x1: c1.x,
|
|
129
|
+
y1: c1.y,
|
|
130
|
+
x2: c2.x,
|
|
131
|
+
y2: c2.y,
|
|
132
|
+
x: p.x,
|
|
133
|
+
y: p.y
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
case "close": return s;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const xs = [];
|
|
140
|
+
const ys = [];
|
|
141
|
+
for (const s of segs) {
|
|
142
|
+
if (s.op === "close") continue;
|
|
143
|
+
xs.push(s.x);
|
|
144
|
+
ys.push(s.y);
|
|
145
|
+
if (s.op === "cubic") xs.push(s.x1, s.x2), ys.push(s.y1, s.y2);
|
|
146
|
+
}
|
|
147
|
+
if (xs.length === 0) return {
|
|
148
|
+
...v,
|
|
149
|
+
segs
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
...v,
|
|
153
|
+
segs,
|
|
154
|
+
minX: Math.min(...xs),
|
|
155
|
+
minY: Math.min(...ys),
|
|
156
|
+
maxX: Math.max(...xs),
|
|
157
|
+
maxY: Math.max(...ys)
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { displayOf, placeImages, placeRuns, placeVectors };
|
|
@@ -5,6 +5,11 @@ export type Rectangle = readonly [number, number, number, number];
|
|
|
5
5
|
export interface PdfPage {
|
|
6
6
|
readonly dict: PdfDict;
|
|
7
7
|
readonly mediaBox: Rectangle;
|
|
8
|
+
/**
|
|
9
|
+
* §14.11.1 `/Rotate` — how far the page turns CLOCKWISE when it is shown,
|
|
10
|
+
* normalised to 0, 90, 180 or 270 and inherited down the page tree.
|
|
11
|
+
*/
|
|
12
|
+
readonly rotate: 0 | 90 | 180 | 270;
|
|
8
13
|
readonly resources: PdfDict | undefined;
|
|
9
14
|
}
|
|
10
15
|
/**
|
|
@@ -19,6 +24,8 @@ export declare class PdfFile {
|
|
|
19
24
|
private readonly buf;
|
|
20
25
|
private readonly xref;
|
|
21
26
|
readonly trailer: PdfDict;
|
|
27
|
+
/** Caller-supplied decoders, by `/Filter` name (see {@link StreamFilters}). */
|
|
28
|
+
readonly filters: StreamFilters;
|
|
22
29
|
private readonly cache;
|
|
23
30
|
private readonly objStmCache;
|
|
24
31
|
private decryptor;
|
|
@@ -31,9 +38,10 @@ export declare class PdfFile {
|
|
|
31
38
|
* @param bytes The complete PDF file bytes.
|
|
32
39
|
* @param password The user password for an encrypted source (EP14); the empty
|
|
33
40
|
* string opens permissions-only encryption.
|
|
41
|
+
* @param filters Decoders for `/Filter` names this reader does not implement.
|
|
34
42
|
* @returns A ready-to-query {@link PdfFile}.
|
|
35
43
|
*/
|
|
36
|
-
static parse(bytes: Uint8Array, password?: string): PdfFile;
|
|
44
|
+
static parse(bytes: Uint8Array, password?: string, filters?: StreamFilters): PdfFile;
|
|
37
45
|
/**
|
|
38
46
|
* Build the decryptor from `/Encrypt` (§7.6). Runs before any other object is
|
|
39
47
|
* resolved, so the `/Encrypt` dictionary itself is read in the clear; its object
|
|
@@ -78,6 +86,33 @@ export declare class PdfFile {
|
|
|
78
86
|
* `/Predictor` supported; unknown filters pass through undecoded).
|
|
79
87
|
*/
|
|
80
88
|
streamData(stream: PdfStream): Uint8Array;
|
|
89
|
+
/** Filter names met in this file that nothing here can undo. */
|
|
90
|
+
readonly unknownFilters: Set<string>;
|
|
81
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* A decoder for one `/Filter` name, supplied by the caller.
|
|
94
|
+
*
|
|
95
|
+
* §7.4 leaves the filter set open, and a reader is not obliged to implement
|
|
96
|
+
* every one — but it cannot pretend, either: an undecoded stream is not the
|
|
97
|
+
* stream. Brotli-Prototype-FileA.pdf compresses all thirty of its streams with
|
|
98
|
+
* `/BrotliDecode` (PDF 2.0), including the cross-reference, so unread it is a
|
|
99
|
+
* document with no pages at all.
|
|
100
|
+
*
|
|
101
|
+
* Rather than carry a decoder for every filter anyone might write — Brotli
|
|
102
|
+
* alone is RFC 7932's context-modelled Huffman scheme and a 122 KB static
|
|
103
|
+
* dictionary, in every bundle, for a filter almost nothing produces — the
|
|
104
|
+
* reader takes one from whoever needs it:
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { brotliDecompressSync } from 'node:zlib';
|
|
108
|
+
* Ream.parse(pdf, { filters: { BrotliDecode: (b) => brotliDecompressSync(b) } });
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* A decoder that throws is treated as one that was never supplied: the filter
|
|
112
|
+
* is reported unreadable rather than its failure escaping into the parse.
|
|
113
|
+
*/
|
|
114
|
+
export type StreamFilter = (bytes: Uint8Array) => Uint8Array;
|
|
115
|
+
/** Caller-supplied {@link StreamFilter}s, keyed by `/Filter` name (no slash). */
|
|
116
|
+
export type StreamFilters = Readonly<Record<string, StreamFilter | undefined>>;
|
|
82
117
|
/** Re-export for callers that walk a resolved dict's array values. */
|
|
83
118
|
export type { PdfArray };
|