reamkit 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,9 +20,9 @@ Runtime dependencies are minimal: `fflate` (ZIP/Deflate) and `fast-xml-parser`.
20
20
 
21
21
  Parse once into the format-neutral interlayer, convert to any target. The
22
22
  format (docx/xlsx) is sniffed from the bytes; no fonts to wire up — an open
23
- substitute font (Roboto for sans, Tinos for serif, Cousine for monospace — the
24
- same families LibreOffice substitutes) is fetched automatically based on the
25
- document's referenced fonts:
23
+ metric-compatible substitute font (Arimo for sans, Tinos for serif, Cousine for
24
+ monospace, plus Carlito/Caladea for Calibri/Cambria — the same families LibreOffice
25
+ substitutes) is fetched automatically based on the document's referenced fonts:
26
26
 
27
27
  ```ts
28
28
  import { Ream } from 'reamkit';
@@ -4,4 +4,4 @@ export interface FontMeasure {
4
4
  readonly textWidthPt: (text: string, fontSize: number) => number;
5
5
  readonly encodeTextAsCidHex: (text: string) => string;
6
6
  }
7
- export declare function createFontMeasure(parsed: ParsedTtf): FontMeasure;
7
+ export declare function createFontMeasure(parsed: ParsedTtf, kern?: boolean): FontMeasure;
@@ -1,7 +1,9 @@
1
1
  import { shapeText } from "./opentype-layout.js";
2
2
  //#region src/core/font/measure.ts
3
- function createFontMeasure(parsed) {
3
+ var EMPTY_KERNING = /* @__PURE__ */ new Map();
4
+ function createFontMeasure(parsed, kern = true) {
4
5
  const scale = 1e3 / parsed.unitsPerEm;
6
+ const kerning = kern ? parsed.kerning : EMPTY_KERNING;
5
7
  const widths = new Array(parsed.numGlyphs);
6
8
  for (let i = 0; i < parsed.numGlyphs; i++) widths[i] = Math.round((parsed.advanceWidths[i] ?? 0) * scale);
7
9
  const pdfWidthForGid = (gid) => {
@@ -9,13 +11,13 @@ function createFontMeasure(parsed) {
9
11
  return widths[gid];
10
12
  };
11
13
  const textWidthPt = (text, fontSize) => {
12
- const shaped = shapeText(text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, parsed.kerning, parsed.joiningForms);
14
+ const shaped = shapeText(text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, kerning, parsed.joiningForms);
13
15
  let totalEm = 0;
14
16
  for (const a of shaped.advances) totalEm += a;
15
17
  return totalEm * fontSize / parsed.unitsPerEm;
16
18
  };
17
19
  const encodeTextAsCidHex = (text) => {
18
- const shaped = shapeText(text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, parsed.kerning, parsed.joiningForms);
20
+ const shaped = shapeText(text, parsed.glyphForCodepoint, parsed.advanceWidths, parsed.ligatures, kerning, parsed.joiningForms);
19
21
  let out = "";
20
22
  for (const gid of shaped.gids) out += gid.toString(16).padStart(4, "0").toUpperCase();
21
23
  return out;
@@ -12,8 +12,17 @@ export interface ParsedTtf {
12
12
  readonly fontBBox: readonly [number, number, number, number];
13
13
  readonly ascender: number;
14
14
  readonly descender: number;
15
+ readonly lineGap: number;
15
16
  readonly capHeight: number;
16
17
  readonly xHeight: number;
18
+ readonly vmetrics: {
19
+ readonly typoAscent: number;
20
+ readonly typoDescent: number;
21
+ readonly typoLineGap: number;
22
+ readonly winAscent: number;
23
+ readonly winDescent: number;
24
+ readonly useTypoMetrics: boolean;
25
+ };
17
26
  readonly italicAngle: number;
18
27
  readonly stemV: number;
19
28
  readonly flags: number;
@@ -41,7 +41,7 @@ function parseTtf(raw) {
41
41
  r.seek(hhea.offset + 4);
42
42
  const ascender = r.i16();
43
43
  const descender = r.i16();
44
- r.skip(2);
44
+ const lineGap = r.i16();
45
45
  r.skip(24);
46
46
  const numberOfHMetrics = r.u16();
47
47
  const maxp = require("maxp");
@@ -74,6 +74,12 @@ function parseTtf(raw) {
74
74
  let xHeight = 0;
75
75
  let isItalic = false;
76
76
  let usWeightClass = 400;
77
+ let typoAscent = ascender;
78
+ let typoDescent = descender;
79
+ let typoLineGap = lineGap;
80
+ let winAscent = Math.abs(ascender);
81
+ let winDescent = Math.abs(descender);
82
+ let useTypoMetrics = false;
77
83
  const os2 = tables.get("OS/2");
78
84
  if (os2) {
79
85
  r.seek(os2.offset);
@@ -81,7 +87,17 @@ function parseTtf(raw) {
81
87
  r.skip(2);
82
88
  usWeightClass = r.u16();
83
89
  r.seek(os2.offset + 62);
84
- isItalic = (r.u16() & 1) !== 0;
90
+ const fsSelection = r.u16();
91
+ isItalic = (fsSelection & 1) !== 0;
92
+ useTypoMetrics = (fsSelection & 128) !== 0;
93
+ if (os2.length >= 78) {
94
+ r.seek(os2.offset + 68);
95
+ typoAscent = r.i16();
96
+ typoDescent = r.i16();
97
+ typoLineGap = r.i16();
98
+ winAscent = r.u16();
99
+ winDescent = r.u16();
100
+ }
85
101
  if (os2Version >= 2 && os2.length >= 90) {
86
102
  r.seek(os2.offset + 86);
87
103
  xHeight = r.i16();
@@ -113,8 +129,17 @@ function parseTtf(raw) {
113
129
  ],
114
130
  ascender,
115
131
  descender,
132
+ lineGap,
116
133
  capHeight,
117
134
  xHeight,
135
+ vmetrics: {
136
+ typoAscent,
137
+ typoDescent,
138
+ typoLineGap,
139
+ winAscent,
140
+ winDescent,
141
+ useTypoMetrics
142
+ },
118
143
  italicAngle,
119
144
  stemV,
120
145
  flags,
@@ -35,7 +35,8 @@ export declare function callerFontProvider(fonts: FontBytesByVariant): FontProvi
35
35
  */
36
36
  export declare function embeddedDocFontProvider(embedded: ReadonlyMap<string, FontRegistry>): FontProvider;
37
37
  /**
38
- * Open CDN substitutes (Roboto / Tinos / Cousine — the LibreOffice mapping).
38
+ * Open CDN substitutes (Arimo / Tinos / Cousine / Carlito / Caladea — the
39
+ * LibreOffice metric-compatible mapping).
39
40
  * Always answers; the chain reports it as a substitution.
40
41
  */
41
42
  export declare function remoteFontProvider(options?: {
@@ -62,7 +62,8 @@ function embeddedDocFontProvider(embedded) {
62
62
  };
63
63
  }
64
64
  /**
65
- * Open CDN substitutes (Roboto / Tinos / Cousine — the LibreOffice mapping).
65
+ * Open CDN substitutes (Arimo / Tinos / Cousine / Carlito / Caladea — the
66
+ * LibreOffice metric-compatible mapping).
66
67
  * Always answers; the chain reports it as a substitution.
67
68
  */
68
69
  function remoteFontProvider(options = {}) {
@@ -1,5 +1,5 @@
1
1
  import { FontBytesByVariant } from '../font/index.js';
2
- export type FamilyKey = 'roboto' | 'tinos' | 'cousine';
2
+ export type FamilyKey = 'arimo' | 'tinos' | 'cousine' | 'carlito' | 'caladea';
3
3
  export declare function resolveFamilyKey(name: string | undefined): FamilyKey;
4
4
  export type FetchLike = (url: string) => Promise<{
5
5
  ok: boolean;
@@ -7,9 +7,9 @@ var VARIANT_SUFFIX = {
7
7
  boldItalic: "700Bold_Italic"
8
8
  };
9
9
  var FAMILIES = {
10
- roboto: {
11
- pkg: "roboto",
12
- file: "Roboto"
10
+ arimo: {
11
+ pkg: "arimo",
12
+ file: "Arimo"
13
13
  },
14
14
  tinos: {
15
15
  pkg: "tinos",
@@ -18,13 +18,32 @@ var FAMILIES = {
18
18
  cousine: {
19
19
  pkg: "cousine",
20
20
  file: "Cousine"
21
+ },
22
+ carlito: {
23
+ pkg: "carlito",
24
+ file: "Carlito",
25
+ nested: true
26
+ },
27
+ caladea: {
28
+ pkg: "caladea",
29
+ file: "Caladea"
21
30
  }
22
31
  };
32
+ var EXACT = {
33
+ calibri: "carlito",
34
+ cambria: "caladea",
35
+ arial: "arimo",
36
+ helvetica: "arimo",
37
+ "liberation sans": "arimo",
38
+ "times new roman": "tinos",
39
+ times: "tinos",
40
+ "liberation serif": "tinos",
41
+ "courier new": "cousine",
42
+ courier: "cousine",
43
+ "liberation mono": "cousine"
44
+ };
23
45
  var SERIF = new Set([
24
- "times",
25
- "times new roman",
26
46
  "georgia",
27
- "cambria",
28
47
  "garamond",
29
48
  "book antiqua",
30
49
  "palatino",
@@ -33,8 +52,6 @@ var SERIF = new Set([
33
52
  "serif"
34
53
  ]);
35
54
  var MONO = new Set([
36
- "courier",
37
- "courier new",
38
55
  "consolas",
39
56
  "monaco",
40
57
  "menlo",
@@ -43,14 +60,18 @@ var MONO = new Set([
43
60
  "monospace"
44
61
  ]);
45
62
  function resolveFamilyKey(name) {
46
- if (!name) return "roboto";
63
+ if (!name) return "arimo";
47
64
  const n = name.trim().toLowerCase();
65
+ const exact = EXACT[n];
66
+ if (exact) return exact;
48
67
  if (MONO.has(n)) return "cousine";
49
68
  if (SERIF.has(n)) return "tinos";
50
- return "roboto";
69
+ return "arimo";
51
70
  }
52
71
  function fontUrl(family, variant) {
53
- return `${CDN_BASE}/${family.pkg}/${family.file}_${VARIANT_SUFFIX[variant]}.ttf`;
72
+ const suffix = VARIANT_SUFFIX[variant];
73
+ const leaf = `${family.file}_${suffix}.ttf`;
74
+ return family.nested ? `${CDN_BASE}/${family.pkg}/${suffix}/${leaf}` : `${CDN_BASE}/${family.pkg}/${leaf}`;
54
75
  }
55
76
  var cache = /* @__PURE__ */ new Map();
56
77
  async function fetchTtf(url, fetchImpl, required) {
@@ -0,0 +1,2 @@
1
+ import { Item } from './knuth-plass.js';
2
+ export declare function greedyBreakLines(items: ReadonlyArray<Item>, lineWidths: number | ReadonlyArray<number>): ReadonlyArray<number>;
@@ -0,0 +1,67 @@
1
+ import { FORBIDDEN_BREAK } from "./knuth-plass.js";
2
+ //#region src/core/line-breaker/greedy.ts
3
+ function greedyBreakLines(items, lineWidths) {
4
+ if (items.length === 0) return [];
5
+ const widthFor = (line) => typeof lineWidths === "number" ? lineWidths : lineWidths[Math.min(line, lineWidths.length - 1)];
6
+ const widthBefore = new Array(items.length + 1);
7
+ widthBefore[0] = 0;
8
+ for (let i = 0; i < items.length; i++) {
9
+ const it = items[i];
10
+ widthBefore[i + 1] = widthBefore[i] + (it.type === "penalty" ? 0 : it.width);
11
+ }
12
+ const lineWidth = (start, b) => {
13
+ let w = widthBefore[b] - widthBefore[start];
14
+ const item = items[b];
15
+ if (item.type === "penalty") w += item.width;
16
+ return w;
17
+ };
18
+ const isFeasible = (i) => {
19
+ const item = items[i];
20
+ if (item.type === "penalty") return item.penalty < FORBIDDEN_BREAK;
21
+ if (item.type === "glue") {
22
+ const prev = items[i - 1];
23
+ return !!prev && prev.type === "box";
24
+ }
25
+ return false;
26
+ };
27
+ const breaks = [];
28
+ let lineStart = 0;
29
+ let lineNo = 0;
30
+ let lastGood = -1;
31
+ let i = 0;
32
+ while (i < items.length) {
33
+ if (!isFeasible(i)) {
34
+ i++;
35
+ continue;
36
+ }
37
+ const item = items[i];
38
+ const forced = item.type === "penalty" && item.penalty <= -1e4;
39
+ const fits = lineWidth(lineStart, i) <= widthFor(lineNo);
40
+ if (!fits && lastGood >= 0) {
41
+ breaks.push(lastGood);
42
+ lineStart = lastGood + 1;
43
+ lineNo++;
44
+ lastGood = -1;
45
+ continue;
46
+ }
47
+ if (forced) {
48
+ breaks.push(i);
49
+ lineStart = i + 1;
50
+ lineNo++;
51
+ lastGood = -1;
52
+ i++;
53
+ continue;
54
+ }
55
+ if (fits) lastGood = i;
56
+ else {
57
+ breaks.push(i);
58
+ lineStart = i + 1;
59
+ lineNo++;
60
+ lastGood = -1;
61
+ }
62
+ i++;
63
+ }
64
+ return breaks;
65
+ }
66
+ //#endregion
67
+ export { greedyBreakLines };
@@ -1,2 +1,3 @@
1
1
  export { FORCED_BREAK, breakLines } from './knuth-plass.js';
2
+ export { greedyBreakLines } from './greedy.js';
2
3
  export type { Item, BoxItem, GlueItem, PenaltyItem, BreakResult, } from './knuth-plass.js';
@@ -20,6 +20,7 @@ export interface BreakResult {
20
20
  readonly ratios: ReadonlyArray<number>;
21
21
  }
22
22
  export declare const FORCED_BREAK = -10000;
23
+ export declare const FORBIDDEN_BREAK = 10000;
23
24
  /**
24
25
  * Break a paragraph into lines using the Knuth-Plass total-fit algorithm.
25
26
  *
@@ -47,7 +47,7 @@ function breakLines(items, lineWidths) {
47
47
  let active = [startNode];
48
48
  const isFeasibleBreakpoint = (i) => {
49
49
  const item = items[i];
50
- if (item.type === "penalty" && item.penalty < FORBIDDEN_BREAK) return true;
50
+ if (item.type === "penalty" && item.penalty < 1e4) return true;
51
51
  if (item.type === "glue") {
52
52
  const prev = items[i - 1];
53
53
  return !!prev && prev.type === "box";
@@ -90,7 +90,7 @@ function breakLines(items, lineWidths) {
90
90
  const lineDemerits = (r, penaltyAtBreak, lastItemPenalty) => {
91
91
  const badness = 100 * Math.pow(Math.abs(r), 3);
92
92
  let d;
93
- if (penaltyAtBreak >= 0 && penaltyAtBreak < FORBIDDEN_BREAK) d = Math.pow(LINE_PENALTY + badness + penaltyAtBreak, 2);
93
+ if (penaltyAtBreak >= 0 && penaltyAtBreak < 1e4) d = Math.pow(LINE_PENALTY + badness + penaltyAtBreak, 2);
94
94
  else if (penaltyAtBreak > -1e4 && penaltyAtBreak < 0) d = Math.pow(LINE_PENALTY + badness, 2) - penaltyAtBreak * penaltyAtBreak;
95
95
  else d = Math.pow(LINE_PENALTY + badness, 2);
96
96
  return d;
@@ -169,4 +169,4 @@ function breakLines(items, lineWidths) {
169
169
  };
170
170
  }
171
171
  //#endregion
172
- export { FORCED_BREAK, breakLines };
172
+ export { FORBIDDEN_BREAK, FORCED_BREAK, breakLines };
@@ -46,7 +46,7 @@ function prepareXlsxStyledRender(xlsx, options) {
46
46
  if (!fonts) throw new Error("convertXlsxToPdfSync requires options.fonts or options.fontBytes");
47
47
  const registry = FontRegistry.fromBytes(fonts);
48
48
  const { doc: flow } = readXlsx(xlsx);
49
- const { fontBytes: _ignoreA, fonts: _ignoreB, section: sectionOverride, info: callerInfo, embedSource, attachments: callerAttachments, signature: _ignoreSig, ...renderOptions } = options;
49
+ const { fontBytes: _ignoreA, fonts: _ignoreB, section: sectionOverride, info: callerInfo, embedSource, attachments: callerAttachments, signature: _ignoreSig, layoutProfile: _ignoreProfile, ...renderOptions } = options;
50
50
  const section = sectionOverride ?? flow.section;
51
51
  const info = flow.info || callerInfo ? {
52
52
  ...flow.info,
@@ -70,6 +70,8 @@ export interface Line {
70
70
  isLastInParagraph: boolean;
71
71
  readonly mathAscentPt?: number;
72
72
  readonly mathDescentPt?: number;
73
+ readonly metricHeightPt?: number;
74
+ readonly metricDescentPt?: number;
73
75
  }
74
76
  export interface ImageResource {
75
77
  readonly resourceName: string;
@@ -15,8 +15,10 @@ export interface PdfAProfile {
15
15
  readonly tagged: boolean;
16
16
  readonly version: '1.4' | '1.7';
17
17
  }
18
+ export type LayoutProfile = 'ream' | 'word' | 'libreoffice';
18
19
  export interface StyledRenderOptions {
19
20
  readonly registry: FontRegistry;
21
+ readonly layoutProfile?: LayoutProfile;
20
22
  readonly registriesByFamily?: ReadonlyMap<FamilyKey, FontRegistry>;
21
23
  readonly embeddedFonts?: ReadonlyMap<string, FontRegistry>;
22
24
  readonly styles: StyleSheet;
@@ -7,6 +7,7 @@ import { prepareImage } from "../core/images.js";
7
7
  import { segmentLevels } from "../core/bidi/segments.js";
8
8
  import { analyzeString, hasBidiCharacters } from "../core/bidi/index.js";
9
9
  import { FORCED_BREAK, breakLines } from "../core/line-breaker/knuth-plass.js";
10
+ import { greedyBreakLines } from "../core/line-breaker/greedy.js";
10
11
  import { applyNumbering, applyNumberingToHeadersFooters } from "../core/numbering/apply.js";
11
12
  import { DEFAULT_RESOLVED_PARAGRAPH, DEFAULT_RESOLVED_RUN } from "../core/style-cascade/types.js";
12
13
  import { resolveParagraphProperties, resolveRunProperties } from "../core/style-cascade/resolver.js";
@@ -930,7 +931,7 @@ function collectFontResources(body, headersFooters, options) {
930
931
  out.set(variant, {
931
932
  resourceName,
932
933
  parsed: info.parsed,
933
- measure: createFontMeasure(info.parsed),
934
+ measure: createFontMeasure(info.parsed, options.layoutProfile !== "word"),
934
935
  gids: info.gids
935
936
  });
936
937
  }
@@ -943,7 +944,7 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
943
944
  ...baseResolved,
944
945
  alignment: "right"
945
946
  } : baseResolved;
946
- const lines = wrap(tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir), paragraphMaxWidth(resolved, contentWidth, true), paragraphMaxWidth(resolved, contentWidth, false), resolved, options.hyphenator, lineWidths);
947
+ const lines = wrap(tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir), paragraphMaxWidth(resolved, contentWidth, true), paragraphMaxWidth(resolved, contentWidth, false), resolved, options.hyphenator, options.layoutProfile ?? "ream", lineWidths);
947
948
  let heightPt = 0;
948
949
  for (const line of lines) heightPt += computeLineHeight(line, resolved);
949
950
  const numbering = paragraph.properties.numbering;
@@ -1290,7 +1291,7 @@ function paragraphItemStream(tokens, hyphenator) {
1290
1291
  });
1291
1292
  return entries;
1292
1293
  }
1293
- function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved) {
1294
+ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, resolved, profile) {
1294
1295
  let st = start;
1295
1296
  let et = breakIdx;
1296
1297
  while (st < et && (entries[st].token === null || entries[st].token?.isSpace)) st++;
@@ -1316,6 +1317,10 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1316
1317
  let maxSize = 0;
1317
1318
  let mathAscent = 0;
1318
1319
  let mathDescent = 0;
1320
+ const useMetric = profile !== "ream";
1321
+ let metricAscent = 0;
1322
+ let metricDescent = 0;
1323
+ let metricLineGap = 0;
1319
1324
  for (const t of lineTokens) {
1320
1325
  contentWidth += t.widthPt;
1321
1326
  const sz = tokenLineSize(t);
@@ -1323,8 +1328,14 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1323
1328
  if (t.kind === "math") {
1324
1329
  mathAscent = Math.max(mathAscent, t.ascentPt);
1325
1330
  mathDescent = Math.max(mathDescent, t.descentPt);
1331
+ } else if (useMetric && t.kind === "text") {
1332
+ const m = fontLeadingPt(t.font.parsed, t.fontSizePt, profile);
1333
+ if (m.ascentPt > metricAscent) metricAscent = m.ascentPt;
1334
+ if (m.descentPt > metricDescent) metricDescent = m.descentPt;
1335
+ if (m.lineGapPt > metricLineGap) metricLineGap = m.lineGapPt;
1326
1336
  }
1327
1337
  }
1338
+ const hasMetric = useMetric && metricAscent + metricDescent > 0;
1328
1339
  return {
1329
1340
  tokens: lineTokens,
1330
1341
  contentWidthPt: contentWidth,
@@ -1334,21 +1345,26 @@ function lineFromRange(entries, start, breakIdx, availableWidthPt, isFirst, reso
1334
1345
  resolved,
1335
1346
  isLastInParagraph: false,
1336
1347
  mathAscentPt: mathAscent,
1337
- mathDescentPt: mathDescent
1348
+ mathDescentPt: mathDescent,
1349
+ ...hasMetric ? {
1350
+ metricHeightPt: metricAscent + metricDescent + metricLineGap,
1351
+ metricDescentPt: metricDescent
1352
+ } : {}
1338
1353
  };
1339
1354
  }
1340
- function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, lineWidths) {
1355
+ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile, lineWidths) {
1341
1356
  if (tokens.length === 0) return [];
1342
1357
  const widths = lineWidths ?? [firstLineWidth, otherWidth];
1343
1358
  const entries = paragraphItemStream(tokens, hyphenator);
1344
- const { breaks } = breakLines(entries.map((e) => e.item), widths);
1359
+ const items = entries.map((e) => e.item);
1360
+ const breaks = profile === "ream" ? breakLines(items, widths).breaks : greedyBreakLines(items, widths);
1345
1361
  const lines = [];
1346
1362
  let start = 0;
1347
1363
  let isFirst = true;
1348
1364
  let lineIdx = 0;
1349
1365
  for (const breakIdx of breaks) {
1350
1366
  const width = lineIdx < widths.length ? widths[lineIdx] : widths[widths.length - 1];
1351
- const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved);
1367
+ const line = lineFromRange(entries, start, breakIdx, width, isFirst, resolved, profile);
1352
1368
  if (line) lines.push(line);
1353
1369
  start = breakIdx + 1;
1354
1370
  isFirst = false;
@@ -1357,19 +1373,38 @@ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, lineWidt
1357
1373
  if (lines.length > 0) lines[lines.length - 1].isLastInParagraph = true;
1358
1374
  return lines;
1359
1375
  }
1376
+ function fontLeadingPt(parsed, sizePt, profile) {
1377
+ const s = sizePt / parsed.unitsPerEm;
1378
+ const vm = parsed.vmetrics;
1379
+ if (profile === "word") return {
1380
+ ascentPt: vm.winAscent * s,
1381
+ descentPt: vm.winDescent * s,
1382
+ lineGapPt: 0
1383
+ };
1384
+ const asc = vm.useTypoMetrics ? vm.typoAscent : parsed.ascender;
1385
+ const desc = vm.useTypoMetrics ? vm.typoDescent : parsed.descender;
1386
+ const gap = vm.useTypoMetrics ? vm.typoLineGap : parsed.lineGap;
1387
+ return {
1388
+ ascentPt: asc * s,
1389
+ descentPt: -desc * s,
1390
+ lineGapPt: gap * s
1391
+ };
1392
+ }
1360
1393
  function computeLineHeight(line, p) {
1361
1394
  const fontSize = line.maxFontSizePt || 12;
1395
+ const natural = line.metricHeightPt ?? fontSize * 1.2;
1362
1396
  const mathH = (line.mathAscentPt ?? 0) + (line.mathDescentPt ?? 0);
1363
1397
  const mathNeed = mathH > 0 ? mathH * 1.05 : 0;
1364
1398
  if (p.spacingLineRule === "exact" && p.spacingLine > 0) return Math.max(p.spacingLine, mathNeed);
1365
- if (p.spacingLineRule === "atLeast" && p.spacingLine > 0) return Math.max(fontSize * 1.2, p.spacingLine, mathNeed);
1399
+ if (p.spacingLineRule === "atLeast" && p.spacingLine > 0) return Math.max(natural, p.spacingLine, mathNeed);
1366
1400
  const lineTwips = Math.round(p.spacingLine * 20);
1367
1401
  const multiple = lineTwips > 0 ? lineTwips / 240 : 1;
1368
- return Math.max(fontSize * 1.2 * multiple, mathNeed);
1402
+ return Math.max(natural * multiple, mathNeed);
1369
1403
  }
1370
1404
  function lineDescent(line) {
1371
1405
  const fs = line.maxFontSizePt || 12;
1372
- return Math.max(fs * .2, line.mathDescentPt ?? 0);
1406
+ const base = line.metricDescentPt ?? fs * .2;
1407
+ return Math.max(base, line.mathDescentPt ?? 0);
1373
1408
  }
1374
1409
  function alignmentOffset(alignment, lineWidth, available) {
1375
1410
  if (alignment === "right") return Math.max(0, available - lineWidth);
@@ -49,7 +49,7 @@ function prepareDocxStyledRender(docx, options) {
49
49
  };
50
50
  }
51
51
  function detectDocxFamilyKeys(docx) {
52
- const keys = new Set(["roboto"]);
52
+ const keys = new Set(["arimo"]);
53
53
  let pkg;
54
54
  try {
55
55
  pkg = OpcPackage.open(docx);
@@ -111,7 +111,7 @@ async function resolveDocxAutoFonts(docx, options = {}) {
111
111
  ...fetchOpt
112
112
  });
113
113
  registriesByFamily.set(key, FontRegistry.fromBytes(bytes));
114
- if (key === "roboto" || !baseBytes) baseBytes = bytes;
114
+ if (key === "arimo" || !baseBytes) baseBytes = bytes;
115
115
  }
116
116
  return {
117
117
  fonts: baseBytes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reamkit",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Ream — convert DOCX, XLSX, PPTX and PDF to PDF, SVG, HTML, DOCX and XLSX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
5
5
  "license": "MIT",
6
6
  "author": "Alex Krassavin <info@reamkit.dev>",