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.
Files changed (65) hide show
  1. package/README.md +6 -5
  2. package/dist/esm/core/converter/ream.d.ts +0 -9
  3. package/dist/esm/core/converter/ream.js +0 -1
  4. package/dist/esm/core/document-model/types.d.ts +5 -3
  5. package/dist/esm/core/font/index.d.ts +1 -0
  6. package/dist/esm/core/font/ligatures.d.ts +17 -0
  7. package/dist/esm/core/font/ligatures.js +49 -0
  8. package/dist/esm/core/font/ttf-parser.d.ts +2 -1
  9. package/dist/esm/core/font/ttf-parser.js +11 -2
  10. package/dist/esm/core/fonts/remote-fonts.d.ts +1 -1
  11. package/dist/esm/core/fonts/remote-fonts.js +47 -2
  12. package/dist/esm/core/fonts/scripts.js +10 -5
  13. package/dist/esm/excel/header-footer.js +53 -8
  14. package/dist/esm/layout/styled-layout.js +53 -3
  15. package/dist/esm/pdf/cid-font.js +35 -8
  16. package/dist/esm/pdf-reader/annot-draw.js +114 -1
  17. package/dist/esm/pdf-reader/annots.d.ts +0 -14
  18. package/dist/esm/pdf-reader/annots.js +13 -1
  19. package/dist/esm/pdf-reader/ccitt.d.ts +2 -3
  20. package/dist/esm/pdf-reader/ccitt.js +32 -4
  21. package/dist/esm/pdf-reader/cff-outline.d.ts +36 -0
  22. package/dist/esm/pdf-reader/cff-outline.js +1122 -0
  23. package/dist/esm/pdf-reader/cmap.js +5 -2
  24. package/dist/esm/pdf-reader/content.d.ts +76 -0
  25. package/dist/esm/pdf-reader/content.js +142 -59
  26. package/dist/esm/pdf-reader/dingbats.d.ts +11 -0
  27. package/dist/esm/pdf-reader/dingbats.js +1033 -0
  28. package/dist/esm/pdf-reader/display.js +41 -1
  29. package/dist/esm/pdf-reader/document.js +26 -9
  30. package/dist/esm/pdf-reader/embedded-fonts.d.ts +11 -0
  31. package/dist/esm/pdf-reader/embedded-fonts.js +14 -1
  32. package/dist/esm/pdf-reader/encodings.d.ts +25 -0
  33. package/dist/esm/pdf-reader/encodings.js +110 -0
  34. package/dist/esm/pdf-reader/flow-build.d.ts +9 -3
  35. package/dist/esm/pdf-reader/flow-build.js +13 -7
  36. package/dist/esm/pdf-reader/font.js +329 -8
  37. package/dist/esm/pdf-reader/glyf-outline.d.ts +43 -0
  38. package/dist/esm/pdf-reader/glyf-outline.js +351 -0
  39. package/dist/esm/pdf-reader/glyph-names.js +20 -0
  40. package/dist/esm/pdf-reader/icc.d.ts +10 -0
  41. package/dist/esm/pdf-reader/icc.js +210 -0
  42. package/dist/esm/pdf-reader/image-decode.d.ts +4 -3
  43. package/dist/esm/pdf-reader/image-decode.js +85 -64
  44. package/dist/esm/pdf-reader/images.d.ts +6 -0
  45. package/dist/esm/pdf-reader/images.js +50 -7
  46. package/dist/esm/pdf-reader/jbig2.js +104 -19
  47. package/dist/esm/pdf-reader/layout.js +1148 -45
  48. package/dist/esm/pdf-reader/math-rows.d.ts +23 -0
  49. package/dist/esm/pdf-reader/math-rows.js +198 -0
  50. package/dist/esm/pdf-reader/predefined-cmap.d.ts +21 -0
  51. package/dist/esm/pdf-reader/predefined-cmap.js +102 -0
  52. package/dist/esm/pdf-reader/reader.d.ts +6 -9
  53. package/dist/esm/pdf-reader/reader.js +34 -21
  54. package/dist/esm/pdf-reader/shading.d.ts +81 -2
  55. package/dist/esm/pdf-reader/shading.js +191 -25
  56. package/dist/esm/pdf-reader/stream-filters.d.ts +6 -0
  57. package/dist/esm/pdf-reader/stream-filters.js +67 -0
  58. package/dist/esm/pdf-reader/text-rules.js +83 -10
  59. package/dist/esm/pdf-reader/text.js +82 -3
  60. package/dist/esm/pdf-reader/type1-outline.d.ts +21 -0
  61. package/dist/esm/pdf-reader/type1-outline.js +576 -0
  62. package/dist/esm/pdf-reader/vector.js +43 -4
  63. package/dist/esm/word/document-parser.js +4 -1
  64. package/dist/esm/word/docx-writer.js +1 -0
  65. 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
- if (c.length >= 4) {
154
- const k = c[3];
155
- return hex255(255 * (1 - c[0]) * (1 - k), 255 * (1 - c[1]) * (1 - k), 255 * (1 - c[2]) * (1 - k));
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 hex255(r, g, b) {
162
- const h = (x) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, "0");
163
- return (h(r) + h(g) + h(b)).toUpperCase();
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 < 1 ? ca : void 0;
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 masked = file.resolve(state.get("SMask") ?? PDF_NULL) instanceof Map;
213
- if (alpha === void 0 && !darkens && blend === void 0 && !masked) continue;
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
- ...darkens ? { darkens: true } : {},
356
+ ...mode !== void 0 ? {
357
+ statesBlend: true,
358
+ darkens
359
+ } : {},
217
360
  ...blend !== void 0 ? { blend } : {},
218
- ...masked ? { masked: true } : {}
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
- return byFamily("ICCBased", typeof n === "number" ? n : 3);
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
- marks.set(run, under.length > 0 && v.fillHex !== void 0 ? {
47
+ const hex = ruleHex(v);
48
+ marks.set(run, under.length > 0 && hex !== void 0 ? {
49
49
  ...had,
50
- underline: v.fillHex
50
+ underline: hex
51
51
  } : {
52
52
  ...had,
53
53
  strike: true
54
54
  });
55
55
  }
56
- consumed.add(v);
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
- /** A thin filled bar, which is the only shape an underline is drawn as. */
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.fillHex === void 0 || v.strokeHex !== void 0 || v.gradient !== void 0) return false;
84
- if (v.fillHex === "FFFFFF") return false;
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
- const w = v.maxX - v.minX;
87
- return h >= THINNEST_PT && h <= THICKEST_PT && w > 2;
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
- if (links.length === 0 && marks.length === 0) return runs;
28
- return runs.flatMap((run) => {
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, void 0, void 0, void 0, hiddenProperties(file, resources));
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;