reamkit 1.29.0 → 1.30.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 (50) hide show
  1. package/dist/esm/core/document-model/types.d.ts +2 -0
  2. package/dist/esm/core/drawingml/shape-render.js +13 -1
  3. package/dist/esm/core/fonts/index.d.ts +1 -1
  4. package/dist/esm/core/fonts/remote-fonts.d.ts +8 -0
  5. package/dist/esm/core/fonts/remote-fonts.js +100 -17
  6. package/dist/esm/core/ir/flow.d.ts +17 -0
  7. package/dist/esm/index.d.ts +1 -1
  8. package/dist/esm/pdf-reader/annot-draw.js +93 -1
  9. package/dist/esm/pdf-reader/annots.d.ts +18 -0
  10. package/dist/esm/pdf-reader/annots.js +86 -9
  11. package/dist/esm/pdf-reader/cmap.js +5 -2
  12. package/dist/esm/pdf-reader/content.d.ts +17 -4
  13. package/dist/esm/pdf-reader/content.js +163 -11
  14. package/dist/esm/pdf-reader/display.js +16 -0
  15. package/dist/esm/pdf-reader/embedded-fonts.d.ts +24 -0
  16. package/dist/esm/pdf-reader/embedded-fonts.js +48 -9
  17. package/dist/esm/pdf-reader/flow-build.d.ts +96 -5
  18. package/dist/esm/pdf-reader/flow-build.js +210 -23
  19. package/dist/esm/pdf-reader/font.d.ts +27 -1
  20. package/dist/esm/pdf-reader/font.js +313 -29
  21. package/dist/esm/pdf-reader/glyf-outline.js +13 -2
  22. package/dist/esm/pdf-reader/glyph-shapes.d.ts +18 -0
  23. package/dist/esm/pdf-reader/glyph-shapes.js +57 -0
  24. package/dist/esm/pdf-reader/image-decode.js +70 -4
  25. package/dist/esm/pdf-reader/images.d.ts +5 -0
  26. package/dist/esm/pdf-reader/images.js +4 -2
  27. package/dist/esm/pdf-reader/jbig2.d.ts +40 -1
  28. package/dist/esm/pdf-reader/jbig2.js +78 -16
  29. package/dist/esm/pdf-reader/jpeg.d.ts +6 -3
  30. package/dist/esm/pdf-reader/jpeg.js +21 -1
  31. package/dist/esm/pdf-reader/layout.d.ts +26 -2
  32. package/dist/esm/pdf-reader/layout.js +638 -92
  33. package/dist/esm/pdf-reader/lexer.d.ts +10 -0
  34. package/dist/esm/pdf-reader/lexer.js +17 -0
  35. package/dist/esm/pdf-reader/pattern-tint.d.ts +11 -1
  36. package/dist/esm/pdf-reader/pattern-tint.js +21 -3
  37. package/dist/esm/pdf-reader/regions.d.ts +25 -0
  38. package/dist/esm/pdf-reader/regions.js +167 -0
  39. package/dist/esm/pdf-reader/shading.d.ts +58 -2
  40. package/dist/esm/pdf-reader/shading.js +181 -11
  41. package/dist/esm/pdf-reader/struct-tree.js +112 -8
  42. package/dist/esm/pdf-reader/tagged.js +27 -7
  43. package/dist/esm/pdf-reader/text-rules.js +1 -1
  44. package/dist/esm/pdf-reader/text.js +9 -14
  45. package/dist/esm/pdf-reader/vector.d.ts +8 -2
  46. package/dist/esm/pdf-reader/vector.js +34 -5
  47. package/dist/esm/word/docx-writer.js +137 -36
  48. package/dist/esm/word/drawing-parser.js +7 -2
  49. package/dist/esm/word/paragraph-properties.js +2 -0
  50. package/package.json +5 -3
@@ -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`.
@@ -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, ShapeGradient>;
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): ShapeGradient | undefined;
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
  *
@@ -1,4 +1,4 @@
1
- import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
1
+ import { PDF_NULL, PdfHexString, PdfName, PdfStream } from "../pdf/objects.js";
2
2
  import { cieToSrgb } from "./cie-color.js";
3
3
  import { iccTransform } from "./icc.js";
4
4
  import { readFunction } from "./function.js";
@@ -26,12 +26,102 @@ function buildShadingMap(file, resources) {
26
26
  if (!(pat instanceof Map)) continue;
27
27
  const shading = dictOf(file.resolve(pat.get("Shading") ?? PDF_NULL));
28
28
  if (!shading) continue;
29
- const gradient = parseShading(file, shading);
29
+ const m = numArray(file, pat.get("Matrix"));
30
+ const gradient = parseShading(file, shading, m && m.length >= 6 ? [
31
+ m[0],
32
+ m[1],
33
+ m[2],
34
+ m[3],
35
+ m[4],
36
+ m[5]
37
+ ] : void 0);
30
38
  if (gradient) out.set(nm, gradient);
31
39
  }
32
40
  return out;
33
41
  }
34
42
  /**
43
+ * A page's gradient as a shape filled with it shows it: its stops re-mapped
44
+ * from the whole axis to the part of it the shape's box covers.
45
+ *
46
+ * A DrawingML gradient spans the shape it fills, and a PDF shading spans its
47
+ * own axis, which may reach far past the shape: issue10572.pdf's pattern
48
+ * runs twenty-four stripes down 1800 points, of which its 450-point square
49
+ * shows six — spread over the square, all twenty-four came back as hairlines.
50
+ * Beyond either end of the axis the end colour carries on, as `/Extend` asks.
51
+ *
52
+ * @param g The gradient, with its axis where the page states one.
53
+ * @param box The shape's box, in the same space as the axis.
54
+ * @returns The gradient as the shape shows it, with no axis.
55
+ */
56
+ function gradientOverBox(g, box) {
57
+ const { axis, ...plain } = g;
58
+ if (!axis || g.kind !== "linear" || g.stops.length === 0) return plain;
59
+ const dx = axis.x1 - axis.x0;
60
+ const dy = axis.y1 - axis.y0;
61
+ const len2 = dx * dx + dy * dy;
62
+ if (!(len2 > 0)) return plain;
63
+ const along = (x, y) => ((x - axis.x0) * dx + (y - axis.y0) * dy) / len2;
64
+ const ts = [
65
+ along(box.minX, box.minY),
66
+ along(box.minX, box.maxY),
67
+ along(box.maxX, box.minY),
68
+ along(box.maxX, box.maxY)
69
+ ];
70
+ const t0 = Math.min(...ts);
71
+ const t1 = Math.max(...ts);
72
+ if (!(t1 - t0 > 1e-9)) return plain;
73
+ if (Math.abs(t0) < SPAN_NOISE && Math.abs(t1 - 1) < SPAN_NOISE) return plain;
74
+ const inner = g.stops.filter((st) => st.offset > t0 && st.offset < t1);
75
+ const stops = [
76
+ {
77
+ offset: 0,
78
+ colorHex: colorAlong(g.stops, t0, "after")
79
+ },
80
+ ...inner.map((st) => ({
81
+ offset: (st.offset - t0) / (t1 - t0),
82
+ colorHex: st.colorHex
83
+ })),
84
+ {
85
+ offset: 1,
86
+ colorHex: colorAlong(g.stops, t1, "before")
87
+ }
88
+ ];
89
+ return {
90
+ ...plain,
91
+ stops
92
+ };
93
+ }
94
+ /** How near the box's span may come to the whole axis and be taken for it. */
95
+ var SPAN_NOISE = .001;
96
+ /**
97
+ * A gradient's colour at `t` along it, its end colours carried on past either
98
+ * end. Where two stops share `t` the gradient changes colour AT it, and `side`
99
+ * says which of the two is wanted: the one the span after `t` starts with, or
100
+ * the one the span before it ends with. issue10572.pdf's square starts on such
101
+ * an edge, and taken from the wrong side its first stripe faded in from blue.
102
+ */
103
+ function colorAlong(stops, t, side) {
104
+ const first = stops[0];
105
+ const last = stops[stops.length - 1];
106
+ if (t <= first.offset) return first.colorHex;
107
+ if (t >= last.offset) return last.colorHex;
108
+ const at = stops.filter((st) => Math.abs(st.offset - t) < 1e-9);
109
+ if (at.length > 0) return (side === "after" ? at[at.length - 1] : at[0]).colorHex;
110
+ for (let i = 1; i < stops.length; i++) {
111
+ const b = stops[i];
112
+ if (t > b.offset) continue;
113
+ const a = stops[i - 1];
114
+ const span = b.offset - a.offset;
115
+ const f = span > 0 ? (t - a.offset) / span : 1;
116
+ const mix = (shift) => {
117
+ const ca = parseInt(a.colorHex, 16) >> shift & 255;
118
+ return ca + ((parseInt(b.colorHex, 16) >> shift & 255) - ca) * f;
119
+ };
120
+ return hex255(mix(16), mix(8), mix(0));
121
+ }
122
+ return last.colorHex;
123
+ }
124
+ /**
35
125
  * §8.7.4.5.2 — an axial or radial shading as a gradient, for a bare `sh`.
36
126
  *
37
127
  * A `sh` paints the CLIP rather than a path, so what it needs is not a fill for
@@ -42,8 +132,8 @@ function buildShadingMap(file, resources) {
42
132
  * @param sh The shading dictionary.
43
133
  * @returns The gradient, or `undefined` for a type this does not read.
44
134
  */
45
- function gradientShading(file, sh) {
46
- return parseShading(file, sh);
135
+ function gradientShading(file, sh, ctm) {
136
+ return parseShading(file, sh, ctm);
47
137
  }
48
138
  /**
49
139
  * §8.7.4.5 — which kind of shading this is, as the file states it.
@@ -55,7 +145,7 @@ function gradientShading(file, sh) {
55
145
  function shadingTypeOf(file, sh) {
56
146
  return numOf(file.get(sh, "ShadingType"));
57
147
  }
58
- function parseShading(file, sh) {
148
+ function parseShading(file, sh, matrix) {
59
149
  const type = numOf(file.get(sh, "ShadingType"));
60
150
  if (type !== 2 && type !== 3) return void 0;
61
151
  const stops = parseFunction(file, sh.get("Function"), shadingSpace(file, sh));
@@ -65,11 +155,33 @@ function parseShading(file, sh) {
65
155
  stops
66
156
  };
67
157
  const c = numArray(file, sh.get("Coords"));
68
- return {
158
+ if (!c || c.length < 4) return {
69
159
  kind: "linear",
70
- angle: c && c.length >= 4 ? (Math.atan2(-(c[3] - c[1]), c[2] - c[0]) * 180 / Math.PI % 360 + 360) % 360 : 0,
160
+ angle: 0,
71
161
  stops
72
162
  };
163
+ const [a, b, cc, d, e, f] = matrix ?? [
164
+ 1,
165
+ 0,
166
+ 0,
167
+ 1,
168
+ 0,
169
+ 0
170
+ ];
171
+ const at = (x, y) => [a * x + cc * y + e, b * x + d * y + f];
172
+ const [x0, y0] = at(c[0], c[1]);
173
+ const [x1, y1] = at(c[2], c[3]);
174
+ return {
175
+ kind: "linear",
176
+ angle: (Math.atan2(-(y1 - y0), x1 - x0) * 180 / Math.PI % 360 + 360) % 360,
177
+ stops,
178
+ axis: {
179
+ x0,
180
+ y0,
181
+ x1,
182
+ y1
183
+ }
184
+ };
73
185
  }
74
186
  function parseFunction(file, value, space) {
75
187
  const resolved = value !== void 0 ? file.resolve(value) : void 0;
@@ -246,6 +358,17 @@ function numArray(file, v) {
246
358
  function colorOf(c, space) {
247
359
  return spaceColor(c, space) ?? spaceColor(c, void 0) ?? grayHex(c[0] ?? 0);
248
360
  }
361
+ /** §8.6.6.3 — an Indexed space's table: a string, a hex string or a stream. */
362
+ function lookupBytes(file, v) {
363
+ const r = file.resolve(v ?? PDF_NULL);
364
+ if (r instanceof PdfHexString) return r.bytes;
365
+ if (typeof r === "string") {
366
+ const out = new Uint8Array(r.length);
367
+ for (let i = 0; i < r.length; i++) out[i] = r.charCodeAt(i) & 255;
368
+ return out;
369
+ }
370
+ if (r instanceof PdfStream) return file.streamData(r);
371
+ }
249
372
  /**
250
373
  * §8.6.8 — the colour a run of `sc` / `scn` components comes to.
251
374
  *
@@ -301,6 +424,10 @@ function grayHex(v) {
301
424
  function cmykHex(c, m, y, k) {
302
425
  return rgbHex((1 - c) * (1 - k), (1 - m) * (1 - k), (1 - y) * (1 - k));
303
426
  }
427
+ function hex255(r, g, b) {
428
+ const h = (x) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, "0");
429
+ return (h(r) + h(g) + h(b)).toUpperCase();
430
+ }
304
431
  function pushStop(stops, offset, colorHex) {
305
432
  const o = Math.max(0, Math.min(1, offset));
306
433
  const last = stops[stops.length - 1];
@@ -350,7 +477,15 @@ function buildAlphaMap(file, resources) {
350
477
  const blend = mode !== void 0 && mode !== "Normal" && mode !== "Compatible" && !darkens ? mode : void 0;
351
478
  const statesMask = state.has("SMask");
352
479
  const masked = statesMask ? file.resolve(state.get("SMask") ?? PDF_NULL) instanceof Map : void 0;
353
- if (alpha === void 0 && mode === void 0 && !statesMask) continue;
480
+ const lw = file.resolve(state.get("LW") ?? PDF_NULL);
481
+ const lineWidth = typeof lw === "number" && lw >= 0 ? lw : void 0;
482
+ const d = file.resolve(state.get("D") ?? PDF_NULL);
483
+ const dash = Array.isArray(d) ? dashLengths(file.resolve(d[0] ?? PDF_NULL)) : void 0;
484
+ const lc = file.resolve(state.get("LC") ?? PDF_NULL);
485
+ const lineCap = lc === 0 || lc === 1 || lc === 2 ? lc : void 0;
486
+ const caStroke = file.resolve(state.get("CA") ?? PDF_NULL);
487
+ const strokeAlpha = typeof caStroke === "number" && caStroke >= 0 && caStroke <= 1 ? caStroke : void 0;
488
+ if (alpha === void 0 && mode === void 0 && !statesMask && !(lineWidth !== void 0 || dash !== void 0 || lineCap !== void 0 || strokeAlpha !== void 0)) continue;
354
489
  out.set(name, {
355
490
  ...alpha !== void 0 ? { alpha } : {},
356
491
  ...mode !== void 0 ? {
@@ -358,12 +493,30 @@ function buildAlphaMap(file, resources) {
358
493
  darkens
359
494
  } : {},
360
495
  ...blend !== void 0 ? { blend } : {},
361
- ...masked !== void 0 ? { masked } : {}
496
+ ...masked !== void 0 ? { masked } : {},
497
+ ...lineWidth !== void 0 ? { lineWidth } : {},
498
+ ...dash !== void 0 ? { dash } : {},
499
+ ...lineCap !== void 0 ? { lineCap } : {},
500
+ ...strokeAlpha !== void 0 ? { strokeAlpha } : {}
362
501
  });
363
502
  }
364
503
  return out;
365
504
  }
366
505
  /**
506
+ * §8.4.3.6 — a dash array as a stroke can use it: lengths that are numbers and
507
+ * not negative, and none at all where every one is zero, which draws nothing
508
+ * and is solid by the spec's own reading.
509
+ *
510
+ * @param value The array, as the file states it.
511
+ * @returns The lengths, or an empty array for a solid line.
512
+ */
513
+ function dashLengths(value) {
514
+ if (!Array.isArray(value)) return [];
515
+ const lengths = value.filter((n) => typeof n === "number" && n >= 0);
516
+ if (lengths.length !== value.length || lengths.every((n) => n === 0)) return [];
517
+ return lengths;
518
+ }
519
+ /**
367
520
  * The spaces a `/ColorSpace` resource dictionary names, by name.
368
521
  *
369
522
  * @param file The owning file.
@@ -405,7 +558,24 @@ function colorSpaceAt(file, cs, depth) {
405
558
  icc
406
559
  } : base;
407
560
  }
408
- if (head.value === "Indexed") return;
561
+ if (head.value === "Indexed" || head.value === "I") {
562
+ const base = depth < MAX_ALTERNATE ? colorSpaceAt(file, file.resolve(cs[1] ?? PDF_NULL), depth + 1) : void 0;
563
+ const hival = file.resolve(cs[2] ?? PDF_NULL);
564
+ const table = lookupBytes(file, cs[3]);
565
+ if (!base || base.kind === "tint" || typeof hival !== "number" || !table) return void 0;
566
+ const transform = ([index = 0]) => {
567
+ const at = Math.min(Math.max(0, Math.round(index)), Math.round(hival));
568
+ return Array.from({ length: base.components }, (_, k) => (table[at * base.components + k] ?? 0) / 255);
569
+ };
570
+ return {
571
+ kind: "tint",
572
+ components: 1,
573
+ tint: {
574
+ transform,
575
+ alternate: base
576
+ }
577
+ };
578
+ }
409
579
  if (head.value === "CalRGB") {
410
580
  const params = file.resolve(cs[1] ?? PDF_NULL);
411
581
  const cie = params instanceof Map ? cieParams(file, params, true) : void 0;
@@ -516,4 +686,4 @@ function byFamily(family, n) {
516
686
  }
517
687
  }
518
688
  //#endregion
519
- export { buildAlphaMap, buildColorSpaceMap, buildShadingMap, cmykHex, gradientShading, grayHex, rgbHex, sampledShading, shadingTypeOf, spaceColor };
689
+ export { buildAlphaMap, buildColorSpaceMap, buildShadingMap, cmykHex, dashLengths, gradientOverBox, gradientShading, grayHex, rgbHex, sampledShading, shadingTypeOf, spaceColor };