reamkit 1.27.0 → 1.29.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/README.md +6 -5
- package/dist/esm/core/converter/ream.d.ts +8 -9
- package/dist/esm/core/converter/ream.js +0 -1
- package/dist/esm/core/document-model/types.d.ts +5 -3
- package/dist/esm/core/font/index.d.ts +1 -0
- package/dist/esm/core/font/ligatures.d.ts +17 -0
- package/dist/esm/core/font/ligatures.js +49 -0
- package/dist/esm/core/font/ttf-parser.d.ts +2 -1
- package/dist/esm/core/font/ttf-parser.js +11 -2
- package/dist/esm/core/fonts/remote-fonts.d.ts +1 -1
- package/dist/esm/core/fonts/remote-fonts.js +47 -2
- package/dist/esm/core/fonts/scripts.js +10 -5
- package/dist/esm/excel/header-footer.js +53 -8
- package/dist/esm/layout/styled-layout.js +53 -3
- package/dist/esm/pdf/cid-font.js +35 -8
- package/dist/esm/pdf-reader/annot-draw.js +114 -1
- package/dist/esm/pdf-reader/annots.d.ts +0 -14
- package/dist/esm/pdf-reader/annots.js +13 -1
- package/dist/esm/pdf-reader/ccitt.d.ts +2 -3
- package/dist/esm/pdf-reader/ccitt.js +32 -4
- package/dist/esm/pdf-reader/cff-outline.d.ts +36 -0
- package/dist/esm/pdf-reader/cff-outline.js +1122 -0
- package/dist/esm/pdf-reader/cmap.js +16 -6
- package/dist/esm/pdf-reader/content.d.ts +91 -0
- package/dist/esm/pdf-reader/content.js +156 -60
- package/dist/esm/pdf-reader/dingbats.d.ts +11 -0
- package/dist/esm/pdf-reader/dingbats.js +1033 -0
- package/dist/esm/pdf-reader/display.js +41 -1
- package/dist/esm/pdf-reader/document.d.ts +8 -0
- package/dist/esm/pdf-reader/document.js +26 -9
- package/dist/esm/pdf-reader/embedded-fonts.d.ts +11 -0
- package/dist/esm/pdf-reader/embedded-fonts.js +14 -1
- package/dist/esm/pdf-reader/encodings.d.ts +25 -0
- package/dist/esm/pdf-reader/encodings.js +110 -0
- package/dist/esm/pdf-reader/flow-build.d.ts +15 -4
- package/dist/esm/pdf-reader/flow-build.js +24 -11
- package/dist/esm/pdf-reader/font.js +329 -8
- package/dist/esm/pdf-reader/glyf-outline.d.ts +43 -0
- package/dist/esm/pdf-reader/glyf-outline.js +351 -0
- package/dist/esm/pdf-reader/glyph-names.js +20 -0
- package/dist/esm/pdf-reader/icc.d.ts +10 -0
- package/dist/esm/pdf-reader/icc.js +210 -0
- package/dist/esm/pdf-reader/image-decode.d.ts +4 -3
- package/dist/esm/pdf-reader/image-decode.js +85 -64
- package/dist/esm/pdf-reader/images.d.ts +6 -0
- package/dist/esm/pdf-reader/images.js +50 -7
- package/dist/esm/pdf-reader/jbig2.js +104 -19
- package/dist/esm/pdf-reader/layout.js +1531 -55
- package/dist/esm/pdf-reader/math-rows.d.ts +23 -0
- package/dist/esm/pdf-reader/math-rows.js +198 -0
- package/dist/esm/pdf-reader/predefined-cmap.d.ts +21 -0
- package/dist/esm/pdf-reader/predefined-cmap.js +102 -0
- package/dist/esm/pdf-reader/reader.d.ts +6 -9
- package/dist/esm/pdf-reader/reader.js +34 -21
- package/dist/esm/pdf-reader/shading.d.ts +81 -2
- package/dist/esm/pdf-reader/shading.js +191 -25
- package/dist/esm/pdf-reader/stream-filters.d.ts +6 -0
- package/dist/esm/pdf-reader/stream-filters.js +67 -0
- package/dist/esm/pdf-reader/tagged.js +1 -1
- package/dist/esm/pdf-reader/text-rules.js +83 -10
- package/dist/esm/pdf-reader/text.js +82 -3
- package/dist/esm/pdf-reader/type1-outline.d.ts +21 -0
- package/dist/esm/pdf-reader/type1-outline.js +576 -0
- package/dist/esm/pdf-reader/vector.d.ts +2 -0
- package/dist/esm/pdf-reader/vector.js +44 -4
- package/dist/esm/word/document-parser.js +4 -1
- package/dist/esm/word/docx-writer.js +28 -11
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MathNode } from '../core/document-model/index.js';
|
|
2
|
+
import { TextRun } from './content.js';
|
|
3
|
+
/** A matrix found on the page: what to draw, where, and which runs it used. */
|
|
4
|
+
export interface MathBlock {
|
|
5
|
+
/** The math object — a row of delimiters and whatever stands between them. */
|
|
6
|
+
readonly math: MathNode;
|
|
7
|
+
/** The top baseline it was drawn on, for ordering the page's blocks. */
|
|
8
|
+
readonly top: number;
|
|
9
|
+
/** Where it stands across the page. */
|
|
10
|
+
readonly x: number;
|
|
11
|
+
readonly width: number;
|
|
12
|
+
/** The runs it consumed, which the prose reading must not read again. */
|
|
13
|
+
readonly used: ReadonlySet<TextRun>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The matrices a page's rows hold (§22.1.2.68 `m:m`).
|
|
17
|
+
*
|
|
18
|
+
* @param runs The column's runs.
|
|
19
|
+
* @param fontSize The text's own size, which says how far apart the lines of
|
|
20
|
+
* PROSE stand — a display's sub-lines stand closer.
|
|
21
|
+
* @returns One entry per matrix found, in page order.
|
|
22
|
+
*/
|
|
23
|
+
export declare function matrixBlocks(runs: ReadonlyArray<TextRun>, fontSize: number): Array<MathBlock>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
//#region src/pdf-reader/math-rows.ts
|
|
2
|
+
/** The brackets a matrix may be written in, opening → closing. */
|
|
3
|
+
var BRACKETS = new Map([
|
|
4
|
+
["(", ")"],
|
|
5
|
+
["[", "]"],
|
|
6
|
+
["{", "}"],
|
|
7
|
+
["⟨", "⟩"],
|
|
8
|
+
["|", "|"]
|
|
9
|
+
]);
|
|
10
|
+
/**
|
|
11
|
+
* The matrices a page's rows hold (§22.1.2.68 `m:m`).
|
|
12
|
+
*
|
|
13
|
+
* @param runs The column's runs.
|
|
14
|
+
* @param fontSize The text's own size, which says how far apart the lines of
|
|
15
|
+
* PROSE stand — a display's sub-lines stand closer.
|
|
16
|
+
* @returns One entry per matrix found, in page order.
|
|
17
|
+
*/
|
|
18
|
+
function matrixBlocks(runs, fontSize) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const cluster of clusters(baselines(runs, fontSize), fontSize)) {
|
|
21
|
+
const found = matrixFrom(cluster);
|
|
22
|
+
if (found) out.push(found);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/** The baseline a row of runs stands on. */
|
|
27
|
+
var baselineOf = (row) => Math.max(...row.map((r) => r.y));
|
|
28
|
+
/**
|
|
29
|
+
* The runs grouped by the baseline they were DRAWN on, top to bottom.
|
|
30
|
+
*
|
|
31
|
+
* Tighter than the reading's own rows, which allow a line half an em of drift
|
|
32
|
+
* so that a footnote mark stays with its word: the sub-lines of a display stand
|
|
33
|
+
* six points apart in a ten-point face, and grouped that loosely
|
|
34
|
+
* bug1997343.pdf's brackets joined the row of numbers under them.
|
|
35
|
+
*/
|
|
36
|
+
function baselines(runs, fontSize) {
|
|
37
|
+
const tol = Math.max(1, fontSize * SAME_BASELINE);
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const run of [...runs].sort((a, b) => b.y - a.y || a.x - b.x)) {
|
|
40
|
+
const last = out[out.length - 1];
|
|
41
|
+
if (last && Math.abs(baselineOf(last) - run.y) <= tol) last.push(run);
|
|
42
|
+
else out.push([run]);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/** How far off a baseline, in ems, a run may sit and still be ON it. */
|
|
47
|
+
var SAME_BASELINE = .15;
|
|
48
|
+
/**
|
|
49
|
+
* The runs of a page grouped into the SUB-LINES of one display: rows standing
|
|
50
|
+
* closer together than a line of prose does, three or more of them (two rows
|
|
51
|
+
* and the brackets between).
|
|
52
|
+
*/
|
|
53
|
+
function clusters(rows, fontSize) {
|
|
54
|
+
const out = [];
|
|
55
|
+
let run = [];
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
const last = run[run.length - 1];
|
|
58
|
+
if (last && baselineOf(last) - baselineOf(row) >= fontSize * SUBLINE_GAP) {
|
|
59
|
+
if (run.length >= MIN_SUBLINES) out.push(run);
|
|
60
|
+
run = [];
|
|
61
|
+
}
|
|
62
|
+
run.push(row);
|
|
63
|
+
}
|
|
64
|
+
if (run.length >= MIN_SUBLINES) out.push(run);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** How far apart, in ems, two baselines have to stand to be separate LINES. */
|
|
68
|
+
var SUBLINE_GAP = .9;
|
|
69
|
+
/** A matrix is a row of cells above another, and the brackets between them. */
|
|
70
|
+
var MIN_SUBLINES = 3;
|
|
71
|
+
/** Build the matrix a cluster of sub-lines holds, or nothing where it holds none. */
|
|
72
|
+
function matrixFrom(cluster) {
|
|
73
|
+
const middle = cluster.find((row) => pairsOf(row).length > 0);
|
|
74
|
+
if (!middle) return void 0;
|
|
75
|
+
const pairs = pairsOf(middle);
|
|
76
|
+
const cells = cluster.filter((row) => row !== middle);
|
|
77
|
+
if (cells.length === 0) return void 0;
|
|
78
|
+
const children = [];
|
|
79
|
+
let used = /* @__PURE__ */ new Set();
|
|
80
|
+
let at = -Infinity;
|
|
81
|
+
for (const pair of pairs) {
|
|
82
|
+
for (const run of middle) if (run.x >= at && run.endX <= pair.open.x && run.text.trim().length > 0) {
|
|
83
|
+
children.push({
|
|
84
|
+
type: "run",
|
|
85
|
+
text: spaced(run.text)
|
|
86
|
+
});
|
|
87
|
+
used.add(run);
|
|
88
|
+
}
|
|
89
|
+
const inside = matrixInside(cluster, middle, pair);
|
|
90
|
+
if (!inside) return void 0;
|
|
91
|
+
children.push({
|
|
92
|
+
type: "delimiter",
|
|
93
|
+
begChr: pair.open.text.trim(),
|
|
94
|
+
endChr: pair.close.text.trim(),
|
|
95
|
+
children: [inside.matrix]
|
|
96
|
+
});
|
|
97
|
+
used = new Set([
|
|
98
|
+
...used,
|
|
99
|
+
...inside.used,
|
|
100
|
+
pair.open,
|
|
101
|
+
pair.close
|
|
102
|
+
]);
|
|
103
|
+
at = pair.close.endX;
|
|
104
|
+
}
|
|
105
|
+
for (const run of middle) if (run.x >= at && run.text.trim().length > 0) {
|
|
106
|
+
children.push({
|
|
107
|
+
type: "run",
|
|
108
|
+
text: spaced(run.text)
|
|
109
|
+
});
|
|
110
|
+
used.add(run);
|
|
111
|
+
}
|
|
112
|
+
if (children.length === 0) return void 0;
|
|
113
|
+
for (const row of cluster) for (const run of row) if (!used.has(run)) return void 0;
|
|
114
|
+
const all = [...used];
|
|
115
|
+
return {
|
|
116
|
+
math: {
|
|
117
|
+
type: "row",
|
|
118
|
+
children
|
|
119
|
+
},
|
|
120
|
+
top: baselineOf(cells[0] ?? middle),
|
|
121
|
+
x: Math.min(...all.map((r) => r.x)),
|
|
122
|
+
width: Math.max(...all.map((r) => r.endX)) - Math.min(...all.map((r) => r.x)),
|
|
123
|
+
used
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* An operator standing between two matrices, with the air the page gave it.
|
|
128
|
+
* The file sets `(1 2 / 3 4)(1 1 / 0 1) = (1 3 / 3 7)` with the equals sign
|
|
129
|
+
* clear of both brackets; set tight against them it reads as one word.
|
|
130
|
+
*/
|
|
131
|
+
var spaced = (text) => ` ${text.trim()} `;
|
|
132
|
+
/** The bracket pairs a row holds, left to right and never nested. */
|
|
133
|
+
function pairsOf(row) {
|
|
134
|
+
const out = [];
|
|
135
|
+
let open;
|
|
136
|
+
for (const run of [...row].sort((a, b) => a.x - b.x)) {
|
|
137
|
+
const text = run.text.trim();
|
|
138
|
+
if (open === void 0) {
|
|
139
|
+
if (BRACKETS.has(text)) open = run;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (text === BRACKETS.get(open.text.trim())) {
|
|
143
|
+
out.push({
|
|
144
|
+
open,
|
|
145
|
+
close: run
|
|
146
|
+
});
|
|
147
|
+
open = void 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The grid inside one bracket pair: every sub-line's runs that fall between the
|
|
154
|
+
* brackets, clustered into columns by where they stand.
|
|
155
|
+
*/
|
|
156
|
+
function matrixInside(cluster, middle, pair) {
|
|
157
|
+
const used = /* @__PURE__ */ new Set();
|
|
158
|
+
const rows = [];
|
|
159
|
+
for (const row of cluster) {
|
|
160
|
+
const inside = row.filter((run) => run !== pair.open && run !== pair.close && run.x >= pair.open.x && run.endX <= pair.close.endX && run.text.trim().length > 0);
|
|
161
|
+
if (inside.length === 0) continue;
|
|
162
|
+
if (row === middle && inside.some((run) => BRACKETS.has(run.text.trim()))) return void 0;
|
|
163
|
+
inside.sort((a, b) => a.x - b.x);
|
|
164
|
+
for (const run of inside) used.add(run);
|
|
165
|
+
rows.push(inside);
|
|
166
|
+
}
|
|
167
|
+
if (rows.length < 2) return void 0;
|
|
168
|
+
const size = Math.max(...rows.flat().map((r) => r.fontSizePt || 10));
|
|
169
|
+
const centres = [];
|
|
170
|
+
const columnOf = (run) => {
|
|
171
|
+
const centre = (run.x + run.endX) / 2;
|
|
172
|
+
const found = centres.findIndex((c) => Math.abs(c - centre) <= size / 2);
|
|
173
|
+
if (found >= 0) return found;
|
|
174
|
+
centres.push(centre);
|
|
175
|
+
return centres.length - 1;
|
|
176
|
+
};
|
|
177
|
+
const grid = rows.map((row) => {
|
|
178
|
+
const cells = [];
|
|
179
|
+
for (const run of row) {
|
|
180
|
+
const at = columnOf(run);
|
|
181
|
+
(cells[at] ??= []).push(run.text.trim());
|
|
182
|
+
}
|
|
183
|
+
return cells;
|
|
184
|
+
});
|
|
185
|
+
const width = Math.max(...grid.map((row) => row.length));
|
|
186
|
+
return {
|
|
187
|
+
matrix: {
|
|
188
|
+
type: "matrix",
|
|
189
|
+
rows: grid.map((row) => Array.from({ length: width }, (_, i) => ({
|
|
190
|
+
type: "run",
|
|
191
|
+
text: (row[i] ?? []).join("")
|
|
192
|
+
})))
|
|
193
|
+
},
|
|
194
|
+
used
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
export { matrixBlocks };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** How a named CMap breaks a string into codes, and what those codes mean. */
|
|
2
|
+
export interface PredefinedCMap {
|
|
3
|
+
/** The `TextDecoder` label the code bytes are in. */
|
|
4
|
+
readonly charset: string;
|
|
5
|
+
/** Whether a byte begins a TWO-byte code; anything else stands alone. */
|
|
6
|
+
readonly leadsPair: (byte: number) => boolean;
|
|
7
|
+
/** §9.7.5.2 — a `…-V` CMap sets its text down the page. */
|
|
8
|
+
readonly vertical: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What a predefined CMap name comes to.
|
|
12
|
+
*
|
|
13
|
+
* @param name The `/Encoding` name, as the font states it.
|
|
14
|
+
* @returns Its encoding and code structure, or `undefined` for `Identity` and
|
|
15
|
+
* for any name whose encoding this cannot decode.
|
|
16
|
+
*/
|
|
17
|
+
export declare function predefinedCMap(name: string): PredefinedCMap | undefined;
|
|
18
|
+
/** Split a shown string the way this CMap's codespace says (§9.7.6.2). */
|
|
19
|
+
export declare function splitPredefined(cmap: PredefinedCMap, bytes: Uint8Array): Array<number>;
|
|
20
|
+
/** The character one code stands for: its own bytes, read in the CMap's encoding. */
|
|
21
|
+
export declare function decodePredefined(cmap: PredefinedCMap, code: number): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/pdf-reader/predefined-cmap.ts
|
|
2
|
+
/** Shift-JIS: 81–9F and E0–FC lead a pair; 00–80 and A0–DF stand alone. */
|
|
3
|
+
var shiftJis = (b) => b >= 129 && b <= 159 || b >= 224 && b <= 252;
|
|
4
|
+
/** The EUC family: A1–FE lead a pair, and 8E/8F introduce one as well. */
|
|
5
|
+
var euc = (b) => b >= 161 || b === 142 || b === 143;
|
|
6
|
+
/** Big5, GBK and UHC: everything from 81 up leads a pair. */
|
|
7
|
+
var highLeads = (b) => b >= 129;
|
|
8
|
+
/** Two bytes always, big-endian — the UCS-2 and UTF-16 CMaps. */
|
|
9
|
+
var always = () => true;
|
|
10
|
+
/** The families, longest name first so `90ms-RKSJ` is not read as `RKSJ`. */
|
|
11
|
+
var FAMILIES = [
|
|
12
|
+
{
|
|
13
|
+
match: /RKSJ/u,
|
|
14
|
+
charset: "shift_jis",
|
|
15
|
+
leadsPair: shiftJis
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
match: /-EUC(-|$)|^EUC-/u,
|
|
19
|
+
charset: "euc-jp",
|
|
20
|
+
leadsPair: euc
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
match: /^(ETen|ETenms|B5pc|HKscs-B5|CNS-EUC)/u,
|
|
24
|
+
charset: "big5",
|
|
25
|
+
leadsPair: highLeads
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
match: /^(GBK|GBpc|GBK2K|GB-EUC|GBKp)/u,
|
|
29
|
+
charset: "gbk",
|
|
30
|
+
leadsPair: highLeads
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
match: /^(KSC|KSCms|KSCpc)/u,
|
|
34
|
+
charset: "euc-kr",
|
|
35
|
+
leadsPair: highLeads
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
match: /UCS2|UTF16/u,
|
|
39
|
+
charset: "utf-16be",
|
|
40
|
+
leadsPair: always
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* What a predefined CMap name comes to.
|
|
45
|
+
*
|
|
46
|
+
* @param name The `/Encoding` name, as the font states it.
|
|
47
|
+
* @returns Its encoding and code structure, or `undefined` for `Identity` and
|
|
48
|
+
* for any name whose encoding this cannot decode.
|
|
49
|
+
*/
|
|
50
|
+
function predefinedCMap(name) {
|
|
51
|
+
if (name.startsWith("Identity")) return void 0;
|
|
52
|
+
const vertical = name.endsWith("-V");
|
|
53
|
+
const family = FAMILIES.find((f) => f.match.test(name));
|
|
54
|
+
if (!family) return void 0;
|
|
55
|
+
if (!canDecode(family.charset)) return void 0;
|
|
56
|
+
return {
|
|
57
|
+
charset: family.charset,
|
|
58
|
+
leadsPair: family.leadsPair,
|
|
59
|
+
vertical
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Split a shown string the way this CMap's codespace says (§9.7.6.2). */
|
|
63
|
+
function splitPredefined(cmap, bytes) {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (let i = 0; i < bytes.length;) {
|
|
66
|
+
const b = bytes[i];
|
|
67
|
+
if (cmap.leadsPair(b) && i + 1 < bytes.length) {
|
|
68
|
+
out.push(b << 8 | bytes[i + 1]);
|
|
69
|
+
i += 2;
|
|
70
|
+
} else {
|
|
71
|
+
out.push(b);
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** The character one code stands for: its own bytes, read in the CMap's encoding. */
|
|
78
|
+
function decodePredefined(cmap, code) {
|
|
79
|
+
const bytes = code > 255 ? Uint8Array.from([code >> 8, code & 255]) : Uint8Array.from([code]);
|
|
80
|
+
try {
|
|
81
|
+
return new TextDecoder(cmap.charset, { fatal: false }).decode(bytes);
|
|
82
|
+
} catch {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Whether this runtime's `TextDecoder` carries the legacy encoding. */
|
|
87
|
+
function canDecode(charset) {
|
|
88
|
+
const had = supported.get(charset);
|
|
89
|
+
if (had !== void 0) return had;
|
|
90
|
+
let ok = false;
|
|
91
|
+
try {
|
|
92
|
+
new TextDecoder(charset);
|
|
93
|
+
ok = true;
|
|
94
|
+
} catch {
|
|
95
|
+
ok = false;
|
|
96
|
+
}
|
|
97
|
+
supported.set(charset, ok);
|
|
98
|
+
return ok;
|
|
99
|
+
}
|
|
100
|
+
var supported = /* @__PURE__ */ new Map();
|
|
101
|
+
//#endregion
|
|
102
|
+
export { decodePredefined, predefinedCMap, splitPredefined };
|
|
@@ -13,17 +13,14 @@ import { FlowDoc } from '../core/ir/flow.js';
|
|
|
13
13
|
* opens permissions-only encryption.
|
|
14
14
|
* @param filters Decoders for `/Filter` names this reader does not implement
|
|
15
15
|
* (§7.4); see {@link StreamFilters}.
|
|
16
|
-
* @param layout `'auto'` (the default) lets the FILE decide: a page that is
|
|
17
|
-
* mostly marks is reproduced, one that is mostly lines is re-set,
|
|
18
|
-
* and the reader records which it chose. `'flow'` reads a
|
|
19
|
-
* re-flowable document out of the page —
|
|
20
|
-
* paragraphs and tables in reading order, from the structure
|
|
21
|
-
* tree where there is one. `'positional'` keeps the page: every
|
|
22
|
-
* line stands where its glyphs do, beside the artwork, which is
|
|
23
|
-
* what a form or a drawing needs and what a paragraph cannot be.
|
|
24
16
|
* @returns The reconstructed FlowDoc and its accumulated {@link Loss} report.
|
|
17
|
+
*
|
|
18
|
+
* Which of the two readings a file gets is the FILE's to decide and no
|
|
19
|
+
* caller's: a page that is mostly marks is reproduced where it stands, one
|
|
20
|
+
* that is mostly lines is re-set as a document, and the reader records which
|
|
21
|
+
* it chose. There is no override — see the note on {@link readingOf}.
|
|
25
22
|
*/
|
|
26
|
-
export declare function readPdf(bytes: Uint8Array, password?: string,
|
|
23
|
+
export declare function readPdf(bytes: Uint8Array, password?: string, filters?: StreamFilters): ReadResult<FlowDoc>;
|
|
27
24
|
/**
|
|
28
25
|
* The `pdfReader` adapter: a {@link DocumentReader} that sniffs the `%PDF-`
|
|
29
26
|
* header and parses the bytes into a {@link FlowDoc} (E-PDF EP5).
|
|
@@ -40,17 +40,14 @@ function endsLikePdf(bytes) {
|
|
|
40
40
|
* opens permissions-only encryption.
|
|
41
41
|
* @param filters Decoders for `/Filter` names this reader does not implement
|
|
42
42
|
* (§7.4); see {@link StreamFilters}.
|
|
43
|
-
* @param layout `'auto'` (the default) lets the FILE decide: a page that is
|
|
44
|
-
* mostly marks is reproduced, one that is mostly lines is re-set,
|
|
45
|
-
* and the reader records which it chose. `'flow'` reads a
|
|
46
|
-
* re-flowable document out of the page —
|
|
47
|
-
* paragraphs and tables in reading order, from the structure
|
|
48
|
-
* tree where there is one. `'positional'` keeps the page: every
|
|
49
|
-
* line stands where its glyphs do, beside the artwork, which is
|
|
50
|
-
* what a form or a drawing needs and what a paragraph cannot be.
|
|
51
43
|
* @returns The reconstructed FlowDoc and its accumulated {@link Loss} report.
|
|
44
|
+
*
|
|
45
|
+
* Which of the two readings a file gets is the FILE's to decide and no
|
|
46
|
+
* caller's: a page that is mostly marks is reproduced where it stands, one
|
|
47
|
+
* that is mostly lines is re-set as a document, and the reader records which
|
|
48
|
+
* it chose. There is no override — see the note on {@link readingOf}.
|
|
52
49
|
*/
|
|
53
|
-
function readPdf(bytes, password = "",
|
|
50
|
+
function readPdf(bytes, password = "", filters = {}) {
|
|
54
51
|
const file = PdfFile.parse(bytes, password, filters);
|
|
55
52
|
const losses = [];
|
|
56
53
|
if (file.encryptionUnsupported) losses.push({
|
|
@@ -63,11 +60,11 @@ function readPdf(bytes, password = "", layout = "auto", filters = {}) {
|
|
|
63
60
|
feature: FEATURES.text,
|
|
64
61
|
detail: `PDF stream filter /${name} is not supported; streams using it are unreadable`
|
|
65
62
|
});
|
|
66
|
-
const reading =
|
|
67
|
-
|
|
63
|
+
const reading = readingOf(file);
|
|
64
|
+
losses.push({
|
|
68
65
|
severity: "degraded",
|
|
69
66
|
feature: FEATURES.text,
|
|
70
|
-
detail: reading === "positional" ? "PDF read as a PAGE (placed): its artwork outweighs its prose, so every line stands where its glyphs stand
|
|
67
|
+
detail: reading === "positional" ? "PDF read as a PAGE (placed): its artwork outweighs its prose, so every line stands where its glyphs stand" : "PDF read as a DOCUMENT (flowing): its prose outweighs its artwork, so the words re-set and the artwork takes its turn in reading order"
|
|
71
68
|
});
|
|
72
69
|
const tagged = reading === "positional" ? void 0 : reconstructTaggedPdf(file);
|
|
73
70
|
const reconstruction = tagged ?? reconstructByLayout(file, reading);
|
|
@@ -77,11 +74,6 @@ function readPdf(bytes, password = "", layout = "auto", filters = {}) {
|
|
|
77
74
|
detail: "untagged PDF — text and headings reconstructed heuristically from glyph positions; structure is approximate"
|
|
78
75
|
});
|
|
79
76
|
losses.push(...reconstruction.losses);
|
|
80
|
-
losses.push({
|
|
81
|
-
severity: "dropped",
|
|
82
|
-
feature: FEATURES.images,
|
|
83
|
-
detail: "PDF bare-shading (sh) vector regions are not reconstructed"
|
|
84
|
-
});
|
|
85
77
|
return {
|
|
86
78
|
doc: reconstruction.doc,
|
|
87
79
|
losses
|
|
@@ -105,22 +97,43 @@ function readPdf(bytes, password = "", layout = "auto", filters = {}) {
|
|
|
105
97
|
* records which reading it took and why, and the caller can name the other.
|
|
106
98
|
*/
|
|
107
99
|
function readingOf(file) {
|
|
108
|
-
/**
|
|
109
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Below this many marks a page is prose with decoration, whatever the ratio.
|
|
102
|
+
*
|
|
103
|
+
* It stood at twenty, and calgray.pdf is a five-by-four grid of grey swatches
|
|
104
|
+
* with a label in each: twenty boxes, of which the one painted white is not a
|
|
105
|
+
* mark anybody can see. Nineteen — one short — and all three pages of it were
|
|
106
|
+
* read as prose, the labels of each row run together into a line and the
|
|
107
|
+
* sheet spilling onto a second page. Nineteen boxes in a grid are not a page
|
|
108
|
+
* of prose with a rule under its heading.
|
|
109
|
+
*/
|
|
110
|
+
const ENOUGH_MARKS = 12;
|
|
110
111
|
/** Twice as many marks as lines is a page that is drawn rather than written. */
|
|
111
112
|
const DRAWN = 2;
|
|
113
|
+
/** Below this many runs, an angle is a stamp or a watermark and not the page. */
|
|
114
|
+
const ENOUGH_TURNED = 8;
|
|
112
115
|
const ratios = [];
|
|
113
116
|
for (const page of file.pages()) {
|
|
114
117
|
let marks = 0;
|
|
115
118
|
let lines = 0;
|
|
119
|
+
let turned = 0;
|
|
120
|
+
let runs = 0;
|
|
116
121
|
try {
|
|
117
122
|
marks = collectPageVectors(file, page, []).vectors.length;
|
|
118
123
|
const ys = /* @__PURE__ */ new Set();
|
|
119
|
-
for (const run of extractPageText(file, page))
|
|
124
|
+
for (const run of extractPageText(file, page)) {
|
|
125
|
+
ys.add(Math.round(run.y));
|
|
126
|
+
runs++;
|
|
127
|
+
if (run.angleDeg !== void 0) turned++;
|
|
128
|
+
}
|
|
120
129
|
lines = ys.size;
|
|
121
130
|
} catch {
|
|
122
131
|
continue;
|
|
123
132
|
}
|
|
133
|
+
if (runs >= ENOUGH_TURNED && turned > runs * .5) {
|
|
134
|
+
ratios.push(DRAWN);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
124
137
|
if (marks < ENOUGH_MARKS) {
|
|
125
138
|
ratios.push(0);
|
|
126
139
|
continue;
|
|
@@ -145,7 +158,7 @@ var pdfReader = {
|
|
|
145
158
|
FEATURES.images
|
|
146
159
|
]),
|
|
147
160
|
sniff: sniffPdf,
|
|
148
|
-
read: (bytes, opts) => readPdf(bytes, typeof opts?.password === "string" ? opts.password : "",
|
|
161
|
+
read: (bytes, opts) => readPdf(bytes, typeof opts?.password === "string" ? opts.password : "", isFilters(opts?.filters) ? opts.filters : {})
|
|
149
162
|
};
|
|
150
163
|
/** A caller's `filters` option, when it is the shape the reader can use. */
|
|
151
164
|
function isFilters(value) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CieSpace } from './cie-color.js';
|
|
2
|
+
import { IccTransform } from './icc.js';
|
|
2
3
|
import { PdfFunction } from './function.js';
|
|
3
4
|
import { ShapeGradient } from '../core/vector.js';
|
|
4
5
|
import { PdfDict } from '../pdf/objects.js';
|
|
@@ -17,6 +18,70 @@ import { PdfFile } from './document.js';
|
|
|
17
18
|
* @returns A map from pattern resource name to its {@link ShapeGradient}.
|
|
18
19
|
*/
|
|
19
20
|
export declare function buildShadingMap(file: PdfFile, resources: PdfDict | undefined): Map<string, ShapeGradient>;
|
|
21
|
+
/**
|
|
22
|
+
* §8.7.4.5.2 — an axial or radial shading as a gradient, for a bare `sh`.
|
|
23
|
+
*
|
|
24
|
+
* A `sh` paints the CLIP rather than a path, so what it needs is not a fill for
|
|
25
|
+
* a shape the page drew but the gradient itself, to fill the region with. It is
|
|
26
|
+
* the same reading `buildShadingMap` does for a pattern.
|
|
27
|
+
*
|
|
28
|
+
* @param file The owning file.
|
|
29
|
+
* @param sh The shading dictionary.
|
|
30
|
+
* @returns The gradient, or `undefined` for a type this does not read.
|
|
31
|
+
*/
|
|
32
|
+
export declare function gradientShading(file: PdfFile, sh: PdfDict): ShapeGradient | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* §8.7.4.5 — which kind of shading this is, as the file states it.
|
|
35
|
+
*
|
|
36
|
+
* @param file The owning file.
|
|
37
|
+
* @param sh The shading dictionary.
|
|
38
|
+
* @returns Its `/ShadingType`, or 0 where the file states none.
|
|
39
|
+
*/
|
|
40
|
+
export declare function shadingTypeOf(file: PdfFile, sh: PdfDict): number;
|
|
41
|
+
/**
|
|
42
|
+
* §8.7.4.5.3 — a FUNCTION-BASED shading, sampled into a picture.
|
|
43
|
+
*
|
|
44
|
+
* Type 1 is not a ramp between two points: it is a function of two variables
|
|
45
|
+
* over a rectangle, and no gradient can stand for one. Painted by a bare `sh`
|
|
46
|
+
* it fills the clip, and nothing here lifted it at all —
|
|
47
|
+
* function_based_shading.pdf is nine such squares and 43% of the page's ink,
|
|
48
|
+
* and reconstructed to a blank sheet.
|
|
49
|
+
*
|
|
50
|
+
* Sampled it is exactly a picture, which every format downstream can show. The
|
|
51
|
+
* grid is fixed: the function is smooth by construction (§8.7.4.5.3 gives it a
|
|
52
|
+
* `/Domain` and nothing else), so more samples buy nothing a reader can see.
|
|
53
|
+
*
|
|
54
|
+
* @param file The owning file.
|
|
55
|
+
* @param shading The shading dictionary.
|
|
56
|
+
* @returns The picture and the domain it covers, or `undefined` for a shading
|
|
57
|
+
* of another type or one whose function cannot be run.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sampledShading(file: PdfFile, shading: PdfDict): {
|
|
60
|
+
rgb: Uint8Array;
|
|
61
|
+
size: number;
|
|
62
|
+
domain: [number, number, number, number];
|
|
63
|
+
} | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* §8.6.8 — the colour a run of `sc` / `scn` components comes to.
|
|
66
|
+
*
|
|
67
|
+
* The space in force decides, and where it was not read the COUNT is the next
|
|
68
|
+
* best witness: three numbers are RGB and four are CMYK on every device space
|
|
69
|
+
* there is. One number is the ambiguous case — grey in a device space, but the
|
|
70
|
+
* strength of a colorant in a Separation, where 1 is the ink at full and reads
|
|
71
|
+
* dark — so a lone component is only taken where the space said what it means.
|
|
72
|
+
*
|
|
73
|
+
* @param nums The components, as the page or a shading's function states them.
|
|
74
|
+
* @param space The space in force, where it was read.
|
|
75
|
+
* @returns The colour as 6 hex digits, or `undefined` where the numbers do not
|
|
76
|
+
* say what colour they are.
|
|
77
|
+
*/
|
|
78
|
+
export declare function spaceColor(nums: ReadonlyArray<number>, space: ColorSpaceInfo | undefined): string | undefined;
|
|
79
|
+
/** Three channels, each 0..1, as 6 upper-case hex digits. */
|
|
80
|
+
export declare function rgbHex(r: number, g: number, b: number): string;
|
|
81
|
+
/** One grey level, 0..1, as 6 upper-case hex digits. */
|
|
82
|
+
export declare function grayHex(v: number): string;
|
|
83
|
+
/** §8.6.4.4 — four inks, each 0..1, as 6 upper-case hex digits. */
|
|
84
|
+
export declare function cmykHex(c: number, m: number, y: number, k: number): string;
|
|
20
85
|
/**
|
|
21
86
|
* §11.6.4.4 — the constant fill alpha (`/ca`) of every `/ExtGState` the page
|
|
22
87
|
* names, by name.
|
|
@@ -39,10 +104,17 @@ export declare function buildShadingMap(file: PdfFile, resources: PdfDict | unde
|
|
|
39
104
|
* state one.
|
|
40
105
|
*/
|
|
41
106
|
export declare function buildAlphaMap(file: PdfFile, resources: PdfDict | undefined): Map<string, GsPaint>;
|
|
42
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* What a `/ExtGState` says about paint that the reconstruction can carry.
|
|
109
|
+
*
|
|
110
|
+
* Every field is absent when the state does not NAME that parameter, because a
|
|
111
|
+
* `gs` leaves what it does not name alone (§8.4.5).
|
|
112
|
+
*/
|
|
43
113
|
export interface GsPaint {
|
|
44
|
-
/** §11.6.4.4 `/ca` — the constant fill alpha,
|
|
114
|
+
/** §11.6.4.4 `/ca` — the constant fill alpha, where the state names one. */
|
|
45
115
|
readonly alpha?: number;
|
|
116
|
+
/** §11.3.5 — whether `/BM` is named at all, since naming `/Normal` ends a blend. */
|
|
117
|
+
readonly statesBlend?: boolean;
|
|
46
118
|
/** §11.3.5 `/BM` — the paint only darkens, so what it covers shows through. */
|
|
47
119
|
readonly darkens?: boolean;
|
|
48
120
|
/**
|
|
@@ -53,6 +125,7 @@ export interface GsPaint {
|
|
|
53
125
|
/**
|
|
54
126
|
* §11.6.5 `/SMask` — the paint's opacity varies from place to place, out of
|
|
55
127
|
* another group's luminosity or alpha. Nothing downstream has a mask like it.
|
|
128
|
+
* `false` where the state names `/None`, which takes a mask off.
|
|
56
129
|
*/
|
|
57
130
|
readonly masked?: boolean;
|
|
58
131
|
}
|
|
@@ -72,6 +145,12 @@ export interface ColorSpaceInfo {
|
|
|
72
145
|
* out of this transform.
|
|
73
146
|
*/
|
|
74
147
|
readonly cie?: CieSpace;
|
|
148
|
+
/**
|
|
149
|
+
* §8.6.5.5 — the transform an `/ICCBased` profile states, where its form is
|
|
150
|
+
* one this reads. Its numbers mean what the PROFILE makes of them, which for
|
|
151
|
+
* anything but an sRGB-like profile is not what a device space would.
|
|
152
|
+
*/
|
|
153
|
+
readonly icc?: IccTransform;
|
|
75
154
|
/**
|
|
76
155
|
* §8.6.6.4/§8.6.6.5 — for a `Separation` or `DeviceN`, the way OUT of it: the
|
|
77
156
|
* tint transform and the space its numbers land in. Absent where the file
|