@hirokisakabe/pom 6.4.0 → 7.1.0

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
@@ -36,11 +36,11 @@
36
36
  ## Features
37
37
 
38
38
  - **AI Friendly** — Simple XML structure designed for LLM code generation. Include [llm.txt](./website/public/llm.txt) in your system prompt for XML reference. Also available at `https://pom.pptx.app/llm.txt`.
39
- - **Declarative** — Describe slides as XML. No imperative API calls needed.
39
+ - **Declarative** — Describe slides as XML. No imperative API calls needed — just data in, PPTX out.
40
40
  - **Flexible Layout** — Flexbox-style layout with VStack / HStack, powered by yoga-layout.
41
41
  - **Rich Nodes** — 18 built-in node types: charts, flowcharts, tables, timelines, org trees, and more.
42
42
  - **Schema-validated** — XML input is validated with Zod schemas at runtime with clear error messages.
43
- - **PowerPoint Native** — Full access to native PowerPoint shape features (roundRect, ellipse, arrows, etc.).
43
+ - **PowerPoint Native** — Generates real editable PowerPoint shapes not images. Recipients can modify everything.
44
44
  - **Pixel Units** — Intuitive pixel-based sizing (internally converted to inches at 96 DPI).
45
45
  - **Master Slide** — Define headers, footers, and page numbers once — applied to all slides automatically.
46
46
  - **Accurate Text Measurement** — Text width measured with opentype.js and bundled Noto Sans JP fonts for consistent layout.
@@ -142,16 +142,16 @@ For detailed node documentation, see [Nodes](./docs/nodes.md).
142
142
 
143
143
  ```xml
144
144
  <Table defaultRowHeight="36" cellBorder='{"color":"CBD5E1","width":1}'>
145
- <TableColumn width="80" />
146
- <TableColumn width="200" />
147
- <TableRow>
148
- <TableCell bold="true" backgroundColor="0F172A" color="FFFFFF">ID</TableCell>
149
- <TableCell bold="true" backgroundColor="0F172A" color="FFFFFF">Name</TableCell>
150
- </TableRow>
151
- <TableRow>
152
- <TableCell>001</TableCell>
153
- <TableCell>Project Alpha</TableCell>
154
- </TableRow>
145
+ <Col width="80" />
146
+ <Col width="200" />
147
+ <Tr>
148
+ <Td bold="true" backgroundColor="0F172A" color="FFFFFF">ID</Td>
149
+ <Td bold="true" backgroundColor="0F172A" color="FFFFFF">Name</Td>
150
+ </Tr>
151
+ <Tr>
152
+ <Td>001</Td>
153
+ <Td>Project Alpha</Td>
154
+ </Tr>
155
155
  </Table>
156
156
  ```
157
157
 
@@ -11,6 +11,7 @@ export declare function buildPptx(xml: string, slideSize: {
11
11
  h: number;
12
12
  }, options?: {
13
13
  master?: SlideMasterOptions;
14
+ masterPptx?: ArrayBuffer | Uint8Array;
14
15
  textMeasurement?: TextMeasurementMode;
15
16
  autoFit?: boolean;
16
17
  strict?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"buildPptx.d.ts","sourceRoot":"","sources":["../src/buildPptx.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAG3E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,OAAO,EAAkB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhE,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,WAAW,EAAE,OAAO,CAAC;IAClC,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,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;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GACA,OAAO,CAAC,eAAe,CAAC,CAmC1B"}
1
+ {"version":3,"file":"buildPptx.d.ts","sourceRoot":"","sources":["../src/buildPptx.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAG3E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAOnD,OAAO,EAAkB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhE,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,WAAW,EAAE,OAAO,CAAC;IAClC,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EACnC,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,UAAU,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;IACtC,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GACA,OAAO,CAAC,eAAe,CAAC,CAoD1B"}
package/dist/buildPptx.js CHANGED
@@ -3,6 +3,7 @@ import { createBuildContext } from "./buildContext.js";
3
3
  import { calcYogaLayout } from "./calcYogaLayout/calcYogaLayout.js";
4
4
  import { extractLayoutResults } from "./calcYogaLayout/types.js";
5
5
  import { DiagnosticsError } from "./diagnostics.js";
6
+ import { parseMasterPptx } from "./parseMasterPptx.js";
6
7
  import { parseXml } from "./parseXml/parseXml.js";
7
8
  import { renderPptx } from "./renderPptx/renderPptx.js";
8
9
  import { freeYogaTree } from "./shared/freeYogaTree.js";
@@ -29,7 +30,29 @@ export async function buildPptx(xml, slideSize, options) {
29
30
  freeYogaTree(map);
30
31
  }
31
32
  }
32
- const pptx = await renderPptx(positionedPages, slideSize, ctx, options?.master);
33
+ // masterPptx から背景を抽出し、master オプションにマージ
34
+ let master = options?.master;
35
+ if (options?.masterPptx) {
36
+ try {
37
+ const bg = await parseMasterPptx(options.masterPptx);
38
+ if (bg) {
39
+ if (master) {
40
+ // 明示的に background が指定されていない場合のみ、masterPptx の背景を使用
41
+ if (!master.background) {
42
+ master = { ...master, background: bg };
43
+ }
44
+ }
45
+ else {
46
+ master = { background: bg };
47
+ }
48
+ }
49
+ }
50
+ catch (e) {
51
+ const message = e instanceof Error ? e.message : "Unknown error parsing masterPptx";
52
+ ctx.diagnostics.add("MASTER_PPTX_PARSE_FAILED", message);
53
+ }
54
+ }
55
+ const pptx = await renderPptx(positionedPages, slideSize, ctx, master);
33
56
  const diagnostics = ctx.diagnostics.items;
34
57
  if (options?.strict && diagnostics.length > 0) {
35
58
  throw new DiagnosticsError(diagnostics);
@@ -1,4 +1,4 @@
1
- export type DiagnosticCode = "IMAGE_MEASURE_FAILED" | "IMAGE_NOT_PREFETCHED" | "AUTOFIT_OVERFLOW" | "SCALE_BELOW_THRESHOLD";
1
+ export type DiagnosticCode = "IMAGE_MEASURE_FAILED" | "IMAGE_NOT_PREFETCHED" | "AUTOFIT_OVERFLOW" | "SCALE_BELOW_THRESHOLD" | "MASTER_PPTX_PARSE_FAILED";
2
2
  export interface Diagnostic {
3
3
  code: DiagnosticCode;
4
4
  message: string;
@@ -1 +1 @@
1
- {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GACtB,sBAAsB,GACtB,sBAAsB,GACtB,kBAAkB,GAClB,uBAAuB,CAAC;AAE5B,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,CAAM;IAElC,GAAG,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;CAGjD;AAED,qBAAa,gBAAiB,SAAQ,KAAK;aACb,WAAW,EAAE,UAAU,EAAE;gBAAzB,WAAW,EAAE,UAAU,EAAE;CAOtD"}
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GACtB,sBAAsB,GACtB,sBAAsB,GACtB,kBAAkB,GAClB,uBAAuB,GACvB,0BAA0B,CAAC;AAE/B,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,CAAM;IAElC,GAAG,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;CAGjD;AAED,qBAAa,gBAAiB,SAAQ,KAAK;aACb,WAAW,EAAE,UAAU,EAAE;gBAAzB,WAAW,EAAE,UAAU,EAAE;CAOtD"}
@@ -0,0 +1,16 @@
1
+ import type { SlideMasterBackground } from "./types.ts";
2
+ /**
3
+ * マスター PPTX のバッファからスライドマスターの背景情報を抽出する。
4
+ *
5
+ * 探索順序:
6
+ * 1. `ppt/slideMasters/slideMaster1.xml` の `p:bg > p:bgPr`
7
+ * 2. 各スライドレイアウト (`ppt/slideLayouts/slideLayoutN.xml`) の `p:bg > p:bgPr`
8
+ *
9
+ * サポートする背景:
10
+ * - 単色塗りつぶし (`a:solidFill` / `a:srgbClr`)
11
+ * - 画像背景 (`a:blipFill` / `a:blip`)
12
+ *
13
+ * @returns 背景情報。背景が設定されていない場合は `undefined`。
14
+ */
15
+ export declare function parseMasterPptx(pptxBuffer: ArrayBuffer | Uint8Array): Promise<SlideMasterBackground | undefined>;
16
+ //# sourceMappingURL=parseMasterPptx.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseMasterPptx.d.ts","sourceRoot":"","sources":["../src/parseMasterPptx.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAqHxD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,WAAW,GAAG,UAAU,GACnC,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAqD5C"}
@@ -0,0 +1,152 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+ // JSZip は CJS パッケージのため動的 import で読み込む
3
+ async function loadJSZip() {
4
+ const mod = await import("jszip");
5
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
6
+ return mod.default ?? mod;
7
+ /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */
8
+ }
9
+ const xmlParser = new XMLParser({
10
+ ignoreAttributes: false,
11
+ attributeNamePrefix: "@_",
12
+ });
13
+ /**
14
+ * MIME タイプを拡張子から判定する
15
+ */
16
+ function mimeTypeFromExt(filePath) {
17
+ const ext = filePath.split(".").pop()?.toLowerCase();
18
+ switch (ext) {
19
+ case "png":
20
+ return "image/png";
21
+ case "jpg":
22
+ case "jpeg":
23
+ return "image/jpeg";
24
+ case "gif":
25
+ return "image/gif";
26
+ case "bmp":
27
+ return "image/bmp";
28
+ case "svg":
29
+ return "image/svg+xml";
30
+ case "tiff":
31
+ case "tif":
32
+ return "image/tiff";
33
+ case "webp":
34
+ return "image/webp";
35
+ default:
36
+ return "image/png";
37
+ }
38
+ }
39
+ /**
40
+ * rels XML から rId に対応するファイルパスを取得する
41
+ */
42
+ function resolveRelId(relsXml, rId) {
43
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
44
+ const parsed = xmlParser.parse(relsXml);
45
+ const relationships = parsed?.Relationships?.Relationship;
46
+ if (!relationships)
47
+ return undefined;
48
+ const rels = Array.isArray(relationships) ? relationships : [relationships];
49
+ for (const rel of rels) {
50
+ if (rel["@_Id"] === rId) {
51
+ return rel["@_Target"];
52
+ }
53
+ }
54
+ /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
55
+ return undefined;
56
+ }
57
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
58
+ /**
59
+ * bgPr 要素から背景情報を抽出する
60
+ */
61
+ async function extractBackgroundFromBgPr(
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ bgPr, zip, relsPath, basePath) {
64
+ if (!bgPr)
65
+ return undefined;
66
+ // 単色塗りつぶし
67
+ const solidFill = bgPr["a:solidFill"];
68
+ if (solidFill) {
69
+ const srgbClr = solidFill["a:srgbClr"];
70
+ if (srgbClr) {
71
+ const color = srgbClr["@_val"] ?? undefined;
72
+ if (color)
73
+ return { color };
74
+ }
75
+ }
76
+ // 画像背景
77
+ const blipFill = bgPr["a:blipFill"];
78
+ if (blipFill) {
79
+ const blip = blipFill["a:blip"];
80
+ const rId = blip?.["@_r:embed"];
81
+ if (!rId)
82
+ return undefined;
83
+ const relsFile = zip.file(relsPath);
84
+ if (!relsFile)
85
+ return undefined;
86
+ const relsXml = await relsFile.async("text");
87
+ const target = resolveRelId(relsXml, rId);
88
+ if (!target)
89
+ return undefined;
90
+ // target は相対パスなので basePath からの相対パスとして解決
91
+ const imagePath = new URL(target, `file:///${basePath}dummy`).pathname.slice(1);
92
+ const imageFile = zip.file(imagePath);
93
+ if (!imageFile)
94
+ return undefined;
95
+ const imageData = await imageFile.async("base64");
96
+ const mimeType = mimeTypeFromExt(imagePath);
97
+ return { data: `data:${mimeType};base64,${imageData}` };
98
+ }
99
+ return undefined;
100
+ }
101
+ /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
102
+ /**
103
+ * マスター PPTX のバッファからスライドマスターの背景情報を抽出する。
104
+ *
105
+ * 探索順序:
106
+ * 1. `ppt/slideMasters/slideMaster1.xml` の `p:bg > p:bgPr`
107
+ * 2. 各スライドレイアウト (`ppt/slideLayouts/slideLayoutN.xml`) の `p:bg > p:bgPr`
108
+ *
109
+ * サポートする背景:
110
+ * - 単色塗りつぶし (`a:solidFill` / `a:srgbClr`)
111
+ * - 画像背景 (`a:blipFill` / `a:blip`)
112
+ *
113
+ * @returns 背景情報。背景が設定されていない場合は `undefined`。
114
+ */
115
+ export async function parseMasterPptx(pptxBuffer) {
116
+ const JSZip = await loadJSZip();
117
+ const zip = await JSZip.loadAsync(pptxBuffer);
118
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
119
+ // 1. スライドマスター本体を探索
120
+ const masterFile = zip.file("ppt/slideMasters/slideMaster1.xml");
121
+ if (masterFile) {
122
+ const masterXml = await masterFile.async("text");
123
+ const parsed = xmlParser.parse(masterXml);
124
+ const bgPr = parsed?.["p:sldMaster"]?.["p:cSld"]?.["p:bg"]?.["p:bgPr"];
125
+ const result = await extractBackgroundFromBgPr(bgPr, zip, "ppt/slideMasters/_rels/slideMaster1.xml.rels", "ppt/slideMasters/");
126
+ if (result)
127
+ return result;
128
+ }
129
+ // 2. スライドレイアウトを探索
130
+ const layoutFiles = Object.keys(zip.files).filter((f) => f.startsWith("ppt/slideLayouts/slideLayout") && f.endsWith(".xml"));
131
+ // 番号順にソート(数値ソートで slideLayout2 < slideLayout10 の順序を保証)
132
+ layoutFiles.sort((a, b) => {
133
+ const numA = parseInt(a.match(/slideLayout(\d+)\.xml$/)?.[1] ?? "0", 10);
134
+ const numB = parseInt(b.match(/slideLayout(\d+)\.xml$/)?.[1] ?? "0", 10);
135
+ return numA - numB;
136
+ });
137
+ for (const layoutPath of layoutFiles) {
138
+ const layoutFile = zip.file(layoutPath);
139
+ if (!layoutFile)
140
+ continue;
141
+ const layoutXml = await layoutFile.async("text");
142
+ const parsed = xmlParser.parse(layoutXml);
143
+ const bgPr = parsed?.["p:sldLayout"]?.["p:cSld"]?.["p:bg"]?.["p:bgPr"];
144
+ const fileName = layoutPath.split("/").pop();
145
+ const relsPath = `ppt/slideLayouts/_rels/${fileName}.rels`;
146
+ const result = await extractBackgroundFromBgPr(bgPr, zip, relsPath, "ppt/slideLayouts/");
147
+ if (result)
148
+ return result;
149
+ }
150
+ /* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
151
+ return undefined;
152
+ }
@@ -442,10 +442,10 @@ export const CHILD_ELEMENT_COERCION_MAP = {
442
442
  label: "string",
443
443
  color: "string",
444
444
  },
445
- TableColumn: {
445
+ Col: {
446
446
  width: "number",
447
447
  },
448
- TableCell: {
448
+ Td: {
449
449
  text: "string",
450
450
  fontSize: "number",
451
451
  color: "string",
@@ -546,19 +546,19 @@ function convertTableChildren(childElements, result, errors) {
546
546
  for (const child of childElements) {
547
547
  const tag = getTagName(child);
548
548
  switch (tag) {
549
- case "TableColumn":
549
+ case "Col":
550
550
  columns.push(coerceChildAttrs("Table", tag, getAttributes(child), errors));
551
551
  break;
552
- case "TableRow": {
552
+ case "Tr": {
553
553
  const rowAttrs = getAttributes(child);
554
554
  const cells = [];
555
555
  for (const cellEl of getChildElements(child)) {
556
556
  const cellTag = getTagName(cellEl);
557
- if (cellTag !== "TableCell") {
558
- errors.push(`Unknown child element <${cellTag}> inside <TableRow>. Expected: <TableCell>`);
557
+ if (cellTag !== "Td") {
558
+ errors.push(`Unknown child element <${cellTag}> inside <Tr>. Expected: <Td>`);
559
559
  continue;
560
560
  }
561
- const cellAttrs = coerceChildAttrs("TableRow", cellTag, getAttributes(cellEl), errors);
561
+ const cellAttrs = coerceChildAttrs("Tr", cellTag, getAttributes(cellEl), errors);
562
562
  const runsResult = buildRunsAndText(cellEl);
563
563
  if (runsResult) {
564
564
  cellAttrs.runs = runsResult.runs;
@@ -576,7 +576,7 @@ function convertTableChildren(childElements, result, errors) {
576
576
  if (rowAttrs.height !== undefined) {
577
577
  const h = Number(rowAttrs.height);
578
578
  if (isNaN(h)) {
579
- errors.push(`Cannot convert "${rowAttrs.height}" to number in <TableRow> "height" attribute`);
579
+ errors.push(`Cannot convert "${rowAttrs.height}" to number in <Tr> "height" attribute`);
580
580
  }
581
581
  else {
582
582
  row.height = h;
@@ -586,14 +586,14 @@ function convertTableChildren(childElements, result, errors) {
586
586
  break;
587
587
  }
588
588
  default:
589
- errors.push(`Unknown child element <${tag}> inside <Table>. Expected: <TableColumn> or <TableRow>`);
589
+ errors.push(`Unknown child element <${tag}> inside <Table>. Expected: <Col> or <Tr>`);
590
590
  }
591
591
  }
592
592
  if (columns.length > 0) {
593
593
  result.columns = columns;
594
594
  }
595
595
  else if (rows.length > 0) {
596
- // TableColumn が未指定の場合、行のセル数(colspan 考慮)からデフォルトの columns を自動生成
596
+ // Col が未指定の場合、行のセル数(colspan 考慮)からデフォルトの columns を自動生成
597
597
  const maxCells = Math.max(...rows.map((row) => row.cells.reduce((sum, cell) => sum + (cell.colspan ?? 1), 0)));
598
598
  result.columns = Array.from({ length: maxCells }, () => ({}));
599
599
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirokisakabe/pom",
3
- "version": "6.4.0",
3
+ "version": "7.1.0",
4
4
  "description": "AI-friendly PowerPoint generation with a Flexbox layout engine.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,6 +62,7 @@
62
62
  "@resvg/resvg-wasm": "^2.6.2",
63
63
  "fast-xml-parser": "^5.3.7",
64
64
  "image-size": "2.0.2",
65
+ "jszip": "^3.10.1",
65
66
  "opentype.js": "^1.3.4",
66
67
  "pptxgenjs": "4.0.1",
67
68
  "yoga-layout": "3.2.1",