reamkit 1.27.0 → 1.28.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 +0 -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 +5 -2
- package/dist/esm/pdf-reader/content.d.ts +76 -0
- package/dist/esm/pdf-reader/content.js +142 -59
- 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.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 +9 -3
- package/dist/esm/pdf-reader/flow-build.js +13 -7
- 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 +1148 -45
- 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/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.js +43 -4
- package/dist/esm/word/document-parser.js +4 -1
- package/dist/esm/word/docx-writer.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
|
|
2
|
+
import { cieToSrgb } from "./cie-color.js";
|
|
3
|
+
import { iccTransform } from "./icc.js";
|
|
2
4
|
import { readFunction } from "./function.js";
|
|
3
5
|
//#region src/pdf-reader/shading.ts
|
|
4
6
|
/**
|
|
@@ -29,10 +31,34 @@ function buildShadingMap(file, resources) {
|
|
|
29
31
|
}
|
|
30
32
|
return out;
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* §8.7.4.5.2 — an axial or radial shading as a gradient, for a bare `sh`.
|
|
36
|
+
*
|
|
37
|
+
* A `sh` paints the CLIP rather than a path, so what it needs is not a fill for
|
|
38
|
+
* a shape the page drew but the gradient itself, to fill the region with. It is
|
|
39
|
+
* the same reading `buildShadingMap` does for a pattern.
|
|
40
|
+
*
|
|
41
|
+
* @param file The owning file.
|
|
42
|
+
* @param sh The shading dictionary.
|
|
43
|
+
* @returns The gradient, or `undefined` for a type this does not read.
|
|
44
|
+
*/
|
|
45
|
+
function gradientShading(file, sh) {
|
|
46
|
+
return parseShading(file, sh);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* §8.7.4.5 — which kind of shading this is, as the file states it.
|
|
50
|
+
*
|
|
51
|
+
* @param file The owning file.
|
|
52
|
+
* @param sh The shading dictionary.
|
|
53
|
+
* @returns Its `/ShadingType`, or 0 where the file states none.
|
|
54
|
+
*/
|
|
55
|
+
function shadingTypeOf(file, sh) {
|
|
56
|
+
return numOf(file.get(sh, "ShadingType"));
|
|
57
|
+
}
|
|
32
58
|
function parseShading(file, sh) {
|
|
33
59
|
const type = numOf(file.get(sh, "ShadingType"));
|
|
34
60
|
if (type !== 2 && type !== 3) return void 0;
|
|
35
|
-
const stops = parseFunction(file, sh.get("Function"));
|
|
61
|
+
const stops = parseFunction(file, sh.get("Function"), shadingSpace(file, sh));
|
|
36
62
|
if (!stops || stops.length === 0) return void 0;
|
|
37
63
|
if (type === 3) return {
|
|
38
64
|
kind: "radial",
|
|
@@ -45,14 +71,14 @@ function parseShading(file, sh) {
|
|
|
45
71
|
stops
|
|
46
72
|
};
|
|
47
73
|
}
|
|
48
|
-
function parseFunction(file, value) {
|
|
74
|
+
function parseFunction(file, value, space) {
|
|
49
75
|
const resolved = value !== void 0 ? file.resolve(value) : void 0;
|
|
50
76
|
const dict = dictOf(resolved);
|
|
51
77
|
if (!dict) return void 0;
|
|
52
78
|
const type = numOf(file.get(dict, "FunctionType"));
|
|
53
79
|
if (type === 2) {
|
|
54
|
-
const c0 = colorOf(numArray(file, dict.get("C0")) ?? [0]);
|
|
55
|
-
const c1 = colorOf(numArray(file, dict.get("C1")) ?? [1]);
|
|
80
|
+
const c0 = colorOf(numArray(file, dict.get("C0")) ?? [0], space);
|
|
81
|
+
const c1 = colorOf(numArray(file, dict.get("C1")) ?? [1], space);
|
|
56
82
|
return [{
|
|
57
83
|
offset: 0,
|
|
58
84
|
colorHex: c0
|
|
@@ -77,7 +103,7 @@ function parseFunction(file, value) {
|
|
|
77
103
|
const stops = [];
|
|
78
104
|
const span = d1 - d0 || 1;
|
|
79
105
|
for (let i = 0; i < subs.length; i++) {
|
|
80
|
-
const sub = parseFunction(file, subs[i]);
|
|
106
|
+
const sub = parseFunction(file, subs[i], space);
|
|
81
107
|
if (!sub || sub.length === 0) continue;
|
|
82
108
|
const lo = ((edges[i] ?? d0) - d0) / span;
|
|
83
109
|
const hi = ((edges[i + 1] ?? d1) - d0) / span;
|
|
@@ -85,7 +111,7 @@ function parseFunction(file, value) {
|
|
|
85
111
|
}
|
|
86
112
|
return stops.length > 0 ? stops : void 0;
|
|
87
113
|
}
|
|
88
|
-
if (type === 0 && resolved instanceof PdfStream) return sampleFunction(file, resolved);
|
|
114
|
+
if (type === 0 && resolved instanceof PdfStream) return sampleFunction(file, resolved, space);
|
|
89
115
|
if (type === 4) {
|
|
90
116
|
const fn = readFunction(file, resolved);
|
|
91
117
|
if (!fn) return void 0;
|
|
@@ -95,14 +121,82 @@ function parseFunction(file, value) {
|
|
|
95
121
|
const stops = [];
|
|
96
122
|
for (let s = 0; s < PS_STOPS; s++) {
|
|
97
123
|
const off = s / (PS_STOPS - 1);
|
|
98
|
-
pushStop(stops, off, colorOf(fn([d0 + off * (d1 - d0)])));
|
|
124
|
+
pushStop(stops, off, colorOf(fn([d0 + off * (d1 - d0)]), space));
|
|
99
125
|
}
|
|
100
126
|
return stops.length > 0 ? stops : void 0;
|
|
101
127
|
}
|
|
102
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* §8.7.4.5.3 — a FUNCTION-BASED shading, sampled into a picture.
|
|
131
|
+
*
|
|
132
|
+
* Type 1 is not a ramp between two points: it is a function of two variables
|
|
133
|
+
* over a rectangle, and no gradient can stand for one. Painted by a bare `sh`
|
|
134
|
+
* it fills the clip, and nothing here lifted it at all —
|
|
135
|
+
* function_based_shading.pdf is nine such squares and 43% of the page's ink,
|
|
136
|
+
* and reconstructed to a blank sheet.
|
|
137
|
+
*
|
|
138
|
+
* Sampled it is exactly a picture, which every format downstream can show. The
|
|
139
|
+
* grid is fixed: the function is smooth by construction (§8.7.4.5.3 gives it a
|
|
140
|
+
* `/Domain` and nothing else), so more samples buy nothing a reader can see.
|
|
141
|
+
*
|
|
142
|
+
* @param file The owning file.
|
|
143
|
+
* @param shading The shading dictionary.
|
|
144
|
+
* @returns The picture and the domain it covers, or `undefined` for a shading
|
|
145
|
+
* of another type or one whose function cannot be run.
|
|
146
|
+
*/
|
|
147
|
+
function sampledShading(file, shading) {
|
|
148
|
+
if (numOf(file.get(shading, "ShadingType")) !== 1) return void 0;
|
|
149
|
+
const space = shadingSpace(file, shading);
|
|
150
|
+
const fn = readFunction(file, shading.get("Function"));
|
|
151
|
+
if (!fn) return void 0;
|
|
152
|
+
const d = numArray(file, shading.get("Domain")) ?? [
|
|
153
|
+
0,
|
|
154
|
+
1,
|
|
155
|
+
0,
|
|
156
|
+
1
|
|
157
|
+
];
|
|
158
|
+
if (d.length < 4) return void 0;
|
|
159
|
+
const domain = [
|
|
160
|
+
d[0],
|
|
161
|
+
d[1],
|
|
162
|
+
d[2],
|
|
163
|
+
d[3]
|
|
164
|
+
];
|
|
165
|
+
const size = SAMPLED_SIDE;
|
|
166
|
+
const rgb = new Uint8Array(size * size * 3);
|
|
167
|
+
for (let row = 0; row < size; row++) {
|
|
168
|
+
const y = domain[3] - (row + .5) / size * (domain[3] - domain[2]);
|
|
169
|
+
for (let col = 0; col < size; col++) {
|
|
170
|
+
const hex = colorOf(fn([domain[0] + (col + .5) / size * (domain[1] - domain[0]), y]), space);
|
|
171
|
+
const at = (row * size + col) * 3;
|
|
172
|
+
rgb[at] = Number.parseInt(hex.slice(0, 2), 16);
|
|
173
|
+
rgb[at + 1] = Number.parseInt(hex.slice(2, 4), 16);
|
|
174
|
+
rgb[at + 2] = Number.parseInt(hex.slice(4, 6), 16);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
rgb,
|
|
179
|
+
size,
|
|
180
|
+
domain
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* §8.7.4.5 — the colour space a shading's function lands in.
|
|
185
|
+
*
|
|
186
|
+
* The components alone do not say: a `/Separation` function gives ONE number
|
|
187
|
+
* and that number is a strength of ink, not a grey level.
|
|
188
|
+
* function_based_shading_cmyk.pdf's third square is a spot colour whose full
|
|
189
|
+
* tint is a warm red, and read as grey it came back a black-to-white ramp.
|
|
190
|
+
*/
|
|
191
|
+
function shadingSpace(file, sh) {
|
|
192
|
+
const cs = sh.get("ColorSpace");
|
|
193
|
+
return cs === void 0 ? void 0 : colorSpaceAt(file, file.resolve(cs), 0);
|
|
194
|
+
}
|
|
195
|
+
/** How many samples a side a function-based shading is drawn at. */
|
|
196
|
+
var SAMPLED_SIDE = 128;
|
|
103
197
|
/** How many places a type-4 gradient is sampled at along its domain. */
|
|
104
198
|
var PS_STOPS = 16;
|
|
105
|
-
function sampleFunction(file, stream) {
|
|
199
|
+
function sampleFunction(file, stream, space) {
|
|
106
200
|
const d = stream.dict;
|
|
107
201
|
const size = numArray(file, d.get("Size"));
|
|
108
202
|
const range = numArray(file, d.get("Range"));
|
|
@@ -149,18 +243,63 @@ function numArray(file, v) {
|
|
|
149
243
|
if (!Array.isArray(r)) return void 0;
|
|
150
244
|
return r.map((x) => typeof x === "number" ? x : 0);
|
|
151
245
|
}
|
|
152
|
-
function colorOf(c) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
246
|
+
function colorOf(c, space) {
|
|
247
|
+
return spaceColor(c, space) ?? spaceColor(c, void 0) ?? grayHex(c[0] ?? 0);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* §8.6.8 — the colour a run of `sc` / `scn` components comes to.
|
|
251
|
+
*
|
|
252
|
+
* The space in force decides, and where it was not read the COUNT is the next
|
|
253
|
+
* best witness: three numbers are RGB and four are CMYK on every device space
|
|
254
|
+
* there is. One number is the ambiguous case — grey in a device space, but the
|
|
255
|
+
* strength of a colorant in a Separation, where 1 is the ink at full and reads
|
|
256
|
+
* dark — so a lone component is only taken where the space said what it means.
|
|
257
|
+
*
|
|
258
|
+
* @param nums The components, as the page or a shading's function states them.
|
|
259
|
+
* @param space The space in force, where it was read.
|
|
260
|
+
* @returns The colour as 6 hex digits, or `undefined` where the numbers do not
|
|
261
|
+
* say what colour they are.
|
|
262
|
+
*/
|
|
263
|
+
function spaceColor(nums, space) {
|
|
264
|
+
if (nums.length === 0) return void 0;
|
|
265
|
+
if (space?.icc && nums.length >= space.components) {
|
|
266
|
+
const [r, g, b] = space.icc(nums);
|
|
267
|
+
return rgbHex(r, g, b);
|
|
268
|
+
}
|
|
269
|
+
if (space?.cie) {
|
|
270
|
+
const [r, g, b] = cieToSrgb(space.cie, nums);
|
|
271
|
+
return rgbHex(r, g, b);
|
|
272
|
+
}
|
|
273
|
+
switch (space?.kind ?? (nums.length === 3 ? "rgb" : nums.length === 4 ? "cmyk" : void 0)) {
|
|
274
|
+
case "rgb": return nums.length >= 3 ? rgbHex(nums[0], nums[1], nums[2]) : void 0;
|
|
275
|
+
case "cmyk": return nums.length >= 4 ? cmykHex(nums[0], nums[1], nums[2], nums[3]) : void 0;
|
|
276
|
+
case "gray": return grayHex(nums[0]);
|
|
277
|
+
case "tint":
|
|
278
|
+
if (space?.tint) {
|
|
279
|
+
const hex = spaceColor(space.tint.transform(nums), space.tint.alternate);
|
|
280
|
+
if (hex !== void 0) return hex;
|
|
281
|
+
}
|
|
282
|
+
return grayHex(1 - Math.max(...nums));
|
|
283
|
+
default: return;
|
|
156
284
|
}
|
|
157
|
-
if (c.length === 3) return hex255(c[0] * 255, c[1] * 255, c[2] * 255);
|
|
158
|
-
const g = (c[0] ?? 0) * 255;
|
|
159
|
-
return hex255(g, g, g);
|
|
160
285
|
}
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
286
|
+
function clamp255(v) {
|
|
287
|
+
return Math.max(0, Math.min(255, Math.round(v * 255)));
|
|
288
|
+
}
|
|
289
|
+
function hex2(v) {
|
|
290
|
+
return clamp255(v).toString(16).padStart(2, "0");
|
|
291
|
+
}
|
|
292
|
+
/** Three channels, each 0..1, as 6 upper-case hex digits. */
|
|
293
|
+
function rgbHex(r, g, b) {
|
|
294
|
+
return (hex2(r) + hex2(g) + hex2(b)).toUpperCase();
|
|
295
|
+
}
|
|
296
|
+
/** One grey level, 0..1, as 6 upper-case hex digits. */
|
|
297
|
+
function grayHex(v) {
|
|
298
|
+
return rgbHex(v, v, v);
|
|
299
|
+
}
|
|
300
|
+
/** §8.6.4.4 — four inks, each 0..1, as 6 upper-case hex digits. */
|
|
301
|
+
function cmykHex(c, m, y, k) {
|
|
302
|
+
return rgbHex((1 - c) * (1 - k), (1 - m) * (1 - k), (1 - y) * (1 - k));
|
|
164
303
|
}
|
|
165
304
|
function pushStop(stops, offset, colorHex) {
|
|
166
305
|
const o = Math.max(0, Math.min(1, offset));
|
|
@@ -203,19 +342,23 @@ function buildAlphaMap(file, resources) {
|
|
|
203
342
|
const state = file.resolve(value);
|
|
204
343
|
if (!(state instanceof Map)) continue;
|
|
205
344
|
const ca = file.resolve(state.get("ca") ?? PDF_NULL);
|
|
206
|
-
const alpha = typeof ca === "number" && ca >= 0 && ca
|
|
345
|
+
const alpha = typeof ca === "number" && ca >= 0 && ca <= 1 ? ca : void 0;
|
|
207
346
|
const bm = file.resolve(state.get("BM") ?? PDF_NULL);
|
|
208
347
|
const first = Array.isArray(bm) ? file.resolve(bm[0] ?? PDF_NULL) : bm;
|
|
209
348
|
const mode = first instanceof PdfName ? first.value : void 0;
|
|
210
349
|
const darkens = mode === "Multiply" || mode === "Darken";
|
|
211
350
|
const blend = mode !== void 0 && mode !== "Normal" && mode !== "Compatible" && !darkens ? mode : void 0;
|
|
212
|
-
const
|
|
213
|
-
|
|
351
|
+
const statesMask = state.has("SMask");
|
|
352
|
+
const masked = statesMask ? file.resolve(state.get("SMask") ?? PDF_NULL) instanceof Map : void 0;
|
|
353
|
+
if (alpha === void 0 && mode === void 0 && !statesMask) continue;
|
|
214
354
|
out.set(name, {
|
|
215
355
|
...alpha !== void 0 ? { alpha } : {},
|
|
216
|
-
...
|
|
356
|
+
...mode !== void 0 ? {
|
|
357
|
+
statesBlend: true,
|
|
358
|
+
darkens
|
|
359
|
+
} : {},
|
|
217
360
|
...blend !== void 0 ? { blend } : {},
|
|
218
|
-
...masked ? { masked
|
|
361
|
+
...masked !== void 0 ? { masked } : {}
|
|
219
362
|
});
|
|
220
363
|
}
|
|
221
364
|
return out;
|
|
@@ -249,9 +392,32 @@ function colorSpaceAt(file, cs, depth) {
|
|
|
249
392
|
if (head.value === "ICCBased") {
|
|
250
393
|
const stream = file.resolve(cs[1] ?? PDF_NULL);
|
|
251
394
|
const n = stream instanceof PdfStream ? file.get(stream.dict, "N") : void 0;
|
|
252
|
-
|
|
395
|
+
const base = byFamily("ICCBased", typeof n === "number" ? n : 3);
|
|
396
|
+
if (!base || !(stream instanceof PdfStream)) return base;
|
|
397
|
+
let icc;
|
|
398
|
+
try {
|
|
399
|
+
icc = iccTransform(file.streamData(stream));
|
|
400
|
+
} catch {
|
|
401
|
+
icc = void 0;
|
|
402
|
+
}
|
|
403
|
+
return icc ? {
|
|
404
|
+
...base,
|
|
405
|
+
icc
|
|
406
|
+
} : base;
|
|
253
407
|
}
|
|
254
408
|
if (head.value === "Indexed") return;
|
|
409
|
+
if (head.value === "CalRGB") {
|
|
410
|
+
const params = file.resolve(cs[1] ?? PDF_NULL);
|
|
411
|
+
const cie = params instanceof Map ? cieParams(file, params, true) : void 0;
|
|
412
|
+
const base = {
|
|
413
|
+
kind: "rgb",
|
|
414
|
+
components: 3
|
|
415
|
+
};
|
|
416
|
+
return cie ? {
|
|
417
|
+
...base,
|
|
418
|
+
cie
|
|
419
|
+
} : base;
|
|
420
|
+
}
|
|
255
421
|
if (head.value === "CalGray") {
|
|
256
422
|
const params = file.resolve(cs[1] ?? PDF_NULL);
|
|
257
423
|
const cie = params instanceof Map ? cieParams(file, params, false) : void 0;
|
|
@@ -350,4 +516,4 @@ function byFamily(family, n) {
|
|
|
350
516
|
}
|
|
351
517
|
}
|
|
352
518
|
//#endregion
|
|
353
|
-
export { buildAlphaMap, buildColorSpaceMap, buildShadingMap };
|
|
519
|
+
export { buildAlphaMap, buildColorSpaceMap, buildShadingMap, cmykHex, gradientShading, grayHex, rgbHex, sampledShading, shadingTypeOf, spaceColor };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** §7.4.5 — a byte, then that many literal bytes, or a run of one repeated. */
|
|
2
|
+
export declare function runLengthDecode(data: Uint8Array): Uint8Array;
|
|
3
|
+
/** §7.4.3 — four bytes as five base-85 digits, `z` for four zeroes, `~>` ends. */
|
|
4
|
+
export declare function ascii85Decode(data: Uint8Array): Uint8Array;
|
|
5
|
+
/** §7.4.2 — one byte as two hex digits, `>` ends, an odd last digit is halved. */
|
|
6
|
+
export declare function asciiHexDecode(data: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/pdf-reader/stream-filters.ts
|
|
2
|
+
/** §7.4.5 — a byte, then that many literal bytes, or a run of one repeated. */
|
|
3
|
+
function runLengthDecode(data) {
|
|
4
|
+
const out = [];
|
|
5
|
+
let i = 0;
|
|
6
|
+
while (i < data.length) {
|
|
7
|
+
const len = data[i++];
|
|
8
|
+
if (len === 128) break;
|
|
9
|
+
if (len < 128) for (let j = 0; j <= len && i < data.length; j++) out.push(data[i++]);
|
|
10
|
+
else {
|
|
11
|
+
const b = data[i++] ?? 0;
|
|
12
|
+
for (let j = 0; j < 257 - len; j++) out.push(b);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return Uint8Array.from(out);
|
|
16
|
+
}
|
|
17
|
+
/** §7.4.3 — four bytes as five base-85 digits, `z` for four zeroes, `~>` ends. */
|
|
18
|
+
function ascii85Decode(data) {
|
|
19
|
+
const out = [];
|
|
20
|
+
let tuple = 0;
|
|
21
|
+
let count = 0;
|
|
22
|
+
for (const c of data) {
|
|
23
|
+
if (c === 126) break;
|
|
24
|
+
if (c <= 32) continue;
|
|
25
|
+
if (c === 122 && count === 0) {
|
|
26
|
+
out.push(0, 0, 0, 0);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (c < 33 || c > 117) continue;
|
|
30
|
+
tuple = tuple * 85 + (c - 33);
|
|
31
|
+
if (++count === 5) {
|
|
32
|
+
out.push(tuple >>> 24 & 255, tuple >>> 16 & 255, tuple >>> 8 & 255, tuple & 255);
|
|
33
|
+
tuple = 0;
|
|
34
|
+
count = 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (count > 0) {
|
|
38
|
+
for (let i = count; i < 5; i++) tuple = tuple * 85 + 84;
|
|
39
|
+
for (let i = 0; i < count - 1; i++) out.push(tuple >>> 24 - i * 8 & 255);
|
|
40
|
+
}
|
|
41
|
+
return Uint8Array.from(out);
|
|
42
|
+
}
|
|
43
|
+
/** §7.4.2 — one byte as two hex digits, `>` ends, an odd last digit is halved. */
|
|
44
|
+
function asciiHexDecode(data) {
|
|
45
|
+
const out = [];
|
|
46
|
+
let hi = -1;
|
|
47
|
+
for (const c of data) {
|
|
48
|
+
if (c === 62) break;
|
|
49
|
+
const v = hexVal(c);
|
|
50
|
+
if (v < 0) continue;
|
|
51
|
+
if (hi < 0) hi = v;
|
|
52
|
+
else {
|
|
53
|
+
out.push(hi << 4 | v);
|
|
54
|
+
hi = -1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (hi >= 0) out.push(hi << 4);
|
|
58
|
+
return Uint8Array.from(out);
|
|
59
|
+
}
|
|
60
|
+
function hexVal(b) {
|
|
61
|
+
if (b >= 48 && b <= 57) return b - 48;
|
|
62
|
+
if (b >= 65 && b <= 70) return b - 65 + 10;
|
|
63
|
+
if (b >= 97 && b <= 102) return b - 97 + 10;
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { ascii85Decode, asciiHexDecode, runLengthDecode };
|
|
@@ -33,8 +33,7 @@ var STRIKE_HIGH = .5;
|
|
|
33
33
|
function markDrawnRules(runs, vectors) {
|
|
34
34
|
const marks = /* @__PURE__ */ new Map();
|
|
35
35
|
const consumed = /* @__PURE__ */ new Set();
|
|
36
|
-
for (const v of vectors) {
|
|
37
|
-
if (!isRule(v)) continue;
|
|
36
|
+
for (const { rule: v, parts } of joinRules(vectors)) {
|
|
38
37
|
const under = coveredRuns(runs, v, "under");
|
|
39
38
|
const through = under.length > 0 ? [] : coveredRuns(runs, v, "through");
|
|
40
39
|
const hit = under.length > 0 ? under : through;
|
|
@@ -45,15 +44,16 @@ function markDrawnRules(runs, vectors) {
|
|
|
45
44
|
if (left - v.minX > OVERHANG_PT || v.maxX - right > OVERHANG_PT) continue;
|
|
46
45
|
for (const run of hit) {
|
|
47
46
|
const had = marks.get(run) ?? {};
|
|
48
|
-
|
|
47
|
+
const hex = ruleHex(v);
|
|
48
|
+
marks.set(run, under.length > 0 && hex !== void 0 ? {
|
|
49
49
|
...had,
|
|
50
|
-
underline:
|
|
50
|
+
underline: hex
|
|
51
51
|
} : {
|
|
52
52
|
...had,
|
|
53
53
|
strike: true
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
|
-
consumed.add(
|
|
56
|
+
for (const part of parts) consumed.add(part);
|
|
57
57
|
}
|
|
58
58
|
if (marks.size === 0) return {
|
|
59
59
|
runs: [...runs],
|
|
@@ -78,13 +78,86 @@ function markDrawnRules(runs, vectors) {
|
|
|
78
78
|
consumed
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
81
|
+
/** Two segments this far apart are still one line: a word space, no more. */
|
|
82
|
+
var JOIN_PT = 6;
|
|
83
|
+
/** Two segments within this of each other sit at the same height. */
|
|
84
|
+
var SAME_HEIGHT_PT = .5;
|
|
85
|
+
/**
|
|
86
|
+
* The rules, with the ones a page drew in PIECES joined back into one.
|
|
87
|
+
*
|
|
88
|
+
* An underline is a mark about a phrase, but a producer may stroke it a word at
|
|
89
|
+
* a time: TAMReview.pdf draws its copyright licence as six segments end to end.
|
|
90
|
+
* Matched one by one none of them covers enough of the long run it belongs to,
|
|
91
|
+
* so each was rejected and the phrase came back unmarked.
|
|
92
|
+
*
|
|
93
|
+
* Joined only along a straight, level line, with no more than a word space
|
|
94
|
+
* between the pieces — which is what a broken underline is and what a row of
|
|
95
|
+
* separate table rules is not.
|
|
96
|
+
*/
|
|
97
|
+
function joinRules(vectors) {
|
|
98
|
+
const rules = vectors.filter((v) => isRule(v));
|
|
99
|
+
const byRow = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const v of rules) {
|
|
101
|
+
const mid = (v.minY + v.maxY) / 2;
|
|
102
|
+
const key = `${ruleHex(v) ?? ""}@${String(Math.round(mid / SAME_HEIGHT_PT))}`;
|
|
103
|
+
const row = byRow.get(key);
|
|
104
|
+
if (row) row.push(v);
|
|
105
|
+
else byRow.set(key, [v]);
|
|
106
|
+
}
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const row of byRow.values()) {
|
|
109
|
+
row.sort((a, b) => a.minX - b.minX);
|
|
110
|
+
let run = [];
|
|
111
|
+
const flush = () => {
|
|
112
|
+
if (run.length === 0) return;
|
|
113
|
+
const first = run[0];
|
|
114
|
+
out.push({
|
|
115
|
+
rule: {
|
|
116
|
+
...first,
|
|
117
|
+
minX: Math.min(...run.map((v) => v.minX)),
|
|
118
|
+
maxX: Math.max(...run.map((v) => v.maxX)),
|
|
119
|
+
minY: Math.min(...run.map((v) => v.minY)),
|
|
120
|
+
maxY: Math.max(...run.map((v) => v.maxY))
|
|
121
|
+
},
|
|
122
|
+
parts: run
|
|
123
|
+
});
|
|
124
|
+
run = [];
|
|
125
|
+
};
|
|
126
|
+
for (const v of row) {
|
|
127
|
+
const last = run[run.length - 1];
|
|
128
|
+
if (last && v.minX - last.maxX > JOIN_PT) flush();
|
|
129
|
+
run.push(v);
|
|
130
|
+
}
|
|
131
|
+
flush();
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A thin bar under words — filled OR stroked.
|
|
137
|
+
*
|
|
138
|
+
* A page draws an underline either way, and the stroked one is the commoner:
|
|
139
|
+
* TAMReview.pdf strokes its copyright licence with six zero-height segments,
|
|
140
|
+
* one per word. Read as a filled bar only, none of them was a mark at all —
|
|
141
|
+
* they stayed artwork, and when the words re-set the artwork stayed where it
|
|
142
|
+
* was drawn and came back crossing the line it should have sat under.
|
|
143
|
+
*/
|
|
82
144
|
function isRule(v) {
|
|
83
|
-
if (v.
|
|
84
|
-
|
|
145
|
+
if (v.gradient !== void 0) return false;
|
|
146
|
+
const stroked = v.strokeHex !== void 0 && v.fillHex === void 0;
|
|
147
|
+
const colour = stroked ? v.strokeHex : v.fillHex;
|
|
148
|
+
if (colour === void 0) return false;
|
|
149
|
+
if (colour === "FFFFFF") return false;
|
|
150
|
+
if (v.maxX - v.minX <= 2) return false;
|
|
151
|
+
if (stroked) {
|
|
152
|
+
if (v.maxY - v.minY > THICKEST_PT) return false;
|
|
153
|
+
return (v.lineWidth ?? 1) <= THICKEST_PT;
|
|
154
|
+
}
|
|
85
155
|
const h = v.maxY - v.minY;
|
|
86
|
-
|
|
87
|
-
|
|
156
|
+
return h >= THINNEST_PT && h <= THICKEST_PT;
|
|
157
|
+
}
|
|
158
|
+
/** The colour a rule marks in, whichever way the page drew it. */
|
|
159
|
+
function ruleHex(v) {
|
|
160
|
+
return v.fillHex ?? v.strokeHex;
|
|
88
161
|
}
|
|
89
162
|
/**
|
|
90
163
|
* The runs a rule marks: on one baseline, covered along their advance, and at
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
|
|
2
|
+
import { buildColorSpaceMap, buildShadingMap } from "./shading.js";
|
|
2
3
|
import { IDENTITY, interpretContent, multiply } from "./content.js";
|
|
3
4
|
import { textMarkupOf } from "./annot-draw.js";
|
|
4
5
|
import { collectPageAppearances } from "./annots.js";
|
|
@@ -24,8 +25,9 @@ function extractPageText(file, page) {
|
|
|
24
25
|
for (const appearance of collectPageAppearances(file, page)) collectRuns(file, appearance.resources ?? page.resources, file.streamData(appearance.stream), appearance.ctm, 1, new Set([appearance.stream]), runs);
|
|
25
26
|
const links = collectLinks(file, page);
|
|
26
27
|
const marks = collectTextMarkup(file, page);
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const shown = withoutRestrikes(runs);
|
|
29
|
+
if (links.length === 0 && marks.length === 0) return shown;
|
|
30
|
+
return shown.flatMap((run) => {
|
|
29
31
|
const link = links.find((l) => inRect(run.x, run.y, l.rect));
|
|
30
32
|
const linked = link ? {
|
|
31
33
|
...run,
|
|
@@ -103,8 +105,85 @@ function collectTextMarkup(file, page) {
|
|
|
103
105
|
}
|
|
104
106
|
return out;
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* §9.4.3 — the same text struck again in the same place, dropped.
|
|
110
|
+
*
|
|
111
|
+
* A page with no bold face to hand fakes one by drawing its text several times
|
|
112
|
+
* a fraction of a point apart, and every copy is a run of its own.
|
|
113
|
+
* bug900822.pdf draws one line FOUR times — twice 0.16pt apart across, twice
|
|
114
|
+
* 0.32pt apart down — and read as four lines its letters came back interleaved:
|
|
115
|
+
* "TTTTeeeesssstttt" where the page shows "Test test".
|
|
116
|
+
*
|
|
117
|
+
* A copy is a run with the SAME text and size within a small fraction of an em
|
|
118
|
+
* of another. That is well inside the advance of even the narrowest letter, so
|
|
119
|
+
* a word that really does repeat a character is never mistaken for one. The
|
|
120
|
+
* LAST copy is the one kept: it is the one painted on top.
|
|
121
|
+
*
|
|
122
|
+
* The extra strikes are not read as WEIGHT. It is tempting — the offset is
|
|
123
|
+
* exactly how a face with no bold cut is emboldened — but two strikes a tenth
|
|
124
|
+
* of a point apart are a hair of ink and not a bold face:
|
|
125
|
+
* issue11150_reduced.pdf strikes each of its three letters twice and every
|
|
126
|
+
* renderer shows them at the weight of the text around them.
|
|
127
|
+
*
|
|
128
|
+
* @param runs The page's runs, in painting order.
|
|
129
|
+
* @returns The runs a reader sees, each drawn once.
|
|
130
|
+
*/
|
|
131
|
+
function withoutRestrikes(runs) {
|
|
132
|
+
const kept = /* @__PURE__ */ new Map();
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const run of runs) {
|
|
135
|
+
if (run.text.length === 0) {
|
|
136
|
+
out.push(run);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const tol = Math.max((run.fontSizePt || 10) * RESTRIKE_EM, .05);
|
|
140
|
+
const cell = (x, y) => `${run.text}|${run.fontSizePt.toFixed(1)}|${String(Math.round(x / tol))}|${String(Math.round(y / tol))}`;
|
|
141
|
+
let at = -1;
|
|
142
|
+
for (const dx of [
|
|
143
|
+
-1,
|
|
144
|
+
0,
|
|
145
|
+
1
|
|
146
|
+
]) for (const dy of [
|
|
147
|
+
-1,
|
|
148
|
+
0,
|
|
149
|
+
1
|
|
150
|
+
]) {
|
|
151
|
+
const had = kept.get(cell(run.x + dx * tol, run.y + dy * tol));
|
|
152
|
+
if (had !== void 0) at = had;
|
|
153
|
+
}
|
|
154
|
+
if (at >= 0) {
|
|
155
|
+
out[at] = run;
|
|
156
|
+
kept.set(cell(run.x, run.y), at);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
kept.set(cell(run.x, run.y), out.length);
|
|
160
|
+
out.push(run);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
/** How near, in ems, a re-strike of the same text lands to the one it thickens. */
|
|
165
|
+
var RESTRIKE_EM = .08;
|
|
166
|
+
var spaceCache = /* @__PURE__ */ new WeakMap();
|
|
167
|
+
/** The page's shading patterns, read once per resource dictionary. */
|
|
168
|
+
var shadingCache = /* @__PURE__ */ new WeakMap();
|
|
169
|
+
function shadingsOf(file, resources) {
|
|
170
|
+
if (!resources) return /* @__PURE__ */ new Map();
|
|
171
|
+
const had = shadingCache.get(resources);
|
|
172
|
+
if (had) return had;
|
|
173
|
+
const made = buildShadingMap(file, resources);
|
|
174
|
+
shadingCache.set(resources, made);
|
|
175
|
+
return made;
|
|
176
|
+
}
|
|
177
|
+
function spacesOf(file, resources) {
|
|
178
|
+
if (!resources) return /* @__PURE__ */ new Map();
|
|
179
|
+
const had = spaceCache.get(resources);
|
|
180
|
+
if (had) return had;
|
|
181
|
+
const made = buildColorSpaceMap(file, resources);
|
|
182
|
+
spaceCache.set(resources, made);
|
|
183
|
+
return made;
|
|
184
|
+
}
|
|
106
185
|
function collectRuns(file, resources, content, baseCtm, depth, visiting, out) {
|
|
107
|
-
const result = interpretContent(content, buildFonts(file, resources), baseCtm,
|
|
186
|
+
const result = interpretContent(content, buildFonts(file, resources), baseCtm, shadingsOf(file, resources), void 0, spacesOf(file, resources), hiddenProperties(file, resources));
|
|
108
187
|
out.push(...result.texts.map((r) => withPatternColour(file, resources, r, visiting)));
|
|
109
188
|
if (depth >= MAX_FORM_DEPTH) return;
|
|
110
189
|
for (const glyph of result.glyphs) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PathSeg } from './content.js';
|
|
2
|
+
/** What an embedded Type 1 program says about its glyphs. */
|
|
3
|
+
export interface Type1Font {
|
|
4
|
+
/** The contours of one glyph, by NAME — a Type 1 font indexes by nothing else. */
|
|
5
|
+
readonly path: (name: string) => Array<PathSeg> | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the program holds a glyph of that name at all. One it holds which
|
|
8
|
+
* draws nothing is a BLANK glyph — a space — and not one it lacks.
|
|
9
|
+
*/
|
|
10
|
+
readonly has: (name: string) => boolean;
|
|
11
|
+
/** The program's own `/Encoding`: code → glyph name, where it states one. */
|
|
12
|
+
readonly encoding?: ReadonlyMap<number, string>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read an embedded Type 1 program.
|
|
16
|
+
*
|
|
17
|
+
* @param program The raw `/FontFile` bytes — the cleartext header, the
|
|
18
|
+
* `eexec`-encrypted body, and the trailing zeros.
|
|
19
|
+
* @returns A reader, or `undefined` where the program cannot be read.
|
|
20
|
+
*/
|
|
21
|
+
export declare function type1Font(program: Uint8Array): Type1Font | undefined;
|