pptx-glimpse 0.11.2 → 1.0.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/pptx-glimpse)](https://www.npmjs.com/package/pptx-glimpse)
4
4
  [![CI](https://github.com/hirokisakabe/pptx-glimpse/actions/workflows/ci.yml/badge.svg)](https://github.com/hirokisakabe/pptx-glimpse/actions/workflows/ci.yml)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)
7
7
 
8
8
  No LibreOffice required — just `npm install`.
9
9
 
@@ -43,7 +43,7 @@ rather than pixel-perfect rendering of every PowerPoint feature.
43
43
 
44
44
  ## Requirements
45
45
 
46
- - **Node.js >= 20**
46
+ - **Node.js >= 22**
47
47
 
48
48
  ## Installation
49
49
 
package/dist/index.cjs CHANGED
@@ -35,6 +35,9 @@ __export(index_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
+ // src/converter.ts
39
+ var import_node_fs2 = require("fs");
40
+
38
41
  // src/font/font-mapping.ts
39
42
  var DEFAULT_FONT_MAPPING = {
40
43
  // ラテン文字フォント
@@ -1801,6 +1804,10 @@ async function svgToPng(svgString, options) {
1801
1804
  } else if (options?.height) {
1802
1805
  resvgOptions.fitTo = { mode: "height", value: options.height };
1803
1806
  }
1807
+ const fontBuffers = options?.fontBuffers;
1808
+ if (fontBuffers && fontBuffers.length > 0) {
1809
+ resvgOptions.font = { fontBuffers };
1810
+ }
1804
1811
  const resvg = new import_resvg_wasm.Resvg(svgString, resvgOptions);
1805
1812
  const rendered = resvg.render();
1806
1813
  return {
@@ -2760,7 +2767,7 @@ function parseChartTitle(titleNode) {
2760
2767
  function parseLegend(legendNode) {
2761
2768
  if (!legendNode) return null;
2762
2769
  const legendPos = legendNode.legendPos;
2763
- const pos = legendPos?.["@_val"] ?? "b";
2770
+ const pos = legendPos?.["@_val"] ?? "r";
2764
2771
  return { position: pos };
2765
2772
  }
2766
2773
 
@@ -5417,6 +5424,7 @@ var DEFAULT_SERIES_COLORS = [
5417
5424
  { hex: "#5B9BD5", alpha: 1 },
5418
5425
  { hex: "#70AD47", alpha: 1 }
5419
5426
  ];
5427
+ var LEGEND_SIDE_WIDTH = 100;
5420
5428
  function renderChart(element) {
5421
5429
  const { transform, chart } = element;
5422
5430
  const w = emuToPixels(transform.extentWidth);
@@ -5435,6 +5443,8 @@ function renderChart(element) {
5435
5443
  if (chart.legend) {
5436
5444
  if (chart.legend.position === "b") margin.bottom = 50;
5437
5445
  else if (chart.legend.position === "t") margin.top += 20;
5446
+ else if (chart.legend.position === "r") margin.right = LEGEND_SIDE_WIDTH;
5447
+ else if (chart.legend.position === "l") margin.left += LEGEND_SIDE_WIDTH;
5438
5448
  }
5439
5449
  const plotX = margin.left;
5440
5450
  const plotY = margin.top;
@@ -6189,18 +6199,34 @@ function renderLegend(chart, chartW, chartH, position) {
6189
6199
  color: s.color
6190
6200
  }));
6191
6201
  if (entries2.length === 0) return "";
6192
- const entryWidth = 80;
6193
- const totalWidth = entries2.length * entryWidth;
6194
- const startX = Math.max((chartW - totalWidth) / 2, 5);
6195
- const legendY = position === "t" ? 25 : chartH - 15;
6196
- for (let i = 0; i < entries2.length; i++) {
6197
- const ex = startX + i * entryWidth;
6198
- parts.push(
6199
- `<rect x="${round(ex)}" y="${round(legendY - 6)}" width="12" height="12" ${fillAttr(entries2[i].color)}/>`
6200
- );
6201
- parts.push(
6202
- `<text x="${round(ex + 16)}" y="${round(legendY + 4)}" font-size="10" fill="#595959">${escapeXml(entries2[i].label)}</text>`
6203
- );
6202
+ const ENTRY_HEIGHT = 20;
6203
+ if (position === "r" || position === "l") {
6204
+ const totalH = entries2.length * ENTRY_HEIGHT;
6205
+ const startY = Math.max((chartH - totalH) / 2, 5);
6206
+ const legendX = position === "r" ? chartW - LEGEND_SIDE_WIDTH + 5 : 5;
6207
+ for (let i = 0; i < entries2.length; i++) {
6208
+ const ey = startY + i * ENTRY_HEIGHT;
6209
+ parts.push(
6210
+ `<rect x="${round(legendX)}" y="${round(ey)}" width="12" height="12" ${fillAttr(entries2[i].color)}/>`
6211
+ );
6212
+ parts.push(
6213
+ `<text x="${round(legendX + 16)}" y="${round(ey + 10)}" font-size="10" fill="#595959">${escapeXml(entries2[i].label)}</text>`
6214
+ );
6215
+ }
6216
+ } else {
6217
+ const entryWidth = 80;
6218
+ const totalWidth = entries2.length * entryWidth;
6219
+ const startX = Math.max((chartW - totalWidth) / 2, 5);
6220
+ const legendY = position === "t" ? 25 : chartH - 15;
6221
+ for (let i = 0; i < entries2.length; i++) {
6222
+ const ex = startX + i * entryWidth;
6223
+ parts.push(
6224
+ `<rect x="${round(ex)}" y="${round(legendY - 6)}" width="12" height="12" ${fillAttr(entries2[i].color)}/>`
6225
+ );
6226
+ parts.push(
6227
+ `<text x="${round(ex + 16)}" y="${round(legendY + 4)}" font-size="10" fill="#595959">${escapeXml(entries2[i].label)}</text>`
6228
+ );
6229
+ }
6204
6230
  }
6205
6231
  return parts.join("");
6206
6232
  }
@@ -9234,13 +9260,50 @@ async function convertPptxToSvg(input, options) {
9234
9260
  resetScriptFonts();
9235
9261
  }
9236
9262
  }
9263
+ var cachedFontBuffers = null;
9264
+ var cachedFontBuffersKey = null;
9265
+ var MAX_TOTAL_FONT_BUFFER_BYTES = 100 * 1024 * 1024;
9266
+ function loadFontBuffers(fontDirs, skipSystemFonts) {
9267
+ const key = `${(fontDirs ?? []).join("\0")}
9268
+ ${skipSystemFonts ?? false}`;
9269
+ if (cachedFontBuffers !== null && cachedFontBuffersKey === key) {
9270
+ return cachedFontBuffers;
9271
+ }
9272
+ const allPaths = collectFontFilePaths(fontDirs, skipSystemFonts);
9273
+ const ttfOtfPaths = allPaths.filter((p) => {
9274
+ const lower = p.toLowerCase();
9275
+ return lower.endsWith(".ttf") || lower.endsWith(".otf");
9276
+ });
9277
+ const pathsWithSize = [];
9278
+ for (const p of ttfOtfPaths) {
9279
+ try {
9280
+ pathsWithSize.push({ path: p, size: (0, import_node_fs2.statSync)(p).size });
9281
+ } catch {
9282
+ }
9283
+ }
9284
+ pathsWithSize.sort((a, b) => a.size - b.size);
9285
+ const buffers = [];
9286
+ let totalSize = 0;
9287
+ for (const { path, size } of pathsWithSize) {
9288
+ if (totalSize + size > MAX_TOTAL_FONT_BUFFER_BYTES) break;
9289
+ try {
9290
+ buffers.push(new Uint8Array((0, import_node_fs2.readFileSync)(path)));
9291
+ totalSize += size;
9292
+ } catch {
9293
+ }
9294
+ }
9295
+ cachedFontBuffers = buffers;
9296
+ cachedFontBuffersKey = key;
9297
+ return buffers;
9298
+ }
9237
9299
  async function convertPptxToPng(input, options) {
9238
9300
  const svgResults = await convertPptxToSvg(input, options);
9239
9301
  const width = options?.width ?? DEFAULT_OUTPUT_WIDTH;
9240
9302
  const height = options?.height;
9303
+ const fontBuffers = loadFontBuffers(options?.fontDirs, options?.skipSystemFonts);
9241
9304
  const results = [];
9242
9305
  for (const { slideNumber, svg } of svgResults) {
9243
- const pngResult = await svgToPng(svg, { width, height });
9306
+ const pngResult = await svgToPng(svg, { width, height, fontBuffers });
9244
9307
  results.push({
9245
9308
  slideNumber,
9246
9309
  png: pngResult.png,
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/converter.ts
2
+ import { readFileSync, statSync } from "fs";
3
+
1
4
  // src/font/font-mapping.ts
2
5
  var DEFAULT_FONT_MAPPING = {
3
6
  // ラテン文字フォント
@@ -1763,6 +1766,10 @@ async function svgToPng(svgString, options) {
1763
1766
  } else if (options?.height) {
1764
1767
  resvgOptions.fitTo = { mode: "height", value: options.height };
1765
1768
  }
1769
+ const fontBuffers = options?.fontBuffers;
1770
+ if (fontBuffers && fontBuffers.length > 0) {
1771
+ resvgOptions.font = { fontBuffers };
1772
+ }
1766
1773
  const resvg = new Resvg(svgString, resvgOptions);
1767
1774
  const rendered = resvg.render();
1768
1775
  return {
@@ -2722,7 +2729,7 @@ function parseChartTitle(titleNode) {
2722
2729
  function parseLegend(legendNode) {
2723
2730
  if (!legendNode) return null;
2724
2731
  const legendPos = legendNode.legendPos;
2725
- const pos = legendPos?.["@_val"] ?? "b";
2732
+ const pos = legendPos?.["@_val"] ?? "r";
2726
2733
  return { position: pos };
2727
2734
  }
2728
2735
 
@@ -5379,6 +5386,7 @@ var DEFAULT_SERIES_COLORS = [
5379
5386
  { hex: "#5B9BD5", alpha: 1 },
5380
5387
  { hex: "#70AD47", alpha: 1 }
5381
5388
  ];
5389
+ var LEGEND_SIDE_WIDTH = 100;
5382
5390
  function renderChart(element) {
5383
5391
  const { transform, chart } = element;
5384
5392
  const w = emuToPixels(transform.extentWidth);
@@ -5397,6 +5405,8 @@ function renderChart(element) {
5397
5405
  if (chart.legend) {
5398
5406
  if (chart.legend.position === "b") margin.bottom = 50;
5399
5407
  else if (chart.legend.position === "t") margin.top += 20;
5408
+ else if (chart.legend.position === "r") margin.right = LEGEND_SIDE_WIDTH;
5409
+ else if (chart.legend.position === "l") margin.left += LEGEND_SIDE_WIDTH;
5400
5410
  }
5401
5411
  const plotX = margin.left;
5402
5412
  const plotY = margin.top;
@@ -6151,18 +6161,34 @@ function renderLegend(chart, chartW, chartH, position) {
6151
6161
  color: s.color
6152
6162
  }));
6153
6163
  if (entries2.length === 0) return "";
6154
- const entryWidth = 80;
6155
- const totalWidth = entries2.length * entryWidth;
6156
- const startX = Math.max((chartW - totalWidth) / 2, 5);
6157
- const legendY = position === "t" ? 25 : chartH - 15;
6158
- for (let i = 0; i < entries2.length; i++) {
6159
- const ex = startX + i * entryWidth;
6160
- parts.push(
6161
- `<rect x="${round(ex)}" y="${round(legendY - 6)}" width="12" height="12" ${fillAttr(entries2[i].color)}/>`
6162
- );
6163
- parts.push(
6164
- `<text x="${round(ex + 16)}" y="${round(legendY + 4)}" font-size="10" fill="#595959">${escapeXml(entries2[i].label)}</text>`
6165
- );
6164
+ const ENTRY_HEIGHT = 20;
6165
+ if (position === "r" || position === "l") {
6166
+ const totalH = entries2.length * ENTRY_HEIGHT;
6167
+ const startY = Math.max((chartH - totalH) / 2, 5);
6168
+ const legendX = position === "r" ? chartW - LEGEND_SIDE_WIDTH + 5 : 5;
6169
+ for (let i = 0; i < entries2.length; i++) {
6170
+ const ey = startY + i * ENTRY_HEIGHT;
6171
+ parts.push(
6172
+ `<rect x="${round(legendX)}" y="${round(ey)}" width="12" height="12" ${fillAttr(entries2[i].color)}/>`
6173
+ );
6174
+ parts.push(
6175
+ `<text x="${round(legendX + 16)}" y="${round(ey + 10)}" font-size="10" fill="#595959">${escapeXml(entries2[i].label)}</text>`
6176
+ );
6177
+ }
6178
+ } else {
6179
+ const entryWidth = 80;
6180
+ const totalWidth = entries2.length * entryWidth;
6181
+ const startX = Math.max((chartW - totalWidth) / 2, 5);
6182
+ const legendY = position === "t" ? 25 : chartH - 15;
6183
+ for (let i = 0; i < entries2.length; i++) {
6184
+ const ex = startX + i * entryWidth;
6185
+ parts.push(
6186
+ `<rect x="${round(ex)}" y="${round(legendY - 6)}" width="12" height="12" ${fillAttr(entries2[i].color)}/>`
6187
+ );
6188
+ parts.push(
6189
+ `<text x="${round(ex + 16)}" y="${round(legendY + 4)}" font-size="10" fill="#595959">${escapeXml(entries2[i].label)}</text>`
6190
+ );
6191
+ }
6166
6192
  }
6167
6193
  return parts.join("");
6168
6194
  }
@@ -9196,13 +9222,50 @@ async function convertPptxToSvg(input, options) {
9196
9222
  resetScriptFonts();
9197
9223
  }
9198
9224
  }
9225
+ var cachedFontBuffers = null;
9226
+ var cachedFontBuffersKey = null;
9227
+ var MAX_TOTAL_FONT_BUFFER_BYTES = 100 * 1024 * 1024;
9228
+ function loadFontBuffers(fontDirs, skipSystemFonts) {
9229
+ const key = `${(fontDirs ?? []).join("\0")}
9230
+ ${skipSystemFonts ?? false}`;
9231
+ if (cachedFontBuffers !== null && cachedFontBuffersKey === key) {
9232
+ return cachedFontBuffers;
9233
+ }
9234
+ const allPaths = collectFontFilePaths(fontDirs, skipSystemFonts);
9235
+ const ttfOtfPaths = allPaths.filter((p) => {
9236
+ const lower = p.toLowerCase();
9237
+ return lower.endsWith(".ttf") || lower.endsWith(".otf");
9238
+ });
9239
+ const pathsWithSize = [];
9240
+ for (const p of ttfOtfPaths) {
9241
+ try {
9242
+ pathsWithSize.push({ path: p, size: statSync(p).size });
9243
+ } catch {
9244
+ }
9245
+ }
9246
+ pathsWithSize.sort((a, b) => a.size - b.size);
9247
+ const buffers = [];
9248
+ let totalSize = 0;
9249
+ for (const { path, size } of pathsWithSize) {
9250
+ if (totalSize + size > MAX_TOTAL_FONT_BUFFER_BYTES) break;
9251
+ try {
9252
+ buffers.push(new Uint8Array(readFileSync(path)));
9253
+ totalSize += size;
9254
+ } catch {
9255
+ }
9256
+ }
9257
+ cachedFontBuffers = buffers;
9258
+ cachedFontBuffersKey = key;
9259
+ return buffers;
9260
+ }
9199
9261
  async function convertPptxToPng(input, options) {
9200
9262
  const svgResults = await convertPptxToSvg(input, options);
9201
9263
  const width = options?.width ?? DEFAULT_OUTPUT_WIDTH;
9202
9264
  const height = options?.height;
9265
+ const fontBuffers = loadFontBuffers(options?.fontDirs, options?.skipSystemFonts);
9203
9266
  const results = [];
9204
9267
  for (const { slideNumber, svg } of svgResults) {
9205
- const pngResult = await svgToPng(svg, { width, height });
9268
+ const pngResult = await svgToPng(svg, { width, height, fontBuffers });
9206
9269
  results.push({
9207
9270
  slideNumber,
9208
9271
  png: pngResult.png,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptx-glimpse",
3
- "version": "0.11.2",
3
+ "version": "1.0.1",
4
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",
@@ -17,7 +17,7 @@
17
17
  "dist"
18
18
  ],
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": ">=22"
21
21
  },
22
22
  "keywords": [
23
23
  "pptx",
@@ -46,7 +46,7 @@
46
46
  "@changesets/cli": "^2.29.8",
47
47
  "@types/node": "^25.2.2",
48
48
  "@types/ws": "^8.18.1",
49
- "@vitest/coverage-v8": "^3.2.4",
49
+ "@vitest/coverage-v8": "4.1.0",
50
50
  "eslint": "^9.0.0",
51
51
  "eslint-config-prettier": "^10.0.0",
52
52
  "eslint-plugin-simple-import-sort": "^12.1.1",
@@ -59,7 +59,7 @@
59
59
  "tsx": "^4.21.0",
60
60
  "typescript": "^5.9.3",
61
61
  "typescript-eslint": "^8.55.0",
62
- "vitest": "^3.0.0",
62
+ "vitest": "^4.1.0",
63
63
  "ws": "^8.19.0"
64
64
  },
65
65
  "scripts": {