pptx-glimpse 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/pptx-glimpse)](https://www.npmjs.com/package/pptx-glimpse)
4
4
 
5
- A Node.js library to render PPTX as SVG / PNG.
5
+ A lightweight JavaScript library for rendering PowerPoint (.pptx) files as SVG or PNG in Node.js. No LibreOffice required.
6
6
 
7
- [Demo](https://pptx-glimpse.vercel.app/) | [npm](https://www.npmjs.com/package/pptx-glimpse)
7
+ **[Try the Demo](https://pptx-glimpse.vercel.app/)** | [npm](https://www.npmjs.com/package/pptx-glimpse)
8
+
9
+ ![pptx-glimpse demo](https://raw.githubusercontent.com/hirokisakabe/pptx-glimpse/main/docs/demo.gif)
8
10
 
9
11
  ## Motivation
10
12
 
package/dist/index.cjs CHANGED
@@ -35,6 +35,8 @@ __export(index_exports, {
35
35
  convertPptxToPng: () => convertPptxToPng,
36
36
  convertPptxToSvg: () => convertPptxToSvg,
37
37
  createFontMapping: () => createFontMapping,
38
+ createOpentypeSetupFromBuffers: () => createOpentypeSetupFromBuffers,
39
+ createOpentypeTextMeasurerFromBuffers: () => createOpentypeTextMeasurerFromBuffers,
38
40
  getMappedFont: () => getMappedFont,
39
41
  getWarningEntries: () => getWarningEntries,
40
42
  getWarningSummary: () => getWarningSummary
@@ -8110,6 +8112,108 @@ function collectFontFilePaths(additionalDirs) {
8110
8112
  return result;
8111
8113
  }
8112
8114
 
8115
+ // src/font/ttc-parser.ts
8116
+ var TTC_TAG = 1953784678;
8117
+ function isTtcBuffer(data) {
8118
+ const view = toDataView(data);
8119
+ if (view.byteLength < 4) return false;
8120
+ return view.getUint32(0) === TTC_TAG;
8121
+ }
8122
+ function extractTtcFonts(data) {
8123
+ const view = toDataView(data);
8124
+ const bytes = toUint8Array(data);
8125
+ if (view.byteLength < 12) return [];
8126
+ if (view.getUint32(0) !== TTC_TAG) return [];
8127
+ const numFonts = view.getUint32(8);
8128
+ if (numFonts === 0) return [];
8129
+ const headerEnd = 12 + numFonts * 4;
8130
+ if (view.byteLength < headerEnd) return [];
8131
+ const results = [];
8132
+ for (let i = 0; i < numFonts; i++) {
8133
+ try {
8134
+ const fontOffset = view.getUint32(12 + i * 4);
8135
+ const extracted = extractSingleFont(view, bytes, fontOffset);
8136
+ if (extracted) results.push(extracted);
8137
+ } catch {
8138
+ }
8139
+ }
8140
+ return results;
8141
+ }
8142
+ function extractSingleFont(view, bytes, fontOffset) {
8143
+ if (fontOffset + 12 > view.byteLength) return null;
8144
+ const sfVersion = view.getUint32(fontOffset);
8145
+ const numTables = view.getUint16(fontOffset + 4);
8146
+ if (numTables === 0) return null;
8147
+ const tableRecordsStart = fontOffset + 12;
8148
+ const tableRecordsEnd = tableRecordsStart + numTables * 16;
8149
+ if (tableRecordsEnd > view.byteLength) return null;
8150
+ const tables = [];
8151
+ for (let i = 0; i < numTables; i++) {
8152
+ const recOffset = tableRecordsStart + i * 16;
8153
+ const tableOffset = view.getUint32(recOffset + 8);
8154
+ const tableLength = view.getUint32(recOffset + 12);
8155
+ if (tableOffset > view.byteLength || tableLength > view.byteLength - tableOffset) {
8156
+ return null;
8157
+ }
8158
+ tables.push({
8159
+ tag: view.getUint32(recOffset),
8160
+ checkSum: view.getUint32(recOffset + 4),
8161
+ offset: tableOffset,
8162
+ length: tableLength
8163
+ });
8164
+ }
8165
+ const headerSize = 12 + numTables * 16;
8166
+ let dataSize = 0;
8167
+ for (const table of tables) {
8168
+ dataSize += alignTo4(table.length);
8169
+ }
8170
+ const totalSize = headerSize + dataSize;
8171
+ const output = new ArrayBuffer(totalSize);
8172
+ const outView = new DataView(output);
8173
+ const outBytes = new Uint8Array(output);
8174
+ outView.setUint32(0, sfVersion);
8175
+ outView.setUint16(4, numTables);
8176
+ const { searchRange, entrySelector, rangeShift } = calcOffsetTableFields(numTables);
8177
+ outView.setUint16(6, searchRange);
8178
+ outView.setUint16(8, entrySelector);
8179
+ outView.setUint16(10, rangeShift);
8180
+ let currentDataOffset = headerSize;
8181
+ for (let i = 0; i < numTables; i++) {
8182
+ const table = tables[i];
8183
+ const recOffset = 12 + i * 16;
8184
+ outView.setUint32(recOffset, table.tag);
8185
+ outView.setUint32(recOffset + 4, table.checkSum);
8186
+ outView.setUint32(recOffset + 8, currentDataOffset);
8187
+ outView.setUint32(recOffset + 12, table.length);
8188
+ outBytes.set(bytes.subarray(table.offset, table.offset + table.length), currentDataOffset);
8189
+ currentDataOffset += alignTo4(table.length);
8190
+ }
8191
+ return output;
8192
+ }
8193
+ function alignTo4(n) {
8194
+ return n + 3 & ~3;
8195
+ }
8196
+ function calcOffsetTableFields(numTables) {
8197
+ let searchRange = 16;
8198
+ let entrySelector = 0;
8199
+ while (searchRange * 2 <= numTables * 16) {
8200
+ searchRange *= 2;
8201
+ entrySelector++;
8202
+ }
8203
+ const rangeShift = numTables * 16 - searchRange;
8204
+ return { searchRange, entrySelector, rangeShift };
8205
+ }
8206
+ function toDataView(data) {
8207
+ if (data instanceof Uint8Array) {
8208
+ return new DataView(data.buffer, data.byteOffset, data.byteLength);
8209
+ }
8210
+ return new DataView(data);
8211
+ }
8212
+ function toUint8Array(data) {
8213
+ if (data instanceof Uint8Array) return data;
8214
+ return new Uint8Array(data);
8215
+ }
8216
+
8113
8217
  // src/font/opentype-helpers.ts
8114
8218
  async function tryLoadOpentype() {
8115
8219
  try {
@@ -8136,6 +8240,63 @@ function toArrayBuffer(data) {
8136
8240
  if (data instanceof ArrayBuffer) return data;
8137
8241
  return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
8138
8242
  }
8243
+ function parseFontBuffer(arrayBuffer, opentype) {
8244
+ if (isTtcBuffer(arrayBuffer)) {
8245
+ const results = [];
8246
+ for (const buf of extractTtcFonts(arrayBuffer)) {
8247
+ try {
8248
+ results.push(opentype.parse(buf));
8249
+ } catch {
8250
+ }
8251
+ }
8252
+ return results;
8253
+ }
8254
+ return [opentype.parse(arrayBuffer)];
8255
+ }
8256
+ async function createOpentypeTextMeasurerFromBuffers(fontBuffers, fontMapping) {
8257
+ const setup = await createOpentypeSetupFromBuffers(fontBuffers, fontMapping);
8258
+ return setup?.measurer ?? null;
8259
+ }
8260
+ async function createOpentypeSetupFromBuffers(fontBuffers, fontMapping) {
8261
+ if (fontBuffers.length === 0) return null;
8262
+ const opentype = await tryLoadOpentype();
8263
+ if (!opentype) return null;
8264
+ const mapping = createFontMapping(fontMapping);
8265
+ const reverseMap = buildReverseMapping(mapping);
8266
+ const measurerFonts = /* @__PURE__ */ new Map();
8267
+ const resolverFonts = /* @__PURE__ */ new Map();
8268
+ let firstMeasurerFont = null;
8269
+ let firstResolverFont = null;
8270
+ for (const buffer of fontBuffers) {
8271
+ try {
8272
+ const arrayBuffer = toArrayBuffer(buffer.data);
8273
+ const isTtc = isTtcBuffer(arrayBuffer);
8274
+ const fonts = parseFontBuffer(arrayBuffer, opentype);
8275
+ for (const font of fonts) {
8276
+ if (!firstMeasurerFont) firstMeasurerFont = font;
8277
+ if (!firstResolverFont) firstResolverFont = font;
8278
+ if (isTtc) {
8279
+ const fontFamily = font.names.fontFamily;
8280
+ if (fontFamily) {
8281
+ for (const name of Object.values(fontFamily)) {
8282
+ registerFont(name, font, reverseMap, measurerFonts, resolverFonts);
8283
+ }
8284
+ }
8285
+ } else if (buffer.name) {
8286
+ registerFont(buffer.name, font, reverseMap, measurerFonts, resolverFonts);
8287
+ }
8288
+ }
8289
+ } catch {
8290
+ }
8291
+ }
8292
+ if (measurerFonts.size === 0 && !firstMeasurerFont) return null;
8293
+ const measurer = new OpentypeTextMeasurer(measurerFonts, firstMeasurerFont ?? void 0);
8294
+ const fontResolver = new DefaultTextPathFontResolver(
8295
+ resolverFonts,
8296
+ firstResolverFont ?? void 0
8297
+ );
8298
+ return { measurer, fontResolver };
8299
+ }
8139
8300
  function registerFont(name, font, reverseMap, measurerFonts, resolverFonts) {
8140
8301
  const fullFont = font;
8141
8302
  if (!measurerFonts.has(name)) {
@@ -8167,13 +8328,15 @@ async function createOpentypeSetupFromSystem(additionalFontDirs, fontMapping) {
8167
8328
  try {
8168
8329
  const data = await (0, import_promises.readFile)(filePath);
8169
8330
  const arrayBuffer = toArrayBuffer(data);
8170
- const font = opentype.parse(arrayBuffer);
8171
- if (!firstMeasurerFont) firstMeasurerFont = font;
8172
- if (!firstResolverFont) firstResolverFont = font;
8173
- const fontFamily = font.names.fontFamily;
8174
- if (fontFamily) {
8175
- for (const name of Object.values(fontFamily)) {
8176
- registerFont(name, font, reverseMap, measurerFonts, resolverFonts);
8331
+ const fonts = parseFontBuffer(arrayBuffer, opentype);
8332
+ for (const font of fonts) {
8333
+ if (!firstMeasurerFont) firstMeasurerFont = font;
8334
+ if (!firstResolverFont) firstResolverFont = font;
8335
+ const fontFamily = font.names.fontFamily;
8336
+ if (fontFamily) {
8337
+ for (const name of Object.values(fontFamily)) {
8338
+ registerFont(name, font, reverseMap, measurerFonts, resolverFonts);
8339
+ }
8177
8340
  }
8178
8341
  }
8179
8342
  } catch {
@@ -8319,6 +8482,8 @@ function collectFontsFromTextBody(textBody, fonts) {
8319
8482
  convertPptxToPng,
8320
8483
  convertPptxToSvg,
8321
8484
  createFontMapping,
8485
+ createOpentypeSetupFromBuffers,
8486
+ createOpentypeTextMeasurerFromBuffers,
8322
8487
  getMappedFont,
8323
8488
  getWarningEntries,
8324
8489
  getWarningSummary
package/dist/index.d.cts CHANGED
@@ -84,4 +84,101 @@ interface UsedFonts {
84
84
  */
85
85
  declare function collectUsedFonts(input: Buffer | Uint8Array): UsedFonts;
86
86
 
87
- export { type ConvertOptions, DEFAULT_FONT_MAPPING, type FontMapping, type LogLevel, type SlideImage, type SlideSvg, type UsedFonts, type WarningEntry, type WarningSummary, collectUsedFonts, convertPptxToPng, convertPptxToSvg, createFontMapping, getMappedFont, getWarningEntries, getWarningSummary };
87
+ /**
88
+ * テキストの幅と行高さを計測するインターフェース。
89
+ * デフォルトでは静的フォントメトリクスによる計測が使われる。
90
+ * ユーザーが独自の実装を ConvertOptions.textMeasurer に渡すことで、
91
+ * Canvas API や opentype.js など任意の計測バックエンドに差し替えられる。
92
+ */
93
+ interface TextMeasurer {
94
+ /**
95
+ * テキストの推定幅をピクセル単位で返す。
96
+ * @param text - 計測対象のテキスト
97
+ * @param fontSizePt - フォントサイズ (ポイント)
98
+ * @param bold - 太字かどうか
99
+ * @param fontFamily - ラテン文字用フォントファミリー名
100
+ * @param fontFamilyEa - 東アジア文字用フォントファミリー名
101
+ */
102
+ measureTextWidth(text: string, fontSizePt: number, bold: boolean, fontFamily?: string | null, fontFamilyEa?: string | null): number;
103
+ /**
104
+ * フォントの自然な行高さ比率 (行高さ / フォントサイズ) を返す。
105
+ * メトリクスが不明な場合は 1.2 をフォールバック値として返す。
106
+ * @param fontFamily - ラテン文字用フォントファミリー名
107
+ * @param fontFamilyEa - 東アジア文字用フォントファミリー名
108
+ */
109
+ getLineHeightRatio(fontFamily?: string | null, fontFamilyEa?: string | null): number;
110
+ /**
111
+ * フォントの ascender 比率 (ascender / unitsPerEm) を返す。
112
+ * 1行目のベースラインオフセット計算に使用する。
113
+ * メトリクスが不明な場合は 1.0 をフォールバック値として返す。
114
+ * @param fontFamily - ラテン文字用フォントファミリー名
115
+ * @param fontFamilyEa - 東アジア文字用フォントファミリー名
116
+ */
117
+ getAscenderRatio(fontFamily?: string | null, fontFamilyEa?: string | null): number;
118
+ }
119
+
120
+ interface OpentypeGlyph {
121
+ advanceWidth?: number;
122
+ }
123
+ interface OpentypeFont {
124
+ unitsPerEm: number;
125
+ ascender: number;
126
+ descender: number;
127
+ stringToGlyphs(text: string): OpentypeGlyph[];
128
+ }
129
+ declare class OpentypeTextMeasurer implements TextMeasurer {
130
+ private fonts;
131
+ private defaultFont;
132
+ /**
133
+ * @param fonts - フォント名 → opentype.js Font オブジェクトのマップ
134
+ * @param defaultFont - フォールバックフォント(省略可)
135
+ */
136
+ constructor(fonts: Map<string, OpentypeFont>, defaultFont?: OpentypeFont);
137
+ measureTextWidth(text: string, fontSizePt: number, bold: boolean, fontFamily?: string | null, fontFamilyEa?: string | null): number;
138
+ getLineHeightRatio(fontFamily?: string | null, fontFamilyEa?: string | null): number;
139
+ getAscenderRatio(fontFamily?: string | null, fontFamilyEa?: string | null): number;
140
+ private resolveFont;
141
+ }
142
+
143
+ /**
144
+ * テキスト→パス変換用のフォントリゾルバーコンテキスト。
145
+ * opentype.js の getPath() メソッドを持つ Font オブジェクトへのアクセスを提供する。
146
+ */
147
+ interface OpentypePath {
148
+ toPathData(decimalPlaces?: number): string;
149
+ }
150
+ interface OpentypeFullFont {
151
+ unitsPerEm: number;
152
+ ascender: number;
153
+ descender: number;
154
+ getPath(text: string, x: number, y: number, fontSize: number): OpentypePath;
155
+ }
156
+ interface TextPathFontResolver {
157
+ resolveFont(fontFamily: string | null | undefined, fontFamilyEa: string | null | undefined): OpentypeFullFont | null;
158
+ }
159
+
160
+ /** フォントバッファの入力形式 */
161
+ interface FontBuffer {
162
+ name?: string;
163
+ data: ArrayBuffer | Uint8Array;
164
+ }
165
+ /**
166
+ * フォントバッファ配列から OpentypeTextMeasurer を構築する。
167
+ *
168
+ * 内部で opentype.js を動的 import してフォントをパースする。
169
+ * opentype.js が利用不可な場合は null を返す。
170
+ */
171
+ declare function createOpentypeTextMeasurerFromBuffers(fontBuffers: FontBuffer[], fontMapping?: FontMapping): Promise<OpentypeTextMeasurer | null>;
172
+ interface OpentypeSetup {
173
+ measurer: OpentypeTextMeasurer;
174
+ fontResolver: TextPathFontResolver;
175
+ }
176
+ /**
177
+ * フォントバッファ配列から OpentypeTextMeasurer と TextPathFontResolver を同時に構築する。
178
+ *
179
+ * opentype.parse() が返すオブジェクトは OpentypeFont と OpentypeFullFont の両方を満たすため、
180
+ * 同じ Font オブジェクトを measurer と fontResolver の両方に渡す。
181
+ */
182
+ declare function createOpentypeSetupFromBuffers(fontBuffers: FontBuffer[], fontMapping?: FontMapping): Promise<OpentypeSetup | null>;
183
+
184
+ export { type ConvertOptions, DEFAULT_FONT_MAPPING, type FontBuffer, type FontMapping, type LogLevel, type OpentypeSetup, type SlideImage, type SlideSvg, type UsedFonts, type WarningEntry, type WarningSummary, collectUsedFonts, convertPptxToPng, convertPptxToSvg, createFontMapping, createOpentypeSetupFromBuffers, createOpentypeTextMeasurerFromBuffers, getMappedFont, getWarningEntries, getWarningSummary };
package/dist/index.d.ts CHANGED
@@ -84,4 +84,101 @@ interface UsedFonts {
84
84
  */
85
85
  declare function collectUsedFonts(input: Buffer | Uint8Array): UsedFonts;
86
86
 
87
- export { type ConvertOptions, DEFAULT_FONT_MAPPING, type FontMapping, type LogLevel, type SlideImage, type SlideSvg, type UsedFonts, type WarningEntry, type WarningSummary, collectUsedFonts, convertPptxToPng, convertPptxToSvg, createFontMapping, getMappedFont, getWarningEntries, getWarningSummary };
87
+ /**
88
+ * テキストの幅と行高さを計測するインターフェース。
89
+ * デフォルトでは静的フォントメトリクスによる計測が使われる。
90
+ * ユーザーが独自の実装を ConvertOptions.textMeasurer に渡すことで、
91
+ * Canvas API や opentype.js など任意の計測バックエンドに差し替えられる。
92
+ */
93
+ interface TextMeasurer {
94
+ /**
95
+ * テキストの推定幅をピクセル単位で返す。
96
+ * @param text - 計測対象のテキスト
97
+ * @param fontSizePt - フォントサイズ (ポイント)
98
+ * @param bold - 太字かどうか
99
+ * @param fontFamily - ラテン文字用フォントファミリー名
100
+ * @param fontFamilyEa - 東アジア文字用フォントファミリー名
101
+ */
102
+ measureTextWidth(text: string, fontSizePt: number, bold: boolean, fontFamily?: string | null, fontFamilyEa?: string | null): number;
103
+ /**
104
+ * フォントの自然な行高さ比率 (行高さ / フォントサイズ) を返す。
105
+ * メトリクスが不明な場合は 1.2 をフォールバック値として返す。
106
+ * @param fontFamily - ラテン文字用フォントファミリー名
107
+ * @param fontFamilyEa - 東アジア文字用フォントファミリー名
108
+ */
109
+ getLineHeightRatio(fontFamily?: string | null, fontFamilyEa?: string | null): number;
110
+ /**
111
+ * フォントの ascender 比率 (ascender / unitsPerEm) を返す。
112
+ * 1行目のベースラインオフセット計算に使用する。
113
+ * メトリクスが不明な場合は 1.0 をフォールバック値として返す。
114
+ * @param fontFamily - ラテン文字用フォントファミリー名
115
+ * @param fontFamilyEa - 東アジア文字用フォントファミリー名
116
+ */
117
+ getAscenderRatio(fontFamily?: string | null, fontFamilyEa?: string | null): number;
118
+ }
119
+
120
+ interface OpentypeGlyph {
121
+ advanceWidth?: number;
122
+ }
123
+ interface OpentypeFont {
124
+ unitsPerEm: number;
125
+ ascender: number;
126
+ descender: number;
127
+ stringToGlyphs(text: string): OpentypeGlyph[];
128
+ }
129
+ declare class OpentypeTextMeasurer implements TextMeasurer {
130
+ private fonts;
131
+ private defaultFont;
132
+ /**
133
+ * @param fonts - フォント名 → opentype.js Font オブジェクトのマップ
134
+ * @param defaultFont - フォールバックフォント(省略可)
135
+ */
136
+ constructor(fonts: Map<string, OpentypeFont>, defaultFont?: OpentypeFont);
137
+ measureTextWidth(text: string, fontSizePt: number, bold: boolean, fontFamily?: string | null, fontFamilyEa?: string | null): number;
138
+ getLineHeightRatio(fontFamily?: string | null, fontFamilyEa?: string | null): number;
139
+ getAscenderRatio(fontFamily?: string | null, fontFamilyEa?: string | null): number;
140
+ private resolveFont;
141
+ }
142
+
143
+ /**
144
+ * テキスト→パス変換用のフォントリゾルバーコンテキスト。
145
+ * opentype.js の getPath() メソッドを持つ Font オブジェクトへのアクセスを提供する。
146
+ */
147
+ interface OpentypePath {
148
+ toPathData(decimalPlaces?: number): string;
149
+ }
150
+ interface OpentypeFullFont {
151
+ unitsPerEm: number;
152
+ ascender: number;
153
+ descender: number;
154
+ getPath(text: string, x: number, y: number, fontSize: number): OpentypePath;
155
+ }
156
+ interface TextPathFontResolver {
157
+ resolveFont(fontFamily: string | null | undefined, fontFamilyEa: string | null | undefined): OpentypeFullFont | null;
158
+ }
159
+
160
+ /** フォントバッファの入力形式 */
161
+ interface FontBuffer {
162
+ name?: string;
163
+ data: ArrayBuffer | Uint8Array;
164
+ }
165
+ /**
166
+ * フォントバッファ配列から OpentypeTextMeasurer を構築する。
167
+ *
168
+ * 内部で opentype.js を動的 import してフォントをパースする。
169
+ * opentype.js が利用不可な場合は null を返す。
170
+ */
171
+ declare function createOpentypeTextMeasurerFromBuffers(fontBuffers: FontBuffer[], fontMapping?: FontMapping): Promise<OpentypeTextMeasurer | null>;
172
+ interface OpentypeSetup {
173
+ measurer: OpentypeTextMeasurer;
174
+ fontResolver: TextPathFontResolver;
175
+ }
176
+ /**
177
+ * フォントバッファ配列から OpentypeTextMeasurer と TextPathFontResolver を同時に構築する。
178
+ *
179
+ * opentype.parse() が返すオブジェクトは OpentypeFont と OpentypeFullFont の両方を満たすため、
180
+ * 同じ Font オブジェクトを measurer と fontResolver の両方に渡す。
181
+ */
182
+ declare function createOpentypeSetupFromBuffers(fontBuffers: FontBuffer[], fontMapping?: FontMapping): Promise<OpentypeSetup | null>;
183
+
184
+ export { type ConvertOptions, DEFAULT_FONT_MAPPING, type FontBuffer, type FontMapping, type LogLevel, type OpentypeSetup, type SlideImage, type SlideSvg, type UsedFonts, type WarningEntry, type WarningSummary, collectUsedFonts, convertPptxToPng, convertPptxToSvg, createFontMapping, createOpentypeSetupFromBuffers, createOpentypeTextMeasurerFromBuffers, getMappedFont, getWarningEntries, getWarningSummary };
package/dist/index.js CHANGED
@@ -8067,6 +8067,108 @@ function collectFontFilePaths(additionalDirs) {
8067
8067
  return result;
8068
8068
  }
8069
8069
 
8070
+ // src/font/ttc-parser.ts
8071
+ var TTC_TAG = 1953784678;
8072
+ function isTtcBuffer(data) {
8073
+ const view = toDataView(data);
8074
+ if (view.byteLength < 4) return false;
8075
+ return view.getUint32(0) === TTC_TAG;
8076
+ }
8077
+ function extractTtcFonts(data) {
8078
+ const view = toDataView(data);
8079
+ const bytes = toUint8Array(data);
8080
+ if (view.byteLength < 12) return [];
8081
+ if (view.getUint32(0) !== TTC_TAG) return [];
8082
+ const numFonts = view.getUint32(8);
8083
+ if (numFonts === 0) return [];
8084
+ const headerEnd = 12 + numFonts * 4;
8085
+ if (view.byteLength < headerEnd) return [];
8086
+ const results = [];
8087
+ for (let i = 0; i < numFonts; i++) {
8088
+ try {
8089
+ const fontOffset = view.getUint32(12 + i * 4);
8090
+ const extracted = extractSingleFont(view, bytes, fontOffset);
8091
+ if (extracted) results.push(extracted);
8092
+ } catch {
8093
+ }
8094
+ }
8095
+ return results;
8096
+ }
8097
+ function extractSingleFont(view, bytes, fontOffset) {
8098
+ if (fontOffset + 12 > view.byteLength) return null;
8099
+ const sfVersion = view.getUint32(fontOffset);
8100
+ const numTables = view.getUint16(fontOffset + 4);
8101
+ if (numTables === 0) return null;
8102
+ const tableRecordsStart = fontOffset + 12;
8103
+ const tableRecordsEnd = tableRecordsStart + numTables * 16;
8104
+ if (tableRecordsEnd > view.byteLength) return null;
8105
+ const tables = [];
8106
+ for (let i = 0; i < numTables; i++) {
8107
+ const recOffset = tableRecordsStart + i * 16;
8108
+ const tableOffset = view.getUint32(recOffset + 8);
8109
+ const tableLength = view.getUint32(recOffset + 12);
8110
+ if (tableOffset > view.byteLength || tableLength > view.byteLength - tableOffset) {
8111
+ return null;
8112
+ }
8113
+ tables.push({
8114
+ tag: view.getUint32(recOffset),
8115
+ checkSum: view.getUint32(recOffset + 4),
8116
+ offset: tableOffset,
8117
+ length: tableLength
8118
+ });
8119
+ }
8120
+ const headerSize = 12 + numTables * 16;
8121
+ let dataSize = 0;
8122
+ for (const table of tables) {
8123
+ dataSize += alignTo4(table.length);
8124
+ }
8125
+ const totalSize = headerSize + dataSize;
8126
+ const output = new ArrayBuffer(totalSize);
8127
+ const outView = new DataView(output);
8128
+ const outBytes = new Uint8Array(output);
8129
+ outView.setUint32(0, sfVersion);
8130
+ outView.setUint16(4, numTables);
8131
+ const { searchRange, entrySelector, rangeShift } = calcOffsetTableFields(numTables);
8132
+ outView.setUint16(6, searchRange);
8133
+ outView.setUint16(8, entrySelector);
8134
+ outView.setUint16(10, rangeShift);
8135
+ let currentDataOffset = headerSize;
8136
+ for (let i = 0; i < numTables; i++) {
8137
+ const table = tables[i];
8138
+ const recOffset = 12 + i * 16;
8139
+ outView.setUint32(recOffset, table.tag);
8140
+ outView.setUint32(recOffset + 4, table.checkSum);
8141
+ outView.setUint32(recOffset + 8, currentDataOffset);
8142
+ outView.setUint32(recOffset + 12, table.length);
8143
+ outBytes.set(bytes.subarray(table.offset, table.offset + table.length), currentDataOffset);
8144
+ currentDataOffset += alignTo4(table.length);
8145
+ }
8146
+ return output;
8147
+ }
8148
+ function alignTo4(n) {
8149
+ return n + 3 & ~3;
8150
+ }
8151
+ function calcOffsetTableFields(numTables) {
8152
+ let searchRange = 16;
8153
+ let entrySelector = 0;
8154
+ while (searchRange * 2 <= numTables * 16) {
8155
+ searchRange *= 2;
8156
+ entrySelector++;
8157
+ }
8158
+ const rangeShift = numTables * 16 - searchRange;
8159
+ return { searchRange, entrySelector, rangeShift };
8160
+ }
8161
+ function toDataView(data) {
8162
+ if (data instanceof Uint8Array) {
8163
+ return new DataView(data.buffer, data.byteOffset, data.byteLength);
8164
+ }
8165
+ return new DataView(data);
8166
+ }
8167
+ function toUint8Array(data) {
8168
+ if (data instanceof Uint8Array) return data;
8169
+ return new Uint8Array(data);
8170
+ }
8171
+
8070
8172
  // src/font/opentype-helpers.ts
8071
8173
  async function tryLoadOpentype() {
8072
8174
  try {
@@ -8093,6 +8195,63 @@ function toArrayBuffer(data) {
8093
8195
  if (data instanceof ArrayBuffer) return data;
8094
8196
  return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
8095
8197
  }
8198
+ function parseFontBuffer(arrayBuffer, opentype) {
8199
+ if (isTtcBuffer(arrayBuffer)) {
8200
+ const results = [];
8201
+ for (const buf of extractTtcFonts(arrayBuffer)) {
8202
+ try {
8203
+ results.push(opentype.parse(buf));
8204
+ } catch {
8205
+ }
8206
+ }
8207
+ return results;
8208
+ }
8209
+ return [opentype.parse(arrayBuffer)];
8210
+ }
8211
+ async function createOpentypeTextMeasurerFromBuffers(fontBuffers, fontMapping) {
8212
+ const setup = await createOpentypeSetupFromBuffers(fontBuffers, fontMapping);
8213
+ return setup?.measurer ?? null;
8214
+ }
8215
+ async function createOpentypeSetupFromBuffers(fontBuffers, fontMapping) {
8216
+ if (fontBuffers.length === 0) return null;
8217
+ const opentype = await tryLoadOpentype();
8218
+ if (!opentype) return null;
8219
+ const mapping = createFontMapping(fontMapping);
8220
+ const reverseMap = buildReverseMapping(mapping);
8221
+ const measurerFonts = /* @__PURE__ */ new Map();
8222
+ const resolverFonts = /* @__PURE__ */ new Map();
8223
+ let firstMeasurerFont = null;
8224
+ let firstResolverFont = null;
8225
+ for (const buffer of fontBuffers) {
8226
+ try {
8227
+ const arrayBuffer = toArrayBuffer(buffer.data);
8228
+ const isTtc = isTtcBuffer(arrayBuffer);
8229
+ const fonts = parseFontBuffer(arrayBuffer, opentype);
8230
+ for (const font of fonts) {
8231
+ if (!firstMeasurerFont) firstMeasurerFont = font;
8232
+ if (!firstResolverFont) firstResolverFont = font;
8233
+ if (isTtc) {
8234
+ const fontFamily = font.names.fontFamily;
8235
+ if (fontFamily) {
8236
+ for (const name of Object.values(fontFamily)) {
8237
+ registerFont(name, font, reverseMap, measurerFonts, resolverFonts);
8238
+ }
8239
+ }
8240
+ } else if (buffer.name) {
8241
+ registerFont(buffer.name, font, reverseMap, measurerFonts, resolverFonts);
8242
+ }
8243
+ }
8244
+ } catch {
8245
+ }
8246
+ }
8247
+ if (measurerFonts.size === 0 && !firstMeasurerFont) return null;
8248
+ const measurer = new OpentypeTextMeasurer(measurerFonts, firstMeasurerFont ?? void 0);
8249
+ const fontResolver = new DefaultTextPathFontResolver(
8250
+ resolverFonts,
8251
+ firstResolverFont ?? void 0
8252
+ );
8253
+ return { measurer, fontResolver };
8254
+ }
8096
8255
  function registerFont(name, font, reverseMap, measurerFonts, resolverFonts) {
8097
8256
  const fullFont = font;
8098
8257
  if (!measurerFonts.has(name)) {
@@ -8124,13 +8283,15 @@ async function createOpentypeSetupFromSystem(additionalFontDirs, fontMapping) {
8124
8283
  try {
8125
8284
  const data = await readFile(filePath);
8126
8285
  const arrayBuffer = toArrayBuffer(data);
8127
- const font = opentype.parse(arrayBuffer);
8128
- if (!firstMeasurerFont) firstMeasurerFont = font;
8129
- if (!firstResolverFont) firstResolverFont = font;
8130
- const fontFamily = font.names.fontFamily;
8131
- if (fontFamily) {
8132
- for (const name of Object.values(fontFamily)) {
8133
- registerFont(name, font, reverseMap, measurerFonts, resolverFonts);
8286
+ const fonts = parseFontBuffer(arrayBuffer, opentype);
8287
+ for (const font of fonts) {
8288
+ if (!firstMeasurerFont) firstMeasurerFont = font;
8289
+ if (!firstResolverFont) firstResolverFont = font;
8290
+ const fontFamily = font.names.fontFamily;
8291
+ if (fontFamily) {
8292
+ for (const name of Object.values(fontFamily)) {
8293
+ registerFont(name, font, reverseMap, measurerFonts, resolverFonts);
8294
+ }
8134
8295
  }
8135
8296
  }
8136
8297
  } catch {
@@ -8275,6 +8436,8 @@ export {
8275
8436
  convertPptxToPng,
8276
8437
  convertPptxToSvg,
8277
8438
  createFontMapping,
8439
+ createOpentypeSetupFromBuffers,
8440
+ createOpentypeTextMeasurerFromBuffers,
8278
8441
  getMappedFont,
8279
8442
  getWarningEntries,
8280
8443
  getWarningSummary
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pptx-glimpse",
3
- "version": "0.2.2",
4
- "description": "A Node.js library to render PPTX as SVG / PNG.",
3
+ "version": "0.3.0",
4
+ "description": "A lightweight JavaScript library for rendering PowerPoint (.pptx) files as SVG or PNG in Node.js. No LibreOffice required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -24,6 +24,7 @@
24
24
  "test:coverage": "vitest run --coverage",
25
25
  "test:watch": "vitest",
26
26
  "typecheck": "tsc --noEmit",
27
+ "knip": "knip",
27
28
  "dev": "tsx scripts/dev-server.ts",
28
29
  "render": "tsx scripts/test-render.ts",
29
30
  "inspect": "tsx scripts/inspect-pptx.ts",
@@ -75,6 +76,7 @@
75
76
  "eslint": "^9.0.0",
76
77
  "eslint-config-prettier": "^10.0.0",
77
78
  "jszip": "^3.10.1",
79
+ "knip": "^5.85.0",
78
80
  "pixelmatch": "^7.1.0",
79
81
  "prettier": "^3.0.0",
80
82
  "tsup": "^8.0.0",