@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 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: number }[];
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. Box
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
- #### 5. VStack
294
+ #### 6. VStack
141
295
 
142
296
  子要素を **縦方向** に並べる。
143
297
 
@@ -156,7 +310,7 @@
156
310
  }
157
311
  ```
158
312
 
159
- #### 6. HStack
313
+ #### 7. HStack
160
314
 
161
315
  子要素を **横方向** に並べる。
162
316
 
@@ -175,35 +329,23 @@
175
329
  }
176
330
  ```
177
331
 
178
- #### 7. Shape
332
+ #### 8. Chart
179
333
 
180
- 図形を描画するノード。テキスト付き/なしで異なる表現が可能で、複雑なビジュアル効果をサポートしています。
334
+ グラフを描画するノード。棒グラフ、折れ線グラフ、円グラフをサポート。
181
335
 
182
336
  ```typescript
183
337
  {
184
- type: "shape";
185
- shapeType: PptxGenJS.SHAPE_NAME; // 例: "roundRect", "ellipse", "cloud", "star5" など
186
- text?: string; // 図形内に表示するテキスト(オプション)
187
- fill?: {
188
- color?: string;
189
- transparency?: number;
190
- };
191
- line?: {
192
- color?: string;
193
- width?: number;
194
- dashType?: "solid" | "dash" | "dashDot" | "lgDash" | "lgDashDot" | "lgDashDotDot" | "sysDash" | "sysDot";
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
- - `roundRect`: 角丸長方形(タイトルボックス、カテゴリ表示)
218
- - `ellipse`: 楕円/円(ステップ番号、バッジ)
219
- - `cloud`: 雲型(コメント、重要ポイント)
220
- - `wedgeRectCallout`: 矢印付き吹き出し(注記)
221
- - `cloudCallout`: 雲吹き出し(コメント)
222
- - `star5`: 5つ星(強調、デコレーション)
223
- - `downArrow`: 下矢印(フロー図)
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
@@ -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;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"}
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?.format === "locale"
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,iBAcpC"}
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
- * @param src 画像のパス(ローカルパス、またはbase64データ)
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":"AAGA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB,CA+BA"}
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
- * @param src 画像のパス(ローカルパス、またはbase64データ)
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
- return {
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
  // エラーが発生した場合はデフォルトサイズを返す