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
@@ -1,4 +1,4 @@
1
- import { resolveFamilyStyle } from "../core/fonts/remote-fonts.js";
1
+ import { knowsFamily, resolveFamilyStyle } from "../core/fonts/remote-fonts.js";
2
2
  import { parseTtf } from "../core/font/ttf-parser.js";
3
3
  import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
4
4
  import { parseToUnicodeCMap } from "./cmap.js";
@@ -7,10 +7,10 @@ import { isZapfDingbats, zapfDingbatsChar } from "./dingbats.js";
7
7
  import { textForGlyphName } from "./glyph-names.js";
8
8
  import { cffCidToGid, cffNameToGid, cffOutlineSource, openTypeCff } from "./cff-outline.js";
9
9
  import { type1Font } from "./type1-outline.js";
10
- import { baseEncodingTable, isStandardLatinFace, standardEncodingTable } from "./encodings.js";
10
+ import { baseEncodingTable, isStandardLatinFace, macGlyphName, standardEncodingTable } from "./encodings.js";
11
11
  import { outlineSource, postGlyphNames } from "./glyf-outline.js";
12
12
  import { standardFace, standardWidth } from "./standard-widths.js";
13
- import { embeddedFontName, hasLiftableProgram } from "./embedded-fonts.js";
13
+ import { eachPageFont, embeddedFontName, hasLiftableProgram, programStyle } from "./embedded-fonts.js";
14
14
  //#region src/pdf-reader/font.ts
15
15
  /** §9.10.2 — what a composite code nothing can answer for comes to. */
16
16
  var UNANSWERABLE = "�";
@@ -40,6 +40,8 @@ function buildContentFont(file, fontDict) {
40
40
  if (isType0 && !encodingNamed) codeBytes = parsed.codeBytes;
41
41
  }
42
42
  const fromProgram = isType0 && toUnicode.size === 0 ? embeddedCmap(file, fontDict) : void 0;
43
+ const fromCids = isType0 && toUnicode.size === 0 && fromProgram === void 0 ? unicodeCids(file, fontDict) : void 0;
44
+ const fromCore = isType0 && toUnicode.size === 0 && fromProgram === void 0 && fromCids === void 0 ? coreGlyphOrder(file, fontDict) : void 0;
43
45
  let programFallback = null;
44
46
  const fromProgramFor = (code) => {
45
47
  if (!isType0 || toUnicode.size === 0) return void 0;
@@ -47,16 +49,27 @@ function buildContentFont(file, fontDict) {
47
49
  return programFallback?.get(code);
48
50
  };
49
51
  const glyphs = isType0 ? void 0 : simpleGlyphs(file, fontDict);
52
+ let byIndex = null;
53
+ const numbering = isType0 ? void 0 : numberingOf(differences(file, fontDict).values());
54
+ const fromIndex = (name) => {
55
+ const gid = numberedGlyph(name, numbering);
56
+ if (gid === void 0) return void 0;
57
+ byIndex ??= programCharacters(file, fontDict);
58
+ return byIndex?.get(gid);
59
+ };
50
60
  const fromNames = !isType0 ? namedGlyphs(file, fontDict, toUnicode.size > 0, {
51
61
  draws: (name) => glyphs?.byName(name) !== void 0,
52
- blank: (name) => glyphs?.blank(name) === true
62
+ blank: (name) => glyphs?.blank(name) === true,
63
+ character: fromIndex
53
64
  }) : void 0;
54
65
  const glyphNames = isType0 ? /* @__PURE__ */ new Map() : differences(file, fontDict);
55
- const unicode = fromProgram ?? (toUnicode.size > 0 ? toUnicode : fromNames ?? toUnicode);
66
+ const unicode = fromProgram ?? fromCids ?? fromCore ?? (toUnicode.size > 0 ? toUnicode : fromNames ?? toUnicode);
56
67
  const bytesPerCode = codeBytes;
57
68
  const indices = !isType0 && glyphs?.indexed === true && fromNames === void 0 && toUnicode.size === 0 && glyphNames.size === 0;
58
69
  const dingbats = !isType0 && isZapfDingbats(asName(file.resolve(fontDict.get("BaseFont") ?? PDF_NULL)));
59
- const baseNames = isType0 ? void 0 : baseEncoding(file, fontDict);
70
+ const stated = file.resolve(fontDict.get("Encoding") ?? PDF_NULL);
71
+ const statesEncoding = stated instanceof Map || stated instanceof PdfName && baseEncodingTable(stated.value) !== void 0;
72
+ const baseNames = isType0 ? void 0 : statesEncoding ? baseEncoding(file, fontDict) : glyphs?.builtIn ?? baseEncoding(file, fontDict);
60
73
  const fromBase = /* @__PURE__ */ new Map();
61
74
  for (const [code, glyph] of baseNames ?? []) {
62
75
  const text = textForGlyphName(glyph);
@@ -66,7 +79,7 @@ function buildContentFont(file, fontDict) {
66
79
  const style = faceStyle(file, fontDict, isType0);
67
80
  const name = runFontName(file, fontDict, isType0);
68
81
  const type3 = asName(file.resolve(fontDict.get("Subtype") ?? PDF_NULL)) === "Type3" ? type3Face(file, fontDict) : void 0;
69
- const decodeOne = (code) => (indices ? UNANSWERABLE : void 0) ?? unicode.get(code) ?? fromProgramFor(code) ?? (named ? decodePredefined(named, code) : void 0) ?? fromNames?.get(code) ?? (dingbats ? zapfDingbatsChar(code) : void 0) ?? fromBase.get(code) ?? (bytesPerCode === 1 ? latin1(code) : UNANSWERABLE);
82
+ const decodeOne = (code) => (indices ? UNANSWERABLE : void 0) ?? unicode.get(code) ?? fromProgramFor(code) ?? (named ? decodePredefined(named, code) : void 0) ?? fromNames?.get(code) ?? (glyphNames.get(code) === NOTDEF ? "" : void 0) ?? (dingbats ? zapfDingbatsChar(code) : void 0) ?? fromBase.get(code) ?? (bytesPerCode === 1 ? latin1(code) : UNANSWERABLE);
70
83
  const simple = simpleWidths(file, fontDict, decodeOne);
71
84
  const width = isType0 ? cidWidths(file, fontDict) : type3 ? (code) => simple(code) * type3.matrix[0] * 1e3 : simple;
72
85
  return {
@@ -76,7 +89,7 @@ function buildContentFont(file, fontDict) {
76
89
  ...named?.vertical ? { verticalAdvance: cidVerticalAdvance(file, fontDict) } : {},
77
90
  ...type3 ? { type3 } : {},
78
91
  ...name !== void 0 ? { name } : {},
79
- decode: (codes) => codes.map((c) => readable(decodeOne(c))).join(""),
92
+ decode: (codes) => codes.map((c) => lettersOf(readable(decodeOne(c)))).join(""),
80
93
  width,
81
94
  ...style
82
95
  };
@@ -114,6 +127,32 @@ function readable(text) {
114
127
  }
115
128
  return out;
116
129
  }
130
+ /**
131
+ * Unicode's presentation forms of the Latin ligatures, U+FB00–U+FB06, as the
132
+ * letters they join.
133
+ *
134
+ * A producer maps a ligature glyph to its compatibility character, and carried
135
+ * as that the word neither searches nor edits as the letters it is — and a
136
+ * face with no such form sets it in another's, wider: copy_paste_ligatures.pdf
137
+ * is one line in Times, and its "fffiflffifflſtst" pushed the line off its page. The long
138
+ * s of `ſt` is kept: it is a letter of its own.
139
+ */
140
+ function lettersOf(text) {
141
+ return LIGATURE.test(text) ? text.replace(LIGATURES, (c) => LATIN_LIGATURES.get(c) ?? c) : text;
142
+ }
143
+ var LIGATURE = /[\uFB00-\uFB06]/u;
144
+ var LIGATURES = /[\uFB00-\uFB06]/gu;
145
+ var LATIN_LIGATURES = new Map([
146
+ ["ff", "ff"],
147
+ ["fi", "fi"],
148
+ ["fl", "fl"],
149
+ ["ffi", "ffi"],
150
+ ["ffl", "ffl"],
151
+ ["ſt", "ſt"],
152
+ ["st", "st"]
153
+ ]);
154
+ /** The glyph name that selects no glyph (§9.6.6.1). */
155
+ var NOTDEF = ".notdef";
117
156
  /** The last Unicode code point in the BMP, and the surrogate block inside it. */
118
157
  var BMP_END = 65535;
119
158
  var SURROGATE_FIRST = 55296;
@@ -133,10 +172,130 @@ var SURROGATE_LAST = 57343;
133
172
  */
134
173
  function embeddedCmap(file, fontDict) {
135
174
  const cidFont = descendantFont(file, fontDict);
175
+ const byGlyph = programCharacters(file, cidFont);
176
+ if (byGlyph === void 0) return void 0;
177
+ const cidToGid = readCidToGid(file, cidFont);
178
+ if (!cidToGid) return byGlyph;
179
+ const out = /* @__PURE__ */ new Map();
180
+ cidToGid.forEach((gid, cid) => {
181
+ const text = byGlyph.get(gid);
182
+ if (text !== void 0) out.set(cid, text);
183
+ });
184
+ return out.size > 0 ? out : void 0;
185
+ }
186
+ /**
187
+ * §9.7.4 — the characters of a composite font that embeds no program, names a
188
+ * core face and states no `/ToUnicode`: its codes are glyph indices into the
189
+ * face the reader's machine carries under that name, and the core faces put
190
+ * their first 258 glyphs in the standard Macintosh order (the `post` table's
191
+ * format 1 names).
192
+ *
193
+ * issue11242_reduced.pdf shows "VAT Code" in a Verdana it does not embed.
194
+ * Nothing in the file says what its codes are but the face's own order, and
195
+ * read as characters they are "9$7&RGH"; read as nothing, the page came back
196
+ * blank. Only faces known to keep that order are read this way — a face's
197
+ * order is its own, and Calibri's is not this one.
198
+ *
199
+ * @param file The owning file.
200
+ * @param fontDict The `/Type0` font dictionary.
201
+ * @returns Code → character, or `undefined` where the font is not such a one.
202
+ */
203
+ function coreGlyphOrder(file, fontDict) {
204
+ const encoding = asName(file.resolve(fontDict.get("Encoding") ?? PDF_NULL));
205
+ if (encoding !== "Identity-H" && encoding !== "Identity-V") return void 0;
206
+ const cidFont = descendantFont(file, fontDict);
207
+ if (asName(file.resolve(cidFont.get("Subtype") ?? PDF_NULL)) !== "CIDFontType2") return void 0;
208
+ const map = file.resolve(cidFont.get("CIDToGIDMap") ?? PDF_NULL);
209
+ if (map !== PDF_NULL && !(map instanceof PdfName && map.value === "Identity")) return void 0;
210
+ const descriptor = file.resolve(cidFont.get("FontDescriptor") ?? PDF_NULL);
211
+ if (descriptor instanceof Map && [
212
+ "FontFile",
213
+ "FontFile2",
214
+ "FontFile3"
215
+ ].some((k) => descriptor.has(k))) return void 0;
216
+ const family = familyOfFace(asName(file.resolve(fontDict.get("BaseFont") ?? PDF_NULL)));
217
+ if (!CORE_FACES.has(family.toLowerCase().replace(/[^a-z]/gu, ""))) return void 0;
218
+ const out = /* @__PURE__ */ new Map();
219
+ for (let gid = 0; gid < MAC_GLYPHS; gid++) {
220
+ const name = macGlyphName(gid);
221
+ const text = name === void 0 ? void 0 : textForGlyphName(name);
222
+ if (text !== void 0 && text !== "") out.set(gid, text);
223
+ }
224
+ return out;
225
+ }
226
+ /** The core faces whose first glyphs stand in the standard Macintosh order. */
227
+ var CORE_FACES = new Set([
228
+ "arial",
229
+ "timesnewroman",
230
+ "couriernew",
231
+ "verdana",
232
+ "tahoma",
233
+ "georgia",
234
+ "trebuchetms"
235
+ ]);
236
+ /** How many glyphs the standard Macintosh order names. */
237
+ var MAC_GLYPHS = 258;
238
+ /**
239
+ * §9.7.4.2 — the CIDs of a composite font that ARE characters.
240
+ *
241
+ * A `/CIDToGIDMap` stream says the CID is not the glyph index, and TCPDF —
242
+ * with tFPDF and mPDF after it — makes it the character's own Unicode value,
243
+ * mapping every character of the face to its glyph whether the subset kept
244
+ * the glyph or not. Without a `/ToUnicode`, and with a subset that carries no
245
+ * `cmap`, that was the only statement of what the glyphs are and it went
246
+ * unread: bug1650302_reduced.pdf's "Výbava na přání" came back as drawings,
247
+ * the `ř` missing from them. Read as characters, the words come back.
248
+ *
249
+ * Only where the map itself says so. A producer that keeps the ORIGINAL
250
+ * glyph index as the CID routes it through a stream too, and read as Unicode
251
+ * complex_ttf_font.pdf's Arabic would come back as `$&')`. What tells the two
252
+ * apart is the space: a map of characters sends U+0020 to a glyph that draws
253
+ * nothing and no control character anywhere, where a map of indices sends 3 —
254
+ * the space in the fonts they come from — and 32 to a glyph with ink on it.
255
+ */
256
+ function unicodeCids(file, fontDict) {
257
+ const encoding = asName(file.resolve(fontDict.get("Encoding") ?? PDF_NULL));
258
+ if (encoding !== "Identity-H" && encoding !== "Identity-V") return void 0;
259
+ const cidFont = descendantFont(file, fontDict);
260
+ const cidToGid = readCidToGid(file, cidFont);
261
+ if (!cidToGid || cidToGid.slice(0, SPACE).some((gid) => gid !== 0)) return void 0;
262
+ const space = cidToGid[SPACE];
263
+ if (space === void 0 || space === 0) return void 0;
136
264
  const descriptor = file.resolve(cidFont.get("FontDescriptor") ?? PDF_NULL);
137
265
  if (!(descriptor instanceof Map)) return void 0;
138
266
  const program = file.resolve(descriptor.get("FontFile2") ?? PDF_NULL);
139
267
  if (!(program instanceof PdfStream)) return void 0;
268
+ let blank;
269
+ try {
270
+ const glyf = outlineSource(file.streamData(program));
271
+ blank = glyf !== void 0 && space < glyf.count && glyf.path(space) === void 0;
272
+ } catch {
273
+ return;
274
+ }
275
+ if (!blank) return void 0;
276
+ const out = /* @__PURE__ */ new Map();
277
+ cidToGid.forEach((gid, cid) => {
278
+ if (gid !== 0 && !(cid >= SURROGATE_FIRST && cid <= SURROGATE_LAST)) out.set(cid, String.fromCharCode(cid));
279
+ });
280
+ return out;
281
+ }
282
+ /** U+0020, the one character whose glyph every text face leaves blank. */
283
+ var SPACE = 32;
284
+ /**
285
+ * The character each glyph of a font's embedded TrueType program stands for:
286
+ * its `cmap` read backwards.
287
+ *
288
+ * @param file The document.
289
+ * @param owner The dictionary that holds the descriptor — the font itself, or
290
+ * a composite font's descendant.
291
+ * @returns Glyph index → character, or `undefined` where there is no program
292
+ * or it maps nothing.
293
+ */
294
+ function programCharacters(file, owner) {
295
+ const descriptor = file.resolve(owner.get("FontDescriptor") ?? PDF_NULL);
296
+ if (!(descriptor instanceof Map)) return void 0;
297
+ const program = file.resolve(descriptor.get("FontFile2") ?? PDF_NULL);
298
+ if (!(program instanceof PdfStream)) return void 0;
140
299
  let glyphOf;
141
300
  try {
142
301
  glyphOf = parseTtf(file.streamData(program)).glyphForCodepoint;
@@ -154,15 +313,7 @@ function embeddedCmap(file, fontDict) {
154
313
  }
155
314
  if (gid > 0 && !byGlyph.has(gid)) byGlyph.set(gid, String.fromCodePoint(cp));
156
315
  }
157
- if (byGlyph.size === 0) return void 0;
158
- const cidToGid = readCidToGid(file, cidFont);
159
- if (!cidToGid) return byGlyph;
160
- const out = /* @__PURE__ */ new Map();
161
- cidToGid.forEach((gid, cid) => {
162
- const text = byGlyph.get(gid);
163
- if (text !== void 0) out.set(cid, text);
164
- });
165
- return out.size > 0 ? out : void 0;
316
+ return byGlyph.size > 0 ? byGlyph : void 0;
166
317
  }
167
318
  /**
168
319
  * §9.6.6 — the outline a code draws, for a code that stands for no character.
@@ -278,6 +429,7 @@ function simpleGlyphs(file, fontDict) {
278
429
  const typeOne = file.resolve(descriptor.get("FontFile") ?? PDF_NULL);
279
430
  const truetype = file.resolve(descriptor.get("FontFile2") ?? PDF_NULL);
280
431
  const compact = file.resolve(descriptor.get("FontFile3") ?? PDF_NULL);
432
+ const numbering = numberingOf(differences(file, fontDict).values());
281
433
  try {
282
434
  if (typeOne instanceof PdfStream) {
283
435
  const face = type1Font(file.streamData(typeOne));
@@ -295,7 +447,7 @@ function simpleGlyphs(file, fontDict) {
295
447
  if (glyf) {
296
448
  const named = postGlyphNames(bytes);
297
449
  const gidOf = (name) => {
298
- const gid = named?.get(name) ?? numberedGlyph(name);
450
+ const gid = named?.get(name) ?? numberedGlyph(name, numbering);
299
451
  return gid !== void 0 && gid < glyf.count ? gid : void 0;
300
452
  };
301
453
  return {
@@ -316,7 +468,7 @@ function simpleGlyphs(file, fontDict) {
316
468
  if (!outlines) return void 0;
317
469
  const names = cffNameToGid(cff);
318
470
  const gidOf = (name) => {
319
- const gid = names?.get(name) ?? numberedGlyph(name);
471
+ const gid = names?.get(name) ?? numberedGlyph(name, numbering);
320
472
  return gid !== void 0 && gid < outlines.count ? gid : void 0;
321
473
  };
322
474
  return {
@@ -340,9 +492,13 @@ function simpleGlyphs(file, fontDict) {
340
492
  * INDEX — `g24`, `glyph24`, `index24`, `cid24` — and that name is then the only
341
493
  * way back to the outline. bug1151216.pdf names them `g24`, `g381`, `g3`, and
342
494
  * its three lines of prices are drawn from nothing else.
495
+ *
496
+ * @param name The glyph's name.
497
+ * @param numbering How the font writes the number, where its names say (see
498
+ * {@link numberingOf}); four digits are hexadecimal otherwise.
343
499
  */
344
- function numberedGlyph(name) {
345
- const hex = /^g([0-9a-f]{4})$/u.exec(name);
500
+ function numberedGlyph(name, numbering) {
501
+ const hex = numbering !== "decimal" ? /^g([0-9a-f]{4})$/u.exec(name) : null;
346
502
  if (hex) return Number.parseInt(hex[1], 16);
347
503
  const m = /^(?:g|glyph|index|cid|G)(\d+)$/u.exec(name);
348
504
  const n = m ? Number(m[1]) : NaN;
@@ -350,6 +506,33 @@ function numberedGlyph(name) {
350
506
  }
351
507
  /** No font holds more glyphs than this; a bigger number is not an index. */
352
508
  var MAX_GLYPH_INDEX = 65536;
509
+ /**
510
+ * How a font's index names write their numbers, where its names say.
511
+ *
512
+ * A subsetter that pads the number to four digits writes it in hexadecimal —
513
+ * bug1027533.pdf's `g0024` is glyph 36 — and one that does not pad writes it
514
+ * in decimal. One name cannot tell the two apart: bug1151216.pdf names its
515
+ * glyphs `g24`, `g381` and `g1004`, and `g1004` is four digits either way.
516
+ * Read as hexadecimal it is glyph 4100, which no subset of a thousand glyphs
517
+ * holds: five of the file's codes drew nothing, fell back to Latin-1, and its
518
+ * prices came back "$@'' for 1" set over the glyphs that were drawn. The rest
519
+ * of the font's names say which it is — a leading zero or a letter is
520
+ * hexadecimal, a number of any length but four is decimal.
521
+ *
522
+ * @param names The glyph names the font's `/Differences` gives.
523
+ * @returns The numbering, or `undefined` where the names do not say.
524
+ */
525
+ function numberingOf(names) {
526
+ let hex = false;
527
+ let decimal = false;
528
+ for (const name of names) {
529
+ const digits = /^g([0-9a-f]+)$/u.exec(name)?.[1];
530
+ if (digits === void 0) continue;
531
+ if (/[a-f]/u.test(digits) || digits.length === 4 && digits.startsWith("0")) hex = true;
532
+ else if (digits.length !== 4) decimal = true;
533
+ }
534
+ return hex === decimal ? void 0 : hex ? "hex" : "decimal";
535
+ }
353
536
  /** §9.7.4.2 `/CIDToGIDMap` — a stream of two-byte glyph indices, CID by CID. */
354
537
  function readCidToGid(file, cidFont) {
355
538
  const map = file.resolve(cidFont.get("CIDToGIDMap") ?? PDF_NULL);
@@ -419,7 +602,7 @@ function namedGlyphs(file, fontDict, statesUnicode, glyph) {
419
602
  if (names.size === 0) return void 0;
420
603
  const out = /* @__PURE__ */ new Map();
421
604
  for (const [code, name] of names) {
422
- const text = textForGlyphName(name);
605
+ const text = textForGlyphName(name) ?? glyph.character(name);
423
606
  if (text !== void 0) {
424
607
  out.set(code, text);
425
608
  continue;
@@ -532,7 +715,8 @@ function differences(file, fontDict) {
532
715
  function runFontName(file, fontDict, isType0) {
533
716
  const name = embeddedFontName(file, fontDict);
534
717
  if (name === void 0 || hasLiftableProgram(file, fontDict)) return name;
535
- if (resolveFamilyStyle(name).key !== "arimo") return name;
718
+ const face = familyOfFace(asName(file.resolve(fontDict.get("BaseFont") ?? PDF_NULL)));
719
+ if (knowsFamily(name) || knowsFamily(face)) return name;
536
720
  const owner = isType0 ? descendantFont(file, fontDict) : fontDict;
537
721
  const descriptor = file.resolve(owner.get("FontDescriptor") ?? PDF_NULL);
538
722
  if (!(descriptor instanceof Map)) return name;
@@ -541,6 +725,101 @@ function runFontName(file, fontDict, isType0) {
541
725
  if ((flags & FLAG_SERIF) !== 0) return `${name} serif`;
542
726
  return name;
543
727
  }
728
+ /**
729
+ * For every face a run may name, the family a word processor knows it by —
730
+ * keyed by the name the run carries (see {@link FaceFamily}).
731
+ *
732
+ * A PDF names the FACE and a .docx names a family: written as the face,
733
+ * `inter-semibold` was a font no reader has, and LibreOffice set the whole of
734
+ * an invoice drawn in Inter in its default serif. The family is the one the
735
+ * descriptor states (§9.8.1 `/FontFamily`) or, where it states none, the one
736
+ * the PostScript name is made of.
737
+ *
738
+ * @param file The document.
739
+ * @param pages The pages whose faces are wanted.
740
+ * @returns Run font name → its family.
741
+ */
742
+ function collectFaceFamilies(file, pages) {
743
+ const out = /* @__PURE__ */ new Map();
744
+ eachPageFont(file, pages, (fontDict) => {
745
+ const isType0 = asName(file.resolve(fontDict.get("Subtype") ?? PDF_NULL)) === "Type0";
746
+ const key = runFontName(file, fontDict, isType0);
747
+ if (key === void 0 || out.has(key)) return;
748
+ const owner = isType0 ? descendantFont(file, fontDict) : fontDict;
749
+ const descriptor = file.resolve(owner.get("FontDescriptor") ?? PDF_NULL);
750
+ const stated = descriptor instanceof Map ? file.resolve(descriptor.get("FontFamily") ?? PDF_NULL) : PDF_NULL;
751
+ const family = typeof stated === "string" && /^[\x20-\x7e]+$/u.test(stated.trim()) ? stated.trim() : familyOfFace(cidFontName(file, fontDict, isType0));
752
+ if (family.length === 0) return;
753
+ const flags = descriptor instanceof Map ? asNumber(file.resolve(descriptor.get("Flags") ?? PDF_NULL), 0) : 0;
754
+ out.set(key, {
755
+ family,
756
+ generic: genericOf(family, flags)
757
+ });
758
+ });
759
+ return out;
760
+ }
761
+ /**
762
+ * §9.7.6.1 — a composite font's `/BaseFont` is its CIDFont's name, a hyphen and
763
+ * the name of the CMap it is encoded by (`HeiseiMin-W3-UniJIS-UCS2-H`); the
764
+ * face is the part before the CMap's.
765
+ */
766
+ function cidFontName(file, fontDict, isType0) {
767
+ const base = asName(file.resolve(fontDict.get("BaseFont") ?? PDF_NULL));
768
+ const cmap = isType0 ? asName(file.resolve(fontDict.get("Encoding") ?? PDF_NULL)) : "";
769
+ return cmap !== "" && base.endsWith(`-${cmap}`) ? base.slice(0, -cmap.length - 1) : base;
770
+ }
771
+ /**
772
+ * The family a PostScript face name is made of: `Inter-SemiBold` → `Inter`,
773
+ * `ArialMT` → `Arial`, `TimesNewRomanPS-BoldMT` → `Times New Roman`.
774
+ *
775
+ * §9.6.2.1 names a face `Family-Style` (Word writes `Family,Style`), with the
776
+ * family's words run together. What follows the separator is a style only if
777
+ * it is made of style words — `MS-Mincho` is a family of its own — and the
778
+ * words come apart where the capitals say they do.
779
+ */
780
+ function familyOfFace(baseFont) {
781
+ const name = plainFace(baseFont);
782
+ const cut = /^(.+?)[-,]([^-,]+)$/u.exec(name);
783
+ const whole = cut && STYLE_WORDS.test(cut[2]) ? cut[1] : cut ? name.replace(/-/gu, " ") : GLUED_STYLE.exec(name)?.[1] ?? name;
784
+ return (whole.replace(/(?:PSMT|PS|MT)$/u, "") || whole).replace(/([a-z])([A-Z])/gu, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/gu, "$1 $2").replace(/\bDeja Vu\b/u, "DejaVu").replace(/\s+/gu, " ").trim();
785
+ }
786
+ /**
787
+ * A face name with what producers write around it taken off: the subset tag
788
+ * (§9.6.4) and, after the face, the charset Acrobat's Office export appends
789
+ * (`Arial,Bold-WinCharSetFFFF`, `-H2`), the encoding a CIDFont was cut for
790
+ * (`-Identity-H`, `-OneByteIdentityH`), a serial number, and the slant or
791
+ * stretch an interpreter synthesised (`-Slant_167`, `-Extend_850`). Left on,
792
+ * bug900822.pdf's .docx asked for "Courier New,Bold Win Char Set FFFF", which
793
+ * no machine has, over the Courier New every machine does.
794
+ */
795
+ function plainFace(baseFont) {
796
+ let name = baseFont.replace(/^[A-Z]{6}\+/u, "").replace(/^[!-/:-@[-`{-~]+/u, "").trim();
797
+ for (let next = name.replace(PRODUCER_TAIL, ""); next !== name;) {
798
+ name = next;
799
+ next = name.replace(PRODUCER_TAIL, "");
800
+ }
801
+ return name;
802
+ }
803
+ /** One piece of producer debris at the end of a face name (see `plainFace`). */
804
+ var PRODUCER_TAIL = /[-_](?:WinCharSet[0-9A-F]+|Identity-[HV]|OneByteIdentity[HV]|H\d+|\d+|(?:Slant|Extend)_\d+)$/iu;
805
+ /**
806
+ * A style run on to a family with no separator — `CalibriBold` — which is the
807
+ * family's name only with the style taken off. Whether the face IS bold is the
808
+ * program's to say (see `faceStyle`): `NewBasrahBold` is a family of its own.
809
+ */
810
+ var GLUED_STYLE = /^(.*?[a-z])((?:Semi|Demi|Extra|Ultra)?(?:Bold|Black|Heavy)(?:Italic|Oblique)?|Italic|Oblique)$/u;
811
+ /** A PostScript style part, made of nothing but style words (`SemiBoldItalic`, `BoldMT`, `Regu`). */
812
+ var STYLE_WORDS = /^(?:regular|regu|roman|book|normal|plain|medium|medi|light|thin|hairline|extra|ultra|semi|demi|bold|bd|black|heavy|italic|ital|it|oblique|obl|condensed|cond|narrow|compressed|extended|mt|ps)+$/iu;
813
+ /** §17.8.3.10 — the kind of face a family is, from the descriptor or, failing that, its name. */
814
+ function genericOf(family, flags) {
815
+ if (!knowsFamily(family)) {
816
+ if ((flags & FLAG_FIXED_PITCH) !== 0) return "modern";
817
+ if ((flags & FLAG_SERIF) !== 0) return "roman";
818
+ }
819
+ const key = resolveFamilyStyle(family).key;
820
+ if (key === "cousine") return "modern";
821
+ return key === "tinos" || key === "caladea" ? "roman" : "swiss";
822
+ }
544
823
  /** §9.8.2 `/Flags` — bit 1 is FixedPitch, bit 2 Serif (bits numbered from 1). */
545
824
  var FLAG_FIXED_PITCH = 1;
546
825
  var FLAG_SERIF = 2;
@@ -575,8 +854,11 @@ function faceStyle(file, fontDict, isType0) {
575
854
  const flags = asNumber(file.resolve(descriptor.get("Flags") ?? PDF_NULL), 0);
576
855
  const weightVal = file.resolve(descriptor.get("FontWeight") ?? PDF_NULL);
577
856
  const slant = asNumber(file.resolve(descriptor.get("ItalicAngle") ?? PDF_NULL), 0);
578
- const bold = typeof weightVal === "number" && weightVal >= LIGHTEST_WEIGHT ? asNumber(weightVal, 0) >= BOLD_WEIGHT : (flags & FLAG_FORCE_BOLD) !== 0 || named.bold;
579
- const italic = slant !== 0 || (flags & FLAG_ITALIC) !== 0 || named.italic;
857
+ const stated = typeof weightVal === "number" && weightVal >= LIGHTEST_WEIGHT;
858
+ const own = programStyle(file, fontDict);
859
+ const forced = (flags & FLAG_FORCE_BOLD) !== 0 && !named.light;
860
+ const bold = stated ? asNumber(weightVal, 0) >= BOLD_WEIGHT : forced || own?.bold === true || named.bold;
861
+ const italic = slant !== 0 || (flags & FLAG_ITALIC) !== 0 || own?.italic === true || named.italic;
580
862
  return {
581
863
  ...bold ? { bold: true } : {},
582
864
  ...italic ? { italic: true } : {}
@@ -597,11 +879,13 @@ function faceStyle(file, fontDict, isType0) {
597
879
  * set two lines heavy that no reader sets heavy. `Times-Bold` is.
598
880
  */
599
881
  function styleFromName(baseFont) {
600
- const name = baseFont.replace(/^[A-Z]{6}\+/u, "");
882
+ const name = plainFace(baseFont);
601
883
  const style = /[-,]([A-Za-z]+)$/u.exec(name)?.[1] ?? "";
884
+ const bold = /bold|black|heavy|semib|demi/iu.test(style);
602
885
  return {
603
- bold: /bold|black|heavy|semib|demi/iu.test(style),
604
- italic: /italic|oblique/iu.test(style)
886
+ bold,
887
+ italic: /italic|oblique/iu.test(style),
888
+ light: !bold && /light|thin|hairline|book|regular|roman|normal|medium/iu.test(style)
605
889
  };
606
890
  }
607
891
  /** §9.7.4 — a `/Type0` font's one descendant CIDFont, which owns the descriptor. */
@@ -661,4 +945,4 @@ function asNumber(v, dflt) {
661
945
  return typeof v === "number" ? v : dflt;
662
946
  }
663
947
  //#endregion
664
- export { buildContentFont };
948
+ export { buildContentFont, collectFaceFamilies };
@@ -29,8 +29,19 @@ function outlineSource(program) {
29
29
  if (at + stride * 2 > loca.offset + loca.length) return void 0;
30
30
  const read = (o) => longLoca ? safeUint32(view, o) : safeUint16(view, o) * 2;
31
31
  const start = read(at);
32
- const end = read(at + stride);
33
- if (end <= start) return void 0;
32
+ let end = read(at + stride);
33
+ if (end === start) return void 0;
34
+ if (end < start) {
35
+ end = glyf.length;
36
+ for (let o = at + stride * 2; o + stride <= loca.offset + loca.length; o += stride) {
37
+ const next = read(o);
38
+ if (next > start) {
39
+ end = Math.min(next, glyf.length);
40
+ break;
41
+ }
42
+ }
43
+ if (end <= start) return void 0;
44
+ }
34
45
  if (glyf.offset + end > program.length) return void 0;
35
46
  return {
36
47
  start: glyf.offset + start,
@@ -0,0 +1,18 @@
1
+ import { PathSeg } from './content.js';
2
+ /** A glyph's contours, and the box they fill, in the page's y-up space. */
3
+ export interface GlyphShape {
4
+ readonly segs: ReadonlyArray<PathSeg>;
5
+ readonly minX: number;
6
+ readonly minY: number;
7
+ readonly maxX: number;
8
+ readonly maxY: number;
9
+ }
10
+ /**
11
+ * The punctuation mark a glyph's shape is, where it can be only one.
12
+ *
13
+ * @param glyph The glyph as the page draws it.
14
+ * @param baseline The y of the line it stands on.
15
+ * @param size The size the line is set at, which the shape is measured in.
16
+ * @returns The character, or `undefined` where the shape does not say.
17
+ */
18
+ export declare function punctuationOf(glyph: GlyphShape, baseline: number, size: number): string | undefined;
@@ -0,0 +1,57 @@
1
+ //#region src/pdf-reader/glyph-shapes.ts
2
+ /**
3
+ * The punctuation mark a glyph's shape is, where it can be only one.
4
+ *
5
+ * @param glyph The glyph as the page draws it.
6
+ * @param baseline The y of the line it stands on.
7
+ * @param size The size the line is set at, which the shape is measured in.
8
+ * @returns The character, or `undefined` where the shape does not say.
9
+ */
10
+ function punctuationOf(glyph, baseline, size) {
11
+ if (!(size > 0)) return void 0;
12
+ const width = (glyph.maxX - glyph.minX) / size;
13
+ const height = (glyph.maxY - glyph.minY) / size;
14
+ const bottom = (glyph.minY - baseline) / size;
15
+ const top = (glyph.maxY - baseline) / size;
16
+ const contours = glyph.segs.filter((s) => s.op === "move").length;
17
+ if (contours === 1 && height <= FLAT_EM && width >= height * 2.5) {
18
+ if (bottom < .15 || top > .6) return void 0;
19
+ if (width < HYPHEN_LONGEST_EM) return "-";
20
+ return width < EN_DASH_LONGEST_EM ? "–" : "—";
21
+ }
22
+ if (contours === 2 && width <= DOT_WIDEST_EM && height >= .3 && height <= .8) {
23
+ if (bottom > .15) return void 0;
24
+ return bottom < -.05 ? ";" : ":";
25
+ }
26
+ if (contours === 1 && width <= DOT_WIDEST_EM && height <= .4 && top <= .3) {
27
+ if (bottom >= -.05 && bottom <= .05 && height <= DOT_WIDEST_EM) return ".";
28
+ return bottom < -.05 ? "," : void 0;
29
+ }
30
+ if (contours === 1 && width <= .45 && height >= .75 && bottom <= -.05 && top >= .6) {
31
+ const peak = highestPoint(glyph.segs);
32
+ if (peak === void 0) return void 0;
33
+ return peak > (glyph.minX + glyph.maxX) / 2 ? "(" : ")";
34
+ }
35
+ }
36
+ /** The x of the highest point a glyph's contours pass through. */
37
+ function highestPoint(segs) {
38
+ let best;
39
+ for (const s of segs) {
40
+ if (s.op === "close") continue;
41
+ if (best === void 0 || s.y > best.y) best = {
42
+ x: s.x,
43
+ y: s.y
44
+ };
45
+ }
46
+ return best?.x;
47
+ }
48
+ /** The tallest a bar may be, in ems, and still be a dash rather than a block. */
49
+ var FLAT_EM = .15;
50
+ /** The longest a hyphen is, in ems; past it the bar is a dash. */
51
+ var HYPHEN_LONGEST_EM = .42;
52
+ /** The longest an en dash is, in ems; past it, an em dash. */
53
+ var EN_DASH_LONGEST_EM = .7;
54
+ /** The widest a dot is, in ems. */
55
+ var DOT_WIDEST_EM = .22;
56
+ //#endregion
57
+ export { punctuationOf };