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
@@ -1127,6 +1127,8 @@ export interface ShapeLine {
1127
1127
  readonly customDash?: ReadonlyArray<number>;
1128
1128
  readonly cap?: 'flat' | 'round' | 'square';
1129
1129
  readonly fill?: 'solid' | 'none';
1130
+ /** §20.1.2.3.1 `a:alpha` on the line's colour — how opaque the stroke is, `0..1`. */
1131
+ readonly alpha?: number;
1130
1132
  /** §20.1.8.24 `a:headEnd` — the decoration at the line's first point. */
1131
1133
  readonly headEnd?: LineEnd;
1132
1134
  /** §20.1.8.42 `a:tailEnd` — the decoration at the line's last point. */
@@ -192,13 +192,25 @@ function buildStroke(line) {
192
192
  const widthPt = line.width ?? DEFAULT_LINE_WIDTH_EMU / 12700;
193
193
  const dash = line.customDash?.map((n) => Math.max(.01, n * widthPt)) ?? (line.dash && line.dash !== "solid" ? dashPattern(line.dash, widthPt) : void 0);
194
194
  const cap = line.cap === "flat" ? "butt" : line.cap;
195
+ const colorHex = line.colorHex ?? "000000";
195
196
  return {
196
- colorHex: line.colorHex ?? "000000",
197
+ colorHex: line.alpha !== void 0 && line.alpha < 1 ? overWhite(colorHex, line.alpha) : colorHex,
197
198
  widthPt,
198
199
  ...dash ? { dash } : {},
199
200
  ...cap ? { cap } : {}
200
201
  };
201
202
  }
203
+ /** A 6-hex colour drawn at `alpha` over white, as the opaque colour it comes to. */
204
+ function overWhite(hex, alpha) {
205
+ const n = parseInt(hex, 16);
206
+ if (Number.isNaN(n)) return hex;
207
+ const a = Math.max(0, Math.min(1, alpha));
208
+ return [
209
+ 16,
210
+ 8,
211
+ 0
212
+ ].map((shift) => Math.round(255 - (255 - (n >> shift & 255)) * a).toString(16).padStart(2, "0")).join("").toUpperCase();
213
+ }
202
214
  function dashPattern(dash, w) {
203
215
  const u = Math.max(w, .1);
204
216
  switch (dash) {
@@ -1,3 +1,3 @@
1
- export { fetchFontSet, fetchScriptFont, isScriptKey, resolveFamilyKey, resolveFamilyStyle, clearFontCache, } from './remote-fonts.js';
1
+ export { fetchFontSet, fetchScriptFont, isScriptKey, knowsFamily, resolveFamilyKey, resolveFamilyStyle, clearFontCache, } from './remote-fonts.js';
2
2
  export { scriptForCodepoint, scriptsInFlow } from './scripts.js';
3
3
  export type { FamilyKey, FamilyStyle, FetchFontSetOptions, FetchLike, ScriptKey, SubstituteKey, } from './remote-fonts.js';
@@ -52,6 +52,14 @@ export interface FamilyStyle {
52
52
  * @returns The chosen family and what the name said about the face.
53
53
  */
54
54
  export declare function resolveFamilyStyle(name: string | undefined): FamilyStyle;
55
+ /**
56
+ * Whether the tables KNOW a family name — so the substitute it maps to is the
57
+ * name's own answer, and not the sans that every name nobody knows falls to.
58
+ *
59
+ * @param name The referenced family or face name.
60
+ * @returns `true` where the name matched a twin, a class or a stem.
61
+ */
62
+ export declare function knowsFamily(name: string | undefined): boolean;
55
63
  /**
56
64
  * The curated substitute for a family name — {@link resolveFamilyStyle} without
57
65
  * the face it also carries.
@@ -98,7 +98,24 @@ var SERIF = new Set([
98
98
  "minion pro",
99
99
  "serif"
100
100
  ]);
101
+ var SANS = new Set([
102
+ "roboto",
103
+ "noto sans",
104
+ "notosans",
105
+ "dejavu sans",
106
+ "dejavusans",
107
+ "lucida sans",
108
+ "lucidasans",
109
+ "ubuntu",
110
+ "lato",
111
+ "inter",
112
+ "din",
113
+ "sans",
114
+ "sans serif"
115
+ ]);
101
116
  var MONO = new Set([
117
+ "mono",
118
+ "monospaced",
102
119
  "consolas",
103
120
  "monaco",
104
121
  "menlo",
@@ -138,14 +155,59 @@ var STEMS = [
138
155
  ["charter", "tinos"],
139
156
  ["baskerville", "tinos"],
140
157
  ["caslon", "tinos"],
141
- ["minionpro", "tinos"],
158
+ ["minion", "tinos"],
159
+ ["stix", "tinos"],
142
160
  ["stoneserif", "tinos"],
143
161
  ["stonesans", "arimo"],
144
- ["myriadpro", "arimo"]
162
+ ["myriad", "arimo"],
163
+ ["arial", "arimo"],
164
+ ["helvetica", "arimo"],
165
+ ["frutiger", "arimo"],
166
+ ["univers", "arimo"],
167
+ ["futura", "arimo"],
168
+ ["gillsans", "arimo"],
169
+ ["optima", "arimo"],
170
+ ["avenir", "arimo"],
171
+ ["verdana", "arimo"],
172
+ ["tahoma", "arimo"],
173
+ ["trebuchet", "arimo"],
174
+ ["segoeui", "arimo"],
175
+ ["franklingothic", "arimo"],
176
+ ["newsgothic", "arimo"],
177
+ ["tradegothic", "arimo"],
178
+ ["centurygothic", "arimo"],
179
+ ["akzidenz", "arimo"],
180
+ ["eurostile", "arimo"],
181
+ ["avantgarde", "arimo"],
182
+ ["gotham", "arimo"],
183
+ ["proximanova", "arimo"],
184
+ ["opensans", "arimo"],
185
+ ["sourcesans", "arimo"],
186
+ ["ptsans", "arimo"],
187
+ ["firasans", "arimo"],
188
+ ["montserrat", "arimo"],
189
+ ["candara", "arimo"],
190
+ ["corbel", "arimo"],
191
+ ["lucidagrande", "arimo"],
192
+ ["mssansserif", "arimo"],
193
+ ["microsoftsansserif", "arimo"],
194
+ ["dinpro", "arimo"],
195
+ ["dinnext", "arimo"],
196
+ ["monospace", "cousine"],
197
+ ["lucidatypewriter", "cousine"],
198
+ ["lucidasanstypewriter", "cousine"]
145
199
  ];
146
200
  /** The family a name STARTS with, for the families named by stem and size. */
147
201
  function familyFromStem(name) {
148
- for (const [stem, key] of STEMS) if (name.startsWith(stem)) return key;
202
+ for (const [stem, key] of STEMS) {
203
+ if (!name.startsWith(stem)) continue;
204
+ if (key === "arimo") {
205
+ const cut = name.slice(stem.length);
206
+ if (/mono|typewriter/u.test(cut)) return "cousine";
207
+ if (/serif|slab/u.test(cut)) return "tinos";
208
+ }
209
+ return key;
210
+ }
149
211
  }
150
212
  var WEIGHT_WORDS = new Map([
151
213
  ["black", "bold"],
@@ -185,6 +247,26 @@ var NARROW_SCALE = .82;
185
247
  */
186
248
  function resolveFamilyStyle(name) {
187
249
  if (!name) return { key: "arimo" };
250
+ const { words, bold, italic, narrow } = readName(name);
251
+ return {
252
+ key: familyOfWords(words) ?? "arimo",
253
+ ...bold ? { bold } : {},
254
+ ...italic ? { italic } : {},
255
+ ...narrow ? { widthScale: NARROW_SCALE } : {}
256
+ };
257
+ }
258
+ /**
259
+ * Whether the tables KNOW a family name — so the substitute it maps to is the
260
+ * name's own answer, and not the sans that every name nobody knows falls to.
261
+ *
262
+ * @param name The referenced family or face name.
263
+ * @returns `true` where the name matched a twin, a class or a stem.
264
+ */
265
+ function knowsFamily(name) {
266
+ return name !== void 0 && name !== "" && familyOfWords(readName(name).words) !== void 0;
267
+ }
268
+ /** A name's family words, with the face words that ended it taken off and read. */
269
+ function readName(name) {
188
270
  const words = name.trim().toLowerCase().split(/[\s\-_,]+/u).filter((w) => w !== "");
189
271
  let bold = false;
190
272
  let italic = false;
@@ -197,25 +279,26 @@ function resolveFamilyStyle(name) {
197
279
  if (word === "narrow") narrow = true;
198
280
  words.pop();
199
281
  }
282
+ return {
283
+ words,
284
+ bold,
285
+ italic,
286
+ narrow
287
+ };
288
+ }
289
+ /** The substitute a family's words name, or `undefined` where no table knows them. */
290
+ function familyOfWords(words) {
291
+ if (words.length === 0) return void 0;
292
+ const whole = words.join(" ");
200
293
  const tries = [
201
- words.join(" "),
294
+ whole,
202
295
  words[words.length - 1],
203
296
  words[0]
204
297
  ];
205
- let key = "arimo";
206
298
  for (const n of tries) {
207
- const found = EXACT[n] ?? (MONO.has(n) ? "cousine" : SERIF.has(n) ? "tinos" : void 0) ?? familyFromStem(n.replace(/[^a-z]/gu, ""));
208
- if (found) {
209
- key = found;
210
- break;
211
- }
299
+ const found = EXACT[n] ?? (MONO.has(n) ? "cousine" : SERIF.has(n) ? "tinos" : void 0) ?? (n === whole && SANS.has(n) ? "arimo" : void 0) ?? familyFromStem(n.replace(/[^a-z]/gu, ""));
300
+ if (found) return found;
212
301
  }
213
- return {
214
- key,
215
- ...bold ? { bold } : {},
216
- ...italic ? { italic } : {},
217
- ...narrow ? { widthScale: NARROW_SCALE } : {}
218
- };
219
302
  }
220
303
  /**
221
304
  * The curated substitute for a family name — {@link resolveFamilyStyle} without
@@ -279,4 +362,4 @@ async function fetchFontSet(options = {}) {
279
362
  };
280
363
  }
281
364
  //#endregion
282
- export { fetchFontSet, fetchScriptFont, resolveFamilyKey, resolveFamilyStyle };
365
+ export { fetchFontSet, fetchScriptFont, knowsFamily, resolveFamilyKey, resolveFamilyStyle };
@@ -1,6 +1,15 @@
1
1
  import { BodyElement, Chart, Comment, DocumentInfo, Numbering, Section, SectionProperties, ShapeFill, StyleSheet } from '../document-model/index.js';
2
2
  import { FontRegistry } from '../font/index.js';
3
3
  import { ResourceStore } from './resources.js';
4
+ /**
5
+ * A face's family as a word processor names it (ECMA-376 §17.8.3.9 `w:font`).
6
+ */
7
+ export interface FaceFamily {
8
+ /** The family's own name: `Inter` for `Inter-SemiBold`, `Arial` for `ArialMT`. */
9
+ readonly family: string;
10
+ /** §17.8.3.10 `w:family` — the kind of face, for a reader that has to substitute. */
11
+ readonly generic: 'roman' | 'swiss' | 'modern';
12
+ }
4
13
  /**
5
14
  * The semantic IR tree (ir-design §5): everything a reader extracts from the
6
15
  * document bytes, format-neutrally — the flow `body` plus its document-scoped
@@ -42,6 +51,14 @@ export interface FlowDoc {
42
51
  readonly resources: ResourceStore;
43
52
  /** Fonts embedded in the source document itself (docx fontTable), by name. */
44
53
  readonly embeddedFonts?: ReadonlyMap<string, FontRegistry>;
54
+ /**
55
+ * The family each face a run names belongs to, where the two are not the
56
+ * same name. A PDF names a FACE — `Inter-SemiBold` — where a word processor
57
+ * names a family and states the weight beside it (`Inter`, bold). The layout
58
+ * finds a document's own program by the face; a writer that hands the text
59
+ * to another program names the family, which is the name that program knows.
60
+ */
61
+ readonly faceFamilies?: ReadonlyMap<string, FaceFamily>;
45
62
  /** Document metadata from docProps/core.xml. */
46
63
  readonly info?: DocumentInfo;
47
64
  /** Document natural language hint (BCP-47), e.g. for tagged-PDF /Lang. */
@@ -34,7 +34,7 @@ export { getHyphenator, createLanguageHyphenator, createHyphenator, splitPattern
34
34
  export type { Hyphenator, HyphenatorOptions, SupportedLanguage } from './core/hyphenation/index.js';
35
35
  export type { Pt, ResourceId, Feature, KnownFeature, Loss, LossReport, LossSeverity, NativeBag, } from './core/ir/index.js';
36
36
  export { FEATURES, ResourceStore, ConversionLossError, formatLoss, pt, twipsToPt, halfPtToPt, eighthPtToPt, emuToPt, pxToPt, inchToPt, mmToPt, } from './core/ir/index.js';
37
- export type { FlowDoc } from './core/ir/flow.js';
37
+ export type { FaceFamily, FlowDoc } from './core/ir/flow.js';
38
38
  export type { DocumentReader, DocumentWriter, ReadOptions, ReadResult, WriteOptions, WriteResult, } from './core/ir/adapters.js';
39
39
  export { docxReader, readDocx } from './word/docx-reader.js';
40
40
  export { xlsxReader, readXlsx } from './excel/xlsx-reader.js';
@@ -1,4 +1,6 @@
1
- import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
1
+ import { PDF_NULL, PdfHexString, PdfName, PdfStream } from "../pdf/objects.js";
2
+ import { reorderVisual } from "../core/bidi/algorithm.js";
3
+ import { analyzeString, hasBidiCharacters } from "../core/bidi/index.js";
2
4
  //#region src/pdf-reader/annot-draw.ts
3
5
  /** The circle constant: how far a Bézier handle reaches to round a quarter. */
4
6
  var KAPPA = .5523;
@@ -20,6 +22,7 @@ function drawnAppearance(file, annot) {
20
22
  const subtype = file.get(annot, "Subtype");
21
23
  if (!(subtype instanceof PdfName)) return void 0;
22
24
  if (subtype.value === "Widget") return typedValue(file, annot) ?? checkedBox(file, annot);
25
+ if (subtype.value === "FreeText") return freeText(file, annot);
23
26
  const markup = markupShape(file, annot, subtype.value);
24
27
  if (markup) return markup;
25
28
  const pen = borderWidth(file, annot);
@@ -162,6 +165,10 @@ var SQUIGGLE_AMP_EM = .07;
162
165
  */
163
166
  function drawnResources(file, annot) {
164
167
  const subtype = file.get(annot, "Subtype");
168
+ if (subtype instanceof PdfName && subtype.value === "FreeText") {
169
+ const text = freeTextContents(file, annot);
170
+ return text === void 0 ? void 0 : new Map([["Font", new Map([[FREE_TEXT_FONT, spelledFace(text)]])]]);
171
+ }
165
172
  if (!(subtype instanceof PdfName) || subtype.value !== "Widget") return void 0;
166
173
  const acro = file.get(file.catalog, "AcroForm");
167
174
  if (!(acro instanceof Map)) return void 0;
@@ -211,6 +218,91 @@ function typedValue(file, annot) {
211
218
  ops.push("ET", "Q");
212
219
  return new PdfStream(/* @__PURE__ */ new Map(), new TextEncoder().encode(ops.join("\n")));
213
220
  }
221
+ /**
222
+ * §12.5.6.6 — a free-text annotation with no appearance: its contents, a line
223
+ * to each line break, from the top of its `/Rect` in the size and colour its
224
+ * `/DA` asks. Left undrawn the words were simply gone: bug1865341.pdf's one
225
+ * note, "Załącznik", and nothing else on the page.
226
+ */
227
+ function freeText(file, annot) {
228
+ const text = freeTextContents(file, annot);
229
+ const rect = rectangle(file.get(annot, "Rect"));
230
+ if (text === void 0 || !rect) return void 0;
231
+ const da = defaultAppearance(file, annot);
232
+ const box = inset(rect, 2);
233
+ const size = da.size > 0 ? da.size : 12;
234
+ const codes = codesOf(text);
235
+ const ops = [
236
+ "q",
237
+ "BT",
238
+ `/${FREE_TEXT_FONT} ${num(size)} Tf`,
239
+ `${da.color} rg`
240
+ ];
241
+ text.split(/\r\n|[\r\n]/u).forEach((line, i) => {
242
+ const y = box[3] - size * (.8 + i * 1.16);
243
+ const hex = visualOrder(line).map((ch) => (codes.get(ch) ?? 0).toString(16).padStart(2, "0")).join("");
244
+ ops.push(`1 0 0 1 ${num(box[0])} ${num(y)} Tm`, `<${hex}> Tj`);
245
+ });
246
+ ops.push("ET", "Q");
247
+ return new PdfStream(/* @__PURE__ */ new Map(), new TextEncoder().encode(ops.join("\n")));
248
+ }
249
+ /** UAX #9 — a line's characters in the order they stand on the page, left to right. */
250
+ function visualOrder(line) {
251
+ const chars = [...line];
252
+ if (!hasBidiCharacters(line)) return chars;
253
+ const { levels } = analyzeString(line);
254
+ return reorderVisual(levels).map((i) => chars[i] ?? "");
255
+ }
256
+ /** The resource name the face of a drawn free-text annotation goes by. */
257
+ var FREE_TEXT_FONT = "FreeText";
258
+ /** §7.9.2.2 — an annotation's `/Contents`, decoded as the text string it is. */
259
+ function freeTextContents(file, annot) {
260
+ const found = file.get(annot, "Contents");
261
+ const raw = found instanceof PdfHexString ? String.fromCharCode(...found.bytes) : typeof found === "string" ? found : void 0;
262
+ if (raw === void 0) return void 0;
263
+ const text = textString(raw).replace(/\s+$/u, "");
264
+ return text.length > 0 ? text : void 0;
265
+ }
266
+ /**
267
+ * §7.9.2.2 — a text string's characters: UTF-16BE behind its byte-order mark,
268
+ * UTF-8 behind its own, and otherwise one byte to a character.
269
+ */
270
+ function textString(raw) {
271
+ if (raw.startsWith("þÿ")) {
272
+ let out = "";
273
+ for (let i = 2; i + 1 < raw.length; i += 2) out += String.fromCharCode(raw.charCodeAt(i) << 8 | raw.charCodeAt(i + 1));
274
+ return out;
275
+ }
276
+ if (raw.startsWith("")) return new TextDecoder("utf-8").decode(Uint8Array.from([...raw.slice(3)].map((c) => c.charCodeAt(0))));
277
+ return raw;
278
+ }
279
+ /**
280
+ * A code for each character a text shows, in the order it first shows them —
281
+ * so a simple face can set a text no standard encoding covers.
282
+ */
283
+ function codesOf(text) {
284
+ const codes = /* @__PURE__ */ new Map();
285
+ for (const ch of text.replace(/[\r\n]/gu, "")) if (!codes.has(ch) && codes.size < 255) codes.set(ch, codes.size + 1);
286
+ return codes;
287
+ }
288
+ /**
289
+ * The Helvetica a drawn free-text annotation is set in, its encoding spelling
290
+ * each code's character out as a glyph name (`uni0142` for ł) — which is how
291
+ * the text reads back as what it is.
292
+ */
293
+ function spelledFace(text) {
294
+ const differences = [1];
295
+ for (const ch of codesOf(text).keys()) {
296
+ const cp = ch.codePointAt(0) ?? 32;
297
+ differences.push(new PdfName(`uni${cp.toString(16).toUpperCase().padStart(4, "0")}`));
298
+ }
299
+ return new Map([
300
+ ["Type", new PdfName("Font")],
301
+ ["Subtype", new PdfName("Type1")],
302
+ ["BaseFont", new PdfName("Helvetica")],
303
+ ["Encoding", new Map([["Differences", differences]])]
304
+ ]);
305
+ }
214
306
  /** §12.7.4.3 — the field is a multi-line text box (bit 13 of `/Ff`). */
215
307
  var MULTILINE = 4096;
216
308
  /**
@@ -8,5 +8,23 @@ export interface Appearance {
8
8
  readonly ctm: Matrix;
9
9
  /** The appearance's `/Resources`, when it states its own. */
10
10
  readonly resources: PdfDict | undefined;
11
+ /** §8.10.1 — the form's `/BBox`, in its own space, which clips what it paints. */
12
+ readonly bbox?: readonly [number, number, number, number];
11
13
  }
14
+ /**
15
+ * §8.10.1 — an appearance's content as it paints: clipped to its `/BBox`.
16
+ *
17
+ * The box is the form's clip, not a hint. A form-filling tool that wrote its
18
+ * field backgrounds in PAGE coordinates inside a box forty points wide paints
19
+ * them nowhere any viewer shows — and painted anyway, with the fill colour it
20
+ * never set, bug1669099.pdf's form came back with three black slabs over its
21
+ * letterhead and its terms of payment, each twice as far across the sheet as
22
+ * the field it belonged to. The clip is installed the way the content would
23
+ * install one, so every pass that reads the appearance sees the same page.
24
+ *
25
+ * @param file The owning file.
26
+ * @param appearance The appearance.
27
+ * @returns Its content, preceded by the clip to its box where it has one.
28
+ */
29
+ export declare function appearanceContent(file: PdfFile, appearance: Appearance): Uint8Array;
12
30
  export declare function collectPageAppearances(file: PdfFile, page: PdfPage): Array<Appearance>;
@@ -2,6 +2,32 @@ import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
2
2
  import { IDENTITY, multiply } from "./content.js";
3
3
  import { drawnAppearance, drawnResources, textMarkupOf } from "./annot-draw.js";
4
4
  //#region src/pdf-reader/annots.ts
5
+ /**
6
+ * §8.10.1 — an appearance's content as it paints: clipped to its `/BBox`.
7
+ *
8
+ * The box is the form's clip, not a hint. A form-filling tool that wrote its
9
+ * field backgrounds in PAGE coordinates inside a box forty points wide paints
10
+ * them nowhere any viewer shows — and painted anyway, with the fill colour it
11
+ * never set, bug1669099.pdf's form came back with three black slabs over its
12
+ * letterhead and its terms of payment, each twice as far across the sheet as
13
+ * the field it belonged to. The clip is installed the way the content would
14
+ * install one, so every pass that reads the appearance sees the same page.
15
+ *
16
+ * @param file The owning file.
17
+ * @param appearance The appearance.
18
+ * @returns Its content, preceded by the clip to its box where it has one.
19
+ */
20
+ function appearanceContent(file, appearance) {
21
+ const data = file.streamData(appearance.stream);
22
+ const box = appearance.bbox;
23
+ if (!box) return data;
24
+ const [x0, y0, x1, y1] = box;
25
+ const clip = new TextEncoder().encode(`${String(x0)} ${String(y0)} ${String(x1 - x0)} ${String(y1 - y0)} re W n\n`);
26
+ const out = new Uint8Array(clip.length + data.length);
27
+ out.set(clip, 0);
28
+ out.set(data, clip.length);
29
+ return out;
30
+ }
5
31
  /** §12.5.3 `/F` — the annotation is not painted at all. */
6
32
  var FLAG_HIDDEN = 2;
7
33
  var FLAG_NOVIEW = 32;
@@ -35,6 +61,7 @@ function collectPageAppearances(file, page) {
35
61
  const annots = file.get(page.dict, "Annots");
36
62
  if (!Array.isArray(annots)) return [];
37
63
  const out = [];
64
+ const regenerate = needsAppearances(file);
38
65
  for (const entry of annots) {
39
66
  const annot = file.resolve(entry);
40
67
  if (!(annot instanceof Map)) continue;
@@ -44,6 +71,18 @@ function collectPageAppearances(file, page) {
44
71
  const flags = file.get(annot, "F");
45
72
  if (typeof flags === "number" && (flags & FLAG_HIDDEN || flags & FLAG_NOVIEW)) continue;
46
73
  const stream = normalAppearance(file, annot);
74
+ const typed = regenerate && isWidget(file, annot) ? drawnAppearance(file, annot) : void 0;
75
+ if (typed && typedField(file, annot)) {
76
+ const kept = stream ? withoutVariableText(file, stream) : void 0;
77
+ const rect = rectangle(file.get(annot, "Rect"));
78
+ if (kept && rect) out.push(fitted(file, kept, rect));
79
+ out.push({
80
+ stream: typed,
81
+ ctm: IDENTITY,
82
+ resources: drawnResources(file, annot)
83
+ });
84
+ continue;
85
+ }
47
86
  if (!stream) {
48
87
  const drawn = drawnAppearance(file, annot);
49
88
  if (drawn) out.push({
@@ -55,17 +94,55 @@ function collectPageAppearances(file, page) {
55
94
  }
56
95
  const rect = rectangle(file.get(annot, "Rect"));
57
96
  if (!rect) continue;
58
- const matrix = matrixOf(file, stream.dict);
59
- const bbox = rectangle(file.get(stream.dict, "BBox"));
60
- const resources = file.get(stream.dict, "Resources");
61
- out.push({
62
- stream,
63
- ctm: multiply(matrix, fitToRect(bbox, matrix, rect)),
64
- resources: resources instanceof Map ? resources : void 0
65
- });
97
+ out.push(fitted(file, stream, rect));
66
98
  }
67
99
  return out;
68
100
  }
101
+ /** An appearance stream placed on its annotation's `/Rect` (§12.5.5). */
102
+ function fitted(file, stream, rect) {
103
+ const matrix = matrixOf(file, stream.dict);
104
+ const bbox = rectangle(file.get(stream.dict, "BBox"));
105
+ const resources = file.get(stream.dict, "Resources");
106
+ return {
107
+ stream,
108
+ ctm: multiply(matrix, fitToRect(bbox, matrix, rect)),
109
+ resources: resources instanceof Map ? resources : void 0,
110
+ ...bbox ? { bbox } : {}
111
+ };
112
+ }
113
+ /** §12.7.2 — whether the form asks the viewer to build its fields' appearances. */
114
+ function needsAppearances(file) {
115
+ const acro = file.get(file.catalog, "AcroForm");
116
+ return acro instanceof Map && file.get(acro, "NeedAppearances") === true;
117
+ }
118
+ function isWidget(file, annot) {
119
+ const subtype = file.get(annot, "Subtype");
120
+ return subtype instanceof PdfName && subtype.value === "Widget";
121
+ }
122
+ /** Whether a widget belongs to a text field — `/FT /Tx`, on it or up its field tree. */
123
+ function typedField(file, annot) {
124
+ let node = annot;
125
+ for (let depth = 0; node && depth < 32; depth++) {
126
+ const type = file.get(node, "FT");
127
+ if (type instanceof PdfName) return type.value === "Tx";
128
+ const parent = file.get(node, "Parent");
129
+ node = parent instanceof Map ? parent : void 0;
130
+ }
131
+ return false;
132
+ }
133
+ /**
134
+ * §12.7.3.3 — an appearance with its variable text taken out: the part it
135
+ * marks `/Tx BMC … EMC`, which is what a viewer rebuilds from the value.
136
+ * bug1844583.pdf's stale appearance still reads "Dlrow Olleh" there, over a
137
+ * field whose value is "Hello World".
138
+ */
139
+ function withoutVariableText(file, stream) {
140
+ const stripped = new TextDecoder("latin1").decode(file.streamData(stream)).replace(/\/Tx\s+BMC[\s\S]*?EMC/gu, "");
141
+ const dict = new Map(stream.dict);
142
+ dict.delete("Filter");
143
+ dict.delete("DecodeParms");
144
+ return new PdfStream(dict, Uint8Array.from([...stripped].map((c) => c.charCodeAt(0))));
145
+ }
69
146
  /** §12.5.5 `/AP` `/N` — the normal appearance, through `/AS` when it is a set. */
70
147
  function normalAppearance(file, annot) {
71
148
  const ap = file.get(annot, "AP");
@@ -149,4 +226,4 @@ function rectangle(v) {
149
226
  ];
150
227
  }
151
228
  //#endregion
152
- export { collectPageAppearances };
229
+ export { appearanceContent, collectPageAppearances };
@@ -45,8 +45,11 @@ function parseToUnicodeCMap(bytes) {
45
45
  const hiN = bytesToInt(hi.bytes);
46
46
  const dst = lexer.nextToken();
47
47
  if (dst.kind === "hexstr") {
48
- const base = stated(utf16be(dst.bytes));
49
- if (base !== void 0) for (let i = 0; loN + i <= hiN && i < MAX_RANGE; i++) map.set(loN + i, incString(base, i));
48
+ const base = utf16be(dst.bytes);
49
+ for (let i = 0; loN + i <= hiN && i < MAX_RANGE; i++) {
50
+ const text = stated(incString(base, i));
51
+ if (text !== void 0) map.set(loN + i, text);
52
+ }
50
53
  } else if (dst.kind === "arrayOpen") {
51
54
  let i = 0;
52
55
  for (;;) {
@@ -1,6 +1,5 @@
1
- import { ColorSpaceInfo, GsPaint } from './shading.js';
1
+ import { ColorSpaceInfo, GsPaint, PageGradient } from './shading.js';
2
2
  import { TextMarkup } from './annot-draw.js';
3
- import { ShapeGradient } from '../core/vector.js';
4
3
  import { PdfDict, PdfStream } from '../pdf/objects.js';
5
4
  /**
6
5
  * A page font as the interpreter needs it (built from the font dictionaries in
@@ -136,6 +135,12 @@ export interface TextRun {
136
135
  * leaves it to the picture.
137
136
  */
138
137
  readonly invisible?: boolean;
138
+ /**
139
+ * §12.5.5 — drawn by an annotation's appearance rather than by the page: a
140
+ * field's value, a button's caption, a tick. It stands in the annotation's
141
+ * box, which is placed where the page has it, not in the page's reading.
142
+ */
143
+ readonly annotation?: boolean;
139
144
  /**
140
145
  * §9.3.6 — the colour the glyphs are STROKED in, when the rendering mode
141
146
  * asks for a stroke, and how wide the pen is.
@@ -245,7 +250,7 @@ export interface VectorPlacement {
245
250
  /** Fill colour (6-hex), present iff the path is filled (`f` / `F` / `f*` / `B` / `b`). */
246
251
  readonly fillHex?: string;
247
252
  /** Shading pattern, present iff filled with one (EP16c). */
248
- readonly gradient?: ShapeGradient;
253
+ readonly gradient?: PageGradient;
249
254
  /** §11.6.4.4 `/ca` — how opaque the fill is, when the page asked for less. */
250
255
  readonly alpha?: number;
251
256
  /**
@@ -271,10 +276,18 @@ export interface VectorPlacement {
271
276
  * known by walking into it; the `fillHex` beside this is not the fill.
272
277
  */
273
278
  readonly patternName?: string;
279
+ /** §8.7.3.3 — the colour `scn` gave an uncoloured pattern, where it gave one. */
280
+ readonly patternPaint?: string;
274
281
  /** Stroke colour (6-hex), present iff the path is stroked (`S` / `s` / `B` / `b`) — EP11. */
275
282
  readonly strokeHex?: string;
276
283
  /** Stroke width in page-space points — EP11. */
277
284
  readonly lineWidth?: number;
285
+ /** §8.4.3.6 — the stroke's dash lengths in page-space points, where it is dashed. */
286
+ readonly dash?: ReadonlyArray<number>;
287
+ /** §8.4.3.3 — the stroke's cap, where it is not the butt cap every line starts with. */
288
+ readonly cap?: 'round' | 'square';
289
+ /** §11.6.4.4 `/CA` — how opaque the stroke is, when the page asked for less than all. */
290
+ readonly strokeAlpha?: number;
278
291
  readonly mcid?: number;
279
292
  }
280
293
  /** Everything {@link interpretContent} extracts from one page's content stream. */
@@ -357,7 +370,7 @@ export type Matrix = readonly [number, number, number, number, number, number];
357
370
  export declare const IDENTITY: Matrix;
358
371
  /** Compose two {@link Matrix matrices}: `a` applied first, then `b`. */
359
372
  export declare function multiply(a: Matrix, b: Matrix): Matrix;
360
- export declare function interpretContent(bytes: Uint8Array, fonts: ReadonlyMap<string, ContentFont>, initialCtm?: Matrix, shadings?: ReadonlyMap<string, ShapeGradient>, alphas?: ReadonlyMap<string, GsPaint>, spaces?: ReadonlyMap<string, ColorSpaceInfo>, hiddenOc?: ReadonlySet<string>): InterpretResult;
373
+ export declare function interpretContent(bytes: Uint8Array, fonts: ReadonlyMap<string, ContentFont>, initialCtm?: Matrix, shadings?: ReadonlyMap<string, PageGradient>, alphas?: ReadonlyMap<string, GsPaint>, spaces?: ReadonlyMap<string, ColorSpaceInfo>, hiddenOc?: ReadonlySet<string>): InterpretResult;
361
374
  /**
362
375
  * Whether a string is wholly right-to-left: at least one letter of an RTL
363
376
  * script and nothing of any other, spaces and joiners aside.