@hirokisakabe/pom 0.1.6 → 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 +173 -35
- package/dist/buildPptx.d.ts +3 -0
- package/dist/buildPptx.d.ts.map +1 -1
- package/dist/buildPptx.js +9 -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/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/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 +18 -2
package/README.md
CHANGED
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
**pom (PowerPoint Object Model)** は、PowerPoint プレゼンテーション(pptx)を TypeScript で宣言的に記述するためのライブラリです。生成 AI に出力させた POM 形式の JSON を、PowerPoint ファイルに変換するユースケースを想定しています。
|
|
4
4
|
|
|
5
|
+
## 動作環境
|
|
6
|
+
|
|
7
|
+
- Node.js 18 以上
|
|
8
|
+
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> pom は Node.js 環境でのみ動作します。ブラウザ環境では動作しません。
|
|
11
|
+
|
|
5
12
|
## 目次
|
|
6
13
|
|
|
14
|
+
- [動作環境](#動作環境)
|
|
7
15
|
- [インストール](#インストール)
|
|
8
16
|
- [クイックスタート](#クイックスタート)
|
|
9
17
|
- [特徴](#特徴)
|
|
10
18
|
- [ノード](#ノード)
|
|
11
19
|
- [マスタースライド](#マスタースライド)
|
|
20
|
+
- [サーバーレス環境での利用](#サーバーレス環境での利用)
|
|
12
21
|
- [LLM 連携](#llm-連携)
|
|
13
22
|
- [ライセンス](#ライセンス)
|
|
14
23
|
|
|
@@ -103,6 +112,7 @@ await pptx.writeFile({ fileName: "presentation.pptx" });
|
|
|
103
112
|
bold?: boolean;
|
|
104
113
|
fontFamily?: string;
|
|
105
114
|
lineSpacingMultiple?: number;
|
|
115
|
+
bullet?: boolean | BulletOptions;
|
|
106
116
|
|
|
107
117
|
// 共通プロパティ
|
|
108
118
|
w?: number | "max" | `${number}%`;
|
|
@@ -115,6 +125,54 @@ await pptx.writeFile({ fileName: "presentation.pptx" });
|
|
|
115
125
|
- `bold` で太字を指定できます。
|
|
116
126
|
- `fontFamily` でフォントファミリーを指定できます(デフォルト: `"Noto Sans JP"`)。
|
|
117
127
|
- `lineSpacingMultiple` で行間倍率を指定できます(デフォルト: `1.3`)。
|
|
128
|
+
- `bullet` で箇条書きを指定できます。`true` を指定するとデフォルトの箇条書き、オブジェクトで詳細設定が可能です。
|
|
129
|
+
|
|
130
|
+
**BulletOptions:**
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
{
|
|
134
|
+
type?: "bullet" | "number"; // "bullet": 記号、"number": 番号付き
|
|
135
|
+
indent?: number; // インデントレベル
|
|
136
|
+
numberType?: "alphaLcParenBoth" | "alphaLcParenR" | "alphaLcPeriod" |
|
|
137
|
+
"alphaUcParenBoth" | "alphaUcParenR" | "alphaUcPeriod" |
|
|
138
|
+
"arabicParenBoth" | "arabicParenR" | "arabicPeriod" | "arabicPlain" |
|
|
139
|
+
"romanLcParenBoth" | "romanLcParenR" | "romanLcPeriod" |
|
|
140
|
+
"romanUcParenBoth" | "romanUcParenR" | "romanUcPeriod";
|
|
141
|
+
numberStartAt?: number; // 番号の開始値
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**使用例:**
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// シンプルな箇条書き
|
|
149
|
+
{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: "項目1\n項目2\n項目3",
|
|
152
|
+
bullet: true,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 番号付きリスト
|
|
156
|
+
{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: "ステップ1\nステップ2\nステップ3",
|
|
159
|
+
bullet: { type: "number" },
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// アルファベット小文字(a. b. c.)
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: "項目A\n項目B\n項目C",
|
|
166
|
+
bullet: { type: "number", numberType: "alphaLcPeriod" },
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 5から始まる番号リスト
|
|
170
|
+
{
|
|
171
|
+
type: "text",
|
|
172
|
+
text: "5番目\n6番目\n7番目",
|
|
173
|
+
bullet: { type: "number", numberStartAt: 5 },
|
|
174
|
+
}
|
|
175
|
+
```
|
|
118
176
|
|
|
119
177
|
#### 2. Image
|
|
120
178
|
|
|
@@ -142,7 +200,7 @@ await pptx.writeFile({ fileName: "presentation.pptx" });
|
|
|
142
200
|
```typescript
|
|
143
201
|
{
|
|
144
202
|
type: "table";
|
|
145
|
-
columns: { width
|
|
203
|
+
columns: { width?: number }[];
|
|
146
204
|
rows: {
|
|
147
205
|
height?: number;
|
|
148
206
|
cells: {
|
|
@@ -163,6 +221,7 @@ await pptx.writeFile({ fileName: "presentation.pptx" });
|
|
|
163
221
|
}
|
|
164
222
|
```
|
|
165
223
|
|
|
224
|
+
- `columns[].width` を省略すると、テーブル全体の幅から均等分割されます。
|
|
166
225
|
- `columns` の合計がテーブルの自然幅になります(必要であれば `w` で上書きできます)。
|
|
167
226
|
- `rows` の `height` を省略すると `defaultRowHeight`(未指定なら32px)が適用されます。
|
|
168
227
|
- セル背景やフォント装飾を `cells` の各要素で個別に指定できます。
|
|
@@ -194,7 +253,7 @@ await pptx.writeFile({ fileName: "presentation.pptx" });
|
|
|
194
253
|
color?: string;
|
|
195
254
|
};
|
|
196
255
|
fontPx?: number;
|
|
197
|
-
|
|
256
|
+
color?: string;
|
|
198
257
|
alignText?: "left" | "center" | "right";
|
|
199
258
|
|
|
200
259
|
// 共通プロパティ
|
|
@@ -271,6 +330,76 @@ await pptx.writeFile({ fileName: "presentation.pptx" });
|
|
|
271
330
|
}
|
|
272
331
|
```
|
|
273
332
|
|
|
333
|
+
#### 8. Chart
|
|
334
|
+
|
|
335
|
+
グラフを描画するノード。棒グラフ、折れ線グラフ、円グラフをサポート。
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
{
|
|
339
|
+
type: "chart";
|
|
340
|
+
chartType: "bar" | "line" | "pie";
|
|
341
|
+
data: {
|
|
342
|
+
name?: string; // 系列名
|
|
343
|
+
labels: string[]; // カテゴリラベル
|
|
344
|
+
values: number[]; // 値
|
|
345
|
+
}[];
|
|
346
|
+
showLegend?: boolean; // 凡例表示(デフォルト: false)
|
|
347
|
+
showTitle?: boolean; // タイトル表示(デフォルト: false)
|
|
348
|
+
title?: string; // タイトル文字列
|
|
349
|
+
chartColors?: string[]; // データカラー配列(16進カラーコード)
|
|
350
|
+
|
|
351
|
+
// 共通プロパティ
|
|
352
|
+
w?: number | "max" | `${number}%`;
|
|
353
|
+
h?: number | "max" | `${number}%`;
|
|
354
|
+
...
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**使用例:**
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// 棒グラフ
|
|
362
|
+
{
|
|
363
|
+
type: "chart",
|
|
364
|
+
chartType: "bar",
|
|
365
|
+
w: 600,
|
|
366
|
+
h: 400,
|
|
367
|
+
data: [
|
|
368
|
+
{
|
|
369
|
+
name: "売上",
|
|
370
|
+
labels: ["1月", "2月", "3月", "4月"],
|
|
371
|
+
values: [100, 200, 150, 300],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: "利益",
|
|
375
|
+
labels: ["1月", "2月", "3月", "4月"],
|
|
376
|
+
values: [30, 60, 45, 90],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
showLegend: true,
|
|
380
|
+
showTitle: true,
|
|
381
|
+
title: "月別売上・利益",
|
|
382
|
+
chartColors: ["0088CC", "00AA00"],
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 円グラフ
|
|
386
|
+
{
|
|
387
|
+
type: "chart",
|
|
388
|
+
chartType: "pie",
|
|
389
|
+
w: 400,
|
|
390
|
+
h: 300,
|
|
391
|
+
data: [
|
|
392
|
+
{
|
|
393
|
+
name: "市場シェア",
|
|
394
|
+
labels: ["製品A", "製品B", "製品C", "その他"],
|
|
395
|
+
values: [40, 30, 20, 10],
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
showLegend: true,
|
|
399
|
+
chartColors: ["0088CC", "00AA00", "FF6600", "888888"],
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
274
403
|
## マスタースライド
|
|
275
404
|
|
|
276
405
|
全ページに共通のヘッダー・フッター・ページ番号を自動挿入できます。
|
|
@@ -367,10 +496,51 @@ type MasterSlideOptions = {
|
|
|
367
496
|
- **動的置換**: プレースホルダーはページごとに自動的に置換されます
|
|
368
497
|
- **後方互換性**: master オプションは省略可能で、既存コードへの影響はありません
|
|
369
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
|
+
|
|
370
529
|
## LLM 連携
|
|
371
530
|
|
|
372
531
|
pom は LLM(GPT-4o、Claude など)で生成した JSON からスライドを作成するユースケースに対応しています。
|
|
373
532
|
|
|
533
|
+
### LLM 向け仕様ガイド
|
|
534
|
+
|
|
535
|
+
[`llm-guide.md`](./llm-guide.md) は、LLM に pom 形式の JSON を生成させるためのコンパクトな仕様書です。システムプロンプトに含めて使用してください。
|
|
536
|
+
|
|
537
|
+
**含まれる内容:**
|
|
538
|
+
|
|
539
|
+
- ノード一覧と主要プロパティ
|
|
540
|
+
- 標準設定(スライドサイズ、padding、gap、フォントサイズ目安)
|
|
541
|
+
- パターン例(基本構造、2 カラム、テーブル、図形、グラフなど)
|
|
542
|
+
- よくある間違いと正しい書き方
|
|
543
|
+
|
|
374
544
|
### 入力用スキーマ
|
|
375
545
|
|
|
376
546
|
`inputPomNodeSchema` を使って、LLM が生成した JSON を検証できます。
|
|
@@ -402,39 +572,6 @@ if (result.success) {
|
|
|
402
572
|
}
|
|
403
573
|
```
|
|
404
574
|
|
|
405
|
-
### OpenAI Structured Outputs との連携
|
|
406
|
-
|
|
407
|
-
OpenAI SDK の `zodResponseFormat` を使用して、LLM に直接スキーマ準拠の JSON を生成させることができます。
|
|
408
|
-
|
|
409
|
-
```typescript
|
|
410
|
-
import { inputPomNodeSchema, buildPptx } from "@hirokisakabe/pom";
|
|
411
|
-
import OpenAI from "openai";
|
|
412
|
-
import { zodResponseFormat } from "openai/helpers/zod";
|
|
413
|
-
|
|
414
|
-
const openai = new OpenAI();
|
|
415
|
-
|
|
416
|
-
const response = await openai.chat.completions.create({
|
|
417
|
-
model: "gpt-4o",
|
|
418
|
-
messages: [
|
|
419
|
-
{
|
|
420
|
-
role: "system",
|
|
421
|
-
content:
|
|
422
|
-
"あなたはプレゼンテーション作成アシスタントです。指定されたスキーマに従ってスライドのJSONを生成してください。",
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
role: "user",
|
|
426
|
-
content:
|
|
427
|
-
"売上報告のスライドを作成して。タイトルと3つの箇条書きを含めて。",
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
response_format: zodResponseFormat(inputPomNodeSchema, "slide"),
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
const slideData = JSON.parse(response.choices[0].message.content!);
|
|
434
|
-
const pptx = await buildPptx([slideData], { w: 1280, h: 720 });
|
|
435
|
-
await pptx.writeFile({ fileName: "sales-report.pptx" });
|
|
436
|
-
```
|
|
437
|
-
|
|
438
575
|
### 利用可能な入力用スキーマ
|
|
439
576
|
|
|
440
577
|
| スキーマ | 説明 |
|
|
@@ -444,6 +581,7 @@ await pptx.writeFile({ fileName: "sales-report.pptx" });
|
|
|
444
581
|
| `inputImageNodeSchema` | 画像ノード用 |
|
|
445
582
|
| `inputTableNodeSchema` | テーブルノード用 |
|
|
446
583
|
| `inputShapeNodeSchema` | 図形ノード用 |
|
|
584
|
+
| `inputChartNodeSchema` | チャートノード用 |
|
|
447
585
|
| `inputBoxNodeSchema` | Boxノード用 |
|
|
448
586
|
| `inputVStackNodeSchema` | VStackノード用 |
|
|
449
587
|
| `inputHStackNodeSchema` | HStackノード用 |
|
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) {
|
|
@@ -29,9 +30,7 @@ function composePage(content, master, pageNumber, totalPages) {
|
|
|
29
30
|
if (!master) {
|
|
30
31
|
return content;
|
|
31
32
|
}
|
|
32
|
-
const date = master.date?.
|
|
33
|
-
? new Date().toLocaleDateString()
|
|
34
|
-
: new Date().toISOString().split("T")[0].replace(/-/g, "/");
|
|
33
|
+
const date = master.date?.value ?? "";
|
|
35
34
|
const children = [];
|
|
36
35
|
// ヘッダーを追加
|
|
37
36
|
if (master.header) {
|
|
@@ -63,6 +62,13 @@ function composePage(content, master, pageNumber, totalPages) {
|
|
|
63
62
|
};
|
|
64
63
|
}
|
|
65
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
|
+
}
|
|
66
72
|
const positionedPages = [];
|
|
67
73
|
const totalPages = nodes.length;
|
|
68
74
|
for (let i = 0; i < nodes.length; i++) {
|
|
@@ -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
|
// エラーが発生した場合はデフォルトサイズを返す
|
|
@@ -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"}
|