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 +3 -3
- package/dist/esm/core/font/measure.d.ts +1 -1
- package/dist/esm/core/font/measure.js +5 -3
- package/dist/esm/core/font/ttf-parser.d.ts +9 -0
- package/dist/esm/core/font/ttf-parser.js +27 -2
- package/dist/esm/core/fonts/provider.d.ts +2 -1
- package/dist/esm/core/fonts/provider.js +2 -1
- package/dist/esm/core/fonts/remote-fonts.d.ts +1 -1
- package/dist/esm/core/fonts/remote-fonts.js +32 -11
- package/dist/esm/core/line-breaker/greedy.d.ts +2 -0
- package/dist/esm/core/line-breaker/greedy.js +67 -0
- package/dist/esm/core/line-breaker/index.d.ts +1 -0
- package/dist/esm/core/line-breaker/knuth-plass.d.ts +1 -0
- package/dist/esm/core/line-breaker/knuth-plass.js +3 -3
- package/dist/esm/excel/xlsx-to-pdf.js +1 -1
- package/dist/esm/layout/page-doc.d.ts +2 -0
- package/dist/esm/layout/styled-layout.d.ts +2 -0
- package/dist/esm/layout/styled-layout.js +45 -10
- package/dist/esm/word/docx-to-pdf.js +2 -2
- package/package.json +1 -1
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 (
|
|
24
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 = '
|
|
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
|
-
|
|
11
|
-
pkg: "
|
|
12
|
-
file: "
|
|
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 "
|
|
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 "
|
|
69
|
+
return "arimo";
|
|
51
70
|
}
|
|
52
71
|
function fontUrl(family, variant) {
|
|
53
|
-
|
|
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,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 };
|
|
@@ -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 <
|
|
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 <
|
|
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
|
|
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(
|
|
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(
|
|
1402
|
+
return Math.max(natural * multiple, mathNeed);
|
|
1369
1403
|
}
|
|
1370
1404
|
function lineDescent(line) {
|
|
1371
1405
|
const fs = line.maxFontSizePt || 12;
|
|
1372
|
-
|
|
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(["
|
|
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 === "
|
|
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.
|
|
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>",
|