@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 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: number }[];
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
- fontColor?: string;
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ノード用 |
@@ -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
@@ -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":"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?.format === "locale"
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,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
  // エラーが発生した場合はデフォルトサイズを返す
@@ -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;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"}
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"}