@hirokisakabe/pom 0.1.3 → 0.1.5
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 +188 -0
- package/dist/buildPptx.d.ts +3 -1
- package/dist/buildPptx.d.ts.map +1 -1
- package/dist/buildPptx.js +68 -4
- package/dist/calcYogaLayout/measureText.d.ts.map +1 -1
- package/dist/calcYogaLayout/measureText.js +26 -17
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/inputSchema.d.ts +483 -0
- package/dist/inputSchema.d.ts.map +1 -0
- package/dist/inputSchema.js +128 -0
- package/dist/renderPptx/renderPptx.d.ts +2 -4
- package/dist/renderPptx/renderPptx.d.ts.map +1 -1
- package/dist/renderPptx/renderPptx.js +6 -18
- package/dist/renderPptx/textOptions.d.ts +20 -0
- package/dist/renderPptx/textOptions.d.ts.map +1 -0
- package/dist/renderPptx/textOptions.js +20 -0
- package/dist/renderPptx/units.d.ts +4 -0
- package/dist/renderPptx/units.d.ts.map +1 -0
- package/dist/renderPptx/units.js +3 -0
- package/dist/types.d.ts +394 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +202 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- **宣言的**: JSON ライクなオブジェクトでスライドを記述
|
|
9
9
|
- **柔軟なレイアウト**: VStack/HStack/Box による自動レイアウト
|
|
10
10
|
- **ピクセル単位**: 直感的なピクセル単位での指定(内部でインチに変換)
|
|
11
|
+
- **マスタースライド**: 全ページ共通のヘッダー・フッター・ページ番号を自動挿入
|
|
11
12
|
- **AI フレンドリー**: LLM がコード生成しやすいシンプルな構造
|
|
12
13
|
|
|
13
14
|
## ノード
|
|
@@ -48,7 +49,11 @@
|
|
|
48
49
|
type: "text";
|
|
49
50
|
text: string;
|
|
50
51
|
fontPx?: number;
|
|
52
|
+
color?: string;
|
|
51
53
|
alignText?: "left" | "center" | "right";
|
|
54
|
+
bold?: boolean;
|
|
55
|
+
fontFamily?: string;
|
|
56
|
+
lineSpacingMultiple?: number;
|
|
52
57
|
|
|
53
58
|
// 共通プロパティ
|
|
54
59
|
w?: number | "max" | `${number}%`;
|
|
@@ -57,6 +62,11 @@
|
|
|
57
62
|
}
|
|
58
63
|
```
|
|
59
64
|
|
|
65
|
+
- `color` で文字色を 16 進カラーコード(例: `"FF0000"`)として指定できます。
|
|
66
|
+
- `bold` で太字を指定できます。
|
|
67
|
+
- `fontFamily` でフォントファミリーを指定できます(デフォルト: `"Noto Sans JP"`)。
|
|
68
|
+
- `lineSpacingMultiple` で行間倍率を指定できます(デフォルト: `1.3`)。
|
|
69
|
+
|
|
60
70
|
#### 2. Image
|
|
61
71
|
|
|
62
72
|
画像を表示するノード。
|
|
@@ -211,3 +221,181 @@
|
|
|
211
221
|
- `cloudCallout`: 雲吹き出し(コメント)
|
|
212
222
|
- `star5`: 5つ星(強調、デコレーション)
|
|
213
223
|
- `downArrow`: 下矢印(フロー図)
|
|
224
|
+
|
|
225
|
+
## マスタースライド
|
|
226
|
+
|
|
227
|
+
全ページに共通のヘッダー・フッター・ページ番号を自動挿入できます。
|
|
228
|
+
|
|
229
|
+
### 基本的な使い方
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { buildPptx } from "@hirokisakabe/pom";
|
|
233
|
+
|
|
234
|
+
const pptx = await buildPptx(
|
|
235
|
+
[page1, page2, page3],
|
|
236
|
+
{ w: 1280, h: 720 },
|
|
237
|
+
{
|
|
238
|
+
master: {
|
|
239
|
+
header: {
|
|
240
|
+
type: "hstack",
|
|
241
|
+
h: 40,
|
|
242
|
+
padding: { left: 48, right: 48, top: 12, bottom: 0 },
|
|
243
|
+
justifyContent: "spaceBetween",
|
|
244
|
+
alignItems: "center",
|
|
245
|
+
backgroundColor: "0F172A",
|
|
246
|
+
children: [
|
|
247
|
+
{
|
|
248
|
+
type: "text",
|
|
249
|
+
text: "会社名",
|
|
250
|
+
fontPx: 14,
|
|
251
|
+
color: "FFFFFF",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: "text",
|
|
255
|
+
text: "{{date}}",
|
|
256
|
+
fontPx: 12,
|
|
257
|
+
color: "E2E8F0",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
footer: {
|
|
262
|
+
type: "hstack",
|
|
263
|
+
h: 30,
|
|
264
|
+
padding: { left: 48, right: 48, top: 0, bottom: 8 },
|
|
265
|
+
justifyContent: "spaceBetween",
|
|
266
|
+
alignItems: "center",
|
|
267
|
+
children: [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: "Confidential",
|
|
271
|
+
fontPx: 10,
|
|
272
|
+
color: "1E293B",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: "Page {{page}} / {{totalPages}}",
|
|
277
|
+
fontPx: 10,
|
|
278
|
+
color: "1E293B",
|
|
279
|
+
alignText: "right",
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
date: {
|
|
284
|
+
format: "YYYY/MM/DD", // または "locale"
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### マスタースライドのオプション
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
type MasterSlideOptions = {
|
|
295
|
+
header?: POMNode; // ヘッダー(任意の POMNode を指定可能)
|
|
296
|
+
footer?: POMNode; // フッター(任意の POMNode を指定可能)
|
|
297
|
+
pageNumber?: {
|
|
298
|
+
position: "left" | "center" | "right"; // ページ番号の位置
|
|
299
|
+
};
|
|
300
|
+
date?: {
|
|
301
|
+
format: "YYYY/MM/DD" | "locale"; // 日付のフォーマット
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### プレースホルダー
|
|
307
|
+
|
|
308
|
+
ヘッダー・フッター内のテキストで以下のプレースホルダーが使用できます:
|
|
309
|
+
|
|
310
|
+
- `{{page}}`: 現在のページ番号
|
|
311
|
+
- `{{totalPages}}`: 総ページ数
|
|
312
|
+
- `{{date}}`: 日付(`date.format` で指定した形式)
|
|
313
|
+
|
|
314
|
+
### 特徴
|
|
315
|
+
|
|
316
|
+
- **柔軟性**: ヘッダー・フッターには任意の POMNode(VStack、HStack、Box など)を使用可能
|
|
317
|
+
- **自動合成**: 各ページのコンテンツに自動的にヘッダー・フッターが追加されます
|
|
318
|
+
- **動的置換**: プレースホルダーはページごとに自動的に置換されます
|
|
319
|
+
- **後方互換性**: master オプションは省略可能で、既存コードへの影響はありません
|
|
320
|
+
|
|
321
|
+
## LLM 連携
|
|
322
|
+
|
|
323
|
+
pom は LLM(GPT-4o、Claude など)で生成した JSON からスライドを作成するユースケースに対応しています。
|
|
324
|
+
|
|
325
|
+
### 入力用スキーマ
|
|
326
|
+
|
|
327
|
+
`inputPomNodeSchema` を使って、LLM が生成した JSON を検証できます。
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { inputPomNodeSchema, buildPptx, InputPOMNode } from "@hirokisakabe/pom";
|
|
331
|
+
|
|
332
|
+
// LLMからのJSON出力を検証
|
|
333
|
+
const jsonFromLLM = `{
|
|
334
|
+
"type": "vstack",
|
|
335
|
+
"padding": 48,
|
|
336
|
+
"gap": 24,
|
|
337
|
+
"children": [
|
|
338
|
+
{ "type": "text", "text": "タイトル", "fontPx": 32, "bold": true },
|
|
339
|
+
{ "type": "text", "text": "本文テキスト", "fontPx": 16 }
|
|
340
|
+
]
|
|
341
|
+
}`;
|
|
342
|
+
|
|
343
|
+
const parsed = JSON.parse(jsonFromLLM);
|
|
344
|
+
const result = inputPomNodeSchema.safeParse(parsed);
|
|
345
|
+
|
|
346
|
+
if (result.success) {
|
|
347
|
+
// 検証成功 - PPTXを生成
|
|
348
|
+
const pptx = await buildPptx([result.data], { w: 1280, h: 720 });
|
|
349
|
+
await pptx.writeFile({ fileName: "output.pptx" });
|
|
350
|
+
} else {
|
|
351
|
+
// 検証失敗 - エラー内容を確認
|
|
352
|
+
console.error("Validation failed:", result.error.format());
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### OpenAI Structured Outputs との連携
|
|
357
|
+
|
|
358
|
+
OpenAI SDK の `zodResponseFormat` を使用して、LLM に直接スキーマ準拠の JSON を生成させることができます。
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { inputPomNodeSchema, buildPptx } from "@hirokisakabe/pom";
|
|
362
|
+
import OpenAI from "openai";
|
|
363
|
+
import { zodResponseFormat } from "openai/helpers/zod";
|
|
364
|
+
|
|
365
|
+
const openai = new OpenAI();
|
|
366
|
+
|
|
367
|
+
const response = await openai.chat.completions.create({
|
|
368
|
+
model: "gpt-4o",
|
|
369
|
+
messages: [
|
|
370
|
+
{
|
|
371
|
+
role: "system",
|
|
372
|
+
content:
|
|
373
|
+
"あなたはプレゼンテーション作成アシスタントです。指定されたスキーマに従ってスライドのJSONを生成してください。",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
role: "user",
|
|
377
|
+
content:
|
|
378
|
+
"売上報告のスライドを作成して。タイトルと3つの箇条書きを含めて。",
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
response_format: zodResponseFormat(inputPomNodeSchema, "slide"),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const slideData = JSON.parse(response.choices[0].message.content!);
|
|
385
|
+
const pptx = await buildPptx([slideData], { w: 1280, h: 720 });
|
|
386
|
+
await pptx.writeFile({ fileName: "sales-report.pptx" });
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### 利用可能な入力用スキーマ
|
|
390
|
+
|
|
391
|
+
| スキーマ | 説明 |
|
|
392
|
+
| ------------------------------- | ---------------------------------------------- |
|
|
393
|
+
| `inputPomNodeSchema` | メインのノードスキーマ(全ノードタイプを含む) |
|
|
394
|
+
| `inputTextNodeSchema` | テキストノード用 |
|
|
395
|
+
| `inputImageNodeSchema` | 画像ノード用 |
|
|
396
|
+
| `inputTableNodeSchema` | テーブルノード用 |
|
|
397
|
+
| `inputShapeNodeSchema` | 図形ノード用 |
|
|
398
|
+
| `inputBoxNodeSchema` | Boxノード用 |
|
|
399
|
+
| `inputVStackNodeSchema` | VStackノード用 |
|
|
400
|
+
| `inputHStackNodeSchema` | HStackノード用 |
|
|
401
|
+
| `inputMasterSlideOptionsSchema` | マスタースライド設定用 |
|
package/dist/buildPptx.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { POMNode } from "./types";
|
|
1
|
+
import { POMNode, MasterSlideOptions } from "./types";
|
|
2
2
|
export declare function buildPptx(nodes: POMNode[], slideSize: {
|
|
3
3
|
w: number;
|
|
4
4
|
h: number;
|
|
5
|
+
}, options?: {
|
|
6
|
+
master?: MasterSlideOptions;
|
|
5
7
|
}): Promise<import("pptxgenjs").default>;
|
|
6
8
|
//# 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":"AAGA,OAAO,EAAE,OAAO,EAAkB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"buildPptx.d.ts","sourceRoot":"","sources":["../src/buildPptx.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAkB,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAiGtE,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;IAAE,MAAM,CAAC,EAAE,kBAAkB,CAAA;CAAE,wCAgB1C"}
|
package/dist/buildPptx.js
CHANGED
|
@@ -1,11 +1,75 @@
|
|
|
1
1
|
import { calcYogaLayout } from "./calcYogaLayout/calcYogaLayout";
|
|
2
2
|
import { renderPptx } from "./renderPptx/renderPptx";
|
|
3
3
|
import { toPositioned } from "./toPositioned/toPositioned";
|
|
4
|
-
|
|
4
|
+
function replacePlaceholders(node, pageNumber, totalPages, date) {
|
|
5
|
+
if (node.type === "text") {
|
|
6
|
+
return {
|
|
7
|
+
...node,
|
|
8
|
+
text: node.text
|
|
9
|
+
.replace(/\{\{page\}\}/g, String(pageNumber))
|
|
10
|
+
.replace(/\{\{totalPages\}\}/g, String(totalPages))
|
|
11
|
+
.replace(/\{\{date\}\}/g, date),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (node.type === "box") {
|
|
15
|
+
return {
|
|
16
|
+
...node,
|
|
17
|
+
children: replacePlaceholders(node.children, pageNumber, totalPages, date),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (node.type === "vstack" || node.type === "hstack") {
|
|
21
|
+
return {
|
|
22
|
+
...node,
|
|
23
|
+
children: node.children.map((child) => replacePlaceholders(child, pageNumber, totalPages, date)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
function composePage(content, master, pageNumber, totalPages) {
|
|
29
|
+
if (!master) {
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
const date = master.date?.format === "locale"
|
|
33
|
+
? new Date().toLocaleDateString()
|
|
34
|
+
: new Date().toISOString().split("T")[0].replace(/-/g, "/");
|
|
35
|
+
const children = [];
|
|
36
|
+
// ヘッダーを追加
|
|
37
|
+
if (master.header) {
|
|
38
|
+
children.push(replacePlaceholders(master.header, pageNumber, totalPages, date));
|
|
39
|
+
}
|
|
40
|
+
// コンテンツを追加
|
|
41
|
+
children.push(content);
|
|
42
|
+
// フッターを追加
|
|
43
|
+
if (master.footer) {
|
|
44
|
+
children.push(replacePlaceholders(master.footer, pageNumber, totalPages, date));
|
|
45
|
+
}
|
|
46
|
+
// ページ番号を追加
|
|
47
|
+
if (master.pageNumber) {
|
|
48
|
+
const pageNumberNode = {
|
|
49
|
+
type: "text",
|
|
50
|
+
text: String(pageNumber),
|
|
51
|
+
fontPx: 12,
|
|
52
|
+
alignText: master.pageNumber.position,
|
|
53
|
+
};
|
|
54
|
+
children.push(pageNumberNode);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
type: "vstack",
|
|
58
|
+
w: "100%",
|
|
59
|
+
h: "100%",
|
|
60
|
+
alignItems: "stretch",
|
|
61
|
+
justifyContent: "start",
|
|
62
|
+
children,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export async function buildPptx(nodes, slideSize, options) {
|
|
5
66
|
const positionedPages = [];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
67
|
+
const totalPages = nodes.length;
|
|
68
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
69
|
+
const node = nodes[i];
|
|
70
|
+
const composedNode = composePage(node, options?.master, i + 1, totalPages);
|
|
71
|
+
await calcYogaLayout(composedNode, slideSize);
|
|
72
|
+
const positioned = toPositioned(composedNode);
|
|
9
73
|
positionedPages.push(positioned);
|
|
10
74
|
}
|
|
11
75
|
const pptx = renderPptx(positionedPages, slideSize);
|
|
@@ -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;AAKF;;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,
|
|
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;AAKF;;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,CA+CA"}
|
|
@@ -6,28 +6,37 @@ const ctx = canvas.getContext("2d");
|
|
|
6
6
|
*/
|
|
7
7
|
export function measureText(text, maxWidthPx, opts) {
|
|
8
8
|
applyFontStyle(opts);
|
|
9
|
-
|
|
9
|
+
// まず改行で段落に分割
|
|
10
|
+
const paragraphs = text.split("\n");
|
|
10
11
|
const lines = [];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (w <= maxWidthPx || !current) {
|
|
17
|
-
// まだ詰められる
|
|
18
|
-
current = candidate;
|
|
19
|
-
currentWidth = w;
|
|
12
|
+
for (const paragraph of paragraphs) {
|
|
13
|
+
// 空の段落(連続した改行)も1行としてカウント
|
|
14
|
+
if (paragraph === "") {
|
|
15
|
+
lines.push({ widthPx: 0 });
|
|
16
|
+
continue;
|
|
20
17
|
}
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const words = splitForWrap(paragraph);
|
|
19
|
+
let current = "";
|
|
20
|
+
let currentWidth = 0;
|
|
21
|
+
for (const word of words) {
|
|
22
|
+
const candidate = current ? current + word : word;
|
|
23
|
+
const w = ctx.measureText(candidate).width;
|
|
24
|
+
if (w <= maxWidthPx || !current) {
|
|
25
|
+
// まだ詰められる
|
|
26
|
+
current = candidate;
|
|
27
|
+
currentWidth = w;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// 折り返す
|
|
31
|
+
lines.push({ widthPx: currentWidth });
|
|
32
|
+
current = word;
|
|
33
|
+
currentWidth = ctx.measureText(word).width;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (current) {
|
|
23
37
|
lines.push({ widthPx: currentWidth });
|
|
24
|
-
current = word;
|
|
25
|
-
currentWidth = ctx.measureText(word).width;
|
|
26
38
|
}
|
|
27
39
|
}
|
|
28
|
-
if (current) {
|
|
29
|
-
lines.push({ widthPx: currentWidth });
|
|
30
|
-
}
|
|
31
40
|
const lineHeightRatio = opts.lineHeight ?? 1.3;
|
|
32
41
|
const lineHeightPx = opts.fontSizePx * lineHeightRatio;
|
|
33
42
|
const widthPx = lines.length ? Math.max(...lines.map((l) => l.widthPx)) : 0;
|
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,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"}
|
package/dist/index.js
CHANGED