reamkit 1.9.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 +11 -5
- package/dist/esm/core/converter/facade.js +2 -0
- package/dist/esm/core/converter/ream.js +1 -0
- 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/pptx/placeholder-cascade.d.ts +9 -0
- package/dist/esm/pptx/placeholder-cascade.js +77 -0
- package/dist/esm/pptx/pptx-reader.d.ts +4 -0
- package/dist/esm/pptx/pptx-reader.js +213 -0
- package/dist/esm/pptx/slide-parser.d.ts +18 -0
- package/dist/esm/pptx/slide-parser.js +359 -0
- package/dist/esm/pptx/sp-helpers.d.ts +17 -0
- package/dist/esm/pptx/sp-helpers.js +60 -0
- package/dist/esm/word/docx-to-pdf.js +2 -2
- package/dist/esm/word/drawing-parser.d.ts +4 -0
- package/dist/esm/word/drawing-parser.js +1 -1
- package/package.json +4 -2
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';
|
|
@@ -30,7 +30,7 @@ import { Ream } from 'reamkit';
|
|
|
30
30
|
// e.g. from an <input type="file"> or a fetch() — anything that yields bytes.
|
|
31
31
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
32
32
|
|
|
33
|
-
const doc = Ream.parse(bytes); // docx, xlsx or pdf — sniffed
|
|
33
|
+
const doc = Ream.parse(bytes); // docx, xlsx, pptx or pdf — sniffed
|
|
34
34
|
const pdf = await doc.convert('pdf'); // async — fetches a font if needed
|
|
35
35
|
const svg = await doc.convert('svg'); // same parse, different target
|
|
36
36
|
const html = await doc.convert('html'); // flowed HTML — needs no fonts at all
|
|
@@ -130,7 +130,7 @@ rendering — inspect or analyze it without converting:
|
|
|
130
130
|
|
|
131
131
|
```ts
|
|
132
132
|
const doc = Ream.parse(bytes);
|
|
133
|
-
doc.format; // 'docx' | 'xlsx' | 'pdf'
|
|
133
|
+
doc.format; // 'docx' | 'xlsx' | 'pptx' | 'pdf'
|
|
134
134
|
doc.flow.body; // paragraphs / tables / images / charts …
|
|
135
135
|
doc.losses; // read-time losses
|
|
136
136
|
```
|
|
@@ -187,6 +187,12 @@ gradient vector shapes. It reads modern compressed files (cross-reference + obje
|
|
|
187
187
|
streams) and encrypted ones (RC4 / AES — the user password is passed to
|
|
188
188
|
`Ream.parse(bytes, { password })`, defaulting to the permissions-only case).
|
|
189
189
|
|
|
190
|
+
**Reads PowerPoint, too.** `Ream.parse` accepts a `.pptx` and turns each slide
|
|
191
|
+
into a page at the deck size — text boxes (with run formatting, alignment,
|
|
192
|
+
bullets and indents), layout/master placeholders, pictures, shapes, DrawingML
|
|
193
|
+
tables, embedded charts, theme colours, slide backgrounds, grouped shapes and
|
|
194
|
+
hyperlinks — then converts onward to PDF, SVG, HTML or DOCX like any source.
|
|
195
|
+
|
|
190
196
|
See [`CHANGELOG.md`](./CHANGELOG.md) for the release history; the docs
|
|
191
197
|
[**Scope**](https://reamkit.dev/guides/scope/) guide has the full feature matrix
|
|
192
198
|
and known limitations.
|
|
@@ -13,6 +13,7 @@ import { writeHtml } from "../../html/html-writer.js";
|
|
|
13
13
|
import { writeSvg } from "../../svg/svg-writer.js";
|
|
14
14
|
import { convertDocxToPdf } from "../../word/docx-to-pdf.js";
|
|
15
15
|
import { convertXlsxToPdf } from "../../excel/xlsx-to-pdf.js";
|
|
16
|
+
import { pptxReader } from "../../pptx/pptx-reader.js";
|
|
16
17
|
import { pdfReader } from "../../pdf-reader/reader.js";
|
|
17
18
|
//#region src/core/converter/facade.ts
|
|
18
19
|
function toFlowDoc(doc) {
|
|
@@ -28,6 +29,7 @@ function readToFlow(reader, bytes) {
|
|
|
28
29
|
var DEFAULT_READERS = [
|
|
29
30
|
docxReader,
|
|
30
31
|
xlsxReader,
|
|
32
|
+
pptxReader,
|
|
31
33
|
pdfReader
|
|
32
34
|
];
|
|
33
35
|
function createConverter(opts = {}) {
|
|
@@ -16,6 +16,7 @@ import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
|
|
|
16
16
|
var SOURCE_MIME = {
|
|
17
17
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
18
18
|
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
19
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
19
20
|
pdf: "application/pdf"
|
|
20
21
|
};
|
|
21
22
|
var Ream = class Ream {
|
|
@@ -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);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RunProperties } from '../core/document-model/index.js';
|
|
2
|
+
import { ColorResolver } from '../core/drawingml/colors.js';
|
|
3
|
+
import { PoNode } from '../core/po-helpers.js';
|
|
4
|
+
import { PlaceholderRef, ShapeBoxEmu } from './sp-helpers.js';
|
|
5
|
+
export interface PlaceholderCascade {
|
|
6
|
+
readonly geometryFor: (ph: PlaceholderRef) => ShapeBoxEmu | undefined;
|
|
7
|
+
readonly defaultsFor: (ph: PlaceholderRef, level: number) => RunProperties;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildPlaceholderCascade(layoutTree: ReadonlyArray<PoNode>, masterTree: ReadonlyArray<PoNode> | undefined, colors: ColorResolver): PlaceholderCascade;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { poChildren, poIs } from "../core/po-helpers.js";
|
|
2
|
+
import { parsePh, parseXfrmBox, rPrToRunProps } from "./sp-helpers.js";
|
|
3
|
+
//#region src/pptx/placeholder-cascade.ts
|
|
4
|
+
function categoryOf(type) {
|
|
5
|
+
if (type === "title" || type === "ctrTitle") return "title";
|
|
6
|
+
if (type === void 0 || type === "body" || type === "subTitle" || type === "obj") return "body";
|
|
7
|
+
return "other";
|
|
8
|
+
}
|
|
9
|
+
function buildPlaceholderCascade(layoutTree, masterTree, colors) {
|
|
10
|
+
const layoutPhs = collectPlaceholders(layoutTree, "p:sldLayout");
|
|
11
|
+
const masterPhs = masterTree ? collectPlaceholders(masterTree, "p:sldMaster") : [];
|
|
12
|
+
const txStyles = masterTree ? collectTxStyles(masterTree, colors) : {
|
|
13
|
+
title: [],
|
|
14
|
+
body: [],
|
|
15
|
+
other: []
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
geometryFor(ph) {
|
|
19
|
+
return matchPlaceholder(layoutPhs, ph)?.box ?? matchPlaceholder(masterPhs, ph)?.box;
|
|
20
|
+
},
|
|
21
|
+
defaultsFor(ph, level) {
|
|
22
|
+
const levels = txStyles[categoryOf(ph.type)];
|
|
23
|
+
if (levels.length === 0) return {};
|
|
24
|
+
return levels[Math.min(Math.max(level, 0), levels.length - 1)] ?? {};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function collectPlaceholders(tree, root) {
|
|
29
|
+
const sld = tree.find((n) => poIs(n, root));
|
|
30
|
+
const cSld = sld ? poChildren(sld).find((c) => poIs(c, "p:cSld")) : void 0;
|
|
31
|
+
const spTree = cSld ? poChildren(cSld).find((c) => poIs(c, "p:spTree")) : void 0;
|
|
32
|
+
if (!spTree) return [];
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const sp of poChildren(spTree)) {
|
|
35
|
+
if (!poIs(sp, "p:sp")) continue;
|
|
36
|
+
const ref = parsePh(sp);
|
|
37
|
+
if (!ref) continue;
|
|
38
|
+
const box = parseXfrmBox(poChildren(sp).find((c) => poIs(c, "p:spPr")));
|
|
39
|
+
out.push({
|
|
40
|
+
ref,
|
|
41
|
+
...box ? { box } : {}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function matchPlaceholder(list, ph) {
|
|
47
|
+
if (ph.idx !== void 0) {
|
|
48
|
+
const byIdx = list.find((p) => p.ref.idx === ph.idx);
|
|
49
|
+
if (byIdx) return byIdx;
|
|
50
|
+
}
|
|
51
|
+
const byType = list.find((p) => p.ref.type === ph.type);
|
|
52
|
+
if (byType) return byType;
|
|
53
|
+
const cat = categoryOf(ph.type);
|
|
54
|
+
return list.find((p) => categoryOf(p.ref.type) === cat);
|
|
55
|
+
}
|
|
56
|
+
function collectTxStyles(masterTree, colors) {
|
|
57
|
+
const sld = masterTree.find((n) => poIs(n, "p:sldMaster"));
|
|
58
|
+
const txStyles = sld ? poChildren(sld).find((c) => poIs(c, "p:txStyles")) : void 0;
|
|
59
|
+
return {
|
|
60
|
+
title: levelStyles(txStyles, "p:titleStyle", colors),
|
|
61
|
+
body: levelStyles(txStyles, "p:bodyStyle", colors),
|
|
62
|
+
other: levelStyles(txStyles, "p:otherStyle", colors)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function levelStyles(txStyles, tag, colors) {
|
|
66
|
+
const style = txStyles ? poChildren(txStyles).find((c) => poIs(c, tag)) : void 0;
|
|
67
|
+
if (!style) return [];
|
|
68
|
+
const out = [];
|
|
69
|
+
for (let lvl = 1; lvl <= 9; lvl++) {
|
|
70
|
+
const lvlPr = poChildren(style).find((c) => poIs(c, `a:lvl${lvl}pPr`));
|
|
71
|
+
const defRPr = lvlPr ? poChildren(lvlPr).find((c) => poIs(c, "a:defRPr")) : void 0;
|
|
72
|
+
out.push(rPrToRunProps(defRPr, colors));
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { buildPlaceholderCascade };
|