reamkit 1.29.0 → 1.31.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 +60 -24
- package/dist/esm/core/converter/project.js +3 -1
- package/dist/esm/core/crypto/offcrypto.js +1 -1
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +65 -0
- package/dist/esm/core/drawingml/shape-render.js +13 -1
- package/dist/esm/core/font/index.d.ts +2 -0
- package/dist/esm/core/font/ttf-build.d.ts +113 -0
- package/dist/esm/core/font/ttf-build.js +1224 -0
- package/dist/esm/core/font/ttf-subset.d.ts +19 -0
- package/dist/esm/core/font/ttf-subset.js +14 -1
- package/dist/esm/core/fonts/index.d.ts +1 -1
- package/dist/esm/core/fonts/provider.d.ts +17 -0
- package/dist/esm/core/fonts/provider.js +27 -2
- package/dist/esm/core/fonts/remote-fonts.d.ts +8 -0
- package/dist/esm/core/fonts/remote-fonts.js +108 -18
- package/dist/esm/core/ir/flow.d.ts +88 -1
- package/dist/esm/core/numbering/index.d.ts +1 -1
- package/dist/esm/core/numbering/state.d.ts +11 -1
- package/dist/esm/core/numbering/state.js +10 -1
- package/dist/esm/core/style-cascade/resolver.js +35 -4
- package/dist/esm/core/style-cascade/types.d.ts +19 -1
- package/dist/esm/core/style-cascade/types.js +3 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/layout/page-doc.d.ts +10 -3
- package/dist/esm/layout/styled-layout.d.ts +48 -7
- package/dist/esm/layout/styled-layout.js +881 -119
- package/dist/esm/layout/turned-section.d.ts +38 -0
- package/dist/esm/layout/turned-section.js +193 -0
- package/dist/esm/pdf/styled-page-emitter.js +78 -2
- package/dist/esm/pdf-reader/annot-draw.js +93 -1
- package/dist/esm/pdf-reader/annots.d.ts +18 -0
- package/dist/esm/pdf-reader/annots.js +86 -9
- package/dist/esm/pdf-reader/cff-outline.d.ts +27 -0
- package/dist/esm/pdf-reader/cff-outline.js +169 -21
- package/dist/esm/pdf-reader/cmap.js +5 -2
- package/dist/esm/pdf-reader/content.d.ts +70 -4
- package/dist/esm/pdf-reader/content.js +172 -12
- package/dist/esm/pdf-reader/display.d.ts +36 -0
- package/dist/esm/pdf-reader/display.js +82 -1
- package/dist/esm/pdf-reader/document.js +5 -1
- package/dist/esm/pdf-reader/embedded-fonts.d.ts +25 -0
- package/dist/esm/pdf-reader/embedded-fonts.js +78 -9
- package/dist/esm/pdf-reader/encodings.d.ts +8 -0
- package/dist/esm/pdf-reader/encodings.js +25 -3
- package/dist/esm/pdf-reader/face-outlines.d.ts +78 -0
- package/dist/esm/pdf-reader/face-outlines.js +362 -0
- package/dist/esm/pdf-reader/figures.d.ts +52 -0
- package/dist/esm/pdf-reader/figures.js +433 -0
- package/dist/esm/pdf-reader/flow-build.d.ts +224 -7
- package/dist/esm/pdf-reader/flow-build.js +545 -39
- package/dist/esm/pdf-reader/font.d.ts +27 -1
- package/dist/esm/pdf-reader/font.js +632 -63
- package/dist/esm/pdf-reader/glyf-outline.d.ts +33 -0
- package/dist/esm/pdf-reader/glyf-outline.js +148 -3
- package/dist/esm/pdf-reader/glyph-names.js +154 -1
- package/dist/esm/pdf-reader/glyph-shapes.d.ts +18 -0
- package/dist/esm/pdf-reader/glyph-shapes.js +57 -0
- package/dist/esm/pdf-reader/image-decode.js +70 -4
- package/dist/esm/pdf-reader/images.d.ts +5 -0
- package/dist/esm/pdf-reader/images.js +4 -2
- package/dist/esm/pdf-reader/jbig2.d.ts +40 -1
- package/dist/esm/pdf-reader/jbig2.js +78 -16
- package/dist/esm/pdf-reader/jpeg.d.ts +6 -3
- package/dist/esm/pdf-reader/jpeg.js +21 -1
- package/dist/esm/pdf-reader/layout.d.ts +119 -2
- package/dist/esm/pdf-reader/layout.js +2219 -184
- package/dist/esm/pdf-reader/lexer.d.ts +10 -0
- package/dist/esm/pdf-reader/lexer.js +17 -0
- package/dist/esm/pdf-reader/page-numbers.d.ts +53 -0
- package/dist/esm/pdf-reader/page-numbers.js +167 -0
- package/dist/esm/pdf-reader/pattern-tint.d.ts +11 -1
- package/dist/esm/pdf-reader/pattern-tint.js +21 -3
- package/dist/esm/pdf-reader/regions.d.ts +25 -0
- package/dist/esm/pdf-reader/regions.js +167 -0
- package/dist/esm/pdf-reader/shading.d.ts +58 -2
- package/dist/esm/pdf-reader/shading.js +181 -11
- package/dist/esm/pdf-reader/struct-tree.js +112 -8
- package/dist/esm/pdf-reader/tagged.js +207 -28
- package/dist/esm/pdf-reader/text-rules.js +1 -1
- package/dist/esm/pdf-reader/text.d.ts +6 -3
- package/dist/esm/pdf-reader/text.js +106 -22
- package/dist/esm/pdf-reader/type1-outline.d.ts +11 -0
- package/dist/esm/pdf-reader/type1-outline.js +63 -8
- package/dist/esm/pdf-reader/vector.d.ts +8 -2
- package/dist/esm/pdf-reader/vector.js +105 -6
- package/dist/esm/word/doc/doc-reader.js +6 -2
- package/dist/esm/word/doc/doc-text.d.ts +6 -0
- package/dist/esm/word/doc/doc-text.js +19 -1
- package/dist/esm/word/document-parser.d.ts +2 -2
- package/dist/esm/word/document-parser.js +11 -1
- package/dist/esm/word/docx-reader.js +5 -3
- package/dist/esm/word/docx-writer.js +341 -54
- package/dist/esm/word/drawing-parser.d.ts +5 -3
- package/dist/esm/word/drawing-parser.js +56 -10
- package/dist/esm/word/font-embed.d.ts +30 -0
- package/dist/esm/word/font-embed.js +173 -0
- package/dist/esm/word/font-table.d.ts +10 -0
- package/dist/esm/word/font-table.js +13 -1
- package/dist/esm/word/index.js +1 -1
- package/dist/esm/word/numbering-parser.d.ts +3 -1
- package/dist/esm/word/numbering-parser.js +2 -1
- package/dist/esm/word/paragraph-properties.d.ts +7 -6
- package/dist/esm/word/paragraph-properties.js +16 -2
- package/dist/esm/word/run-properties.js +26 -0
- package/package.json +12 -5
|
@@ -71,6 +71,16 @@ export declare class Lexer {
|
|
|
71
71
|
* `endstream` when a stream's `/Length` is missing or an unresolved reference.
|
|
72
72
|
*/
|
|
73
73
|
indexOfAscii(needle: string, from: number): number;
|
|
74
|
+
/**
|
|
75
|
+
* Where `word` next stands as a keyword of its own — whitespace (or the
|
|
76
|
+
* start) before it, whitespace, a delimiter or the end after it — rather
|
|
77
|
+
* than as two bytes inside something else.
|
|
78
|
+
*
|
|
79
|
+
* @param word The keyword.
|
|
80
|
+
* @param from Where to start looking.
|
|
81
|
+
* @returns Its offset, or −1 where it does not stand anywhere after `from`.
|
|
82
|
+
*/
|
|
83
|
+
indexOfKeyword(word: string, from: number): number;
|
|
74
84
|
/**
|
|
75
85
|
* §7.3.8.1 — read a stream's raw bytes. `pos` must sit right after the `stream`
|
|
76
86
|
* keyword. The keyword is followed by CRLF (or a lone LF); the data then runs
|
|
@@ -252,6 +252,23 @@ var Lexer = class {
|
|
|
252
252
|
return -1;
|
|
253
253
|
}
|
|
254
254
|
/**
|
|
255
|
+
* Where `word` next stands as a keyword of its own — whitespace (or the
|
|
256
|
+
* start) before it, whitespace, a delimiter or the end after it — rather
|
|
257
|
+
* than as two bytes inside something else.
|
|
258
|
+
*
|
|
259
|
+
* @param word The keyword.
|
|
260
|
+
* @param from Where to start looking.
|
|
261
|
+
* @returns Its offset, or −1 where it does not stand anywhere after `from`.
|
|
262
|
+
*/
|
|
263
|
+
indexOfKeyword(word, from) {
|
|
264
|
+
for (let at = this.indexOfAscii(word, from); at >= 0; at = this.indexOfAscii(word, at + 1)) {
|
|
265
|
+
const before = this.byteAt(at - 1);
|
|
266
|
+
const after = this.byteAt(at + word.length);
|
|
267
|
+
if ((at === from || before < 0 || isWhitespace(before)) && (after < 0 || isWhitespace(after) || isDelimiter(after))) return at;
|
|
268
|
+
}
|
|
269
|
+
return -1;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
255
272
|
* §7.3.8.1 — read a stream's raw bytes. `pos` must sit right after the `stream`
|
|
256
273
|
* keyword. The keyword is followed by CRLF (or a lone LF); the data then runs
|
|
257
274
|
* for `length` bytes, or — when the length is unknown — up to `endstream`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NumberingFormat } from '../core/document-model/index.js';
|
|
2
|
+
/** A page's number as its band prints it. */
|
|
3
|
+
export interface PageNumber {
|
|
4
|
+
/** The numeral as the page shows it: "iv", "47". */
|
|
5
|
+
readonly text: string;
|
|
6
|
+
readonly value: number;
|
|
7
|
+
readonly format: NumberingFormat;
|
|
8
|
+
}
|
|
9
|
+
/** A stretch of pages counted in one sequence (§17.6.12). */
|
|
10
|
+
export interface NumberingRun {
|
|
11
|
+
/** The first page of the stretch. */
|
|
12
|
+
readonly from: number;
|
|
13
|
+
readonly format: NumberingFormat;
|
|
14
|
+
/** The number the stretch's first page carries. */
|
|
15
|
+
readonly start: number;
|
|
16
|
+
}
|
|
17
|
+
/** What the pages' bands say about their numbering. */
|
|
18
|
+
export interface PageNumbering {
|
|
19
|
+
/** Each page's number, where its band shows one. */
|
|
20
|
+
readonly numbers: ReadonlyArray<PageNumber | undefined>;
|
|
21
|
+
/** The sequences the pages are counted in, first page first. */
|
|
22
|
+
readonly runs: ReadonlyArray<NumberingRun>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The numerals a band's text holds, in order.
|
|
26
|
+
*
|
|
27
|
+
* @param text The band's text on one page.
|
|
28
|
+
* @returns Its numerals; a roman one only where it is a numeral as written.
|
|
29
|
+
*/
|
|
30
|
+
export declare function numeralsIn(text: string): Array<PageNumber>;
|
|
31
|
+
/**
|
|
32
|
+
* The page numbers a document's running band prints, and the sequences they
|
|
33
|
+
* run in.
|
|
34
|
+
*
|
|
35
|
+
* The number is the numeral that CHANGES from page to page: "Page 3 of 10" is
|
|
36
|
+
* counted by its first, "Chapter I — 47" by its last. A sequence goes on while
|
|
37
|
+
* each page's number is one more than the last in the same numerals, and
|
|
38
|
+
* starts again where it does not — i, ii, iii and then 1 are two sequences.
|
|
39
|
+
* A page whose band shows no number is counted on.
|
|
40
|
+
*
|
|
41
|
+
* @param bands Each page's band text, or undefined where the page has none.
|
|
42
|
+
* @returns The numbering, or undefined where the bands show no page number
|
|
43
|
+
* worth reading as one.
|
|
44
|
+
*/
|
|
45
|
+
export declare function pageNumberingOf(bands: ReadonlyArray<string | undefined>): PageNumbering | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* The sequence a page is counted in.
|
|
48
|
+
*
|
|
49
|
+
* @param runs The document's sequences, first page first.
|
|
50
|
+
* @param page The page.
|
|
51
|
+
* @returns The run that page belongs to.
|
|
52
|
+
*/
|
|
53
|
+
export declare function runOf(runs: ReadonlyArray<NumberingRun>, page: number): NumberingRun | undefined;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//#region src/pdf-reader/page-numbers.ts
|
|
2
|
+
/** A numeral standing as a word of its own: digits, or roman in one case. */
|
|
3
|
+
var NUMERAL = /(?<=^|\s)(\d{1,4}|[ivxlcdm]{1,9}|[IVXLCDM]{1,9})(?=\s|$)/gu;
|
|
4
|
+
/**
|
|
5
|
+
* More sequences than this and the "numbers" are something else that changes
|
|
6
|
+
* from page to page — a date, a figure count — and the pages are left counted
|
|
7
|
+
* the ordinary way.
|
|
8
|
+
*/
|
|
9
|
+
var MOST_RUNS = 4;
|
|
10
|
+
/**
|
|
11
|
+
* …unless the sequences run long: a book leaves its blank pages out and skips
|
|
12
|
+
* their numbers, and freeculture.pdf's count jumps two at five of its chapters
|
|
13
|
+
* — seven sequences over three hundred and thirty-five numbered pages, and
|
|
14
|
+
* read as none its footer printed no number at all. A date or a figure count
|
|
15
|
+
* starts a sequence of its own on nearly every page.
|
|
16
|
+
*/
|
|
17
|
+
var LEAST_RUN_PAGES = 10;
|
|
18
|
+
/**
|
|
19
|
+
* The numerals a band's text holds, in order.
|
|
20
|
+
*
|
|
21
|
+
* @param text The band's text on one page.
|
|
22
|
+
* @returns Its numerals; a roman one only where it is a numeral as written.
|
|
23
|
+
*/
|
|
24
|
+
function numeralsIn(text) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const m of text.matchAll(NUMERAL)) {
|
|
27
|
+
const token = m[1];
|
|
28
|
+
if (/^\d+$/u.test(token)) {
|
|
29
|
+
out.push({
|
|
30
|
+
text: token,
|
|
31
|
+
value: Number(token),
|
|
32
|
+
format: "decimal"
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const value = romanValue(token);
|
|
37
|
+
if (value === void 0) continue;
|
|
38
|
+
out.push({
|
|
39
|
+
text: token,
|
|
40
|
+
value,
|
|
41
|
+
format: token === token.toLowerCase() ? "lowerRoman" : "upperRoman"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A roman numeral's value, where the letters ARE one as a numeral is written
|
|
48
|
+
* — "iv", "xiv", "MCMXC" — and not merely letters a numeral uses: "ic", "vx"
|
|
49
|
+
* and "mix" are words or nothing.
|
|
50
|
+
*/
|
|
51
|
+
function romanValue(token) {
|
|
52
|
+
const upper = token.toUpperCase();
|
|
53
|
+
const worth = {
|
|
54
|
+
I: 1,
|
|
55
|
+
V: 5,
|
|
56
|
+
X: 10,
|
|
57
|
+
L: 50,
|
|
58
|
+
C: 100,
|
|
59
|
+
D: 500,
|
|
60
|
+
M: 1e3
|
|
61
|
+
};
|
|
62
|
+
let value = 0;
|
|
63
|
+
for (let i = 0; i < upper.length; i++) {
|
|
64
|
+
const here = worth[upper[i]];
|
|
65
|
+
const next = worth[upper[i + 1] ?? ""] ?? 0;
|
|
66
|
+
value += here < next ? -here : here;
|
|
67
|
+
}
|
|
68
|
+
return value > 0 && value < 4e3 && toRoman(value) === upper ? value : void 0;
|
|
69
|
+
}
|
|
70
|
+
/** The canonical roman numeral for a value, which is how a valid one reads. */
|
|
71
|
+
function toRoman(value) {
|
|
72
|
+
const steps = [
|
|
73
|
+
[1e3, "M"],
|
|
74
|
+
[900, "CM"],
|
|
75
|
+
[500, "D"],
|
|
76
|
+
[400, "CD"],
|
|
77
|
+
[100, "C"],
|
|
78
|
+
[90, "XC"],
|
|
79
|
+
[50, "L"],
|
|
80
|
+
[40, "XL"],
|
|
81
|
+
[10, "X"],
|
|
82
|
+
[9, "IX"],
|
|
83
|
+
[5, "V"],
|
|
84
|
+
[4, "IV"],
|
|
85
|
+
[1, "I"]
|
|
86
|
+
];
|
|
87
|
+
let out = "";
|
|
88
|
+
let left = value;
|
|
89
|
+
for (const [n, s] of steps) while (left >= n) {
|
|
90
|
+
out += s;
|
|
91
|
+
left -= n;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The page numbers a document's running band prints, and the sequences they
|
|
97
|
+
* run in.
|
|
98
|
+
*
|
|
99
|
+
* The number is the numeral that CHANGES from page to page: "Page 3 of 10" is
|
|
100
|
+
* counted by its first, "Chapter I — 47" by its last. A sequence goes on while
|
|
101
|
+
* each page's number is one more than the last in the same numerals, and
|
|
102
|
+
* starts again where it does not — i, ii, iii and then 1 are two sequences.
|
|
103
|
+
* A page whose band shows no number is counted on.
|
|
104
|
+
*
|
|
105
|
+
* @param bands Each page's band text, or undefined where the page has none.
|
|
106
|
+
* @returns The numbering, or undefined where the bands show no page number
|
|
107
|
+
* worth reading as one.
|
|
108
|
+
*/
|
|
109
|
+
function pageNumberingOf(bands) {
|
|
110
|
+
const found = bands.map((text) => text === void 0 ? [] : numeralsIn(text));
|
|
111
|
+
const counted = found.filter((n) => n.length > 0);
|
|
112
|
+
if (counted.length < 2) return void 0;
|
|
113
|
+
const width = Math.min(...counted.map((n) => n.length));
|
|
114
|
+
let at = -1;
|
|
115
|
+
for (let k = 0; k < width && at < 0; k++) if (new Set(counted.map((n) => n[k].text)).size > 1) at = k;
|
|
116
|
+
if (at < 0) return void 0;
|
|
117
|
+
const numbers = found.map((n) => n[at]);
|
|
118
|
+
const runs = [];
|
|
119
|
+
numbers.forEach((number, page) => {
|
|
120
|
+
if (number === void 0) return;
|
|
121
|
+
const run = runs[runs.length - 1];
|
|
122
|
+
if (run !== void 0) {
|
|
123
|
+
const expected = run.start + (page - run.from);
|
|
124
|
+
if (number.format === run.format && number.value === expected) return;
|
|
125
|
+
runs.push({
|
|
126
|
+
from: page,
|
|
127
|
+
format: number.format,
|
|
128
|
+
start: number.value
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const start = number.value - page;
|
|
133
|
+
if (start >= 1) runs.push({
|
|
134
|
+
from: 0,
|
|
135
|
+
format: number.format,
|
|
136
|
+
start
|
|
137
|
+
});
|
|
138
|
+
else runs.push({
|
|
139
|
+
from: 0,
|
|
140
|
+
format: "decimal",
|
|
141
|
+
start: 1
|
|
142
|
+
}, {
|
|
143
|
+
from: page,
|
|
144
|
+
format: number.format,
|
|
145
|
+
start: number.value
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
if (runs.length === 0 || runs.length > Math.max(MOST_RUNS, counted.length / LEAST_RUN_PAGES)) return;
|
|
149
|
+
return {
|
|
150
|
+
numbers,
|
|
151
|
+
runs
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The sequence a page is counted in.
|
|
156
|
+
*
|
|
157
|
+
* @param runs The document's sequences, first page first.
|
|
158
|
+
* @param page The page.
|
|
159
|
+
* @returns The run that page belongs to.
|
|
160
|
+
*/
|
|
161
|
+
function runOf(runs, page) {
|
|
162
|
+
let found;
|
|
163
|
+
for (const run of runs) if (run.from <= page) found = run;
|
|
164
|
+
return found;
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { pageNumberingOf, runOf };
|
|
@@ -12,6 +12,16 @@ export interface PatternTint {
|
|
|
12
12
|
* @param file The owning file.
|
|
13
13
|
* @param resources The resources the pattern is named in.
|
|
14
14
|
* @param name The pattern's resource name.
|
|
15
|
+
* @param paint §8.7.3.3 — the colour an UNCOLOURED pattern (`/PaintType
|
|
16
|
+
* 2`) is painted in, which the page gives with `scn` and its
|
|
17
|
+
* own content may not state.
|
|
15
18
|
* @returns Its colour and coverage, or `undefined` when neither can be told.
|
|
16
19
|
*/
|
|
17
|
-
export declare function patternTint(file: PdfFile, resources: PdfDict | undefined, name: string): PatternTint | undefined;
|
|
20
|
+
export declare function patternTint(file: PdfFile, resources: PdfDict | undefined, name: string, paint?: string): PatternTint | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* A colour laid over white paper at `coverage` strength, as a 6-hex string.
|
|
23
|
+
*
|
|
24
|
+
* @param colorHex The colour at full strength.
|
|
25
|
+
* @param coverage How much of the paper it covers, `0..1`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function tintedHex(colorHex: string, coverage: number): string;
|
|
@@ -12,9 +12,12 @@ var MAX_DEPTH = 4;
|
|
|
12
12
|
* @param file The owning file.
|
|
13
13
|
* @param resources The resources the pattern is named in.
|
|
14
14
|
* @param name The pattern's resource name.
|
|
15
|
+
* @param paint §8.7.3.3 — the colour an UNCOLOURED pattern (`/PaintType
|
|
16
|
+
* 2`) is painted in, which the page gives with `scn` and its
|
|
17
|
+
* own content may not state.
|
|
15
18
|
* @returns Its colour and coverage, or `undefined` when neither can be told.
|
|
16
19
|
*/
|
|
17
|
-
function patternTint(file, resources, name) {
|
|
20
|
+
function patternTint(file, resources, name, paint) {
|
|
18
21
|
if (!resources) return void 0;
|
|
19
22
|
const patterns = file.get(resources, "Pattern");
|
|
20
23
|
if (!(patterns instanceof Map)) return void 0;
|
|
@@ -25,13 +28,28 @@ function patternTint(file, resources, name) {
|
|
|
25
28
|
const marks = [];
|
|
26
29
|
const colours = [];
|
|
27
30
|
collect(file, stream, resources, marks, colours, 0, /* @__PURE__ */ new Set());
|
|
28
|
-
const colorHex = colours[0];
|
|
31
|
+
const colorHex = file.get(stream.dict, "PaintType") === 2 ? paint : colours[0] ?? paint;
|
|
29
32
|
if (colorHex === void 0) return void 0;
|
|
30
33
|
return {
|
|
31
34
|
colorHex,
|
|
32
35
|
coverage: sample(marks, cell)
|
|
33
36
|
};
|
|
34
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A colour laid over white paper at `coverage` strength, as a 6-hex string.
|
|
40
|
+
*
|
|
41
|
+
* @param colorHex The colour at full strength.
|
|
42
|
+
* @param coverage How much of the paper it covers, `0..1`.
|
|
43
|
+
*/
|
|
44
|
+
function tintedHex(colorHex, coverage) {
|
|
45
|
+
const k = Math.min(1, Math.max(0, coverage));
|
|
46
|
+
if (k >= 1) return colorHex;
|
|
47
|
+
const channel = (at) => {
|
|
48
|
+
const c = Number.parseInt(colorHex.slice(at, at + 2), 16);
|
|
49
|
+
return Math.round(255 - (255 - (Number.isFinite(c) ? c : 0)) * k).toString(16).toUpperCase().padStart(2, "0");
|
|
50
|
+
};
|
|
51
|
+
return `${channel(0)}${channel(2)}${channel(4)}`;
|
|
52
|
+
}
|
|
35
53
|
/** The cell a tile repeats on: `/XStep` × `/YStep` from the `/BBox`'s corner. */
|
|
36
54
|
function cellOf(file, dict) {
|
|
37
55
|
const bbox = numbers(file, dict.get("BBox"));
|
|
@@ -178,4 +196,4 @@ function asNumber(v) {
|
|
|
178
196
|
return typeof v === "number" ? v : NaN;
|
|
179
197
|
}
|
|
180
198
|
//#endregion
|
|
181
|
-
export { patternTint };
|
|
199
|
+
export { patternTint, tintedHex };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TextRun } from './content.js';
|
|
2
|
+
/** A band of blocks side by side, each a column of its own. */
|
|
3
|
+
export interface SideBySide {
|
|
4
|
+
readonly kind: 'side';
|
|
5
|
+
readonly cells: ReadonlyArray<{
|
|
6
|
+
readonly runs: ReadonlyArray<TextRun>;
|
|
7
|
+
/** Where the cell's own ink starts and ends across the page. */
|
|
8
|
+
readonly from: number;
|
|
9
|
+
readonly to: number;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
/** A stretch of the column read line by line, straight down. */
|
|
13
|
+
export interface Flowing {
|
|
14
|
+
readonly kind: 'flow';
|
|
15
|
+
readonly runs: ReadonlyArray<TextRun>;
|
|
16
|
+
}
|
|
17
|
+
export type Region = Flowing | SideBySide;
|
|
18
|
+
/**
|
|
19
|
+
* The column's runs cut into its regions, top of the page first.
|
|
20
|
+
*
|
|
21
|
+
* @param runs The column's runs.
|
|
22
|
+
* @returns The regions, in reading order; one `flow` region where the column
|
|
23
|
+
* has no side-by-side band.
|
|
24
|
+
*/
|
|
25
|
+
export declare function regionsOf(runs: ReadonlyArray<TextRun>): Array<Region>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//#region src/pdf-reader/regions.ts
|
|
2
|
+
/**
|
|
3
|
+
* The column's runs cut into its regions, top of the page first.
|
|
4
|
+
*
|
|
5
|
+
* @param runs The column's runs.
|
|
6
|
+
* @returns The regions, in reading order; one `flow` region where the column
|
|
7
|
+
* has no side-by-side band.
|
|
8
|
+
*/
|
|
9
|
+
function regionsOf(runs) {
|
|
10
|
+
const lines = clusterLines(runs);
|
|
11
|
+
const out = [];
|
|
12
|
+
let pending = [];
|
|
13
|
+
const flush = (flow) => {
|
|
14
|
+
if (flow.length > 0) out.push({
|
|
15
|
+
kind: "flow",
|
|
16
|
+
runs: flow.flatMap((l) => l.runs)
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
let floor = 0;
|
|
20
|
+
for (let i = 0; i < lines.length;) {
|
|
21
|
+
const band = bandFrom(lines, i, floor);
|
|
22
|
+
if (band === void 0) {
|
|
23
|
+
pending.push(lines[i]);
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
flush(pending.slice(0, pending.length - (i - band.start)));
|
|
28
|
+
out.push(band.region);
|
|
29
|
+
pending = [];
|
|
30
|
+
i = band.end;
|
|
31
|
+
floor = band.end;
|
|
32
|
+
}
|
|
33
|
+
flush(pending);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function clusterLines(runs) {
|
|
37
|
+
const sorted = [...runs].sort((a, b) => b.y - a.y || a.x - b.x);
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const run of sorted) {
|
|
40
|
+
const last = out[out.length - 1];
|
|
41
|
+
if (last && Math.abs(last.y - run.y) <= Math.max(1, (run.fontSizePt || 10) * .5)) {
|
|
42
|
+
last.runs.push(run);
|
|
43
|
+
last.size = Math.max(last.size, run.fontSizePt || 0);
|
|
44
|
+
} else out.push({
|
|
45
|
+
y: run.y,
|
|
46
|
+
size: run.fontSizePt || 10,
|
|
47
|
+
runs: [run]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/** What a run marks: nothing for a blank or for a glyph that names no character. */
|
|
53
|
+
var inked = (r) => r.text.replaceAll("�", "").trim() !== "";
|
|
54
|
+
/** A line's pieces: its runs cut where a gap only a column leaves opens. */
|
|
55
|
+
function piecesOf(line) {
|
|
56
|
+
const runs = line.runs.filter(inked).sort((a, b) => a.x - b.x);
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const run of runs) {
|
|
59
|
+
const last = out[out.length - 1];
|
|
60
|
+
const size = run.fontSizePt || 10;
|
|
61
|
+
if (last && run.x - last.to < Math.max(size, last.size) * PIECE_GAP_EM) {
|
|
62
|
+
last.to = Math.max(last.to, run.endX);
|
|
63
|
+
last.ys.push(run.y);
|
|
64
|
+
last.size = Math.max(last.size, size);
|
|
65
|
+
} else out.push({
|
|
66
|
+
from: run.x,
|
|
67
|
+
to: run.endX,
|
|
68
|
+
ys: [run.y],
|
|
69
|
+
size
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return out.map((p) => ({
|
|
73
|
+
from: p.from,
|
|
74
|
+
to: p.to,
|
|
75
|
+
y: middle(p.ys),
|
|
76
|
+
size: p.size
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether a line's pieces stand on different baselines — the mark of two
|
|
81
|
+
* blocks set side by side with leading of their own, which the clustering has
|
|
82
|
+
* run together.
|
|
83
|
+
*/
|
|
84
|
+
function misaligned(line) {
|
|
85
|
+
const pieces = piecesOf(line);
|
|
86
|
+
if (pieces.length < 2) return false;
|
|
87
|
+
const ys = pieces.map((p) => p.y);
|
|
88
|
+
const size = Math.min(...pieces.map((p) => p.size));
|
|
89
|
+
return Math.max(...ys) - Math.min(...ys) > Math.max(ALIGN_SLACK_PT, size * ALIGN_SLACK_EM);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The side-by-side band starting at line `i`, where one does: lines around a
|
|
93
|
+
* misaligned one that keep clear of the same gutters, cut into the blocks
|
|
94
|
+
* between them.
|
|
95
|
+
*/
|
|
96
|
+
function bandFrom(lines, i, floor) {
|
|
97
|
+
if (!misaligned(lines[i])) return void 0;
|
|
98
|
+
let from = i;
|
|
99
|
+
let to = i + 1;
|
|
100
|
+
const count = (a, b) => guttersOf(lines.slice(a, b)).length;
|
|
101
|
+
if (count(from, to) === 0) return void 0;
|
|
102
|
+
const near = (a, b) => Math.abs(a.y - b.y) <= Math.max(a.size, b.size) * BAND_REACH_EM;
|
|
103
|
+
while (to < lines.length && near(lines[to - 1], lines[to]) && count(from, to + 1) >= count(from, to)) to++;
|
|
104
|
+
while (from > floor && near(lines[from], lines[from - 1]) && count(from - 1, to) >= count(from, to)) from--;
|
|
105
|
+
const band = lines.slice(from, to);
|
|
106
|
+
const gutters = guttersOf(band);
|
|
107
|
+
if (gutters.length === 0) return void 0;
|
|
108
|
+
const edges = [
|
|
109
|
+
-Infinity,
|
|
110
|
+
...gutters.map((g) => (g.from + g.to) / 2),
|
|
111
|
+
Infinity
|
|
112
|
+
];
|
|
113
|
+
const cells = edges.slice(0, -1).map((left, k) => {
|
|
114
|
+
const right = edges[k + 1];
|
|
115
|
+
const runs = band.flatMap((l) => l.runs.filter((r) => {
|
|
116
|
+
const mid = (r.x + r.endX) / 2;
|
|
117
|
+
return mid >= left && mid < right;
|
|
118
|
+
}));
|
|
119
|
+
const ink = runs.filter(inked);
|
|
120
|
+
return {
|
|
121
|
+
runs,
|
|
122
|
+
from: Math.min(...ink.map((r) => r.x)),
|
|
123
|
+
to: Math.max(...ink.map((r) => r.endX))
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
if (cells.filter((c) => c.runs.some(inked)).length < 2) return void 0;
|
|
127
|
+
return {
|
|
128
|
+
region: {
|
|
129
|
+
kind: "side",
|
|
130
|
+
cells: cells.filter((c) => c.runs.some(inked))
|
|
131
|
+
},
|
|
132
|
+
start: from,
|
|
133
|
+
end: to
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** The strips across a band that no line's ink crosses, and that are wide enough to be gutters. */
|
|
137
|
+
function guttersOf(band) {
|
|
138
|
+
const spans = band.flatMap((l) => piecesOf(l)).map((p) => [p.from, p.to]).sort((a, b) => a[0] - b[0]);
|
|
139
|
+
const size = middle(band.map((l) => l.size));
|
|
140
|
+
const out = [];
|
|
141
|
+
let reach = spans[0]?.[1] ?? 0;
|
|
142
|
+
for (const [from, to] of spans.slice(1)) {
|
|
143
|
+
if (from - reach >= size * PIECE_GAP_EM) out.push({
|
|
144
|
+
from: reach,
|
|
145
|
+
to: from
|
|
146
|
+
});
|
|
147
|
+
reach = Math.max(reach, to);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function middle(values) {
|
|
152
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
153
|
+
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
154
|
+
}
|
|
155
|
+
/** A gap this wide, in ems, is one a column leaves, not a word space or a tab within a line. */
|
|
156
|
+
var PIECE_GAP_EM = 1.5;
|
|
157
|
+
/**
|
|
158
|
+
* How far apart two pieces' baselines may stand and still be one line: past a
|
|
159
|
+
* point and a half — or a sixth of the size — the two were set with leading of
|
|
160
|
+
* their own, not on one line with a rounding in it.
|
|
161
|
+
*/
|
|
162
|
+
var ALIGN_SLACK_PT = 1.5;
|
|
163
|
+
var ALIGN_SLACK_EM = 1 / 6;
|
|
164
|
+
/** How far, in ems, a line may stand from the band and still belong to it. */
|
|
165
|
+
var BAND_REACH_EM = 2.5;
|
|
166
|
+
//#endregion
|
|
167
|
+
export { regionsOf };
|
|
@@ -4,6 +4,7 @@ import { PdfFunction } from './function.js';
|
|
|
4
4
|
import { ShapeGradient } from '../core/vector.js';
|
|
5
5
|
import { PdfDict } from '../pdf/objects.js';
|
|
6
6
|
import { PdfFile } from './document.js';
|
|
7
|
+
import { Matrix } from './content.js';
|
|
7
8
|
/**
|
|
8
9
|
* Resolve a page's `/Pattern` resources into gradient fills (E-PDF EP16c, ISO
|
|
9
10
|
* 32000-1 §8.7.4.5). Every `PatternType` 2 (shading) pattern is evaluated — its
|
|
@@ -17,7 +18,45 @@ import { PdfFile } from './document.js';
|
|
|
17
18
|
* annotation appearance's own.
|
|
18
19
|
* @returns A map from pattern resource name to its {@link ShapeGradient}.
|
|
19
20
|
*/
|
|
20
|
-
export declare function buildShadingMap(file: PdfFile, resources: PdfDict | undefined): Map<string,
|
|
21
|
+
export declare function buildShadingMap(file: PdfFile, resources: PdfDict | undefined): Map<string, PageGradient>;
|
|
22
|
+
/**
|
|
23
|
+
* §8.7.4.5.3 — where an axial shading's axis runs on the page, y up: its
|
|
24
|
+
* first stop at (x0, y0) and its last at (x1, y1).
|
|
25
|
+
*/
|
|
26
|
+
export interface GradientAxis {
|
|
27
|
+
readonly x0: number;
|
|
28
|
+
readonly y0: number;
|
|
29
|
+
readonly x1: number;
|
|
30
|
+
readonly y1: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A gradient as the page states it: the model's, and where its axis runs. A
|
|
34
|
+
* shape shows the part of the axis its box covers, and the model's gradient
|
|
35
|
+
* spans the box — see {@link gradientOverBox}.
|
|
36
|
+
*/
|
|
37
|
+
export type PageGradient = ShapeGradient & {
|
|
38
|
+
readonly axis?: GradientAxis;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* A page's gradient as a shape filled with it shows it: its stops re-mapped
|
|
42
|
+
* from the whole axis to the part of it the shape's box covers.
|
|
43
|
+
*
|
|
44
|
+
* A DrawingML gradient spans the shape it fills, and a PDF shading spans its
|
|
45
|
+
* own axis, which may reach far past the shape: issue10572.pdf's pattern
|
|
46
|
+
* runs twenty-four stripes down 1800 points, of which its 450-point square
|
|
47
|
+
* shows six — spread over the square, all twenty-four came back as hairlines.
|
|
48
|
+
* Beyond either end of the axis the end colour carries on, as `/Extend` asks.
|
|
49
|
+
*
|
|
50
|
+
* @param g The gradient, with its axis where the page states one.
|
|
51
|
+
* @param box The shape's box, in the same space as the axis.
|
|
52
|
+
* @returns The gradient as the shape shows it, with no axis.
|
|
53
|
+
*/
|
|
54
|
+
export declare function gradientOverBox(g: PageGradient, box: {
|
|
55
|
+
readonly minX: number;
|
|
56
|
+
readonly minY: number;
|
|
57
|
+
readonly maxX: number;
|
|
58
|
+
readonly maxY: number;
|
|
59
|
+
}): ShapeGradient;
|
|
21
60
|
/**
|
|
22
61
|
* §8.7.4.5.2 — an axial or radial shading as a gradient, for a bare `sh`.
|
|
23
62
|
*
|
|
@@ -29,7 +68,7 @@ export declare function buildShadingMap(file: PdfFile, resources: PdfDict | unde
|
|
|
29
68
|
* @param sh The shading dictionary.
|
|
30
69
|
* @returns The gradient, or `undefined` for a type this does not read.
|
|
31
70
|
*/
|
|
32
|
-
export declare function gradientShading(file: PdfFile, sh: PdfDict):
|
|
71
|
+
export declare function gradientShading(file: PdfFile, sh: PdfDict, ctm?: Matrix): PageGradient | undefined;
|
|
33
72
|
/**
|
|
34
73
|
* §8.7.4.5 — which kind of shading this is, as the file states it.
|
|
35
74
|
*
|
|
@@ -128,7 +167,24 @@ export interface GsPaint {
|
|
|
128
167
|
* `false` where the state names `/None`, which takes a mask off.
|
|
129
168
|
*/
|
|
130
169
|
readonly masked?: boolean;
|
|
170
|
+
/** §8.4.5 `/LW` — the line width, in user space. */
|
|
171
|
+
readonly lineWidth?: number;
|
|
172
|
+
/** §8.4.5 `/D` — the dash pattern's lengths, in user space; empty is solid. */
|
|
173
|
+
readonly dash?: ReadonlyArray<number>;
|
|
174
|
+
/** §8.4.5 `/LC` — the line cap: 0 butt, 1 round, 2 projecting square. */
|
|
175
|
+
readonly lineCap?: number;
|
|
176
|
+
/** §11.6.4.4 `/CA` — the constant STROKE alpha, where the state names one. */
|
|
177
|
+
readonly strokeAlpha?: number;
|
|
131
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* §8.4.3.6 — a dash array as a stroke can use it: lengths that are numbers and
|
|
181
|
+
* not negative, and none at all where every one is zero, which draws nothing
|
|
182
|
+
* and is solid by the spec's own reading.
|
|
183
|
+
*
|
|
184
|
+
* @param value The array, as the file states it.
|
|
185
|
+
* @returns The lengths, or an empty array for a solid line.
|
|
186
|
+
*/
|
|
187
|
+
export declare function dashLengths(value: unknown): ReadonlyArray<number>;
|
|
132
188
|
/**
|
|
133
189
|
* §8.6 — how many components a colour space takes, and what they mean.
|
|
134
190
|
*
|