@hirokisakabe/pom 0.1.7 → 0.1.8
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 +31 -0
- package/dist/buildPptx.d.ts +3 -0
- package/dist/buildPptx.d.ts.map +1 -1
- package/dist/buildPptx.js +8 -0
- package/dist/calcYogaLayout/measureText.d.ts +9 -0
- package/dist/calcYogaLayout/measureText.d.ts.map +1 -1
- package/dist/calcYogaLayout/measureText.js +155 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- [特徴](#特徴)
|
|
18
18
|
- [ノード](#ノード)
|
|
19
19
|
- [マスタースライド](#マスタースライド)
|
|
20
|
+
- [サーバーレス環境での利用](#サーバーレス環境での利用)
|
|
20
21
|
- [LLM 連携](#llm-連携)
|
|
21
22
|
- [ライセンス](#ライセンス)
|
|
22
23
|
|
|
@@ -495,6 +496,36 @@ type MasterSlideOptions = {
|
|
|
495
496
|
- **動的置換**: プレースホルダーはページごとに自動的に置換されます
|
|
496
497
|
- **後方互換性**: master オプションは省略可能で、既存コードへの影響はありません
|
|
497
498
|
|
|
499
|
+
## サーバーレス環境での利用
|
|
500
|
+
|
|
501
|
+
pom はデフォルトで `canvas` パッケージを使用してテキストの幅を計測し、折り返し位置を決定しています。しかし、Vercel や AWS Lambda などのサーバーレス環境では日本語フォント(Noto Sans JP など)がインストールされていないため、テキストの折り返し位置がずれる場合があります。
|
|
502
|
+
|
|
503
|
+
この問題に対処するため、`textMeasurement` オプションでテキスト計測の方法を指定できます。
|
|
504
|
+
|
|
505
|
+
### textMeasurement オプション
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
const pptx = await buildPptx(
|
|
509
|
+
[slide],
|
|
510
|
+
{ w: 1280, h: 720 },
|
|
511
|
+
{
|
|
512
|
+
textMeasurement: "auto", // "canvas" | "fallback" | "auto"
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
| 値 | 説明 |
|
|
518
|
+
| ------------ | ---------------------------------------------------------------------------------- |
|
|
519
|
+
| `"canvas"` | 常に canvas を使用してテキスト幅を計測(フォントがインストールされている環境向け) |
|
|
520
|
+
| `"fallback"` | 常にフォールバック計算を使用(CJK文字は1em、英数字は0.5emで推定) |
|
|
521
|
+
| `"auto"` | フォントの利用可否を自動検出し、利用できない場合はフォールバック(デフォルト) |
|
|
522
|
+
|
|
523
|
+
### 推奨設定
|
|
524
|
+
|
|
525
|
+
- **ローカル開発環境・Docker**: デフォルト(`"auto"`)のままで問題ありません
|
|
526
|
+
- **サーバーレス環境**: デフォルトの `"auto"` で自動的にフォールバックされます
|
|
527
|
+
- **フォントがインストールされている環境**: `"canvas"` を明示的に指定するとより正確な計測が可能です
|
|
528
|
+
|
|
498
529
|
## LLM 連携
|
|
499
530
|
|
|
500
531
|
pom は LLM(GPT-4o、Claude など)で生成した JSON からスライドを作成するユースケースに対応しています。
|
package/dist/buildPptx.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { TextMeasurementMode } from "./calcYogaLayout/measureText";
|
|
1
2
|
import { POMNode, MasterSlideOptions } from "./types";
|
|
3
|
+
export type { TextMeasurementMode };
|
|
2
4
|
export declare function buildPptx(nodes: POMNode[], slideSize: {
|
|
3
5
|
w: number;
|
|
4
6
|
h: number;
|
|
5
7
|
}, options?: {
|
|
6
8
|
master?: MasterSlideOptions;
|
|
9
|
+
textMeasurement?: TextMeasurementMode;
|
|
7
10
|
}): Promise<import("pptxgenjs").default>;
|
|
8
11
|
//# sourceMappingURL=buildPptx.d.ts.map
|
package/dist/buildPptx.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildPptx.d.ts","sourceRoot":"","sources":["../src/buildPptx.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"buildPptx.d.ts","sourceRoot":"","sources":["../src/buildPptx.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,mBAAmB,EACpB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,OAAO,EAAkB,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEtE,YAAY,EAAE,mBAAmB,EAAE,CAAC;AA8FpC,wBAAsB,SAAS,CAC7B,KAAK,EAAE,OAAO,EAAE,EAChB,SAAS,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACnC,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,eAAe,CAAC,EAAE,mBAAmB,CAAC;CACvC,wCAsBF"}
|
package/dist/buildPptx.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { calcYogaLayout } from "./calcYogaLayout/calcYogaLayout";
|
|
2
|
+
import { setTextMeasurementMode, } from "./calcYogaLayout/measureText";
|
|
2
3
|
import { renderPptx } from "./renderPptx/renderPptx";
|
|
3
4
|
import { toPositioned } from "./toPositioned/toPositioned";
|
|
4
5
|
function replacePlaceholders(node, pageNumber, totalPages, date) {
|
|
@@ -61,6 +62,13 @@ function composePage(content, master, pageNumber, totalPages) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
export async function buildPptx(nodes, slideSize, options) {
|
|
65
|
+
// テキスト計測モードを設定(デフォルトは auto)
|
|
66
|
+
if (options?.textMeasurement) {
|
|
67
|
+
setTextMeasurementMode(options.textMeasurement);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
setTextMeasurementMode("auto");
|
|
71
|
+
}
|
|
64
72
|
const positionedPages = [];
|
|
65
73
|
const totalPages = nodes.length;
|
|
66
74
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -4,6 +4,15 @@ type MeasureOptions = {
|
|
|
4
4
|
fontWeight?: "normal" | "bold" | number;
|
|
5
5
|
lineHeight?: number;
|
|
6
6
|
};
|
|
7
|
+
export type TextMeasurementMode = "canvas" | "fallback" | "auto";
|
|
8
|
+
/**
|
|
9
|
+
* テキスト計測モードを設定する
|
|
10
|
+
*/
|
|
11
|
+
export declare function setTextMeasurementMode(mode: TextMeasurementMode): void;
|
|
12
|
+
/**
|
|
13
|
+
* 現在のテキスト計測モードを取得する
|
|
14
|
+
*/
|
|
15
|
+
export declare function getTextMeasurementMode(): TextMeasurementMode;
|
|
7
16
|
/**
|
|
8
17
|
* テキストを折り返し付きでレイアウトし、そのサイズを測定する
|
|
9
18
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"measureText.d.ts","sourceRoot":"","sources":["../../src/calcYogaLayout/measureText.ts"],"names":[],"mappings":"AAEA,KAAK,cAAc,GAAG;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;
|
|
1
|
+
{"version":3,"file":"measureText.d.ts","sourceRoot":"","sources":["../../src/calcYogaLayout/measureText.ts"],"names":[],"mappings":"AAEA,KAAK,cAAc,GAAG;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC;AA0FjE;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAEtE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,mBAAmB,CAE5D;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,cAAc,GACnB;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAoBA"}
|
|
@@ -1,10 +1,122 @@
|
|
|
1
1
|
import { createCanvas } from "canvas";
|
|
2
2
|
const canvas = createCanvas(1, 1);
|
|
3
3
|
const ctx = canvas.getContext("2d");
|
|
4
|
+
// フォント利用可否のキャッシュ
|
|
5
|
+
const fontAvailabilityCache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* 指定されたフォントが利用可能かどうかをチェックする
|
|
8
|
+
* 既知のフォントと未知のフォントで同じ幅になるかチェックし、
|
|
9
|
+
* 同じなら「フォントが利用できない」と判定する
|
|
10
|
+
*/
|
|
11
|
+
function isFontAvailable(fontFamily, fontSizePx) {
|
|
12
|
+
const cacheKey = `${fontFamily}:${fontSizePx}`;
|
|
13
|
+
const cached = fontAvailabilityCache.get(cacheKey);
|
|
14
|
+
if (cached !== undefined) {
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
// テスト文字列(日本語と英語を含む)
|
|
18
|
+
const testString = "あいうABC123";
|
|
19
|
+
// 存在しないフォント名でテスト
|
|
20
|
+
const nonExistentFont = "NonExistentFont_12345_XYZ";
|
|
21
|
+
ctx.font = `${fontSizePx}px "${fontFamily}"`;
|
|
22
|
+
const targetWidth = ctx.measureText(testString).width;
|
|
23
|
+
ctx.font = `${fontSizePx}px "${nonExistentFont}"`;
|
|
24
|
+
const fallbackWidth = ctx.measureText(testString).width;
|
|
25
|
+
// 幅が同じなら、フォントが利用できない(フォールバックされている)
|
|
26
|
+
const isAvailable = Math.abs(targetWidth - fallbackWidth) > 0.1;
|
|
27
|
+
fontAvailabilityCache.set(cacheKey, isAvailable);
|
|
28
|
+
return isAvailable;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 文字がCJK(日本語・中国語・韓国語)文字かどうかを判定する
|
|
32
|
+
*/
|
|
33
|
+
function isCJKChar(char) {
|
|
34
|
+
const code = char.codePointAt(0);
|
|
35
|
+
if (code === undefined)
|
|
36
|
+
return false;
|
|
37
|
+
// CJK統合漢字
|
|
38
|
+
if (code >= 0x4e00 && code <= 0x9fff)
|
|
39
|
+
return true;
|
|
40
|
+
// CJK統合漢字拡張A
|
|
41
|
+
if (code >= 0x3400 && code <= 0x4dbf)
|
|
42
|
+
return true;
|
|
43
|
+
// CJK統合漢字拡張B-F
|
|
44
|
+
if (code >= 0x20000 && code <= 0x2ebef)
|
|
45
|
+
return true;
|
|
46
|
+
// ひらがな
|
|
47
|
+
if (code >= 0x3040 && code <= 0x309f)
|
|
48
|
+
return true;
|
|
49
|
+
// カタカナ
|
|
50
|
+
if (code >= 0x30a0 && code <= 0x30ff)
|
|
51
|
+
return true;
|
|
52
|
+
// 全角英数字・記号
|
|
53
|
+
if (code >= 0xff00 && code <= 0xffef)
|
|
54
|
+
return true;
|
|
55
|
+
// CJK記号
|
|
56
|
+
if (code >= 0x3000 && code <= 0x303f)
|
|
57
|
+
return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* フォールバック計算で文字の幅を推定する
|
|
62
|
+
* - CJK文字: 1em(= fontSizePx)
|
|
63
|
+
* - 英数字・半角記号: 0.5em
|
|
64
|
+
*/
|
|
65
|
+
function estimateCharWidth(char, fontSizePx) {
|
|
66
|
+
if (isCJKChar(char)) {
|
|
67
|
+
return fontSizePx; // 1em
|
|
68
|
+
}
|
|
69
|
+
return fontSizePx * 0.5; // 0.5em
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* フォールバック計算でテキスト幅を推定する
|
|
73
|
+
*/
|
|
74
|
+
function estimateTextWidth(text, fontSizePx) {
|
|
75
|
+
let width = 0;
|
|
76
|
+
for (const char of text) {
|
|
77
|
+
width += estimateCharWidth(char, fontSizePx);
|
|
78
|
+
}
|
|
79
|
+
return width;
|
|
80
|
+
}
|
|
81
|
+
// 現在のテキスト計測モード
|
|
82
|
+
let currentMode = "auto";
|
|
83
|
+
/**
|
|
84
|
+
* テキスト計測モードを設定する
|
|
85
|
+
*/
|
|
86
|
+
export function setTextMeasurementMode(mode) {
|
|
87
|
+
currentMode = mode;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 現在のテキスト計測モードを取得する
|
|
91
|
+
*/
|
|
92
|
+
export function getTextMeasurementMode() {
|
|
93
|
+
return currentMode;
|
|
94
|
+
}
|
|
4
95
|
/**
|
|
5
96
|
* テキストを折り返し付きでレイアウトし、そのサイズを測定する
|
|
6
97
|
*/
|
|
7
98
|
export function measureText(text, maxWidthPx, opts) {
|
|
99
|
+
const { fontFamily, fontSizePx } = opts;
|
|
100
|
+
// 計測方法を決定
|
|
101
|
+
const shouldUseFallback = (() => {
|
|
102
|
+
switch (currentMode) {
|
|
103
|
+
case "canvas":
|
|
104
|
+
return false;
|
|
105
|
+
case "fallback":
|
|
106
|
+
return true;
|
|
107
|
+
case "auto":
|
|
108
|
+
return !isFontAvailable(fontFamily, fontSizePx);
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
if (shouldUseFallback) {
|
|
112
|
+
return measureTextFallback(text, maxWidthPx, opts);
|
|
113
|
+
}
|
|
114
|
+
return measureTextCanvas(text, maxWidthPx, opts);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* canvas を使ったテキスト計測
|
|
118
|
+
*/
|
|
119
|
+
function measureTextCanvas(text, maxWidthPx, opts) {
|
|
8
120
|
applyFontStyle(opts);
|
|
9
121
|
// まず改行で段落に分割
|
|
10
122
|
const paragraphs = text.split("\n");
|
|
@@ -44,6 +156,49 @@ export function measureText(text, maxWidthPx, opts) {
|
|
|
44
156
|
// 端数切り上げ+余裕分 10px を足す
|
|
45
157
|
return { widthPx: widthPx + 10, heightPx };
|
|
46
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* フォールバック計算を使ったテキスト計測
|
|
161
|
+
*/
|
|
162
|
+
function measureTextFallback(text, maxWidthPx, opts) {
|
|
163
|
+
const { fontSizePx } = opts;
|
|
164
|
+
// まず改行で段落に分割
|
|
165
|
+
const paragraphs = text.split("\n");
|
|
166
|
+
const lines = [];
|
|
167
|
+
for (const paragraph of paragraphs) {
|
|
168
|
+
// 空の段落(連続した改行)も1行としてカウント
|
|
169
|
+
if (paragraph === "") {
|
|
170
|
+
lines.push({ widthPx: 0 });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const words = splitForWrap(paragraph);
|
|
174
|
+
let current = "";
|
|
175
|
+
let currentWidth = 0;
|
|
176
|
+
for (const word of words) {
|
|
177
|
+
const candidate = current ? current + word : word;
|
|
178
|
+
const w = estimateTextWidth(candidate, fontSizePx);
|
|
179
|
+
if (w <= maxWidthPx || !current) {
|
|
180
|
+
// まだ詰められる
|
|
181
|
+
current = candidate;
|
|
182
|
+
currentWidth = w;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// 折り返す
|
|
186
|
+
lines.push({ widthPx: currentWidth });
|
|
187
|
+
current = word;
|
|
188
|
+
currentWidth = estimateTextWidth(word, fontSizePx);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (current) {
|
|
192
|
+
lines.push({ widthPx: currentWidth });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const lineHeightRatio = opts.lineHeight ?? 1.3;
|
|
196
|
+
const lineHeightPx = fontSizePx * lineHeightRatio;
|
|
197
|
+
const widthPx = lines.length ? Math.max(...lines.map((l) => l.widthPx)) : 0;
|
|
198
|
+
const heightPx = lines.length * lineHeightPx;
|
|
199
|
+
// 端数切り上げ+余裕分 10px を足す
|
|
200
|
+
return { widthPx: widthPx + 10, heightPx };
|
|
201
|
+
}
|
|
47
202
|
function applyFontStyle(opts) {
|
|
48
203
|
const { fontFamily, fontSizePx, fontWeight = "normal" } = opts;
|
|
49
204
|
ctx.font = `${fontWeight} ${fontSizePx}px "${fontFamily}"`;
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hirokisakabe/pom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "PowerPoint Object Model - A declarative TypeScript library for creating PowerPoint presentations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
"vrt": "tsx vrt/runVrt.ts",
|
|
54
54
|
"vrt:update": "tsx vrt/runVrt.ts --update",
|
|
55
55
|
"vrt:docker": "docker compose run --rm vrt",
|
|
56
|
-
"vrt:docker:update": "docker compose run --rm vrt-update"
|
|
56
|
+
"vrt:docker:update": "docker compose run --rm vrt-update",
|
|
57
|
+
"preview": "tsx preview/lib/previewPptx.ts",
|
|
58
|
+
"preview:docker": "docker compose run --rm preview"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"@eslint/js": "^9.39.1",
|