pptx-glimpse 1.0.1 → 1.1.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 +13 -0
- package/dist/index.cjs +274 -82
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +274 -82
- 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) {
|
|
@@ -8062,7 +8205,19 @@ function renderTextBody(textBody, transform) {
|
|
|
8062
8205
|
getLineSpacing(para, lnSpcReduction),
|
|
8063
8206
|
paragraphGapPx
|
|
8064
8207
|
);
|
|
8065
|
-
const
|
|
8208
|
+
const firstSeg = line.segments[0];
|
|
8209
|
+
const bulletFontChain = buildBulletFontChain(
|
|
8210
|
+
para.properties,
|
|
8211
|
+
firstSeg?.properties.fontFamily,
|
|
8212
|
+
firstSeg?.properties.fontFamilyEa
|
|
8213
|
+
);
|
|
8214
|
+
const bulletStyles = buildBulletStyleAttrs(
|
|
8215
|
+
para.properties,
|
|
8216
|
+
lineFontSize,
|
|
8217
|
+
fontScale,
|
|
8218
|
+
bulletFontChain
|
|
8219
|
+
);
|
|
8220
|
+
getFontUsageCollector()?.record(bulletFontChain, bulletText);
|
|
8066
8221
|
tspans.push(
|
|
8067
8222
|
`<tspan x="${bulletX}" dy="${dy}" text-anchor="start" ${bulletStyles}>${escapeXml2(bulletText)}</tspan>`
|
|
8068
8223
|
);
|
|
@@ -8107,7 +8262,18 @@ function renderTextBody(textBody, transform) {
|
|
|
8107
8262
|
getLineSpacing(para, lnSpcReduction),
|
|
8108
8263
|
paragraphGapPx
|
|
8109
8264
|
);
|
|
8110
|
-
const
|
|
8265
|
+
const bulletFontChain = buildBulletFontChain(
|
|
8266
|
+
para.properties,
|
|
8267
|
+
firstRun?.properties.fontFamily,
|
|
8268
|
+
firstRun?.properties.fontFamilyEa
|
|
8269
|
+
);
|
|
8270
|
+
const bulletStyles = buildBulletStyleAttrs(
|
|
8271
|
+
para.properties,
|
|
8272
|
+
fontSize,
|
|
8273
|
+
fontScale,
|
|
8274
|
+
bulletFontChain
|
|
8275
|
+
);
|
|
8276
|
+
getFontUsageCollector()?.record(bulletFontChain, bulletText);
|
|
8111
8277
|
tspans.push(
|
|
8112
8278
|
`<tspan x="${bulletX}" dy="${dy}" text-anchor="start" ${bulletStyles}>${escapeXml2(bulletText)}</tspan>`
|
|
8113
8279
|
);
|
|
@@ -8242,14 +8408,18 @@ function toAlpha(num) {
|
|
|
8242
8408
|
}
|
|
8243
8409
|
return result;
|
|
8244
8410
|
}
|
|
8245
|
-
function
|
|
8411
|
+
function buildBulletFontChain(props, runFontFamily, runFontFamilyEa) {
|
|
8412
|
+
return [props.bulletFont, runFontFamily ?? null, runFontFamilyEa ?? null];
|
|
8413
|
+
}
|
|
8414
|
+
function buildBulletStyleAttrs(props, textFontSizePt, fontScale, bulletFontChain) {
|
|
8246
8415
|
const styles = [];
|
|
8247
8416
|
if (props.bulletSizePct !== null) {
|
|
8248
8417
|
const size = textFontSizePt * (props.bulletSizePct / 1e5);
|
|
8249
8418
|
styles.push(`font-size="${size}pt"`);
|
|
8250
8419
|
}
|
|
8251
|
-
|
|
8252
|
-
|
|
8420
|
+
const fontFamilyValue = buildFontFamilyValue(bulletFontChain);
|
|
8421
|
+
if (fontFamilyValue) {
|
|
8422
|
+
styles.push(`font-family="${fontFamilyValue}"`);
|
|
8253
8423
|
}
|
|
8254
8424
|
if (props.bulletColor) {
|
|
8255
8425
|
styles.push(`fill="${props.bulletColor.hex}"`);
|
|
@@ -8433,6 +8603,7 @@ function renderSegment(text, props, fontScale, prefix) {
|
|
|
8433
8603
|
let tspanContent;
|
|
8434
8604
|
if (!needsScriptSplit(props)) {
|
|
8435
8605
|
const styles = buildStyleAttrs(props, fontScale);
|
|
8606
|
+
getFontUsageCollector()?.record([props.fontFamily, props.fontFamilyEa], text);
|
|
8436
8607
|
tspanContent = `<tspan ${prefix}${styles}>${escapeXml2(text)}</tspan>`;
|
|
8437
8608
|
} else {
|
|
8438
8609
|
const parts = splitByScript(text);
|
|
@@ -8441,6 +8612,7 @@ function renderSegment(text, props, fontScale, prefix) {
|
|
|
8441
8612
|
const part = parts[i];
|
|
8442
8613
|
const fonts = part.isEa ? [props.fontFamilyEa, getJpanFallbackFont(), props.fontFamily] : [props.fontFamily, props.fontFamilyEa];
|
|
8443
8614
|
const styles = buildStyleAttrs(props, fontScale, fonts);
|
|
8615
|
+
getFontUsageCollector()?.record(fonts, part.text);
|
|
8444
8616
|
if (i === 0) {
|
|
8445
8617
|
result.push(`<tspan ${prefix}${styles}>${escapeXml2(part.text)}</tspan>`);
|
|
8446
8618
|
} else {
|
|
@@ -9221,6 +9393,7 @@ function escapeXmlAttr(str) {
|
|
|
9221
9393
|
|
|
9222
9394
|
// src/converter.ts
|
|
9223
9395
|
async function convertPptxToSvg(input, options) {
|
|
9396
|
+
const textOutput = options?.textOutput ?? "path";
|
|
9224
9397
|
const setup = await createOpentypeSetupFromSystem(
|
|
9225
9398
|
options?.fontDirs,
|
|
9226
9399
|
options?.fontMapping,
|
|
@@ -9228,7 +9401,13 @@ async function convertPptxToSvg(input, options) {
|
|
|
9228
9401
|
);
|
|
9229
9402
|
if (setup) {
|
|
9230
9403
|
setTextMeasurer(setup.measurer);
|
|
9231
|
-
|
|
9404
|
+
if (textOutput !== "text") {
|
|
9405
|
+
setTextPathFontResolver(setup.fontResolver);
|
|
9406
|
+
}
|
|
9407
|
+
}
|
|
9408
|
+
const fontUsageCollector = textOutput === "text" ? new FontUsageCollector() : null;
|
|
9409
|
+
if (fontUsageCollector) {
|
|
9410
|
+
setFontUsageCollector(fontUsageCollector);
|
|
9232
9411
|
}
|
|
9233
9412
|
setFontMapping(createFontMapping(options?.fontMapping));
|
|
9234
9413
|
enableXmlCache();
|
|
@@ -9247,7 +9426,14 @@ async function convertPptxToSvg(input, options) {
|
|
|
9247
9426
|
const { slide, layoutElements, layoutShowMasterSp, masterElements } = parsed;
|
|
9248
9427
|
const effectiveMasterElements = slide.showMasterSp && layoutShowMasterSp ? masterElements : [];
|
|
9249
9428
|
slide.elements = mergeElements(effectiveMasterElements, layoutElements, slide.elements);
|
|
9250
|
-
|
|
9429
|
+
fontUsageCollector?.reset();
|
|
9430
|
+
let svg = renderSlideToSvg(slide, data.presInfo.slideSize);
|
|
9431
|
+
if (fontUsageCollector && setup) {
|
|
9432
|
+
const style = await buildFontFaceStyle(fontUsageCollector.getUsages(), setup.fontResolver);
|
|
9433
|
+
if (style) {
|
|
9434
|
+
svg = injectIntoSvgDefs(svg, style);
|
|
9435
|
+
}
|
|
9436
|
+
}
|
|
9251
9437
|
results.push({ slideNumber, svg });
|
|
9252
9438
|
}
|
|
9253
9439
|
flushWarnings();
|
|
@@ -9256,10 +9442,16 @@ async function convertPptxToSvg(input, options) {
|
|
|
9256
9442
|
clearXmlCache();
|
|
9257
9443
|
resetTextMeasurer();
|
|
9258
9444
|
resetTextPathFontResolver();
|
|
9445
|
+
resetFontUsageCollector();
|
|
9259
9446
|
resetFontMapping();
|
|
9260
9447
|
resetScriptFonts();
|
|
9261
9448
|
}
|
|
9262
9449
|
}
|
|
9450
|
+
function injectIntoSvgDefs(svg, content) {
|
|
9451
|
+
const openTagEnd = svg.indexOf(">");
|
|
9452
|
+
if (openTagEnd === -1) return svg;
|
|
9453
|
+
return `${svg.slice(0, openTagEnd + 1)}<defs>${content}</defs>${svg.slice(openTagEnd + 1)}`;
|
|
9454
|
+
}
|
|
9263
9455
|
var cachedFontBuffers = null;
|
|
9264
9456
|
var cachedFontBuffersKey = null;
|
|
9265
9457
|
var MAX_TOTAL_FONT_BUFFER_BYTES = 100 * 1024 * 1024;
|
|
@@ -9297,7 +9489,7 @@ ${skipSystemFonts ?? false}`;
|
|
|
9297
9489
|
return buffers;
|
|
9298
9490
|
}
|
|
9299
9491
|
async function convertPptxToPng(input, options) {
|
|
9300
|
-
const svgResults = await convertPptxToSvg(input, options);
|
|
9492
|
+
const svgResults = await convertPptxToSvg(input, { ...options, textOutput: "path" });
|
|
9301
9493
|
const width = options?.width ?? DEFAULT_OUTPUT_WIDTH;
|
|
9302
9494
|
const height = options?.height;
|
|
9303
9495
|
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) {
|
|
@@ -8024,7 +8167,19 @@ function renderTextBody(textBody, transform) {
|
|
|
8024
8167
|
getLineSpacing(para, lnSpcReduction),
|
|
8025
8168
|
paragraphGapPx
|
|
8026
8169
|
);
|
|
8027
|
-
const
|
|
8170
|
+
const firstSeg = line.segments[0];
|
|
8171
|
+
const bulletFontChain = buildBulletFontChain(
|
|
8172
|
+
para.properties,
|
|
8173
|
+
firstSeg?.properties.fontFamily,
|
|
8174
|
+
firstSeg?.properties.fontFamilyEa
|
|
8175
|
+
);
|
|
8176
|
+
const bulletStyles = buildBulletStyleAttrs(
|
|
8177
|
+
para.properties,
|
|
8178
|
+
lineFontSize,
|
|
8179
|
+
fontScale,
|
|
8180
|
+
bulletFontChain
|
|
8181
|
+
);
|
|
8182
|
+
getFontUsageCollector()?.record(bulletFontChain, bulletText);
|
|
8028
8183
|
tspans.push(
|
|
8029
8184
|
`<tspan x="${bulletX}" dy="${dy}" text-anchor="start" ${bulletStyles}>${escapeXml2(bulletText)}</tspan>`
|
|
8030
8185
|
);
|
|
@@ -8069,7 +8224,18 @@ function renderTextBody(textBody, transform) {
|
|
|
8069
8224
|
getLineSpacing(para, lnSpcReduction),
|
|
8070
8225
|
paragraphGapPx
|
|
8071
8226
|
);
|
|
8072
|
-
const
|
|
8227
|
+
const bulletFontChain = buildBulletFontChain(
|
|
8228
|
+
para.properties,
|
|
8229
|
+
firstRun?.properties.fontFamily,
|
|
8230
|
+
firstRun?.properties.fontFamilyEa
|
|
8231
|
+
);
|
|
8232
|
+
const bulletStyles = buildBulletStyleAttrs(
|
|
8233
|
+
para.properties,
|
|
8234
|
+
fontSize,
|
|
8235
|
+
fontScale,
|
|
8236
|
+
bulletFontChain
|
|
8237
|
+
);
|
|
8238
|
+
getFontUsageCollector()?.record(bulletFontChain, bulletText);
|
|
8073
8239
|
tspans.push(
|
|
8074
8240
|
`<tspan x="${bulletX}" dy="${dy}" text-anchor="start" ${bulletStyles}>${escapeXml2(bulletText)}</tspan>`
|
|
8075
8241
|
);
|
|
@@ -8204,14 +8370,18 @@ function toAlpha(num) {
|
|
|
8204
8370
|
}
|
|
8205
8371
|
return result;
|
|
8206
8372
|
}
|
|
8207
|
-
function
|
|
8373
|
+
function buildBulletFontChain(props, runFontFamily, runFontFamilyEa) {
|
|
8374
|
+
return [props.bulletFont, runFontFamily ?? null, runFontFamilyEa ?? null];
|
|
8375
|
+
}
|
|
8376
|
+
function buildBulletStyleAttrs(props, textFontSizePt, fontScale, bulletFontChain) {
|
|
8208
8377
|
const styles = [];
|
|
8209
8378
|
if (props.bulletSizePct !== null) {
|
|
8210
8379
|
const size = textFontSizePt * (props.bulletSizePct / 1e5);
|
|
8211
8380
|
styles.push(`font-size="${size}pt"`);
|
|
8212
8381
|
}
|
|
8213
|
-
|
|
8214
|
-
|
|
8382
|
+
const fontFamilyValue = buildFontFamilyValue(bulletFontChain);
|
|
8383
|
+
if (fontFamilyValue) {
|
|
8384
|
+
styles.push(`font-family="${fontFamilyValue}"`);
|
|
8215
8385
|
}
|
|
8216
8386
|
if (props.bulletColor) {
|
|
8217
8387
|
styles.push(`fill="${props.bulletColor.hex}"`);
|
|
@@ -8395,6 +8565,7 @@ function renderSegment(text, props, fontScale, prefix) {
|
|
|
8395
8565
|
let tspanContent;
|
|
8396
8566
|
if (!needsScriptSplit(props)) {
|
|
8397
8567
|
const styles = buildStyleAttrs(props, fontScale);
|
|
8568
|
+
getFontUsageCollector()?.record([props.fontFamily, props.fontFamilyEa], text);
|
|
8398
8569
|
tspanContent = `<tspan ${prefix}${styles}>${escapeXml2(text)}</tspan>`;
|
|
8399
8570
|
} else {
|
|
8400
8571
|
const parts = splitByScript(text);
|
|
@@ -8403,6 +8574,7 @@ function renderSegment(text, props, fontScale, prefix) {
|
|
|
8403
8574
|
const part = parts[i];
|
|
8404
8575
|
const fonts = part.isEa ? [props.fontFamilyEa, getJpanFallbackFont(), props.fontFamily] : [props.fontFamily, props.fontFamilyEa];
|
|
8405
8576
|
const styles = buildStyleAttrs(props, fontScale, fonts);
|
|
8577
|
+
getFontUsageCollector()?.record(fonts, part.text);
|
|
8406
8578
|
if (i === 0) {
|
|
8407
8579
|
result.push(`<tspan ${prefix}${styles}>${escapeXml2(part.text)}</tspan>`);
|
|
8408
8580
|
} else {
|
|
@@ -9183,6 +9355,7 @@ function escapeXmlAttr(str) {
|
|
|
9183
9355
|
|
|
9184
9356
|
// src/converter.ts
|
|
9185
9357
|
async function convertPptxToSvg(input, options) {
|
|
9358
|
+
const textOutput = options?.textOutput ?? "path";
|
|
9186
9359
|
const setup = await createOpentypeSetupFromSystem(
|
|
9187
9360
|
options?.fontDirs,
|
|
9188
9361
|
options?.fontMapping,
|
|
@@ -9190,7 +9363,13 @@ async function convertPptxToSvg(input, options) {
|
|
|
9190
9363
|
);
|
|
9191
9364
|
if (setup) {
|
|
9192
9365
|
setTextMeasurer(setup.measurer);
|
|
9193
|
-
|
|
9366
|
+
if (textOutput !== "text") {
|
|
9367
|
+
setTextPathFontResolver(setup.fontResolver);
|
|
9368
|
+
}
|
|
9369
|
+
}
|
|
9370
|
+
const fontUsageCollector = textOutput === "text" ? new FontUsageCollector() : null;
|
|
9371
|
+
if (fontUsageCollector) {
|
|
9372
|
+
setFontUsageCollector(fontUsageCollector);
|
|
9194
9373
|
}
|
|
9195
9374
|
setFontMapping(createFontMapping(options?.fontMapping));
|
|
9196
9375
|
enableXmlCache();
|
|
@@ -9209,7 +9388,14 @@ async function convertPptxToSvg(input, options) {
|
|
|
9209
9388
|
const { slide, layoutElements, layoutShowMasterSp, masterElements } = parsed;
|
|
9210
9389
|
const effectiveMasterElements = slide.showMasterSp && layoutShowMasterSp ? masterElements : [];
|
|
9211
9390
|
slide.elements = mergeElements(effectiveMasterElements, layoutElements, slide.elements);
|
|
9212
|
-
|
|
9391
|
+
fontUsageCollector?.reset();
|
|
9392
|
+
let svg = renderSlideToSvg(slide, data.presInfo.slideSize);
|
|
9393
|
+
if (fontUsageCollector && setup) {
|
|
9394
|
+
const style = await buildFontFaceStyle(fontUsageCollector.getUsages(), setup.fontResolver);
|
|
9395
|
+
if (style) {
|
|
9396
|
+
svg = injectIntoSvgDefs(svg, style);
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9213
9399
|
results.push({ slideNumber, svg });
|
|
9214
9400
|
}
|
|
9215
9401
|
flushWarnings();
|
|
@@ -9218,10 +9404,16 @@ async function convertPptxToSvg(input, options) {
|
|
|
9218
9404
|
clearXmlCache();
|
|
9219
9405
|
resetTextMeasurer();
|
|
9220
9406
|
resetTextPathFontResolver();
|
|
9407
|
+
resetFontUsageCollector();
|
|
9221
9408
|
resetFontMapping();
|
|
9222
9409
|
resetScriptFonts();
|
|
9223
9410
|
}
|
|
9224
9411
|
}
|
|
9412
|
+
function injectIntoSvgDefs(svg, content) {
|
|
9413
|
+
const openTagEnd = svg.indexOf(">");
|
|
9414
|
+
if (openTagEnd === -1) return svg;
|
|
9415
|
+
return `${svg.slice(0, openTagEnd + 1)}<defs>${content}</defs>${svg.slice(openTagEnd + 1)}`;
|
|
9416
|
+
}
|
|
9225
9417
|
var cachedFontBuffers = null;
|
|
9226
9418
|
var cachedFontBuffersKey = null;
|
|
9227
9419
|
var MAX_TOTAL_FONT_BUFFER_BYTES = 100 * 1024 * 1024;
|
|
@@ -9259,7 +9451,7 @@ ${skipSystemFonts ?? false}`;
|
|
|
9259
9451
|
return buffers;
|
|
9260
9452
|
}
|
|
9261
9453
|
async function convertPptxToPng(input, options) {
|
|
9262
|
-
const svgResults = await convertPptxToSvg(input, options);
|
|
9454
|
+
const svgResults = await convertPptxToSvg(input, { ...options, textOutput: "path" });
|
|
9263
9455
|
const width = options?.width ?? DEFAULT_OUTPUT_WIDTH;
|
|
9264
9456
|
const height = options?.height;
|
|
9265
9457
|
const fontBuffers = loadFontBuffers(options?.fontDirs, options?.skipSystemFonts);
|
package/package.json
CHANGED