pptx-glimpse 1.0.1 → 1.1.1
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 +13 -0
- package/dist/index.cjs +302 -113
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +302 -113
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,6 +88,19 @@ const results = await convertPptxToPng(pptx, {
|
|
|
88
88
|
});
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
#### Text output mode (`textOutput`)
|
|
92
|
+
|
|
93
|
+
By default, text is converted to `<path>` outlines, which renders consistently in any environment but bypasses the browser's native text rasterization (hinting, text-specific anti-aliasing), so glyph edges can look jagged when SVGs are displayed inline in a browser.
|
|
94
|
+
|
|
95
|
+
With `textOutput: "text"`, `convertPptxToSvg` emits native `<text>` elements along with `@font-face` rules that embed subsetted fonts as data URIs. This produces smoother text rendering in browsers, smaller SVGs for CJK-heavy slides, and selectable/copyable text. Characters not covered by the resolved font fall back to the viewer's fonts via the `font-family` chain.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// SVG only — convertPptxToPng always uses path output
|
|
99
|
+
const svgResults = await convertPptxToSvg(pptx, { textOutput: "text" });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Note: embedded fonts and `<text>` may not render as expected when the SVG is referenced via `<img src="...svg">` or sanitized. `convertPptxToPng` always uses path output regardless of this option.
|
|
103
|
+
|
|
91
104
|
### Advanced Usage
|
|
92
105
|
|
|
93
106
|
<details>
|
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,189 @@ module.exports = __toCommonJS(index_exports);
|
|
|
38
38
|
// src/converter.ts
|
|
39
39
|
var import_node_fs2 = require("fs");
|
|
40
40
|
|
|
41
|
+
// src/font/font-embedder.ts
|
|
42
|
+
var import_node_buffer = require("buffer");
|
|
43
|
+
|
|
44
|
+
// src/warning-logger.ts
|
|
45
|
+
var PREFIX = "[pptx-glimpse]";
|
|
46
|
+
var currentLevel = "off";
|
|
47
|
+
var entries = [];
|
|
48
|
+
var featureCounts = /* @__PURE__ */ new Map();
|
|
49
|
+
function initWarningLogger(level) {
|
|
50
|
+
currentLevel = level;
|
|
51
|
+
entries = [];
|
|
52
|
+
featureCounts.clear();
|
|
53
|
+
}
|
|
54
|
+
function warn(feature, message, context) {
|
|
55
|
+
if (currentLevel === "off") return;
|
|
56
|
+
entries.push({ feature, message, ...context !== void 0 && { context } });
|
|
57
|
+
const existing = featureCounts.get(feature);
|
|
58
|
+
if (existing) {
|
|
59
|
+
existing.count++;
|
|
60
|
+
} else {
|
|
61
|
+
featureCounts.set(feature, { message, count: 1 });
|
|
62
|
+
}
|
|
63
|
+
if (currentLevel === "debug") {
|
|
64
|
+
const ctx = context ? ` (${context})` : "";
|
|
65
|
+
console.warn(`${PREFIX} SKIP: ${feature} - ${message}${ctx}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function debug(feature, message, context) {
|
|
69
|
+
if (currentLevel !== "debug") return;
|
|
70
|
+
entries.push({ feature, message, ...context !== void 0 && { context } });
|
|
71
|
+
const existing = featureCounts.get(feature);
|
|
72
|
+
if (existing) {
|
|
73
|
+
existing.count++;
|
|
74
|
+
} else {
|
|
75
|
+
featureCounts.set(feature, { message, count: 1 });
|
|
76
|
+
}
|
|
77
|
+
const ctx = context ? ` (${context})` : "";
|
|
78
|
+
console.warn(`${PREFIX} DEBUG: ${feature} - ${message}${ctx}`);
|
|
79
|
+
}
|
|
80
|
+
function getWarningSummary() {
|
|
81
|
+
const features = [];
|
|
82
|
+
for (const [feature, { message, count }] of featureCounts) {
|
|
83
|
+
features.push({ feature, message, count });
|
|
84
|
+
}
|
|
85
|
+
return { totalCount: entries.length, features };
|
|
86
|
+
}
|
|
87
|
+
function flushWarnings() {
|
|
88
|
+
const summary = getWarningSummary();
|
|
89
|
+
if (currentLevel !== "off" && summary.features.length > 0) {
|
|
90
|
+
console.warn(`${PREFIX} Summary: ${summary.features.length} unsupported feature(s) detected`);
|
|
91
|
+
for (const { feature, count } of summary.features) {
|
|
92
|
+
console.warn(` - ${feature}: ${count} occurrence(s)`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
entries = [];
|
|
96
|
+
featureCounts.clear();
|
|
97
|
+
return summary;
|
|
98
|
+
}
|
|
99
|
+
function getWarningEntries() {
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/font/font-subsetter.ts
|
|
104
|
+
async function tryLoadOpentypeCtors() {
|
|
105
|
+
try {
|
|
106
|
+
const specifier = "opentype.js";
|
|
107
|
+
const mod = await import(
|
|
108
|
+
/* @vite-ignore */
|
|
109
|
+
specifier
|
|
110
|
+
);
|
|
111
|
+
return { Font: mod.Font, Glyph: mod.Glyph };
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function glyphName(glyph, firstUnicode) {
|
|
117
|
+
if (glyph.name) return glyph.name;
|
|
118
|
+
return `uni${firstUnicode.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
119
|
+
}
|
|
120
|
+
async function subsetFont(font, chars, familyName) {
|
|
121
|
+
const opentype = await tryLoadOpentypeCtors();
|
|
122
|
+
if (!opentype) return null;
|
|
123
|
+
const source = font;
|
|
124
|
+
if (typeof source.charToGlyph !== "function" || !source.glyphs) return null;
|
|
125
|
+
const glyphMap = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const char of chars) {
|
|
127
|
+
const codePoint = char.codePointAt(0);
|
|
128
|
+
if (codePoint === void 0) continue;
|
|
129
|
+
let glyph;
|
|
130
|
+
try {
|
|
131
|
+
glyph = source.charToGlyph(char);
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!glyph || !glyph.index) continue;
|
|
136
|
+
const entry = glyphMap.get(glyph.index);
|
|
137
|
+
if (entry) {
|
|
138
|
+
entry.unicodes.add(codePoint);
|
|
139
|
+
} else {
|
|
140
|
+
glyphMap.set(glyph.index, { glyph, unicodes: /* @__PURE__ */ new Set([codePoint]) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (glyphMap.size === 0) return null;
|
|
144
|
+
try {
|
|
145
|
+
const notdefSource = source.glyphs.get(0);
|
|
146
|
+
const glyphs = [
|
|
147
|
+
new opentype.Glyph({
|
|
148
|
+
name: ".notdef",
|
|
149
|
+
advanceWidth: notdefSource?.advanceWidth ?? source.unitsPerEm / 2,
|
|
150
|
+
path: notdefSource?.path
|
|
151
|
+
})
|
|
152
|
+
];
|
|
153
|
+
for (const { glyph, unicodes } of glyphMap.values()) {
|
|
154
|
+
const unicodeList = [...unicodes].sort((a, b) => a - b);
|
|
155
|
+
glyphs.push(
|
|
156
|
+
new opentype.Glyph({
|
|
157
|
+
name: glyphName(glyph, unicodeList[0]),
|
|
158
|
+
unicode: unicodeList[0],
|
|
159
|
+
unicodes: unicodeList,
|
|
160
|
+
advanceWidth: glyph.advanceWidth ?? 0,
|
|
161
|
+
path: glyph.path
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const subset = new opentype.Font({
|
|
166
|
+
familyName: familyName || "EmbeddedFont",
|
|
167
|
+
styleName: "Regular",
|
|
168
|
+
unitsPerEm: source.unitsPerEm,
|
|
169
|
+
ascender: source.ascender,
|
|
170
|
+
// opentype.js は descender に負値を要求する
|
|
171
|
+
descender: source.descender < 0 ? source.descender : -1,
|
|
172
|
+
glyphs
|
|
173
|
+
});
|
|
174
|
+
return new Uint8Array(subset.toArrayBuffer());
|
|
175
|
+
} catch (e) {
|
|
176
|
+
warn(
|
|
177
|
+
"font.subsetFailed",
|
|
178
|
+
`Failed to subset font "${familyName}": ${e instanceof Error ? e.message : String(e)}`
|
|
179
|
+
);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/font/script-font-context.ts
|
|
185
|
+
var jpanMajorFont = null;
|
|
186
|
+
var jpanMinorFont = null;
|
|
187
|
+
function setScriptFonts(majorJpan, minorJpan) {
|
|
188
|
+
jpanMajorFont = majorJpan;
|
|
189
|
+
jpanMinorFont = minorJpan;
|
|
190
|
+
}
|
|
191
|
+
function resetScriptFonts() {
|
|
192
|
+
jpanMajorFont = null;
|
|
193
|
+
jpanMinorFont = null;
|
|
194
|
+
}
|
|
195
|
+
function getJpanFallbackFont() {
|
|
196
|
+
return jpanMajorFont ?? jpanMinorFont;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/font/font-embedder.ts
|
|
200
|
+
function escapeCssFamilyName(name) {
|
|
201
|
+
return name.replace(/[\\"]/g, (c) => `\\${c}`).replace(/[<>&]/g, (c) => `\\${c.codePointAt(0).toString(16)} `);
|
|
202
|
+
}
|
|
203
|
+
async function buildFontFaceStyle(usages, fontResolver) {
|
|
204
|
+
const faces = [];
|
|
205
|
+
const jpanFallback = getJpanFallbackFont();
|
|
206
|
+
for (const [familyName, usage] of usages) {
|
|
207
|
+
const font = fontResolver.resolveFont(
|
|
208
|
+
usage.fonts[0],
|
|
209
|
+
usage.fonts[1] ?? null,
|
|
210
|
+
usage.fonts[2] ?? jpanFallback
|
|
211
|
+
);
|
|
212
|
+
if (!font) continue;
|
|
213
|
+
const buffer = await subsetFont(font, usage.chars, familyName);
|
|
214
|
+
if (!buffer) continue;
|
|
215
|
+
const base64 = import_node_buffer.Buffer.from(buffer).toString("base64");
|
|
216
|
+
faces.push(
|
|
217
|
+
`@font-face{font-family:"${escapeCssFamilyName(familyName)}";src:url(data:font/otf;base64,${base64}) format("opentype");}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (faces.length === 0) return "";
|
|
221
|
+
return `<style type="text/css">${faces.join("")}</style>`;
|
|
222
|
+
}
|
|
223
|
+
|
|
41
224
|
// src/font/font-mapping.ts
|
|
42
225
|
var DEFAULT_FONT_MAPPING = {
|
|
43
226
|
// ラテン文字フォント
|
|
@@ -104,6 +287,40 @@ function getCurrentMappedFont(fontFamily) {
|
|
|
104
287
|
return getMappedFont(fontFamily, currentMapping);
|
|
105
288
|
}
|
|
106
289
|
|
|
290
|
+
// src/font/font-usage-collector.ts
|
|
291
|
+
var FontUsageCollector = class {
|
|
292
|
+
/** key: tspan の font-family リスト先頭のフォント名 (= @font-face で宣言する名前) */
|
|
293
|
+
usages = /* @__PURE__ */ new Map();
|
|
294
|
+
record(fonts, text) {
|
|
295
|
+
const primary = fonts.find((f) => f !== null && f !== void 0);
|
|
296
|
+
if (!primary || text.length === 0) return;
|
|
297
|
+
let usage = this.usages.get(primary);
|
|
298
|
+
if (!usage) {
|
|
299
|
+
usage = { fonts: [...fonts], chars: /* @__PURE__ */ new Set() };
|
|
300
|
+
this.usages.set(primary, usage);
|
|
301
|
+
}
|
|
302
|
+
for (const char of text) {
|
|
303
|
+
usage.chars.add(char);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
getUsages() {
|
|
307
|
+
return this.usages;
|
|
308
|
+
}
|
|
309
|
+
reset() {
|
|
310
|
+
this.usages.clear();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var currentCollector = null;
|
|
314
|
+
function setFontUsageCollector(collector) {
|
|
315
|
+
currentCollector = collector;
|
|
316
|
+
}
|
|
317
|
+
function getFontUsageCollector() {
|
|
318
|
+
return currentCollector;
|
|
319
|
+
}
|
|
320
|
+
function resetFontUsageCollector() {
|
|
321
|
+
currentCollector = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
107
324
|
// src/font/opentype-helpers.ts
|
|
108
325
|
var import_promises = require("fs/promises");
|
|
109
326
|
|
|
@@ -1047,65 +1264,6 @@ function measureTextWidth(text, fontSizePt, bold, fontFamily, fontFamilyEa) {
|
|
|
1047
1264
|
return totalWidth;
|
|
1048
1265
|
}
|
|
1049
1266
|
|
|
1050
|
-
// src/warning-logger.ts
|
|
1051
|
-
var PREFIX = "[pptx-glimpse]";
|
|
1052
|
-
var currentLevel = "off";
|
|
1053
|
-
var entries = [];
|
|
1054
|
-
var featureCounts = /* @__PURE__ */ new Map();
|
|
1055
|
-
function initWarningLogger(level) {
|
|
1056
|
-
currentLevel = level;
|
|
1057
|
-
entries = [];
|
|
1058
|
-
featureCounts.clear();
|
|
1059
|
-
}
|
|
1060
|
-
function warn(feature, message, context) {
|
|
1061
|
-
if (currentLevel === "off") return;
|
|
1062
|
-
entries.push({ feature, message, ...context !== void 0 && { context } });
|
|
1063
|
-
const existing = featureCounts.get(feature);
|
|
1064
|
-
if (existing) {
|
|
1065
|
-
existing.count++;
|
|
1066
|
-
} else {
|
|
1067
|
-
featureCounts.set(feature, { message, count: 1 });
|
|
1068
|
-
}
|
|
1069
|
-
if (currentLevel === "debug") {
|
|
1070
|
-
const ctx = context ? ` (${context})` : "";
|
|
1071
|
-
console.warn(`${PREFIX} SKIP: ${feature} - ${message}${ctx}`);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
function debug(feature, message, context) {
|
|
1075
|
-
if (currentLevel !== "debug") return;
|
|
1076
|
-
entries.push({ feature, message, ...context !== void 0 && { context } });
|
|
1077
|
-
const existing = featureCounts.get(feature);
|
|
1078
|
-
if (existing) {
|
|
1079
|
-
existing.count++;
|
|
1080
|
-
} else {
|
|
1081
|
-
featureCounts.set(feature, { message, count: 1 });
|
|
1082
|
-
}
|
|
1083
|
-
const ctx = context ? ` (${context})` : "";
|
|
1084
|
-
console.warn(`${PREFIX} DEBUG: ${feature} - ${message}${ctx}`);
|
|
1085
|
-
}
|
|
1086
|
-
function getWarningSummary() {
|
|
1087
|
-
const features = [];
|
|
1088
|
-
for (const [feature, { message, count }] of featureCounts) {
|
|
1089
|
-
features.push({ feature, message, count });
|
|
1090
|
-
}
|
|
1091
|
-
return { totalCount: entries.length, features };
|
|
1092
|
-
}
|
|
1093
|
-
function flushWarnings() {
|
|
1094
|
-
const summary = getWarningSummary();
|
|
1095
|
-
if (currentLevel !== "off" && summary.features.length > 0) {
|
|
1096
|
-
console.warn(`${PREFIX} Summary: ${summary.features.length} unsupported feature(s) detected`);
|
|
1097
|
-
for (const { feature, count } of summary.features) {
|
|
1098
|
-
console.warn(` - ${feature}: ${count} occurrence(s)`);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
entries = [];
|
|
1102
|
-
featureCounts.clear();
|
|
1103
|
-
return summary;
|
|
1104
|
-
}
|
|
1105
|
-
function getWarningEntries() {
|
|
1106
|
-
return entries;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
1267
|
// src/font/cjk-font-fallback.ts
|
|
1110
1268
|
var import_node_os = require("os");
|
|
1111
1269
|
var MACOS_FALLBACKS = {
|
|
@@ -1642,21 +1800,6 @@ async function createOpentypeSetupFromSystem(additionalFontDirs, fontMapping, sk
|
|
|
1642
1800
|
return setup;
|
|
1643
1801
|
}
|
|
1644
1802
|
|
|
1645
|
-
// src/font/script-font-context.ts
|
|
1646
|
-
var jpanMajorFont = null;
|
|
1647
|
-
var jpanMinorFont = null;
|
|
1648
|
-
function setScriptFonts(majorJpan, minorJpan) {
|
|
1649
|
-
jpanMajorFont = majorJpan;
|
|
1650
|
-
jpanMinorFont = minorJpan;
|
|
1651
|
-
}
|
|
1652
|
-
function resetScriptFonts() {
|
|
1653
|
-
jpanMajorFont = null;
|
|
1654
|
-
jpanMinorFont = null;
|
|
1655
|
-
}
|
|
1656
|
-
function getJpanFallbackFont() {
|
|
1657
|
-
return jpanMajorFont ?? jpanMinorFont;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
1803
|
// src/font/text-measurer.ts
|
|
1661
1804
|
var DefaultTextMeasurer = class {
|
|
1662
1805
|
measureTextWidth(text, fontSizePt, bold, fontFamily, fontFamilyEa) {
|
|
@@ -4256,11 +4399,10 @@ function parseParagraph(p, colorResolver, rels, fontScheme, lstStyle, orderedPCh
|
|
|
4256
4399
|
const lstLevelProps = lstStyle?.levels[level];
|
|
4257
4400
|
const { bullet, bulletFont, bulletColor, bulletSizePct } = parseBullet(pPr, colorResolver);
|
|
4258
4401
|
const lnSpc = pPr?.lnSpc;
|
|
4259
|
-
const lnSpcSpcPct = lnSpc?.spcPct;
|
|
4260
4402
|
const tabStops = parseTabStops(pPr);
|
|
4261
4403
|
const properties = {
|
|
4262
4404
|
alignment: pPr?.["@_algn"] ?? lstLevelProps?.alignment ?? null,
|
|
4263
|
-
lineSpacing:
|
|
4405
|
+
lineSpacing: lnSpc?.spcPts || lnSpc?.spcPct ? parseSpacing(lnSpc) : null,
|
|
4264
4406
|
spaceBefore: parseSpacing(pPr?.spcBef),
|
|
4265
4407
|
spaceAfter: parseSpacing(pPr?.spcAft),
|
|
4266
4408
|
level,
|
|
@@ -8022,7 +8164,11 @@ function renderTextBody(textBody, transform) {
|
|
|
8022
8164
|
const paragraphGapPx = Math.max(prevSpaceAfterPx, spaceBeforePx);
|
|
8023
8165
|
if (para.runs.length === 0 || !para.runs.some((r) => r.text.length > 0)) {
|
|
8024
8166
|
const emptyParaHeightPt = paraFontSizePt > 0 ? paraFontSizePt : defaultNaturalHeightPt;
|
|
8025
|
-
const dy = computeDy(
|
|
8167
|
+
const dy = computeDy(
|
|
8168
|
+
isFirstLine,
|
|
8169
|
+
getLineHeightPx(para, emptyParaHeightPt, lnSpcReduction),
|
|
8170
|
+
paragraphGapPx
|
|
8171
|
+
);
|
|
8026
8172
|
tspans.push(`<tspan x="${xPos}" dy="${dy}" text-anchor="${anchorValue}"> </tspan>`);
|
|
8027
8173
|
isFirstLine = false;
|
|
8028
8174
|
prevSpaceAfterPx = resolveSpacingPx(para.properties.spaceAfter, paraFontSizePt);
|
|
@@ -8041,8 +8187,7 @@ function renderTextBody(textBody, transform) {
|
|
|
8041
8187
|
if (line.segments.length === 0) {
|
|
8042
8188
|
const dy = computeDy(
|
|
8043
8189
|
isFirstLine,
|
|
8044
|
-
defaultNaturalHeightPt,
|
|
8045
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8190
|
+
getLineHeightPx(para, defaultNaturalHeightPt, lnSpcReduction),
|
|
8046
8191
|
lineGapPx
|
|
8047
8192
|
);
|
|
8048
8193
|
tspans.push(`<tspan x="${xPos}" dy="${dy}" text-anchor="${anchorValue}"> </tspan>`);
|
|
@@ -8058,11 +8203,22 @@ function renderTextBody(textBody, transform) {
|
|
|
8058
8203
|
);
|
|
8059
8204
|
const dy = computeDy(
|
|
8060
8205
|
isFirstLine,
|
|
8061
|
-
lineNaturalHeightPt,
|
|
8062
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8206
|
+
getLineHeightPx(para, lineNaturalHeightPt, lnSpcReduction),
|
|
8063
8207
|
paragraphGapPx
|
|
8064
8208
|
);
|
|
8065
|
-
const
|
|
8209
|
+
const firstSeg = line.segments[0];
|
|
8210
|
+
const bulletFontChain = buildBulletFontChain(
|
|
8211
|
+
para.properties,
|
|
8212
|
+
firstSeg?.properties.fontFamily,
|
|
8213
|
+
firstSeg?.properties.fontFamilyEa
|
|
8214
|
+
);
|
|
8215
|
+
const bulletStyles = buildBulletStyleAttrs(
|
|
8216
|
+
para.properties,
|
|
8217
|
+
lineFontSize,
|
|
8218
|
+
fontScale,
|
|
8219
|
+
bulletFontChain
|
|
8220
|
+
);
|
|
8221
|
+
getFontUsageCollector()?.record(bulletFontChain, bulletText);
|
|
8066
8222
|
tspans.push(
|
|
8067
8223
|
`<tspan x="${bulletX}" dy="${dy}" text-anchor="start" ${bulletStyles}>${escapeXml2(bulletText)}</tspan>`
|
|
8068
8224
|
);
|
|
@@ -8082,8 +8238,7 @@ function renderTextBody(textBody, transform) {
|
|
|
8082
8238
|
);
|
|
8083
8239
|
const dy = computeDy(
|
|
8084
8240
|
isFirstLine,
|
|
8085
|
-
lineNaturalHeightPt,
|
|
8086
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8241
|
+
getLineHeightPx(para, lineNaturalHeightPt, lnSpcReduction),
|
|
8087
8242
|
lineGapPx
|
|
8088
8243
|
);
|
|
8089
8244
|
const prefix = `x="${xPos}" dy="${dy}" text-anchor="${anchorValue}" `;
|
|
@@ -8103,11 +8258,21 @@ function renderTextBody(textBody, transform) {
|
|
|
8103
8258
|
const naturalHeightPt = computeLineNaturalHeight(para.runs, defaultFontSize, fontScale);
|
|
8104
8259
|
const dy = computeDy(
|
|
8105
8260
|
isFirstLine,
|
|
8106
|
-
naturalHeightPt,
|
|
8107
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8261
|
+
getLineHeightPx(para, naturalHeightPt, lnSpcReduction),
|
|
8108
8262
|
paragraphGapPx
|
|
8109
8263
|
);
|
|
8110
|
-
const
|
|
8264
|
+
const bulletFontChain = buildBulletFontChain(
|
|
8265
|
+
para.properties,
|
|
8266
|
+
firstRun?.properties.fontFamily,
|
|
8267
|
+
firstRun?.properties.fontFamilyEa
|
|
8268
|
+
);
|
|
8269
|
+
const bulletStyles = buildBulletStyleAttrs(
|
|
8270
|
+
para.properties,
|
|
8271
|
+
fontSize,
|
|
8272
|
+
fontScale,
|
|
8273
|
+
bulletFontChain
|
|
8274
|
+
);
|
|
8275
|
+
getFontUsageCollector()?.record(bulletFontChain, bulletText);
|
|
8111
8276
|
tspans.push(
|
|
8112
8277
|
`<tspan x="${bulletX}" dy="${dy}" text-anchor="start" ${bulletStyles}>${escapeXml2(bulletText)}</tspan>`
|
|
8113
8278
|
);
|
|
@@ -8123,8 +8288,7 @@ function renderTextBody(textBody, transform) {
|
|
|
8123
8288
|
const naturalHeightPt = computeLineNaturalHeight(para.runs, defaultFontSize, fontScale);
|
|
8124
8289
|
const dy = computeDy(
|
|
8125
8290
|
isFirstLine,
|
|
8126
|
-
naturalHeightPt,
|
|
8127
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8291
|
+
getLineHeightPx(para, naturalHeightPt, lnSpcReduction),
|
|
8128
8292
|
paragraphGapPx
|
|
8129
8293
|
);
|
|
8130
8294
|
const prefix = `x="${xPos}" dy="${dy}" text-anchor="${anchorValue}" `;
|
|
@@ -8242,14 +8406,18 @@ function toAlpha(num) {
|
|
|
8242
8406
|
}
|
|
8243
8407
|
return result;
|
|
8244
8408
|
}
|
|
8245
|
-
function
|
|
8409
|
+
function buildBulletFontChain(props, runFontFamily, runFontFamilyEa) {
|
|
8410
|
+
return [props.bulletFont, runFontFamily ?? null, runFontFamilyEa ?? null];
|
|
8411
|
+
}
|
|
8412
|
+
function buildBulletStyleAttrs(props, textFontSizePt, fontScale, bulletFontChain) {
|
|
8246
8413
|
const styles = [];
|
|
8247
8414
|
if (props.bulletSizePct !== null) {
|
|
8248
8415
|
const size = textFontSizePt * (props.bulletSizePct / 1e5);
|
|
8249
8416
|
styles.push(`font-size="${size}pt"`);
|
|
8250
8417
|
}
|
|
8251
|
-
|
|
8252
|
-
|
|
8418
|
+
const fontFamilyValue = buildFontFamilyValue(bulletFontChain);
|
|
8419
|
+
if (fontFamilyValue) {
|
|
8420
|
+
styles.push(`font-family="${fontFamilyValue}"`);
|
|
8253
8421
|
}
|
|
8254
8422
|
if (props.bulletColor) {
|
|
8255
8423
|
styles.push(`fill="${props.bulletColor.hex}"`);
|
|
@@ -8270,15 +8438,13 @@ function getAlignmentInfo(alignment, marginLeftPx, textWidth, width, marginRight
|
|
|
8270
8438
|
return { xPos: marginLeftPx, anchorValue: "start" };
|
|
8271
8439
|
}
|
|
8272
8440
|
}
|
|
8273
|
-
function
|
|
8274
|
-
|
|
8275
|
-
if (
|
|
8276
|
-
|
|
8277
|
-
spacing = Math.max(0.5, factor);
|
|
8278
|
-
} else {
|
|
8279
|
-
spacing = DEFAULT_LINE_SPACING;
|
|
8441
|
+
function getLineHeightPx(para, naturalHeightPt, lnSpcReduction = 0) {
|
|
8442
|
+
const lineSpacing = para.properties.lineSpacing;
|
|
8443
|
+
if (lineSpacing?.type === "pts") {
|
|
8444
|
+
return lineSpacing.value / 100 * PX_PER_PT3 * (1 - lnSpcReduction);
|
|
8280
8445
|
}
|
|
8281
|
-
|
|
8446
|
+
const factor = lineSpacing !== null ? Math.max(0.5, lineSpacing.value / 1e5) : DEFAULT_LINE_SPACING;
|
|
8447
|
+
return naturalHeightPt * PX_PER_PT3 * factor * (1 - lnSpcReduction);
|
|
8282
8448
|
}
|
|
8283
8449
|
function resolveSpacingPx(spacing, fontSizePt) {
|
|
8284
8450
|
if (spacing.type === "pts") {
|
|
@@ -8297,11 +8463,9 @@ function getParagraphFontSize(para, defaultFontSize) {
|
|
|
8297
8463
|
}
|
|
8298
8464
|
return defaultFontSize;
|
|
8299
8465
|
}
|
|
8300
|
-
function computeDy(isFirstLine,
|
|
8466
|
+
function computeDy(isFirstLine, lineHeightPx, paragraphGapPx) {
|
|
8301
8467
|
if (isFirstLine) return "0";
|
|
8302
|
-
|
|
8303
|
-
const dy = lineHeight + paragraphGapPx;
|
|
8304
|
-
return dy.toFixed(2);
|
|
8468
|
+
return (lineHeightPx + paragraphGapPx).toFixed(2);
|
|
8305
8469
|
}
|
|
8306
8470
|
function getLineFontSize(segments, defaultFontSize) {
|
|
8307
8471
|
for (const seg of segments) {
|
|
@@ -8433,6 +8597,7 @@ function renderSegment(text, props, fontScale, prefix) {
|
|
|
8433
8597
|
let tspanContent;
|
|
8434
8598
|
if (!needsScriptSplit(props)) {
|
|
8435
8599
|
const styles = buildStyleAttrs(props, fontScale);
|
|
8600
|
+
getFontUsageCollector()?.record([props.fontFamily, props.fontFamilyEa], text);
|
|
8436
8601
|
tspanContent = `<tspan ${prefix}${styles}>${escapeXml2(text)}</tspan>`;
|
|
8437
8602
|
} else {
|
|
8438
8603
|
const parts = splitByScript(text);
|
|
@@ -8441,6 +8606,7 @@ function renderSegment(text, props, fontScale, prefix) {
|
|
|
8441
8606
|
const part = parts[i];
|
|
8442
8607
|
const fonts = part.isEa ? [props.fontFamilyEa, getJpanFallbackFont(), props.fontFamily] : [props.fontFamily, props.fontFamilyEa];
|
|
8443
8608
|
const styles = buildStyleAttrs(props, fontScale, fonts);
|
|
8609
|
+
getFontUsageCollector()?.record(fonts, part.text);
|
|
8444
8610
|
if (i === 0) {
|
|
8445
8611
|
result.push(`<tspan ${prefix}${styles}>${escapeXml2(part.text)}</tspan>`);
|
|
8446
8612
|
} else {
|
|
@@ -8535,10 +8701,13 @@ function estimateTextHeight(paragraphs, defaultFontSize, shouldWrap, textWidth,
|
|
|
8535
8701
|
const scaledDefaultForWrap = defaultFontSize * fontScale;
|
|
8536
8702
|
for (let pIdx = 0; pIdx < paragraphs.length; pIdx++) {
|
|
8537
8703
|
const para = paragraphs[pIdx];
|
|
8538
|
-
const lineSpacing = getLineSpacing(para, lnSpcReduction);
|
|
8539
8704
|
const isEmpty = !para.runs.some((r) => r.text.length > 0);
|
|
8540
8705
|
const naturalHeightPt = isEmpty && para.endParaRunProperties?.fontSize ? para.endParaRunProperties.fontSize * fontScale * defaultRatio : computeLineNaturalHeight(para.runs, defaultFontSize, fontScale);
|
|
8541
|
-
const lineHeight = (
|
|
8706
|
+
const lineHeight = getLineHeightPx(
|
|
8707
|
+
para,
|
|
8708
|
+
naturalHeightPt > 0 ? naturalHeightPt : defaultFontSize * fontScale * defaultRatio,
|
|
8709
|
+
lnSpcReduction
|
|
8710
|
+
);
|
|
8542
8711
|
let lineCount;
|
|
8543
8712
|
if (shouldWrap && para.runs.length > 0 && para.runs.some((r) => r.text.length > 0)) {
|
|
8544
8713
|
const wrappedLines = wrapParagraph(para, textWidth, scaledDefaultForWrap, fontScale);
|
|
@@ -8805,7 +8974,7 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
|
|
|
8805
8974
|
if (para.runs.length === 0 || !para.runs.some((r) => r.text.length > 0)) {
|
|
8806
8975
|
if (!isFirstLine) {
|
|
8807
8976
|
const emptyParaHeightPt = paraFontSizePt > 0 ? paraFontSizePt : defaultNaturalHeightPt;
|
|
8808
|
-
currentY += emptyParaHeightPt
|
|
8977
|
+
currentY += getLineHeightPx(para, emptyParaHeightPt, lnSpcReduction) + paragraphGapPx;
|
|
8809
8978
|
}
|
|
8810
8979
|
isFirstLine = false;
|
|
8811
8980
|
prevSpaceAfterPx = resolveSpacingPx(para.properties.spaceAfter, paraFontSizePt);
|
|
@@ -8823,7 +8992,7 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
|
|
|
8823
8992
|
const lineGapPx = lineIdx === 0 ? paragraphGapPx : 0;
|
|
8824
8993
|
if (line.segments.length === 0) {
|
|
8825
8994
|
if (!isFirstLine) {
|
|
8826
|
-
currentY +=
|
|
8995
|
+
currentY += getLineHeightPx(para, defaultNaturalHeightPt, lnSpcReduction) + lineGapPx;
|
|
8827
8996
|
}
|
|
8828
8997
|
isFirstLine = false;
|
|
8829
8998
|
continue;
|
|
@@ -8834,7 +9003,7 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
|
|
|
8834
9003
|
fontScale
|
|
8835
9004
|
);
|
|
8836
9005
|
if (!isFirstLine) {
|
|
8837
|
-
currentY +=
|
|
9006
|
+
currentY += getLineHeightPx(para, lineNaturalHeightPt, lnSpcReduction) + lineGapPx;
|
|
8838
9007
|
}
|
|
8839
9008
|
const lineWidth = measureLineWidth(line.segments, defaultFontSize, fontScale, fontResolver);
|
|
8840
9009
|
const lineStartX = computePathLineX(
|
|
@@ -8882,7 +9051,7 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
|
|
|
8882
9051
|
} else {
|
|
8883
9052
|
const naturalHeightPt = computeLineNaturalHeight(para.runs, defaultFontSize, fontScale);
|
|
8884
9053
|
if (!isFirstLine) {
|
|
8885
|
-
currentY +=
|
|
9054
|
+
currentY += getLineHeightPx(para, naturalHeightPt, lnSpcReduction) + paragraphGapPx;
|
|
8886
9055
|
}
|
|
8887
9056
|
const runsAsSegments = para.runs.filter((r) => r.text.length > 0).map((r) => ({ text: r.text, properties: r.properties }));
|
|
8888
9057
|
const lineWidth = measureLineWidth(runsAsSegments, defaultFontSize, fontScale, fontResolver);
|
|
@@ -9221,6 +9390,7 @@ function escapeXmlAttr(str) {
|
|
|
9221
9390
|
|
|
9222
9391
|
// src/converter.ts
|
|
9223
9392
|
async function convertPptxToSvg(input, options) {
|
|
9393
|
+
const textOutput = options?.textOutput ?? "path";
|
|
9224
9394
|
const setup = await createOpentypeSetupFromSystem(
|
|
9225
9395
|
options?.fontDirs,
|
|
9226
9396
|
options?.fontMapping,
|
|
@@ -9228,7 +9398,13 @@ async function convertPptxToSvg(input, options) {
|
|
|
9228
9398
|
);
|
|
9229
9399
|
if (setup) {
|
|
9230
9400
|
setTextMeasurer(setup.measurer);
|
|
9231
|
-
|
|
9401
|
+
if (textOutput !== "text") {
|
|
9402
|
+
setTextPathFontResolver(setup.fontResolver);
|
|
9403
|
+
}
|
|
9404
|
+
}
|
|
9405
|
+
const fontUsageCollector = textOutput === "text" ? new FontUsageCollector() : null;
|
|
9406
|
+
if (fontUsageCollector) {
|
|
9407
|
+
setFontUsageCollector(fontUsageCollector);
|
|
9232
9408
|
}
|
|
9233
9409
|
setFontMapping(createFontMapping(options?.fontMapping));
|
|
9234
9410
|
enableXmlCache();
|
|
@@ -9247,7 +9423,14 @@ async function convertPptxToSvg(input, options) {
|
|
|
9247
9423
|
const { slide, layoutElements, layoutShowMasterSp, masterElements } = parsed;
|
|
9248
9424
|
const effectiveMasterElements = slide.showMasterSp && layoutShowMasterSp ? masterElements : [];
|
|
9249
9425
|
slide.elements = mergeElements(effectiveMasterElements, layoutElements, slide.elements);
|
|
9250
|
-
|
|
9426
|
+
fontUsageCollector?.reset();
|
|
9427
|
+
let svg = renderSlideToSvg(slide, data.presInfo.slideSize);
|
|
9428
|
+
if (fontUsageCollector && setup) {
|
|
9429
|
+
const style = await buildFontFaceStyle(fontUsageCollector.getUsages(), setup.fontResolver);
|
|
9430
|
+
if (style) {
|
|
9431
|
+
svg = injectIntoSvgDefs(svg, style);
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9251
9434
|
results.push({ slideNumber, svg });
|
|
9252
9435
|
}
|
|
9253
9436
|
flushWarnings();
|
|
@@ -9256,10 +9439,16 @@ async function convertPptxToSvg(input, options) {
|
|
|
9256
9439
|
clearXmlCache();
|
|
9257
9440
|
resetTextMeasurer();
|
|
9258
9441
|
resetTextPathFontResolver();
|
|
9442
|
+
resetFontUsageCollector();
|
|
9259
9443
|
resetFontMapping();
|
|
9260
9444
|
resetScriptFonts();
|
|
9261
9445
|
}
|
|
9262
9446
|
}
|
|
9447
|
+
function injectIntoSvgDefs(svg, content) {
|
|
9448
|
+
const openTagEnd = svg.indexOf(">");
|
|
9449
|
+
if (openTagEnd === -1) return svg;
|
|
9450
|
+
return `${svg.slice(0, openTagEnd + 1)}<defs>${content}</defs>${svg.slice(openTagEnd + 1)}`;
|
|
9451
|
+
}
|
|
9263
9452
|
var cachedFontBuffers = null;
|
|
9264
9453
|
var cachedFontBuffersKey = null;
|
|
9265
9454
|
var MAX_TOTAL_FONT_BUFFER_BYTES = 100 * 1024 * 1024;
|
|
@@ -9297,7 +9486,7 @@ ${skipSystemFonts ?? false}`;
|
|
|
9297
9486
|
return buffers;
|
|
9298
9487
|
}
|
|
9299
9488
|
async function convertPptxToPng(input, options) {
|
|
9300
|
-
const svgResults = await convertPptxToSvg(input, options);
|
|
9489
|
+
const svgResults = await convertPptxToSvg(input, { ...options, textOutput: "path" });
|
|
9301
9490
|
const width = options?.width ?? DEFAULT_OUTPUT_WIDTH;
|
|
9302
9491
|
const height = options?.height;
|
|
9303
9492
|
const fontBuffers = loadFontBuffers(options?.fontDirs, options?.skipSystemFonts);
|
package/dist/index.d.cts
CHANGED
|
@@ -48,6 +48,15 @@ interface ConvertOptions {
|
|
|
48
48
|
fontMapping?: FontMapping;
|
|
49
49
|
/** true のとき OS のシステムフォントをスキャンせず fontDirs のみを使用する */
|
|
50
50
|
skipSystemFonts?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* SVG でのテキスト出力方式。デフォルト: "path"
|
|
53
|
+
* - "path": グリフをアウトライン化した <path> として出力する。フォント環境に依存しない
|
|
54
|
+
* - "text": ネイティブ <text> 要素 + サブセット化フォントの @font-face (data URI) 埋め込みで出力する。
|
|
55
|
+
* ブラウザでのインライン表示時にネイティブテキスト描画 (ヒンティング等) が効き、テキスト選択も可能になる。
|
|
56
|
+
* <img src="...svg"> 参照やサニタイズ環境では期待どおり描画されないことがある。
|
|
57
|
+
* convertPptxToPng では無視され、常に "path" で変換される (resvg は @font-face を解釈しないため)
|
|
58
|
+
*/
|
|
59
|
+
textOutput?: "path" | "text";
|
|
51
60
|
}
|
|
52
61
|
interface SlideSvg {
|
|
53
62
|
slideNumber: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,15 @@ interface ConvertOptions {
|
|
|
48
48
|
fontMapping?: FontMapping;
|
|
49
49
|
/** true のとき OS のシステムフォントをスキャンせず fontDirs のみを使用する */
|
|
50
50
|
skipSystemFonts?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* SVG でのテキスト出力方式。デフォルト: "path"
|
|
53
|
+
* - "path": グリフをアウトライン化した <path> として出力する。フォント環境に依存しない
|
|
54
|
+
* - "text": ネイティブ <text> 要素 + サブセット化フォントの @font-face (data URI) 埋め込みで出力する。
|
|
55
|
+
* ブラウザでのインライン表示時にネイティブテキスト描画 (ヒンティング等) が効き、テキスト選択も可能になる。
|
|
56
|
+
* <img src="...svg"> 参照やサニタイズ環境では期待どおり描画されないことがある。
|
|
57
|
+
* convertPptxToPng では無視され、常に "path" で変換される (resvg は @font-face を解釈しないため)
|
|
58
|
+
*/
|
|
59
|
+
textOutput?: "path" | "text";
|
|
51
60
|
}
|
|
52
61
|
interface SlideSvg {
|
|
53
62
|
slideNumber: number;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,189 @@
|
|
|
1
1
|
// src/converter.ts
|
|
2
2
|
import { readFileSync, statSync } from "fs";
|
|
3
3
|
|
|
4
|
+
// src/font/font-embedder.ts
|
|
5
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
6
|
+
|
|
7
|
+
// src/warning-logger.ts
|
|
8
|
+
var PREFIX = "[pptx-glimpse]";
|
|
9
|
+
var currentLevel = "off";
|
|
10
|
+
var entries = [];
|
|
11
|
+
var featureCounts = /* @__PURE__ */ new Map();
|
|
12
|
+
function initWarningLogger(level) {
|
|
13
|
+
currentLevel = level;
|
|
14
|
+
entries = [];
|
|
15
|
+
featureCounts.clear();
|
|
16
|
+
}
|
|
17
|
+
function warn(feature, message, context) {
|
|
18
|
+
if (currentLevel === "off") return;
|
|
19
|
+
entries.push({ feature, message, ...context !== void 0 && { context } });
|
|
20
|
+
const existing = featureCounts.get(feature);
|
|
21
|
+
if (existing) {
|
|
22
|
+
existing.count++;
|
|
23
|
+
} else {
|
|
24
|
+
featureCounts.set(feature, { message, count: 1 });
|
|
25
|
+
}
|
|
26
|
+
if (currentLevel === "debug") {
|
|
27
|
+
const ctx = context ? ` (${context})` : "";
|
|
28
|
+
console.warn(`${PREFIX} SKIP: ${feature} - ${message}${ctx}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function debug(feature, message, context) {
|
|
32
|
+
if (currentLevel !== "debug") return;
|
|
33
|
+
entries.push({ feature, message, ...context !== void 0 && { context } });
|
|
34
|
+
const existing = featureCounts.get(feature);
|
|
35
|
+
if (existing) {
|
|
36
|
+
existing.count++;
|
|
37
|
+
} else {
|
|
38
|
+
featureCounts.set(feature, { message, count: 1 });
|
|
39
|
+
}
|
|
40
|
+
const ctx = context ? ` (${context})` : "";
|
|
41
|
+
console.warn(`${PREFIX} DEBUG: ${feature} - ${message}${ctx}`);
|
|
42
|
+
}
|
|
43
|
+
function getWarningSummary() {
|
|
44
|
+
const features = [];
|
|
45
|
+
for (const [feature, { message, count }] of featureCounts) {
|
|
46
|
+
features.push({ feature, message, count });
|
|
47
|
+
}
|
|
48
|
+
return { totalCount: entries.length, features };
|
|
49
|
+
}
|
|
50
|
+
function flushWarnings() {
|
|
51
|
+
const summary = getWarningSummary();
|
|
52
|
+
if (currentLevel !== "off" && summary.features.length > 0) {
|
|
53
|
+
console.warn(`${PREFIX} Summary: ${summary.features.length} unsupported feature(s) detected`);
|
|
54
|
+
for (const { feature, count } of summary.features) {
|
|
55
|
+
console.warn(` - ${feature}: ${count} occurrence(s)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
entries = [];
|
|
59
|
+
featureCounts.clear();
|
|
60
|
+
return summary;
|
|
61
|
+
}
|
|
62
|
+
function getWarningEntries() {
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/font/font-subsetter.ts
|
|
67
|
+
async function tryLoadOpentypeCtors() {
|
|
68
|
+
try {
|
|
69
|
+
const specifier = "opentype.js";
|
|
70
|
+
const mod = await import(
|
|
71
|
+
/* @vite-ignore */
|
|
72
|
+
specifier
|
|
73
|
+
);
|
|
74
|
+
return { Font: mod.Font, Glyph: mod.Glyph };
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function glyphName(glyph, firstUnicode) {
|
|
80
|
+
if (glyph.name) return glyph.name;
|
|
81
|
+
return `uni${firstUnicode.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
82
|
+
}
|
|
83
|
+
async function subsetFont(font, chars, familyName) {
|
|
84
|
+
const opentype = await tryLoadOpentypeCtors();
|
|
85
|
+
if (!opentype) return null;
|
|
86
|
+
const source = font;
|
|
87
|
+
if (typeof source.charToGlyph !== "function" || !source.glyphs) return null;
|
|
88
|
+
const glyphMap = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const char of chars) {
|
|
90
|
+
const codePoint = char.codePointAt(0);
|
|
91
|
+
if (codePoint === void 0) continue;
|
|
92
|
+
let glyph;
|
|
93
|
+
try {
|
|
94
|
+
glyph = source.charToGlyph(char);
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!glyph || !glyph.index) continue;
|
|
99
|
+
const entry = glyphMap.get(glyph.index);
|
|
100
|
+
if (entry) {
|
|
101
|
+
entry.unicodes.add(codePoint);
|
|
102
|
+
} else {
|
|
103
|
+
glyphMap.set(glyph.index, { glyph, unicodes: /* @__PURE__ */ new Set([codePoint]) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (glyphMap.size === 0) return null;
|
|
107
|
+
try {
|
|
108
|
+
const notdefSource = source.glyphs.get(0);
|
|
109
|
+
const glyphs = [
|
|
110
|
+
new opentype.Glyph({
|
|
111
|
+
name: ".notdef",
|
|
112
|
+
advanceWidth: notdefSource?.advanceWidth ?? source.unitsPerEm / 2,
|
|
113
|
+
path: notdefSource?.path
|
|
114
|
+
})
|
|
115
|
+
];
|
|
116
|
+
for (const { glyph, unicodes } of glyphMap.values()) {
|
|
117
|
+
const unicodeList = [...unicodes].sort((a, b) => a - b);
|
|
118
|
+
glyphs.push(
|
|
119
|
+
new opentype.Glyph({
|
|
120
|
+
name: glyphName(glyph, unicodeList[0]),
|
|
121
|
+
unicode: unicodeList[0],
|
|
122
|
+
unicodes: unicodeList,
|
|
123
|
+
advanceWidth: glyph.advanceWidth ?? 0,
|
|
124
|
+
path: glyph.path
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const subset = new opentype.Font({
|
|
129
|
+
familyName: familyName || "EmbeddedFont",
|
|
130
|
+
styleName: "Regular",
|
|
131
|
+
unitsPerEm: source.unitsPerEm,
|
|
132
|
+
ascender: source.ascender,
|
|
133
|
+
// opentype.js は descender に負値を要求する
|
|
134
|
+
descender: source.descender < 0 ? source.descender : -1,
|
|
135
|
+
glyphs
|
|
136
|
+
});
|
|
137
|
+
return new Uint8Array(subset.toArrayBuffer());
|
|
138
|
+
} catch (e) {
|
|
139
|
+
warn(
|
|
140
|
+
"font.subsetFailed",
|
|
141
|
+
`Failed to subset font "${familyName}": ${e instanceof Error ? e.message : String(e)}`
|
|
142
|
+
);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/font/script-font-context.ts
|
|
148
|
+
var jpanMajorFont = null;
|
|
149
|
+
var jpanMinorFont = null;
|
|
150
|
+
function setScriptFonts(majorJpan, minorJpan) {
|
|
151
|
+
jpanMajorFont = majorJpan;
|
|
152
|
+
jpanMinorFont = minorJpan;
|
|
153
|
+
}
|
|
154
|
+
function resetScriptFonts() {
|
|
155
|
+
jpanMajorFont = null;
|
|
156
|
+
jpanMinorFont = null;
|
|
157
|
+
}
|
|
158
|
+
function getJpanFallbackFont() {
|
|
159
|
+
return jpanMajorFont ?? jpanMinorFont;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/font/font-embedder.ts
|
|
163
|
+
function escapeCssFamilyName(name) {
|
|
164
|
+
return name.replace(/[\\"]/g, (c) => `\\${c}`).replace(/[<>&]/g, (c) => `\\${c.codePointAt(0).toString(16)} `);
|
|
165
|
+
}
|
|
166
|
+
async function buildFontFaceStyle(usages, fontResolver) {
|
|
167
|
+
const faces = [];
|
|
168
|
+
const jpanFallback = getJpanFallbackFont();
|
|
169
|
+
for (const [familyName, usage] of usages) {
|
|
170
|
+
const font = fontResolver.resolveFont(
|
|
171
|
+
usage.fonts[0],
|
|
172
|
+
usage.fonts[1] ?? null,
|
|
173
|
+
usage.fonts[2] ?? jpanFallback
|
|
174
|
+
);
|
|
175
|
+
if (!font) continue;
|
|
176
|
+
const buffer = await subsetFont(font, usage.chars, familyName);
|
|
177
|
+
if (!buffer) continue;
|
|
178
|
+
const base64 = Buffer2.from(buffer).toString("base64");
|
|
179
|
+
faces.push(
|
|
180
|
+
`@font-face{font-family:"${escapeCssFamilyName(familyName)}";src:url(data:font/otf;base64,${base64}) format("opentype");}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (faces.length === 0) return "";
|
|
184
|
+
return `<style type="text/css">${faces.join("")}</style>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
4
187
|
// src/font/font-mapping.ts
|
|
5
188
|
var DEFAULT_FONT_MAPPING = {
|
|
6
189
|
// ラテン文字フォント
|
|
@@ -67,6 +250,40 @@ function getCurrentMappedFont(fontFamily) {
|
|
|
67
250
|
return getMappedFont(fontFamily, currentMapping);
|
|
68
251
|
}
|
|
69
252
|
|
|
253
|
+
// src/font/font-usage-collector.ts
|
|
254
|
+
var FontUsageCollector = class {
|
|
255
|
+
/** key: tspan の font-family リスト先頭のフォント名 (= @font-face で宣言する名前) */
|
|
256
|
+
usages = /* @__PURE__ */ new Map();
|
|
257
|
+
record(fonts, text) {
|
|
258
|
+
const primary = fonts.find((f) => f !== null && f !== void 0);
|
|
259
|
+
if (!primary || text.length === 0) return;
|
|
260
|
+
let usage = this.usages.get(primary);
|
|
261
|
+
if (!usage) {
|
|
262
|
+
usage = { fonts: [...fonts], chars: /* @__PURE__ */ new Set() };
|
|
263
|
+
this.usages.set(primary, usage);
|
|
264
|
+
}
|
|
265
|
+
for (const char of text) {
|
|
266
|
+
usage.chars.add(char);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
getUsages() {
|
|
270
|
+
return this.usages;
|
|
271
|
+
}
|
|
272
|
+
reset() {
|
|
273
|
+
this.usages.clear();
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
var currentCollector = null;
|
|
277
|
+
function setFontUsageCollector(collector) {
|
|
278
|
+
currentCollector = collector;
|
|
279
|
+
}
|
|
280
|
+
function getFontUsageCollector() {
|
|
281
|
+
return currentCollector;
|
|
282
|
+
}
|
|
283
|
+
function resetFontUsageCollector() {
|
|
284
|
+
currentCollector = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
70
287
|
// src/font/opentype-helpers.ts
|
|
71
288
|
import { readFile } from "fs/promises";
|
|
72
289
|
|
|
@@ -1010,65 +1227,6 @@ function measureTextWidth(text, fontSizePt, bold, fontFamily, fontFamilyEa) {
|
|
|
1010
1227
|
return totalWidth;
|
|
1011
1228
|
}
|
|
1012
1229
|
|
|
1013
|
-
// src/warning-logger.ts
|
|
1014
|
-
var PREFIX = "[pptx-glimpse]";
|
|
1015
|
-
var currentLevel = "off";
|
|
1016
|
-
var entries = [];
|
|
1017
|
-
var featureCounts = /* @__PURE__ */ new Map();
|
|
1018
|
-
function initWarningLogger(level) {
|
|
1019
|
-
currentLevel = level;
|
|
1020
|
-
entries = [];
|
|
1021
|
-
featureCounts.clear();
|
|
1022
|
-
}
|
|
1023
|
-
function warn(feature, message, context) {
|
|
1024
|
-
if (currentLevel === "off") return;
|
|
1025
|
-
entries.push({ feature, message, ...context !== void 0 && { context } });
|
|
1026
|
-
const existing = featureCounts.get(feature);
|
|
1027
|
-
if (existing) {
|
|
1028
|
-
existing.count++;
|
|
1029
|
-
} else {
|
|
1030
|
-
featureCounts.set(feature, { message, count: 1 });
|
|
1031
|
-
}
|
|
1032
|
-
if (currentLevel === "debug") {
|
|
1033
|
-
const ctx = context ? ` (${context})` : "";
|
|
1034
|
-
console.warn(`${PREFIX} SKIP: ${feature} - ${message}${ctx}`);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
function debug(feature, message, context) {
|
|
1038
|
-
if (currentLevel !== "debug") return;
|
|
1039
|
-
entries.push({ feature, message, ...context !== void 0 && { context } });
|
|
1040
|
-
const existing = featureCounts.get(feature);
|
|
1041
|
-
if (existing) {
|
|
1042
|
-
existing.count++;
|
|
1043
|
-
} else {
|
|
1044
|
-
featureCounts.set(feature, { message, count: 1 });
|
|
1045
|
-
}
|
|
1046
|
-
const ctx = context ? ` (${context})` : "";
|
|
1047
|
-
console.warn(`${PREFIX} DEBUG: ${feature} - ${message}${ctx}`);
|
|
1048
|
-
}
|
|
1049
|
-
function getWarningSummary() {
|
|
1050
|
-
const features = [];
|
|
1051
|
-
for (const [feature, { message, count }] of featureCounts) {
|
|
1052
|
-
features.push({ feature, message, count });
|
|
1053
|
-
}
|
|
1054
|
-
return { totalCount: entries.length, features };
|
|
1055
|
-
}
|
|
1056
|
-
function flushWarnings() {
|
|
1057
|
-
const summary = getWarningSummary();
|
|
1058
|
-
if (currentLevel !== "off" && summary.features.length > 0) {
|
|
1059
|
-
console.warn(`${PREFIX} Summary: ${summary.features.length} unsupported feature(s) detected`);
|
|
1060
|
-
for (const { feature, count } of summary.features) {
|
|
1061
|
-
console.warn(` - ${feature}: ${count} occurrence(s)`);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
entries = [];
|
|
1065
|
-
featureCounts.clear();
|
|
1066
|
-
return summary;
|
|
1067
|
-
}
|
|
1068
|
-
function getWarningEntries() {
|
|
1069
|
-
return entries;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
1230
|
// src/font/cjk-font-fallback.ts
|
|
1073
1231
|
import { platform } from "os";
|
|
1074
1232
|
var MACOS_FALLBACKS = {
|
|
@@ -1605,21 +1763,6 @@ async function createOpentypeSetupFromSystem(additionalFontDirs, fontMapping, sk
|
|
|
1605
1763
|
return setup;
|
|
1606
1764
|
}
|
|
1607
1765
|
|
|
1608
|
-
// src/font/script-font-context.ts
|
|
1609
|
-
var jpanMajorFont = null;
|
|
1610
|
-
var jpanMinorFont = null;
|
|
1611
|
-
function setScriptFonts(majorJpan, minorJpan) {
|
|
1612
|
-
jpanMajorFont = majorJpan;
|
|
1613
|
-
jpanMinorFont = minorJpan;
|
|
1614
|
-
}
|
|
1615
|
-
function resetScriptFonts() {
|
|
1616
|
-
jpanMajorFont = null;
|
|
1617
|
-
jpanMinorFont = null;
|
|
1618
|
-
}
|
|
1619
|
-
function getJpanFallbackFont() {
|
|
1620
|
-
return jpanMajorFont ?? jpanMinorFont;
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
1766
|
// src/font/text-measurer.ts
|
|
1624
1767
|
var DefaultTextMeasurer = class {
|
|
1625
1768
|
measureTextWidth(text, fontSizePt, bold, fontFamily, fontFamilyEa) {
|
|
@@ -4218,11 +4361,10 @@ function parseParagraph(p, colorResolver, rels, fontScheme, lstStyle, orderedPCh
|
|
|
4218
4361
|
const lstLevelProps = lstStyle?.levels[level];
|
|
4219
4362
|
const { bullet, bulletFont, bulletColor, bulletSizePct } = parseBullet(pPr, colorResolver);
|
|
4220
4363
|
const lnSpc = pPr?.lnSpc;
|
|
4221
|
-
const lnSpcSpcPct = lnSpc?.spcPct;
|
|
4222
4364
|
const tabStops = parseTabStops(pPr);
|
|
4223
4365
|
const properties = {
|
|
4224
4366
|
alignment: pPr?.["@_algn"] ?? lstLevelProps?.alignment ?? null,
|
|
4225
|
-
lineSpacing:
|
|
4367
|
+
lineSpacing: lnSpc?.spcPts || lnSpc?.spcPct ? parseSpacing(lnSpc) : null,
|
|
4226
4368
|
spaceBefore: parseSpacing(pPr?.spcBef),
|
|
4227
4369
|
spaceAfter: parseSpacing(pPr?.spcAft),
|
|
4228
4370
|
level,
|
|
@@ -7984,7 +8126,11 @@ function renderTextBody(textBody, transform) {
|
|
|
7984
8126
|
const paragraphGapPx = Math.max(prevSpaceAfterPx, spaceBeforePx);
|
|
7985
8127
|
if (para.runs.length === 0 || !para.runs.some((r) => r.text.length > 0)) {
|
|
7986
8128
|
const emptyParaHeightPt = paraFontSizePt > 0 ? paraFontSizePt : defaultNaturalHeightPt;
|
|
7987
|
-
const dy = computeDy(
|
|
8129
|
+
const dy = computeDy(
|
|
8130
|
+
isFirstLine,
|
|
8131
|
+
getLineHeightPx(para, emptyParaHeightPt, lnSpcReduction),
|
|
8132
|
+
paragraphGapPx
|
|
8133
|
+
);
|
|
7988
8134
|
tspans.push(`<tspan x="${xPos}" dy="${dy}" text-anchor="${anchorValue}"> </tspan>`);
|
|
7989
8135
|
isFirstLine = false;
|
|
7990
8136
|
prevSpaceAfterPx = resolveSpacingPx(para.properties.spaceAfter, paraFontSizePt);
|
|
@@ -8003,8 +8149,7 @@ function renderTextBody(textBody, transform) {
|
|
|
8003
8149
|
if (line.segments.length === 0) {
|
|
8004
8150
|
const dy = computeDy(
|
|
8005
8151
|
isFirstLine,
|
|
8006
|
-
defaultNaturalHeightPt,
|
|
8007
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8152
|
+
getLineHeightPx(para, defaultNaturalHeightPt, lnSpcReduction),
|
|
8008
8153
|
lineGapPx
|
|
8009
8154
|
);
|
|
8010
8155
|
tspans.push(`<tspan x="${xPos}" dy="${dy}" text-anchor="${anchorValue}"> </tspan>`);
|
|
@@ -8020,11 +8165,22 @@ function renderTextBody(textBody, transform) {
|
|
|
8020
8165
|
);
|
|
8021
8166
|
const dy = computeDy(
|
|
8022
8167
|
isFirstLine,
|
|
8023
|
-
lineNaturalHeightPt,
|
|
8024
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8168
|
+
getLineHeightPx(para, lineNaturalHeightPt, lnSpcReduction),
|
|
8025
8169
|
paragraphGapPx
|
|
8026
8170
|
);
|
|
8027
|
-
const
|
|
8171
|
+
const firstSeg = line.segments[0];
|
|
8172
|
+
const bulletFontChain = buildBulletFontChain(
|
|
8173
|
+
para.properties,
|
|
8174
|
+
firstSeg?.properties.fontFamily,
|
|
8175
|
+
firstSeg?.properties.fontFamilyEa
|
|
8176
|
+
);
|
|
8177
|
+
const bulletStyles = buildBulletStyleAttrs(
|
|
8178
|
+
para.properties,
|
|
8179
|
+
lineFontSize,
|
|
8180
|
+
fontScale,
|
|
8181
|
+
bulletFontChain
|
|
8182
|
+
);
|
|
8183
|
+
getFontUsageCollector()?.record(bulletFontChain, bulletText);
|
|
8028
8184
|
tspans.push(
|
|
8029
8185
|
`<tspan x="${bulletX}" dy="${dy}" text-anchor="start" ${bulletStyles}>${escapeXml2(bulletText)}</tspan>`
|
|
8030
8186
|
);
|
|
@@ -8044,8 +8200,7 @@ function renderTextBody(textBody, transform) {
|
|
|
8044
8200
|
);
|
|
8045
8201
|
const dy = computeDy(
|
|
8046
8202
|
isFirstLine,
|
|
8047
|
-
lineNaturalHeightPt,
|
|
8048
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8203
|
+
getLineHeightPx(para, lineNaturalHeightPt, lnSpcReduction),
|
|
8049
8204
|
lineGapPx
|
|
8050
8205
|
);
|
|
8051
8206
|
const prefix = `x="${xPos}" dy="${dy}" text-anchor="${anchorValue}" `;
|
|
@@ -8065,11 +8220,21 @@ function renderTextBody(textBody, transform) {
|
|
|
8065
8220
|
const naturalHeightPt = computeLineNaturalHeight(para.runs, defaultFontSize, fontScale);
|
|
8066
8221
|
const dy = computeDy(
|
|
8067
8222
|
isFirstLine,
|
|
8068
|
-
naturalHeightPt,
|
|
8069
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8223
|
+
getLineHeightPx(para, naturalHeightPt, lnSpcReduction),
|
|
8070
8224
|
paragraphGapPx
|
|
8071
8225
|
);
|
|
8072
|
-
const
|
|
8226
|
+
const bulletFontChain = buildBulletFontChain(
|
|
8227
|
+
para.properties,
|
|
8228
|
+
firstRun?.properties.fontFamily,
|
|
8229
|
+
firstRun?.properties.fontFamilyEa
|
|
8230
|
+
);
|
|
8231
|
+
const bulletStyles = buildBulletStyleAttrs(
|
|
8232
|
+
para.properties,
|
|
8233
|
+
fontSize,
|
|
8234
|
+
fontScale,
|
|
8235
|
+
bulletFontChain
|
|
8236
|
+
);
|
|
8237
|
+
getFontUsageCollector()?.record(bulletFontChain, bulletText);
|
|
8073
8238
|
tspans.push(
|
|
8074
8239
|
`<tspan x="${bulletX}" dy="${dy}" text-anchor="start" ${bulletStyles}>${escapeXml2(bulletText)}</tspan>`
|
|
8075
8240
|
);
|
|
@@ -8085,8 +8250,7 @@ function renderTextBody(textBody, transform) {
|
|
|
8085
8250
|
const naturalHeightPt = computeLineNaturalHeight(para.runs, defaultFontSize, fontScale);
|
|
8086
8251
|
const dy = computeDy(
|
|
8087
8252
|
isFirstLine,
|
|
8088
|
-
naturalHeightPt,
|
|
8089
|
-
getLineSpacing(para, lnSpcReduction),
|
|
8253
|
+
getLineHeightPx(para, naturalHeightPt, lnSpcReduction),
|
|
8090
8254
|
paragraphGapPx
|
|
8091
8255
|
);
|
|
8092
8256
|
const prefix = `x="${xPos}" dy="${dy}" text-anchor="${anchorValue}" `;
|
|
@@ -8204,14 +8368,18 @@ function toAlpha(num) {
|
|
|
8204
8368
|
}
|
|
8205
8369
|
return result;
|
|
8206
8370
|
}
|
|
8207
|
-
function
|
|
8371
|
+
function buildBulletFontChain(props, runFontFamily, runFontFamilyEa) {
|
|
8372
|
+
return [props.bulletFont, runFontFamily ?? null, runFontFamilyEa ?? null];
|
|
8373
|
+
}
|
|
8374
|
+
function buildBulletStyleAttrs(props, textFontSizePt, fontScale, bulletFontChain) {
|
|
8208
8375
|
const styles = [];
|
|
8209
8376
|
if (props.bulletSizePct !== null) {
|
|
8210
8377
|
const size = textFontSizePt * (props.bulletSizePct / 1e5);
|
|
8211
8378
|
styles.push(`font-size="${size}pt"`);
|
|
8212
8379
|
}
|
|
8213
|
-
|
|
8214
|
-
|
|
8380
|
+
const fontFamilyValue = buildFontFamilyValue(bulletFontChain);
|
|
8381
|
+
if (fontFamilyValue) {
|
|
8382
|
+
styles.push(`font-family="${fontFamilyValue}"`);
|
|
8215
8383
|
}
|
|
8216
8384
|
if (props.bulletColor) {
|
|
8217
8385
|
styles.push(`fill="${props.bulletColor.hex}"`);
|
|
@@ -8232,15 +8400,13 @@ function getAlignmentInfo(alignment, marginLeftPx, textWidth, width, marginRight
|
|
|
8232
8400
|
return { xPos: marginLeftPx, anchorValue: "start" };
|
|
8233
8401
|
}
|
|
8234
8402
|
}
|
|
8235
|
-
function
|
|
8236
|
-
|
|
8237
|
-
if (
|
|
8238
|
-
|
|
8239
|
-
spacing = Math.max(0.5, factor);
|
|
8240
|
-
} else {
|
|
8241
|
-
spacing = DEFAULT_LINE_SPACING;
|
|
8403
|
+
function getLineHeightPx(para, naturalHeightPt, lnSpcReduction = 0) {
|
|
8404
|
+
const lineSpacing = para.properties.lineSpacing;
|
|
8405
|
+
if (lineSpacing?.type === "pts") {
|
|
8406
|
+
return lineSpacing.value / 100 * PX_PER_PT3 * (1 - lnSpcReduction);
|
|
8242
8407
|
}
|
|
8243
|
-
|
|
8408
|
+
const factor = lineSpacing !== null ? Math.max(0.5, lineSpacing.value / 1e5) : DEFAULT_LINE_SPACING;
|
|
8409
|
+
return naturalHeightPt * PX_PER_PT3 * factor * (1 - lnSpcReduction);
|
|
8244
8410
|
}
|
|
8245
8411
|
function resolveSpacingPx(spacing, fontSizePt) {
|
|
8246
8412
|
if (spacing.type === "pts") {
|
|
@@ -8259,11 +8425,9 @@ function getParagraphFontSize(para, defaultFontSize) {
|
|
|
8259
8425
|
}
|
|
8260
8426
|
return defaultFontSize;
|
|
8261
8427
|
}
|
|
8262
|
-
function computeDy(isFirstLine,
|
|
8428
|
+
function computeDy(isFirstLine, lineHeightPx, paragraphGapPx) {
|
|
8263
8429
|
if (isFirstLine) return "0";
|
|
8264
|
-
|
|
8265
|
-
const dy = lineHeight + paragraphGapPx;
|
|
8266
|
-
return dy.toFixed(2);
|
|
8430
|
+
return (lineHeightPx + paragraphGapPx).toFixed(2);
|
|
8267
8431
|
}
|
|
8268
8432
|
function getLineFontSize(segments, defaultFontSize) {
|
|
8269
8433
|
for (const seg of segments) {
|
|
@@ -8395,6 +8559,7 @@ function renderSegment(text, props, fontScale, prefix) {
|
|
|
8395
8559
|
let tspanContent;
|
|
8396
8560
|
if (!needsScriptSplit(props)) {
|
|
8397
8561
|
const styles = buildStyleAttrs(props, fontScale);
|
|
8562
|
+
getFontUsageCollector()?.record([props.fontFamily, props.fontFamilyEa], text);
|
|
8398
8563
|
tspanContent = `<tspan ${prefix}${styles}>${escapeXml2(text)}</tspan>`;
|
|
8399
8564
|
} else {
|
|
8400
8565
|
const parts = splitByScript(text);
|
|
@@ -8403,6 +8568,7 @@ function renderSegment(text, props, fontScale, prefix) {
|
|
|
8403
8568
|
const part = parts[i];
|
|
8404
8569
|
const fonts = part.isEa ? [props.fontFamilyEa, getJpanFallbackFont(), props.fontFamily] : [props.fontFamily, props.fontFamilyEa];
|
|
8405
8570
|
const styles = buildStyleAttrs(props, fontScale, fonts);
|
|
8571
|
+
getFontUsageCollector()?.record(fonts, part.text);
|
|
8406
8572
|
if (i === 0) {
|
|
8407
8573
|
result.push(`<tspan ${prefix}${styles}>${escapeXml2(part.text)}</tspan>`);
|
|
8408
8574
|
} else {
|
|
@@ -8497,10 +8663,13 @@ function estimateTextHeight(paragraphs, defaultFontSize, shouldWrap, textWidth,
|
|
|
8497
8663
|
const scaledDefaultForWrap = defaultFontSize * fontScale;
|
|
8498
8664
|
for (let pIdx = 0; pIdx < paragraphs.length; pIdx++) {
|
|
8499
8665
|
const para = paragraphs[pIdx];
|
|
8500
|
-
const lineSpacing = getLineSpacing(para, lnSpcReduction);
|
|
8501
8666
|
const isEmpty = !para.runs.some((r) => r.text.length > 0);
|
|
8502
8667
|
const naturalHeightPt = isEmpty && para.endParaRunProperties?.fontSize ? para.endParaRunProperties.fontSize * fontScale * defaultRatio : computeLineNaturalHeight(para.runs, defaultFontSize, fontScale);
|
|
8503
|
-
const lineHeight = (
|
|
8668
|
+
const lineHeight = getLineHeightPx(
|
|
8669
|
+
para,
|
|
8670
|
+
naturalHeightPt > 0 ? naturalHeightPt : defaultFontSize * fontScale * defaultRatio,
|
|
8671
|
+
lnSpcReduction
|
|
8672
|
+
);
|
|
8504
8673
|
let lineCount;
|
|
8505
8674
|
if (shouldWrap && para.runs.length > 0 && para.runs.some((r) => r.text.length > 0)) {
|
|
8506
8675
|
const wrappedLines = wrapParagraph(para, textWidth, scaledDefaultForWrap, fontScale);
|
|
@@ -8767,7 +8936,7 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
|
|
|
8767
8936
|
if (para.runs.length === 0 || !para.runs.some((r) => r.text.length > 0)) {
|
|
8768
8937
|
if (!isFirstLine) {
|
|
8769
8938
|
const emptyParaHeightPt = paraFontSizePt > 0 ? paraFontSizePt : defaultNaturalHeightPt;
|
|
8770
|
-
currentY += emptyParaHeightPt
|
|
8939
|
+
currentY += getLineHeightPx(para, emptyParaHeightPt, lnSpcReduction) + paragraphGapPx;
|
|
8771
8940
|
}
|
|
8772
8941
|
isFirstLine = false;
|
|
8773
8942
|
prevSpaceAfterPx = resolveSpacingPx(para.properties.spaceAfter, paraFontSizePt);
|
|
@@ -8785,7 +8954,7 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
|
|
|
8785
8954
|
const lineGapPx = lineIdx === 0 ? paragraphGapPx : 0;
|
|
8786
8955
|
if (line.segments.length === 0) {
|
|
8787
8956
|
if (!isFirstLine) {
|
|
8788
|
-
currentY +=
|
|
8957
|
+
currentY += getLineHeightPx(para, defaultNaturalHeightPt, lnSpcReduction) + lineGapPx;
|
|
8789
8958
|
}
|
|
8790
8959
|
isFirstLine = false;
|
|
8791
8960
|
continue;
|
|
@@ -8796,7 +8965,7 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
|
|
|
8796
8965
|
fontScale
|
|
8797
8966
|
);
|
|
8798
8967
|
if (!isFirstLine) {
|
|
8799
|
-
currentY +=
|
|
8968
|
+
currentY += getLineHeightPx(para, lineNaturalHeightPt, lnSpcReduction) + lineGapPx;
|
|
8800
8969
|
}
|
|
8801
8970
|
const lineWidth = measureLineWidth(line.segments, defaultFontSize, fontScale, fontResolver);
|
|
8802
8971
|
const lineStartX = computePathLineX(
|
|
@@ -8844,7 +9013,7 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
|
|
|
8844
9013
|
} else {
|
|
8845
9014
|
const naturalHeightPt = computeLineNaturalHeight(para.runs, defaultFontSize, fontScale);
|
|
8846
9015
|
if (!isFirstLine) {
|
|
8847
|
-
currentY +=
|
|
9016
|
+
currentY += getLineHeightPx(para, naturalHeightPt, lnSpcReduction) + paragraphGapPx;
|
|
8848
9017
|
}
|
|
8849
9018
|
const runsAsSegments = para.runs.filter((r) => r.text.length > 0).map((r) => ({ text: r.text, properties: r.properties }));
|
|
8850
9019
|
const lineWidth = measureLineWidth(runsAsSegments, defaultFontSize, fontScale, fontResolver);
|
|
@@ -9183,6 +9352,7 @@ function escapeXmlAttr(str) {
|
|
|
9183
9352
|
|
|
9184
9353
|
// src/converter.ts
|
|
9185
9354
|
async function convertPptxToSvg(input, options) {
|
|
9355
|
+
const textOutput = options?.textOutput ?? "path";
|
|
9186
9356
|
const setup = await createOpentypeSetupFromSystem(
|
|
9187
9357
|
options?.fontDirs,
|
|
9188
9358
|
options?.fontMapping,
|
|
@@ -9190,7 +9360,13 @@ async function convertPptxToSvg(input, options) {
|
|
|
9190
9360
|
);
|
|
9191
9361
|
if (setup) {
|
|
9192
9362
|
setTextMeasurer(setup.measurer);
|
|
9193
|
-
|
|
9363
|
+
if (textOutput !== "text") {
|
|
9364
|
+
setTextPathFontResolver(setup.fontResolver);
|
|
9365
|
+
}
|
|
9366
|
+
}
|
|
9367
|
+
const fontUsageCollector = textOutput === "text" ? new FontUsageCollector() : null;
|
|
9368
|
+
if (fontUsageCollector) {
|
|
9369
|
+
setFontUsageCollector(fontUsageCollector);
|
|
9194
9370
|
}
|
|
9195
9371
|
setFontMapping(createFontMapping(options?.fontMapping));
|
|
9196
9372
|
enableXmlCache();
|
|
@@ -9209,7 +9385,14 @@ async function convertPptxToSvg(input, options) {
|
|
|
9209
9385
|
const { slide, layoutElements, layoutShowMasterSp, masterElements } = parsed;
|
|
9210
9386
|
const effectiveMasterElements = slide.showMasterSp && layoutShowMasterSp ? masterElements : [];
|
|
9211
9387
|
slide.elements = mergeElements(effectiveMasterElements, layoutElements, slide.elements);
|
|
9212
|
-
|
|
9388
|
+
fontUsageCollector?.reset();
|
|
9389
|
+
let svg = renderSlideToSvg(slide, data.presInfo.slideSize);
|
|
9390
|
+
if (fontUsageCollector && setup) {
|
|
9391
|
+
const style = await buildFontFaceStyle(fontUsageCollector.getUsages(), setup.fontResolver);
|
|
9392
|
+
if (style) {
|
|
9393
|
+
svg = injectIntoSvgDefs(svg, style);
|
|
9394
|
+
}
|
|
9395
|
+
}
|
|
9213
9396
|
results.push({ slideNumber, svg });
|
|
9214
9397
|
}
|
|
9215
9398
|
flushWarnings();
|
|
@@ -9218,10 +9401,16 @@ async function convertPptxToSvg(input, options) {
|
|
|
9218
9401
|
clearXmlCache();
|
|
9219
9402
|
resetTextMeasurer();
|
|
9220
9403
|
resetTextPathFontResolver();
|
|
9404
|
+
resetFontUsageCollector();
|
|
9221
9405
|
resetFontMapping();
|
|
9222
9406
|
resetScriptFonts();
|
|
9223
9407
|
}
|
|
9224
9408
|
}
|
|
9409
|
+
function injectIntoSvgDefs(svg, content) {
|
|
9410
|
+
const openTagEnd = svg.indexOf(">");
|
|
9411
|
+
if (openTagEnd === -1) return svg;
|
|
9412
|
+
return `${svg.slice(0, openTagEnd + 1)}<defs>${content}</defs>${svg.slice(openTagEnd + 1)}`;
|
|
9413
|
+
}
|
|
9225
9414
|
var cachedFontBuffers = null;
|
|
9226
9415
|
var cachedFontBuffersKey = null;
|
|
9227
9416
|
var MAX_TOTAL_FONT_BUFFER_BYTES = 100 * 1024 * 1024;
|
|
@@ -9259,7 +9448,7 @@ ${skipSystemFonts ?? false}`;
|
|
|
9259
9448
|
return buffers;
|
|
9260
9449
|
}
|
|
9261
9450
|
async function convertPptxToPng(input, options) {
|
|
9262
|
-
const svgResults = await convertPptxToSvg(input, options);
|
|
9451
|
+
const svgResults = await convertPptxToSvg(input, { ...options, textOutput: "path" });
|
|
9263
9452
|
const width = options?.width ?? DEFAULT_OUTPUT_WIDTH;
|
|
9264
9453
|
const height = options?.height;
|
|
9265
9454
|
const fontBuffers = loadFontBuffers(options?.fontDirs, options?.skipSystemFonts);
|
package/package.json
CHANGED