@hirokisakabe/pom 0.1.5 → 0.1.7
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 +231 -71
- package/dist/buildPptx.d.ts.map +1 -1
- package/dist/buildPptx.js +1 -3
- package/dist/calcYogaLayout/calcYogaLayout.d.ts.map +1 -1
- package/dist/calcYogaLayout/calcYogaLayout.js +41 -6
- package/dist/calcYogaLayout/measureImage.d.ts +13 -2
- package/dist/calcYogaLayout/measureImage.d.ts.map +1 -1
- package/dist/calcYogaLayout/measureImage.js +79 -3
- package/dist/inputSchema.d.ts +75 -7
- package/dist/inputSchema.d.ts.map +1 -1
- package/dist/inputSchema.js +14 -3
- package/dist/renderPptx/renderPptx.d.ts.map +1 -1
- package/dist/renderPptx/renderPptx.js +22 -3
- package/dist/renderPptx/textOptions.d.ts +22 -1
- package/dist/renderPptx/textOptions.d.ts.map +1 -1
- package/dist/renderPptx/textOptions.js +27 -1
- package/dist/table/utils.d.ts +10 -0
- package/dist/table/utils.d.ts.map +1 -1
- package/dist/table/utils.js +18 -1
- package/dist/toPositioned/toPositioned.d.ts.map +1 -1
- package/dist/toPositioned/toPositioned.js +9 -0
- package/dist/types.d.ts +137 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +46 -4
- package/package.json +24 -2
package/README.md
CHANGED
|
@@ -1,11 +1,68 @@
|
|
|
1
1
|
# pom
|
|
2
2
|
|
|
3
|
-
**pom (PowerPoint Object Model)** は、PowerPoint プレゼンテーション(pptx)を TypeScript
|
|
3
|
+
**pom (PowerPoint Object Model)** は、PowerPoint プレゼンテーション(pptx)を TypeScript で宣言的に記述するためのライブラリです。生成 AI に出力させた POM 形式の JSON を、PowerPoint ファイルに変換するユースケースを想定しています。
|
|
4
|
+
|
|
5
|
+
## 動作環境
|
|
6
|
+
|
|
7
|
+
- Node.js 18 以上
|
|
8
|
+
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> pom は Node.js 環境でのみ動作します。ブラウザ環境では動作しません。
|
|
11
|
+
|
|
12
|
+
## 目次
|
|
13
|
+
|
|
14
|
+
- [動作環境](#動作環境)
|
|
15
|
+
- [インストール](#インストール)
|
|
16
|
+
- [クイックスタート](#クイックスタート)
|
|
17
|
+
- [特徴](#特徴)
|
|
18
|
+
- [ノード](#ノード)
|
|
19
|
+
- [マスタースライド](#マスタースライド)
|
|
20
|
+
- [LLM 連携](#llm-連携)
|
|
21
|
+
- [ライセンス](#ライセンス)
|
|
22
|
+
|
|
23
|
+
## インストール
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @hirokisakabe/pom
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## クイックスタート
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { buildPptx, POMNode } from "@hirokisakabe/pom";
|
|
33
|
+
|
|
34
|
+
const slide: POMNode = {
|
|
35
|
+
type: "vstack",
|
|
36
|
+
w: "100%",
|
|
37
|
+
h: "max",
|
|
38
|
+
padding: 48,
|
|
39
|
+
gap: 24,
|
|
40
|
+
alignItems: "start",
|
|
41
|
+
children: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: "プレゼンテーションタイトル",
|
|
45
|
+
fontPx: 48,
|
|
46
|
+
bold: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: "サブタイトル",
|
|
51
|
+
fontPx: 24,
|
|
52
|
+
color: "666666",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const pptx = await buildPptx([slide], { w: 1280, h: 720 });
|
|
58
|
+
await pptx.writeFile({ fileName: "presentation.pptx" });
|
|
59
|
+
```
|
|
4
60
|
|
|
5
61
|
## 特徴
|
|
6
62
|
|
|
7
63
|
- **型安全**: TypeScript による厳密な型定義
|
|
8
64
|
- **宣言的**: JSON ライクなオブジェクトでスライドを記述
|
|
65
|
+
- **PowerPoint ファースト**: Shape 機能をネイティブサポート
|
|
9
66
|
- **柔軟なレイアウト**: VStack/HStack/Box による自動レイアウト
|
|
10
67
|
- **ピクセル単位**: 直感的なピクセル単位での指定(内部でインチに変換)
|
|
11
68
|
- **マスタースライド**: 全ページ共通のヘッダー・フッター・ページ番号を自動挿入
|
|
@@ -54,6 +111,7 @@
|
|
|
54
111
|
bold?: boolean;
|
|
55
112
|
fontFamily?: string;
|
|
56
113
|
lineSpacingMultiple?: number;
|
|
114
|
+
bullet?: boolean | BulletOptions;
|
|
57
115
|
|
|
58
116
|
// 共通プロパティ
|
|
59
117
|
w?: number | "max" | `${number}%`;
|
|
@@ -66,6 +124,54 @@
|
|
|
66
124
|
- `bold` で太字を指定できます。
|
|
67
125
|
- `fontFamily` でフォントファミリーを指定できます(デフォルト: `"Noto Sans JP"`)。
|
|
68
126
|
- `lineSpacingMultiple` で行間倍率を指定できます(デフォルト: `1.3`)。
|
|
127
|
+
- `bullet` で箇条書きを指定できます。`true` を指定するとデフォルトの箇条書き、オブジェクトで詳細設定が可能です。
|
|
128
|
+
|
|
129
|
+
**BulletOptions:**
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
{
|
|
133
|
+
type?: "bullet" | "number"; // "bullet": 記号、"number": 番号付き
|
|
134
|
+
indent?: number; // インデントレベル
|
|
135
|
+
numberType?: "alphaLcParenBoth" | "alphaLcParenR" | "alphaLcPeriod" |
|
|
136
|
+
"alphaUcParenBoth" | "alphaUcParenR" | "alphaUcPeriod" |
|
|
137
|
+
"arabicParenBoth" | "arabicParenR" | "arabicPeriod" | "arabicPlain" |
|
|
138
|
+
"romanLcParenBoth" | "romanLcParenR" | "romanLcPeriod" |
|
|
139
|
+
"romanUcParenBoth" | "romanUcParenR" | "romanUcPeriod";
|
|
140
|
+
numberStartAt?: number; // 番号の開始値
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**使用例:**
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// シンプルな箇条書き
|
|
148
|
+
{
|
|
149
|
+
type: "text",
|
|
150
|
+
text: "項目1\n項目2\n項目3",
|
|
151
|
+
bullet: true,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 番号付きリスト
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: "ステップ1\nステップ2\nステップ3",
|
|
158
|
+
bullet: { type: "number" },
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// アルファベット小文字(a. b. c.)
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: "項目A\n項目B\n項目C",
|
|
165
|
+
bullet: { type: "number", numberType: "alphaLcPeriod" },
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 5から始まる番号リスト
|
|
169
|
+
{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: "5番目\n6番目\n7番目",
|
|
172
|
+
bullet: { type: "number", numberStartAt: 5 },
|
|
173
|
+
}
|
|
174
|
+
```
|
|
69
175
|
|
|
70
176
|
#### 2. Image
|
|
71
177
|
|
|
@@ -93,7 +199,7 @@
|
|
|
93
199
|
```typescript
|
|
94
200
|
{
|
|
95
201
|
type: "table";
|
|
96
|
-
columns: { width
|
|
202
|
+
columns: { width?: number }[];
|
|
97
203
|
rows: {
|
|
98
204
|
height?: number;
|
|
99
205
|
cells: {
|
|
@@ -114,11 +220,59 @@
|
|
|
114
220
|
}
|
|
115
221
|
```
|
|
116
222
|
|
|
223
|
+
- `columns[].width` を省略すると、テーブル全体の幅から均等分割されます。
|
|
117
224
|
- `columns` の合計がテーブルの自然幅になります(必要であれば `w` で上書きできます)。
|
|
118
225
|
- `rows` の `height` を省略すると `defaultRowHeight`(未指定なら32px)が適用されます。
|
|
119
226
|
- セル背景やフォント装飾を `cells` の各要素で個別に指定できます。
|
|
120
227
|
|
|
121
|
-
#### 4.
|
|
228
|
+
#### 4. Shape
|
|
229
|
+
|
|
230
|
+
図形を描画するノード。テキスト付き/なしで異なる表現が可能で、複雑なビジュアル効果をサポートしています。
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
{
|
|
234
|
+
type: "shape";
|
|
235
|
+
shapeType: PptxGenJS.SHAPE_NAME; // 例: "roundRect", "ellipse", "cloud", "star5" など
|
|
236
|
+
text?: string; // 図形内に表示するテキスト(オプション)
|
|
237
|
+
fill?: {
|
|
238
|
+
color?: string;
|
|
239
|
+
transparency?: number;
|
|
240
|
+
};
|
|
241
|
+
line?: {
|
|
242
|
+
color?: string;
|
|
243
|
+
width?: number;
|
|
244
|
+
dashType?: "solid" | "dash" | "dashDot" | "lgDash" | "lgDashDot" | "lgDashDotDot" | "sysDash" | "sysDot";
|
|
245
|
+
};
|
|
246
|
+
shadow?: {
|
|
247
|
+
type: "outer" | "inner";
|
|
248
|
+
opacity?: number;
|
|
249
|
+
blur?: number;
|
|
250
|
+
angle?: number;
|
|
251
|
+
offset?: number;
|
|
252
|
+
color?: string;
|
|
253
|
+
};
|
|
254
|
+
fontPx?: number;
|
|
255
|
+
color?: string;
|
|
256
|
+
alignText?: "left" | "center" | "right";
|
|
257
|
+
|
|
258
|
+
// 共通プロパティ
|
|
259
|
+
w?: number | "max" | `${number}%`;
|
|
260
|
+
h?: number | "max" | `${number}%`;
|
|
261
|
+
...
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**主な図形タイプの例:**
|
|
266
|
+
|
|
267
|
+
- `roundRect`: 角丸長方形(タイトルボックス、カテゴリ表示)
|
|
268
|
+
- `ellipse`: 楕円/円(ステップ番号、バッジ)
|
|
269
|
+
- `cloud`: 雲型(コメント、重要ポイント)
|
|
270
|
+
- `wedgeRectCallout`: 矢印付き吹き出し(注記)
|
|
271
|
+
- `cloudCallout`: 雲吹き出し(コメント)
|
|
272
|
+
- `star5`: 5つ星(強調、デコレーション)
|
|
273
|
+
- `downArrow`: 下矢印(フロー図)
|
|
274
|
+
|
|
275
|
+
#### 5. Box
|
|
122
276
|
|
|
123
277
|
単一の子要素をラップする汎用コンテナ。
|
|
124
278
|
|
|
@@ -137,7 +291,7 @@
|
|
|
137
291
|
}
|
|
138
292
|
```
|
|
139
293
|
|
|
140
|
-
####
|
|
294
|
+
#### 6. VStack
|
|
141
295
|
|
|
142
296
|
子要素を **縦方向** に並べる。
|
|
143
297
|
|
|
@@ -156,7 +310,7 @@
|
|
|
156
310
|
}
|
|
157
311
|
```
|
|
158
312
|
|
|
159
|
-
####
|
|
313
|
+
#### 7. HStack
|
|
160
314
|
|
|
161
315
|
子要素を **横方向** に並べる。
|
|
162
316
|
|
|
@@ -175,35 +329,23 @@
|
|
|
175
329
|
}
|
|
176
330
|
```
|
|
177
331
|
|
|
178
|
-
####
|
|
332
|
+
#### 8. Chart
|
|
179
333
|
|
|
180
|
-
|
|
334
|
+
グラフを描画するノード。棒グラフ、折れ線グラフ、円グラフをサポート。
|
|
181
335
|
|
|
182
336
|
```typescript
|
|
183
337
|
{
|
|
184
|
-
type: "
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
196
|
-
shadow?: {
|
|
197
|
-
type: "outer" | "inner";
|
|
198
|
-
opacity?: number;
|
|
199
|
-
blur?: number;
|
|
200
|
-
angle?: number;
|
|
201
|
-
offset?: number;
|
|
202
|
-
color?: string;
|
|
203
|
-
};
|
|
204
|
-
fontPx?: number;
|
|
205
|
-
fontColor?: string;
|
|
206
|
-
alignText?: "left" | "center" | "right";
|
|
338
|
+
type: "chart";
|
|
339
|
+
chartType: "bar" | "line" | "pie";
|
|
340
|
+
data: {
|
|
341
|
+
name?: string; // 系列名
|
|
342
|
+
labels: string[]; // カテゴリラベル
|
|
343
|
+
values: number[]; // 値
|
|
344
|
+
}[];
|
|
345
|
+
showLegend?: boolean; // 凡例表示(デフォルト: false)
|
|
346
|
+
showTitle?: boolean; // タイトル表示(デフォルト: false)
|
|
347
|
+
title?: string; // タイトル文字列
|
|
348
|
+
chartColors?: string[]; // データカラー配列(16進カラーコード)
|
|
207
349
|
|
|
208
350
|
// 共通プロパティ
|
|
209
351
|
w?: number | "max" | `${number}%`;
|
|
@@ -212,15 +354,50 @@
|
|
|
212
354
|
}
|
|
213
355
|
```
|
|
214
356
|
|
|
215
|
-
|
|
357
|
+
**使用例:**
|
|
216
358
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
359
|
+
```typescript
|
|
360
|
+
// 棒グラフ
|
|
361
|
+
{
|
|
362
|
+
type: "chart",
|
|
363
|
+
chartType: "bar",
|
|
364
|
+
w: 600,
|
|
365
|
+
h: 400,
|
|
366
|
+
data: [
|
|
367
|
+
{
|
|
368
|
+
name: "売上",
|
|
369
|
+
labels: ["1月", "2月", "3月", "4月"],
|
|
370
|
+
values: [100, 200, 150, 300],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "利益",
|
|
374
|
+
labels: ["1月", "2月", "3月", "4月"],
|
|
375
|
+
values: [30, 60, 45, 90],
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
showLegend: true,
|
|
379
|
+
showTitle: true,
|
|
380
|
+
title: "月別売上・利益",
|
|
381
|
+
chartColors: ["0088CC", "00AA00"],
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 円グラフ
|
|
385
|
+
{
|
|
386
|
+
type: "chart",
|
|
387
|
+
chartType: "pie",
|
|
388
|
+
w: 400,
|
|
389
|
+
h: 300,
|
|
390
|
+
data: [
|
|
391
|
+
{
|
|
392
|
+
name: "市場シェア",
|
|
393
|
+
labels: ["製品A", "製品B", "製品C", "その他"],
|
|
394
|
+
values: [40, 30, 20, 10],
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
showLegend: true,
|
|
398
|
+
chartColors: ["0088CC", "00AA00", "FF6600", "888888"],
|
|
399
|
+
}
|
|
400
|
+
```
|
|
224
401
|
|
|
225
402
|
## マスタースライド
|
|
226
403
|
|
|
@@ -322,6 +499,17 @@ type MasterSlideOptions = {
|
|
|
322
499
|
|
|
323
500
|
pom は LLM(GPT-4o、Claude など)で生成した JSON からスライドを作成するユースケースに対応しています。
|
|
324
501
|
|
|
502
|
+
### LLM 向け仕様ガイド
|
|
503
|
+
|
|
504
|
+
[`llm-guide.md`](./llm-guide.md) は、LLM に pom 形式の JSON を生成させるためのコンパクトな仕様書です。システムプロンプトに含めて使用してください。
|
|
505
|
+
|
|
506
|
+
**含まれる内容:**
|
|
507
|
+
|
|
508
|
+
- ノード一覧と主要プロパティ
|
|
509
|
+
- 標準設定(スライドサイズ、padding、gap、フォントサイズ目安)
|
|
510
|
+
- パターン例(基本構造、2 カラム、テーブル、図形、グラフなど)
|
|
511
|
+
- よくある間違いと正しい書き方
|
|
512
|
+
|
|
325
513
|
### 入力用スキーマ
|
|
326
514
|
|
|
327
515
|
`inputPomNodeSchema` を使って、LLM が生成した JSON を検証できます。
|
|
@@ -353,39 +541,6 @@ if (result.success) {
|
|
|
353
541
|
}
|
|
354
542
|
```
|
|
355
543
|
|
|
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
544
|
### 利用可能な入力用スキーマ
|
|
390
545
|
|
|
391
546
|
| スキーマ | 説明 |
|
|
@@ -395,7 +550,12 @@ await pptx.writeFile({ fileName: "sales-report.pptx" });
|
|
|
395
550
|
| `inputImageNodeSchema` | 画像ノード用 |
|
|
396
551
|
| `inputTableNodeSchema` | テーブルノード用 |
|
|
397
552
|
| `inputShapeNodeSchema` | 図形ノード用 |
|
|
553
|
+
| `inputChartNodeSchema` | チャートノード用 |
|
|
398
554
|
| `inputBoxNodeSchema` | Boxノード用 |
|
|
399
555
|
| `inputVStackNodeSchema` | VStackノード用 |
|
|
400
556
|
| `inputHStackNodeSchema` | HStackノード用 |
|
|
401
557
|
| `inputMasterSlideOptionsSchema` | マスタースライド設定用 |
|
|
558
|
+
|
|
559
|
+
## ライセンス
|
|
560
|
+
|
|
561
|
+
MIT
|
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,kBAAkB,EAAE,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;AA8FtE,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
|
@@ -29,9 +29,7 @@ function composePage(content, master, pageNumber, totalPages) {
|
|
|
29
29
|
if (!master) {
|
|
30
30
|
return content;
|
|
31
31
|
}
|
|
32
|
-
const date = master.date?.
|
|
33
|
-
? new Date().toLocaleDateString()
|
|
34
|
-
: new Date().toISOString().split("T")[0].replace(/-/g, "/");
|
|
32
|
+
const date = master.date?.value ?? "";
|
|
35
33
|
const children = [];
|
|
36
34
|
// ヘッダーを追加
|
|
37
35
|
if (master.header) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calcYogaLayout.d.ts","sourceRoot":"","sources":["../../src/calcYogaLayout/calcYogaLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAOxC;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"calcYogaLayout.d.ts","sourceRoot":"","sources":["../../src/calcYogaLayout/calcYogaLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAOxC;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,iBAiBpC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadYoga } from "yoga-layout/load";
|
|
2
2
|
import { measureText } from "./measureText";
|
|
3
|
-
import { measureImage } from "./measureImage";
|
|
3
|
+
import { measureImage, prefetchImageSize } from "./measureImage";
|
|
4
4
|
import { calcTableIntrinsicSize } from "../table/utils";
|
|
5
5
|
/**
|
|
6
6
|
* POMNode ツリーを Yoga でレイアウト計算する
|
|
@@ -11,6 +11,8 @@ import { calcTableIntrinsicSize } from "../table/utils";
|
|
|
11
11
|
*/
|
|
12
12
|
export async function calcYogaLayout(root, slideSize) {
|
|
13
13
|
const Yoga = await getYoga();
|
|
14
|
+
// 事前に全画像のサイズを取得(HTTPS対応のため)
|
|
15
|
+
await prefetchAllImageSizes(root);
|
|
14
16
|
const rootYoga = Yoga.Node.create();
|
|
15
17
|
root.yogaNode = rootYoga;
|
|
16
18
|
await buildPomWithYogaTree(root, rootYoga);
|
|
@@ -19,6 +21,34 @@ export async function calcYogaLayout(root, slideSize) {
|
|
|
19
21
|
rootYoga.setHeight(slideSize.h);
|
|
20
22
|
rootYoga.calculateLayout(slideSize.w, slideSize.h, Yoga.DIRECTION_LTR);
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* POMNode ツリー内のすべての画像のサイズを事前取得する
|
|
26
|
+
*/
|
|
27
|
+
async function prefetchAllImageSizes(node) {
|
|
28
|
+
const imageSources = collectImageSources(node);
|
|
29
|
+
await Promise.all(imageSources.map((src) => prefetchImageSize(src)));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* POMNode ツリー内のすべての画像のsrcを収集する
|
|
33
|
+
*/
|
|
34
|
+
function collectImageSources(node) {
|
|
35
|
+
const sources = [];
|
|
36
|
+
function traverse(n) {
|
|
37
|
+
if (n.type === "image") {
|
|
38
|
+
sources.push(n.src);
|
|
39
|
+
}
|
|
40
|
+
else if (n.type === "box") {
|
|
41
|
+
traverse(n.children);
|
|
42
|
+
}
|
|
43
|
+
else if (n.type === "vstack" || n.type === "hstack") {
|
|
44
|
+
for (const child of n.children) {
|
|
45
|
+
traverse(child);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
traverse(node);
|
|
50
|
+
return sources;
|
|
51
|
+
}
|
|
22
52
|
/**
|
|
23
53
|
* Yogaシングルトン
|
|
24
54
|
*/
|
|
@@ -31,21 +61,26 @@ async function getYoga() {
|
|
|
31
61
|
/**
|
|
32
62
|
* POMNode ツリーを再帰的に走査し、YogaNode ツリーを構築する
|
|
33
63
|
*/
|
|
34
|
-
async function buildPomWithYogaTree(node, parentYoga) {
|
|
64
|
+
async function buildPomWithYogaTree(node, parentYoga, parentNode) {
|
|
35
65
|
const yoga = await getYoga();
|
|
36
66
|
const yn = yoga.Node.create();
|
|
37
67
|
node.yogaNode = yn; // 対応する YogaNode をセット
|
|
38
68
|
await applyStyleToYogaNode(node, yn);
|
|
69
|
+
// HStack の子要素で幅が指定されていない場合、デフォルトで均等分割
|
|
70
|
+
if (parentNode?.type === "hstack" && node.w === undefined) {
|
|
71
|
+
yn.setFlexGrow(1);
|
|
72
|
+
yn.setFlexBasis(0);
|
|
73
|
+
}
|
|
39
74
|
parentYoga.insertChild(yn, parentYoga.getChildCount());
|
|
40
75
|
switch (node.type) {
|
|
41
76
|
case "box": {
|
|
42
|
-
await buildPomWithYogaTree(node.children, yn);
|
|
77
|
+
await buildPomWithYogaTree(node.children, yn, node);
|
|
43
78
|
break;
|
|
44
79
|
}
|
|
45
80
|
case "vstack":
|
|
46
81
|
case "hstack": {
|
|
47
82
|
for (const child of node.children) {
|
|
48
|
-
await buildPomWithYogaTree(child, yn);
|
|
83
|
+
await buildPomWithYogaTree(child, yn, node);
|
|
49
84
|
}
|
|
50
85
|
break;
|
|
51
86
|
}
|
|
@@ -227,7 +262,7 @@ async function applyStyleToYogaNode(node, yn) {
|
|
|
227
262
|
const text = node.text;
|
|
228
263
|
const fontSizePx = node.fontPx ?? 24;
|
|
229
264
|
const fontFamily = "Noto Sans JP";
|
|
230
|
-
const fontWeight = "normal";
|
|
265
|
+
const fontWeight = node.bold ? "bold" : "normal";
|
|
231
266
|
const lineHeight = 1.3;
|
|
232
267
|
yn.setMeasureFunc((width, widthMode) => {
|
|
233
268
|
const maxWidthPx = (() => {
|
|
@@ -283,7 +318,7 @@ async function applyStyleToYogaNode(node, yn) {
|
|
|
283
318
|
const text = node.text;
|
|
284
319
|
const fontSizePx = node.fontPx ?? 24;
|
|
285
320
|
const fontFamily = "Noto Sans JP";
|
|
286
|
-
const fontWeight = "normal";
|
|
321
|
+
const fontWeight = node.bold ? "bold" : "normal";
|
|
287
322
|
const lineHeight = 1.3;
|
|
288
323
|
yn.setMeasureFunc((width, widthMode) => {
|
|
289
324
|
const maxWidthPx = (() => {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 画像サイズを事前取得してキャッシュする(非同期)
|
|
3
|
+
* HTTPS URLの画像を処理する際に使用
|
|
4
|
+
* @param src 画像のパス(ローカルパス、base64データ、またはHTTPS URL)
|
|
5
|
+
* @returns 画像の幅と高さ(px)
|
|
6
|
+
*/
|
|
7
|
+
export declare function prefetchImageSize(src: string): Promise<{
|
|
8
|
+
widthPx: number;
|
|
9
|
+
heightPx: number;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* 画像ファイルのサイズを取得する(同期)
|
|
13
|
+
* 事前にprefetchImageSizeでキャッシュしておくこと
|
|
14
|
+
* @param src 画像のパス(ローカルパス、base64データ、またはHTTPS URL)
|
|
4
15
|
* @returns 画像の幅と高さ(px)
|
|
5
16
|
*/
|
|
6
17
|
export declare function measureImage(src: string): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"measureImage.d.ts","sourceRoot":"","sources":["../../src/calcYogaLayout/measureImage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"measureImage.d.ts","sourceRoot":"","sources":["../../src/calcYogaLayout/measureImage.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAqDD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAqDA"}
|
|
@@ -1,11 +1,76 @@
|
|
|
1
1
|
import imageSize from "image-size";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
|
|
4
|
+
* 画像サイズのキャッシュ(事前取得した画像サイズを保持)
|
|
5
|
+
*/
|
|
6
|
+
const imageSizeCache = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* 画像サイズを事前取得してキャッシュする(非同期)
|
|
9
|
+
* HTTPS URLの画像を処理する際に使用
|
|
10
|
+
* @param src 画像のパス(ローカルパス、base64データ、またはHTTPS URL)
|
|
11
|
+
* @returns 画像の幅と高さ(px)
|
|
12
|
+
*/
|
|
13
|
+
export async function prefetchImageSize(src) {
|
|
14
|
+
// キャッシュにあればそれを返す
|
|
15
|
+
const cached = imageSizeCache.get(src);
|
|
16
|
+
if (cached) {
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
let buffer;
|
|
21
|
+
// base64データの場合
|
|
22
|
+
if (src.startsWith("data:")) {
|
|
23
|
+
const base64Data = src.split(",")[1];
|
|
24
|
+
buffer = new Uint8Array(Buffer.from(base64Data, "base64"));
|
|
25
|
+
}
|
|
26
|
+
// HTTPS/HTTP URLの場合
|
|
27
|
+
else if (src.startsWith("https://") || src.startsWith("http://")) {
|
|
28
|
+
const response = await fetch(src);
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Failed to fetch image: ${response.status}`);
|
|
31
|
+
}
|
|
32
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
33
|
+
buffer = new Uint8Array(arrayBuffer);
|
|
34
|
+
}
|
|
35
|
+
// ローカルファイルパスの場合
|
|
36
|
+
else {
|
|
37
|
+
buffer = new Uint8Array(fs.readFileSync(src));
|
|
38
|
+
}
|
|
39
|
+
const dimensions = imageSize(buffer);
|
|
40
|
+
const width = dimensions.width ?? 100; // デフォルト100px
|
|
41
|
+
const height = dimensions.height ?? 100; // デフォルト100px
|
|
42
|
+
const result = {
|
|
43
|
+
widthPx: width,
|
|
44
|
+
heightPx: height,
|
|
45
|
+
};
|
|
46
|
+
// キャッシュに保存
|
|
47
|
+
imageSizeCache.set(src, result);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// エラーが発生した場合はデフォルトサイズを返す
|
|
52
|
+
console.warn(`Failed to measure image size for ${src}:`, error);
|
|
53
|
+
const result = {
|
|
54
|
+
widthPx: 100,
|
|
55
|
+
heightPx: 100,
|
|
56
|
+
};
|
|
57
|
+
imageSizeCache.set(src, result);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 画像ファイルのサイズを取得する(同期)
|
|
63
|
+
* 事前にprefetchImageSizeでキャッシュしておくこと
|
|
64
|
+
* @param src 画像のパス(ローカルパス、base64データ、またはHTTPS URL)
|
|
6
65
|
* @returns 画像の幅と高さ(px)
|
|
7
66
|
*/
|
|
8
67
|
export function measureImage(src) {
|
|
68
|
+
// キャッシュにあればそれを返す
|
|
69
|
+
const cached = imageSizeCache.get(src);
|
|
70
|
+
if (cached) {
|
|
71
|
+
return cached;
|
|
72
|
+
}
|
|
73
|
+
// キャッシュにない場合(ローカルファイルやbase64のみ同期処理可能)
|
|
9
74
|
try {
|
|
10
75
|
let buffer;
|
|
11
76
|
// base64データの場合
|
|
@@ -13,6 +78,14 @@ export function measureImage(src) {
|
|
|
13
78
|
const base64Data = src.split(",")[1];
|
|
14
79
|
buffer = new Uint8Array(Buffer.from(base64Data, "base64"));
|
|
15
80
|
}
|
|
81
|
+
// HTTPS/HTTP URLの場合はキャッシュがないとデフォルト値を返す
|
|
82
|
+
else if (src.startsWith("https://") || src.startsWith("http://")) {
|
|
83
|
+
console.warn(`Image size for URL ${src} was not prefetched. Using default size.`);
|
|
84
|
+
return {
|
|
85
|
+
widthPx: 100,
|
|
86
|
+
heightPx: 100,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
16
89
|
// ローカルファイルパスの場合
|
|
17
90
|
else {
|
|
18
91
|
buffer = new Uint8Array(fs.readFileSync(src));
|
|
@@ -20,10 +93,13 @@ export function measureImage(src) {
|
|
|
20
93
|
const dimensions = imageSize(buffer);
|
|
21
94
|
const width = dimensions.width ?? 100; // デフォルト100px
|
|
22
95
|
const height = dimensions.height ?? 100; // デフォルト100px
|
|
23
|
-
|
|
96
|
+
const result = {
|
|
24
97
|
widthPx: width,
|
|
25
98
|
heightPx: height,
|
|
26
99
|
};
|
|
100
|
+
// キャッシュに保存
|
|
101
|
+
imageSizeCache.set(src, result);
|
|
102
|
+
return result;
|
|
27
103
|
}
|
|
28
104
|
catch (error) {
|
|
29
105
|
// エラーが発生した場合はデフォルトサイズを返す
|