@hirokisakabe/pom 9.1.0 → 9.1.1

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.
@@ -1,6 +1,7 @@
1
1
  import { GradientFillRegistry } from "./renderPptx/gradientFills.js";
2
2
  import { DiagnosticCollector } from "./diagnostics.js";
3
3
  import { GlowEffectRegistry } from "./renderPptx/glowEffects.js";
4
+ import { GlimpseTextBoxRegistry } from "./renderPptx/glimpseTextBoxes.js";
4
5
  //#region src/buildContext.ts
5
6
  function createBuildContext(textMeasurementMode = "auto") {
6
7
  return {
@@ -10,7 +11,8 @@ function createBuildContext(textMeasurementMode = "auto") {
10
11
  iconRasterCache: /* @__PURE__ */ new Map(),
11
12
  diagnostics: new DiagnosticCollector(),
12
13
  gradientFills: new GradientFillRegistry(),
13
- glowEffects: new GlowEffectRegistry()
14
+ glowEffects: new GlowEffectRegistry(),
15
+ glimpseTextBoxes: new GlimpseTextBoxRegistry()
14
16
  };
15
17
  }
16
18
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"buildContext.js","names":[],"sources":["../src/buildContext.ts"],"sourcesContent":["import type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport { DiagnosticCollector } from \"./diagnostics.ts\";\nimport { GlowEffectRegistry } from \"./renderPptx/glowEffects.ts\";\nimport { GradientFillRegistry } from \"./renderPptx/gradientFills.ts\";\n\nexport interface BuildContext {\n textMeasurementMode: TextMeasurementMode;\n imageSizeCache: Map<string, { widthPx: number; heightPx: number }>;\n imageDataCache: Map<string, string>;\n iconRasterCache: Map<string, string>;\n diagnostics: DiagnosticCollector;\n gradientFills: GradientFillRegistry;\n glowEffects: GlowEffectRegistry;\n}\n\nexport function createBuildContext(\n textMeasurementMode: TextMeasurementMode = \"auto\",\n): BuildContext {\n return {\n textMeasurementMode,\n imageSizeCache: new Map(),\n imageDataCache: new Map(),\n iconRasterCache: new Map(),\n diagnostics: new DiagnosticCollector(),\n gradientFills: new GradientFillRegistry(),\n glowEffects: new GlowEffectRegistry(),\n };\n}\n"],"mappings":";;;;AAeA,SAAgB,mBACd,sBAA2C,QAC7B;CACd,OAAO;EACL;EACA,gCAAgB,IAAI,IAAI;EACxB,gCAAgB,IAAI,IAAI;EACxB,iCAAiB,IAAI,IAAI;EACzB,aAAa,IAAI,oBAAoB;EACrC,eAAe,IAAI,qBAAqB;EACxC,aAAa,IAAI,mBAAmB;CACtC;AACF"}
1
+ {"version":3,"file":"buildContext.js","names":[],"sources":["../src/buildContext.ts"],"sourcesContent":["import type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport { DiagnosticCollector } from \"./diagnostics.ts\";\nimport { GlowEffectRegistry } from \"./renderPptx/glowEffects.ts\";\nimport { GradientFillRegistry } from \"./renderPptx/gradientFills.ts\";\nimport { GlimpseTextBoxRegistry } from \"./renderPptx/glimpseTextBoxes.ts\";\n\nexport interface BuildContext {\n textMeasurementMode: TextMeasurementMode;\n imageSizeCache: Map<string, { widthPx: number; heightPx: number }>;\n imageDataCache: Map<string, string>;\n iconRasterCache: Map<string, string>;\n diagnostics: DiagnosticCollector;\n gradientFills: GradientFillRegistry;\n glowEffects: GlowEffectRegistry;\n glimpseTextBoxes: GlimpseTextBoxRegistry;\n}\n\nexport function createBuildContext(\n textMeasurementMode: TextMeasurementMode = \"auto\",\n): BuildContext {\n return {\n textMeasurementMode,\n imageSizeCache: new Map(),\n imageDataCache: new Map(),\n iconRasterCache: new Map(),\n diagnostics: new DiagnosticCollector(),\n gradientFills: new GradientFillRegistry(),\n glowEffects: new GlowEffectRegistry(),\n glimpseTextBoxes: new GlimpseTextBoxRegistry(),\n };\n}\n"],"mappings":";;;;;AAiBA,SAAgB,mBACd,sBAA2C,QAC7B;CACd,OAAO;EACL;EACA,gCAAgB,IAAI,IAAI;EACxB,gCAAgB,IAAI,IAAI;EACxB,iCAAiB,IAAI,IAAI;EACzB,aAAa,IAAI,oBAAoB;EACrC,eAAe,IAAI,qBAAqB;EACxC,aAAa,IAAI,mBAAmB;EACpC,kBAAkB,IAAI,uBAAuB;CAC/C;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"buildPptx.d.ts","names":[],"sources":["../src/buildPptx.ts"],"mappings":";;;;;UAoBiB,eAAA;EACf,IAAA,sBAA0B,OAAA;EAC1B,WAAA,EAAa,UAAU;AAAA;AAAA,iBAGH,SAAA,CACpB,GAAA,UACA,SAAA;EAAa,CAAA;EAAW,CAAA;AAAA,GACxB,OAAA;EACE,MAAA,GAAS,kBAAA;EACT,UAAA,GAAa,WAAA,GAAc,UAAA;EAC3B,eAAA,GAAkB,mBAAA;EAClB,OAAA;EACA,MAAA;AAAA,IAED,OAAA,CAAQ,eAAA"}
1
+ {"version":3,"file":"buildPptx.d.ts","names":[],"sources":["../src/buildPptx.ts"],"mappings":";;;;;UAqBiB,eAAA;EACf,IAAA,sBAA0B,OAAA;EAC1B,WAAA,EAAa,UAAU;AAAA;AAAA,iBAGH,SAAA,CACpB,GAAA,UACA,SAAA;EAAa,CAAA;EAAW,CAAA;AAAA,GACxB,OAAA;EACE,MAAA,GAAS,kBAAA;EACT,UAAA,GAAa,WAAA,GAAc,UAAA;EAC3B,eAAA,GAAkB,mBAAA;EAClB,OAAA;EACA,MAAA;AAAA,IAED,OAAA,CAAQ,eAAA"}
package/dist/buildPptx.js CHANGED
@@ -4,6 +4,7 @@ import { calcYogaLayout } from "./calcYogaLayout/calcYogaLayout.js";
4
4
  import { autoFitSlide } from "./autoFit/autoFit.js";
5
5
  import { DiagnosticsError } from "./diagnostics.js";
6
6
  import { patchPptxWriteForGlowEffects } from "./renderPptx/glowEffects.js";
7
+ import { patchPptxWriteForGlimpseTextBoxes } from "./renderPptx/glimpseTextBoxes.js";
7
8
  import { createBuildContext } from "./buildContext.js";
8
9
  import { extractLayoutResults } from "./calcYogaLayout/types.js";
9
10
  import { parseMasterPptx } from "./parseMasterPptx.js";
@@ -45,6 +46,7 @@ async function buildPptx(xml, slideSize, options) {
45
46
  }
46
47
  const pptx = await renderPptx(positionedPages, slideSize, ctx, master);
47
48
  patchPptxWriteForGradientFills(pptx, ctx.gradientFills);
49
+ patchPptxWriteForGlimpseTextBoxes(pptx, ctx.glimpseTextBoxes);
48
50
  patchPptxWriteForGlowEffects(pptx, ctx.glowEffects);
49
51
  const diagnostics = ctx.diagnostics.items;
50
52
  if (options?.strict && diagnostics.length > 0) throw new DiagnosticsError(diagnostics);
@@ -1 +1 @@
1
- {"version":3,"file":"buildPptx.js","names":[],"sources":["../src/buildPptx.ts"],"sourcesContent":["import { autoFitSlide } from \"./autoFit/autoFit.ts\";\nimport { createBuildContext } from \"./buildContext.ts\";\nimport { calcYogaLayout } from \"./calcYogaLayout/calcYogaLayout.ts\";\nimport type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport type { YogaNodeMap } from \"./calcYogaLayout/types.ts\";\nimport { extractLayoutResults } from \"./calcYogaLayout/types.ts\";\nimport type { Diagnostic } from \"./diagnostics.ts\";\nimport { DiagnosticsError } from \"./diagnostics.ts\";\nimport { parseMasterPptx } from \"./parseMasterPptx.ts\";\nimport { parseXml } from \"./parseXml/parseXml.ts\";\nimport { patchPptxWriteForGlowEffects } from \"./renderPptx/glowEffects.ts\";\nimport { patchPptxWriteForGradientFills } from \"./renderPptx/gradientFills.ts\";\nimport { renderPptx } from \"./renderPptx/renderPptx.ts\";\nimport { freeYogaTree } from \"./shared/freeYogaTree.ts\";\nimport { toPositioned } from \"./toPositioned/toPositioned.ts\";\nimport { PositionedNode, SlideMasterOptions } from \"./types.ts\";\nimport { validatePositioned } from \"./validatePositioned/validatePositioned.ts\";\n\nexport type { TextMeasurementMode };\n\nexport interface BuildPptxResult {\n pptx: import(\"pptxgenjs\").default;\n diagnostics: Diagnostic[];\n}\n\nexport async function buildPptx(\n xml: string,\n slideSize: { w: number; h: number },\n options?: {\n master?: SlideMasterOptions;\n masterPptx?: ArrayBuffer | Uint8Array;\n textMeasurement?: TextMeasurementMode;\n autoFit?: boolean;\n strict?: boolean;\n },\n): Promise<BuildPptxResult> {\n const ctx = createBuildContext(options?.textMeasurement ?? \"auto\");\n\n // グラデーション後処理のマーカー色がユーザー指定色と衝突しないよう、\n // 入力 XML / master オプション中に現れる色を予約しておく\n ctx.gradientFills.reserveColors(xml);\n if (options?.master) {\n ctx.gradientFills.reserveColors(JSON.stringify(options.master));\n }\n\n const nodes = parseXml(xml);\n const positionedPages: PositionedNode[] = [];\n\n for (const [slideIndex, node] of nodes.entries()) {\n let map: YogaNodeMap | undefined;\n try {\n if (options?.autoFit !== false) {\n map = await autoFitSlide(node, slideSize, ctx);\n } else {\n map = await calcYogaLayout(node, slideSize, ctx);\n }\n const layoutMap = extractLayoutResults(map);\n const positioned = await toPositioned(node, ctx, layoutMap);\n validatePositioned(positioned, slideSize, ctx, slideIndex);\n positionedPages.push(positioned);\n } finally {\n if (map) freeYogaTree(map);\n }\n }\n\n // masterPptx から背景を抽出し、master オプションにマージ\n let master = options?.master;\n if (options?.masterPptx) {\n try {\n const bg = await parseMasterPptx(options.masterPptx);\n if (bg) {\n if (master) {\n // 明示的に background が指定されていない場合のみ、masterPptx の背景を使用\n if (!master.background) {\n master = { ...master, background: bg };\n }\n } else {\n master = { background: bg };\n }\n }\n } catch (e) {\n const message =\n e instanceof Error ? e.message : \"Unknown error parsing masterPptx\";\n ctx.diagnostics.add(\"MASTER_PPTX_PARSE_FAILED\", message);\n }\n }\n\n const pptx = await renderPptx(positionedPages, slideSize, ctx, master);\n\n // backgroundGradient / textGradient 使用時は write/writeFile に gradFill 置換の後処理を仕込む\n patchPptxWriteForGradientFills(pptx, ctx.gradientFills);\n\n // Shape / Icon の glow 指定がある場合は write/writeFile に effectLst 挿入の\n // 後処理を仕込む (gradientFills の patch 後にチェーンする)\n patchPptxWriteForGlowEffects(pptx, ctx.glowEffects);\n\n const diagnostics = ctx.diagnostics.items;\n\n if (options?.strict && diagnostics.length > 0) {\n throw new DiagnosticsError(diagnostics);\n }\n\n return { pptx, diagnostics };\n}\n"],"mappings":";;;;;;;;;;;;;;AAyBA,eAAsB,UACpB,KACA,WACA,SAO0B;CAC1B,MAAM,MAAM,mBAAmB,SAAS,mBAAmB,MAAM;CAIjE,IAAI,cAAc,cAAc,GAAG;CACnC,IAAI,SAAS,QACX,IAAI,cAAc,cAAc,KAAK,UAAU,QAAQ,MAAM,CAAC;CAGhE,MAAM,QAAQ,SAAS,GAAG;CAC1B,MAAM,kBAAoC,CAAC;CAE3C,KAAK,MAAM,CAAC,YAAY,SAAS,MAAM,QAAQ,GAAG;EAChD,IAAI;EACJ,IAAI;GACF,IAAI,SAAS,YAAY,OACvB,MAAM,MAAM,aAAa,MAAM,WAAW,GAAG;QAE7C,MAAM,MAAM,eAAe,MAAM,WAAW,GAAG;GAGjD,MAAM,aAAa,MAAM,aAAa,MAAM,KAD1B,qBAAqB,GACkB,CAAC;GAC1D,mBAAmB,YAAY,WAAW,KAAK,UAAU;GACzD,gBAAgB,KAAK,UAAU;EACjC,UAAU;GACR,IAAI,KAAK,aAAa,GAAG;EAC3B;CACF;CAGA,IAAI,SAAS,SAAS;CACtB,IAAI,SAAS,YACX,IAAI;EACF,MAAM,KAAK,MAAM,gBAAgB,QAAQ,UAAU;EACnD,IAAI,IACF,IAAI;OAEE,CAAC,OAAO,YACV,SAAS;IAAE,GAAG;IAAQ,YAAY;GAAG;EAAA,OAGvC,SAAS,EAAE,YAAY,GAAG;CAGhC,SAAS,GAAG;EACV,MAAM,UACJ,aAAa,QAAQ,EAAE,UAAU;EACnC,IAAI,YAAY,IAAI,4BAA4B,OAAO;CACzD;CAGF,MAAM,OAAO,MAAM,WAAW,iBAAiB,WAAW,KAAK,MAAM;CAGrE,+BAA+B,MAAM,IAAI,aAAa;CAItD,6BAA6B,MAAM,IAAI,WAAW;CAElD,MAAM,cAAc,IAAI,YAAY;CAEpC,IAAI,SAAS,UAAU,YAAY,SAAS,GAC1C,MAAM,IAAI,iBAAiB,WAAW;CAGxC,OAAO;EAAE;EAAM;CAAY;AAC7B"}
1
+ {"version":3,"file":"buildPptx.js","names":[],"sources":["../src/buildPptx.ts"],"sourcesContent":["import { autoFitSlide } from \"./autoFit/autoFit.ts\";\nimport { createBuildContext } from \"./buildContext.ts\";\nimport { calcYogaLayout } from \"./calcYogaLayout/calcYogaLayout.ts\";\nimport type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport type { YogaNodeMap } from \"./calcYogaLayout/types.ts\";\nimport { extractLayoutResults } from \"./calcYogaLayout/types.ts\";\nimport type { Diagnostic } from \"./diagnostics.ts\";\nimport { DiagnosticsError } from \"./diagnostics.ts\";\nimport { parseMasterPptx } from \"./parseMasterPptx.ts\";\nimport { parseXml } from \"./parseXml/parseXml.ts\";\nimport { patchPptxWriteForGlowEffects } from \"./renderPptx/glowEffects.ts\";\nimport { patchPptxWriteForGlimpseTextBoxes } from \"./renderPptx/glimpseTextBoxes.ts\";\nimport { patchPptxWriteForGradientFills } from \"./renderPptx/gradientFills.ts\";\nimport { renderPptx } from \"./renderPptx/renderPptx.ts\";\nimport { freeYogaTree } from \"./shared/freeYogaTree.ts\";\nimport { toPositioned } from \"./toPositioned/toPositioned.ts\";\nimport { PositionedNode, SlideMasterOptions } from \"./types.ts\";\nimport { validatePositioned } from \"./validatePositioned/validatePositioned.ts\";\n\nexport type { TextMeasurementMode };\n\nexport interface BuildPptxResult {\n pptx: import(\"pptxgenjs\").default;\n diagnostics: Diagnostic[];\n}\n\nexport async function buildPptx(\n xml: string,\n slideSize: { w: number; h: number },\n options?: {\n master?: SlideMasterOptions;\n masterPptx?: ArrayBuffer | Uint8Array;\n textMeasurement?: TextMeasurementMode;\n autoFit?: boolean;\n strict?: boolean;\n },\n): Promise<BuildPptxResult> {\n const ctx = createBuildContext(options?.textMeasurement ?? \"auto\");\n\n // グラデーション後処理のマーカー色がユーザー指定色と衝突しないよう、\n // 入力 XML / master オプション中に現れる色を予約しておく\n ctx.gradientFills.reserveColors(xml);\n if (options?.master) {\n ctx.gradientFills.reserveColors(JSON.stringify(options.master));\n }\n\n const nodes = parseXml(xml);\n const positionedPages: PositionedNode[] = [];\n\n for (const [slideIndex, node] of nodes.entries()) {\n let map: YogaNodeMap | undefined;\n try {\n if (options?.autoFit !== false) {\n map = await autoFitSlide(node, slideSize, ctx);\n } else {\n map = await calcYogaLayout(node, slideSize, ctx);\n }\n const layoutMap = extractLayoutResults(map);\n const positioned = await toPositioned(node, ctx, layoutMap);\n validatePositioned(positioned, slideSize, ctx, slideIndex);\n positionedPages.push(positioned);\n } finally {\n if (map) freeYogaTree(map);\n }\n }\n\n // masterPptx から背景を抽出し、master オプションにマージ\n let master = options?.master;\n if (options?.masterPptx) {\n try {\n const bg = await parseMasterPptx(options.masterPptx);\n if (bg) {\n if (master) {\n // 明示的に background が指定されていない場合のみ、masterPptx の背景を使用\n if (!master.background) {\n master = { ...master, background: bg };\n }\n } else {\n master = { background: bg };\n }\n }\n } catch (e) {\n const message =\n e instanceof Error ? e.message : \"Unknown error parsing masterPptx\";\n ctx.diagnostics.add(\"MASTER_PPTX_PARSE_FAILED\", message);\n }\n }\n\n const pptx = await renderPptx(positionedPages, slideSize, ctx, master);\n\n // backgroundGradient 使用時は write/writeFile に gradFill 置換の後処理を仕込む\n patchPptxWriteForGradientFills(pptx, ctx.gradientFills);\n\n // Text primitive は glimpse writer の text box XML で marker shape を置換する\n patchPptxWriteForGlimpseTextBoxes(pptx, ctx.glimpseTextBoxes);\n\n // Shape / Icon の glow 指定がある場合は write/writeFile に effectLst 挿入の\n // 後処理を仕込む (gradientFills の patch 後にチェーンする)\n patchPptxWriteForGlowEffects(pptx, ctx.glowEffects);\n\n const diagnostics = ctx.diagnostics.items;\n\n if (options?.strict && diagnostics.length > 0) {\n throw new DiagnosticsError(diagnostics);\n }\n\n return { pptx, diagnostics };\n}\n"],"mappings":";;;;;;;;;;;;;;;AA0BA,eAAsB,UACpB,KACA,WACA,SAO0B;CAC1B,MAAM,MAAM,mBAAmB,SAAS,mBAAmB,MAAM;CAIjE,IAAI,cAAc,cAAc,GAAG;CACnC,IAAI,SAAS,QACX,IAAI,cAAc,cAAc,KAAK,UAAU,QAAQ,MAAM,CAAC;CAGhE,MAAM,QAAQ,SAAS,GAAG;CAC1B,MAAM,kBAAoC,CAAC;CAE3C,KAAK,MAAM,CAAC,YAAY,SAAS,MAAM,QAAQ,GAAG;EAChD,IAAI;EACJ,IAAI;GACF,IAAI,SAAS,YAAY,OACvB,MAAM,MAAM,aAAa,MAAM,WAAW,GAAG;QAE7C,MAAM,MAAM,eAAe,MAAM,WAAW,GAAG;GAGjD,MAAM,aAAa,MAAM,aAAa,MAAM,KAD1B,qBAAqB,GACkB,CAAC;GAC1D,mBAAmB,YAAY,WAAW,KAAK,UAAU;GACzD,gBAAgB,KAAK,UAAU;EACjC,UAAU;GACR,IAAI,KAAK,aAAa,GAAG;EAC3B;CACF;CAGA,IAAI,SAAS,SAAS;CACtB,IAAI,SAAS,YACX,IAAI;EACF,MAAM,KAAK,MAAM,gBAAgB,QAAQ,UAAU;EACnD,IAAI,IACF,IAAI;OAEE,CAAC,OAAO,YACV,SAAS;IAAE,GAAG;IAAQ,YAAY;GAAG;EAAA,OAGvC,SAAS,EAAE,YAAY,GAAG;CAGhC,SAAS,GAAG;EACV,MAAM,UACJ,aAAa,QAAQ,EAAE,UAAU;EACnC,IAAI,YAAY,IAAI,4BAA4B,OAAO;CACzD;CAGF,MAAM,OAAO,MAAM,WAAW,iBAAiB,WAAW,KAAK,MAAM;CAGrE,+BAA+B,MAAM,IAAI,aAAa;CAGtD,kCAAkC,MAAM,IAAI,gBAAgB;CAI5D,6BAA6B,MAAM,IAAI,WAAW;CAElD,MAAM,cAAc,IAAI,YAAY;CAEpC,IAAI,SAAS,UAAU,YAAY,SAAS,GAC1C,MAAM,IAAI,iBAAiB,WAAW;CAGxC,OAAO;EAAE;EAAM;CAAY;AAC7B"}
@@ -1,6 +1,6 @@
1
1
  import { getMasterLayoutPartPaths, getPresentationMasterPartPaths, isVisibleLayout, toUint8Array } from "./pptxLayoutEnumeration.js";
2
- import { XMLBuilder, XMLParser } from "fast-xml-parser";
3
2
  import { readPptx } from "@pptx-glimpse/document";
3
+ import { XMLBuilder, XMLParser } from "fast-xml-parser";
4
4
  //#region src/extractSlideMastersAsPptx.ts
5
5
  const SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
6
6
  const SLIDE_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
@@ -0,0 +1,347 @@
1
+ import { EMU_PER_IN, pxToEmu, pxToPt } from "./units.js";
2
+ import { createTextOptions, resolveSubSup } from "./textOptions.js";
3
+ import { parseLinearGradient } from "../shared/gradient.js";
4
+ import { addTextBox, asEmu, asHundredthPt, asOoxmlAngle, asOoxmlPercent, asPt, createPptx } from "@pptx-glimpse/document";
5
+ //#region src/renderPptx/glimpseTextBoxes.ts
6
+ /**
7
+ * Text primitive の @pptx-glimpse/document writer への段階的 swap。
8
+ *
9
+ * 混在期間の合成方式は「pptxgenjs 出力 zip をベースに、swap 済み Text
10
+ * primitive の shape XML だけを glimpse writer で生成して該当 marker shape と
11
+ * 差し替える」方式を採用する。pptxgenjs 側にはまだ Shape / Image / Table /
12
+ * Chart など未 swap primitive と slide master 生成が残っているため、既存 zip を
13
+ * ベースにすると [Content_Types].xml / rels / media parts の管理を現行実装へ
14
+ * 寄せられる。代替案として glimpse の package をベースに未 swap primitive を
15
+ * pptxgenjs から取り込む方式も検討したが、初回スライス時点では pptxgenjs 側の
16
+ * 非 text primitive と master 出力を XML part 単位で切り出す責務が増え、以降の
17
+ * primitive swap より先に package 合成の複雑さが大きくなるため採用しない。
18
+ *
19
+ * marker shape は描画順を保持するためだけに pptxgenjs へ追加し、write 時に
20
+ * glimpse の `<p:sp>` で丸ごと置換する。Text content 自体は pptxgenjs `addText`
21
+ * を経由しない。
22
+ */
23
+ const MARKER_PREFIX = "pom-text:";
24
+ const HYPERLINK_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
25
+ async function loadJSZip() {
26
+ const mod = await import("jszip");
27
+ return mod.default ?? mod;
28
+ }
29
+ function cleanHex(color) {
30
+ return color?.replace(/^#/, "").toUpperCase();
31
+ }
32
+ function toColorInput(color) {
33
+ const hex = cleanHex(color);
34
+ return hex ? {
35
+ kind: "srgb",
36
+ hex
37
+ } : void 0;
38
+ }
39
+ function toUnderlineInput(underline) {
40
+ if (underline === void 0 || underline === false) return void 0;
41
+ if (underline === true) return true;
42
+ return {
43
+ style: underline.style,
44
+ color: toColorInput(underline.color)
45
+ };
46
+ }
47
+ function toBaselineInput(subscript, superscript) {
48
+ if (subscript) return "subscript";
49
+ if (superscript) return "superscript";
50
+ }
51
+ function toGlowInput(glow) {
52
+ if (!glow) return void 0;
53
+ return {
54
+ radius: asEmu(Math.round(pxToEmu(glow.size ?? 8))),
55
+ color: toColorInput(glow.color ?? "FFFFFF")
56
+ };
57
+ }
58
+ function toOutlineInput(outline) {
59
+ if (!outline) return void 0;
60
+ return {
61
+ width: asEmu(Math.round(pxToEmu(outline.size ?? 1))),
62
+ color: toColorInput(outline.color ?? "FFFFFF")
63
+ };
64
+ }
65
+ function toCharSpacing(letterSpacingPx) {
66
+ if (letterSpacingPx === void 0) return void 0;
67
+ return Math.round(pxToPt(letterSpacingPx) * 100);
68
+ }
69
+ function toTextGradientInput(value) {
70
+ if (!value) return void 0;
71
+ const linear = parseLinearGradient(value);
72
+ if (!linear) return void 0;
73
+ const dmlAngle = ((linear.angle - 90) % 360 + 360) % 360;
74
+ return {
75
+ angle: asOoxmlAngle(Math.round(dmlAngle * 6e4)),
76
+ stops: linear.stops.map((stop) => ({
77
+ position: asOoxmlPercent(Math.round(stop.position * 1e3)),
78
+ color: toColorInput(stop.color)
79
+ }))
80
+ };
81
+ }
82
+ function stripUndefined(value) {
83
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
84
+ }
85
+ function buildRunProperties(node, run, gradientFill) {
86
+ const fontSizePx = run?.fontSize ?? node.fontSize ?? 24;
87
+ const letterSpacingPx = run?.letterSpacing ?? node.letterSpacing;
88
+ const subSup = run ? resolveSubSup(run, node) : node;
89
+ const color = gradientFill ? void 0 : toColorInput(run?.color ?? node.color);
90
+ return stripUndefined({
91
+ fontFace: run?.fontFamily ?? node.fontFamily ?? "Noto Sans JP",
92
+ fontSize: asPt(pxToPt(fontSizePx)),
93
+ color,
94
+ gradientFill,
95
+ bold: run?.bold ?? node.bold,
96
+ italic: run?.italic ?? node.italic,
97
+ underline: toUnderlineInput(resolveUnderline(node, run)),
98
+ strike: run?.strike ?? node.strike,
99
+ baseline: toBaselineInput(subSup.subscript, subSup.superscript),
100
+ highlight: toColorInput(run?.highlight ?? node.highlight),
101
+ glow: toGlowInput(node.glow),
102
+ outline: toOutlineInput(node.outline),
103
+ charSpacing: toCharSpacing(letterSpacingPx)
104
+ });
105
+ }
106
+ function resolveUnderline(node, run) {
107
+ if (run?.underline !== void 0) return run.underline;
108
+ if (node.underline !== void 0) return node.underline;
109
+ return run?.href ? true : void 0;
110
+ }
111
+ function createParagraphProperties(node) {
112
+ const lineHeight = node.lineHeight ?? 1.3;
113
+ const fontSizePx = node.fontSize ?? 24;
114
+ return stripUndefined({
115
+ align: node.textAlign,
116
+ lineSpacing: asHundredthPt(Math.round(pxToPt(fontSizePx * lineHeight) * 100))
117
+ });
118
+ }
119
+ function buildParagraphs(node) {
120
+ const gradientFill = toTextGradientInput(node.textGradient);
121
+ const sourceRuns = node.runs && node.runs.length > 0 ? node.runs.map((run) => ({
122
+ text: run.text,
123
+ properties: buildRunProperties(node, run, gradientFill),
124
+ href: run.href
125
+ })) : [{
126
+ text: node.text ?? "",
127
+ properties: buildRunProperties(node, void 0, gradientFill)
128
+ }];
129
+ const paragraphRuns = [[]];
130
+ for (const run of sourceRuns) run.text.replace(/\r*\n/g, "\n").split("\n").forEach((line, index) => {
131
+ if (index > 0) paragraphRuns.push([]);
132
+ paragraphRuns[paragraphRuns.length - 1]?.push({
133
+ ...run,
134
+ text: line
135
+ });
136
+ });
137
+ return {
138
+ paragraphs: paragraphRuns.map((runs) => ({
139
+ properties: createParagraphProperties(node),
140
+ runs
141
+ })),
142
+ hyperlinks: paragraphRuns.flatMap((runs) => runs.map((run) => run.text ? run.href : void 0))
143
+ };
144
+ }
145
+ function withGlowAlpha(xml, node) {
146
+ const glow = node.glow;
147
+ if (!glow) return xml;
148
+ const alpha = Math.round((glow.opacity ?? .75) * 1e5);
149
+ const color = cleanHex(glow.color ?? "FFFFFF");
150
+ const target = `<a:glow rad="${Math.round(pxToEmu(glow.size ?? 8))}"><a:srgbClr val="${color}"/></a:glow>`;
151
+ const replacement = `<a:glow rad="${Math.round(pxToEmu(glow.size ?? 8))}"><a:srgbClr val="${color}"><a:alpha val="${alpha}"/></a:srgbClr></a:glow>`;
152
+ return xml.replaceAll(target, replacement);
153
+ }
154
+ function withPptxGenParagraphDefaults(xml) {
155
+ let result = xml.replaceAll("baseline=\"-25000\"", "baseline=\"-40000\"");
156
+ result = result.replace(/<a:bodyPr\b([^>]*)\/>/g, (_match, attrs) => {
157
+ const attrText = attrs;
158
+ const withRtl = /(?:^|\s)rtlCol=/.test(attrText) ? attrText : `${attrText} rtlCol="0"`;
159
+ return `<a:bodyPr${/(?:^|\s)anchor=/.test(withRtl) ? withRtl : `${withRtl} anchor="t"`}/>`;
160
+ });
161
+ result = result.replace(/<a:pPr([^>]*)>([\s\S]*?)<\/a:pPr>/g, (match, attrs, body) => {
162
+ const attrText = attrs;
163
+ const nextAttrs = /(?:^|\s)indent=/.test(attrText) ? attrText : `${attrText} indent="0" marL="0"`;
164
+ const bodyText = body;
165
+ return `<a:pPr${nextAttrs}>${bodyText.includes("<a:buNone/>") ? bodyText : `${bodyText}<a:buNone/>`}</a:pPr>`;
166
+ });
167
+ result = result.replace(/<a:p>(?!<a:pPr)/g, "<a:p><a:pPr indent=\"0\" marL=\"0\"><a:buNone/></a:pPr>");
168
+ return result;
169
+ }
170
+ function createTextBoxXml(node, name) {
171
+ const source = createPptx();
172
+ const slideHandle = source.slides[0]?.handle;
173
+ if (!slideHandle) throw new Error("createPptx did not create an editable slide");
174
+ const textOptions = createTextOptions(node);
175
+ const { paragraphs, hyperlinks } = buildParagraphs(node);
176
+ const edit = addTextBox(source, slideHandle, {
177
+ offsetX: asEmu(Math.round(textOptions.x * EMU_PER_IN)),
178
+ offsetY: asEmu(Math.round(textOptions.y * EMU_PER_IN)),
179
+ width: asEmu(Math.round(textOptions.w * EMU_PER_IN)),
180
+ height: asEmu(Math.round(textOptions.h * EMU_PER_IN)),
181
+ rotation: node.rotate !== void 0 ? asOoxmlAngle(Math.round(node.rotate * 6e4)) : void 0,
182
+ name,
183
+ body: {
184
+ marginLeft: asEmu(0),
185
+ marginRight: asEmu(0),
186
+ marginTop: asEmu(0),
187
+ marginBottom: asEmu(0)
188
+ },
189
+ paragraphs
190
+ }).edits?.at(-1);
191
+ if (edit?.kind !== "addTextBox") throw new Error("addTextBox did not produce an addTextBox edit");
192
+ return {
193
+ xml: withPptxGenParagraphDefaults(withGlowAlpha(edit.xml, node)),
194
+ hyperlinks
195
+ };
196
+ }
197
+ var GlimpseTextBoxRegistry = class {
198
+ registered = [];
199
+ register(node) {
200
+ const index = this.registered.length;
201
+ const marker = `${MARKER_PREFIX}${index}`;
202
+ const name = `Text ${index + 1}`;
203
+ const { xml, hyperlinks } = createTextBoxXml(node, name);
204
+ this.registered.push({
205
+ marker,
206
+ name,
207
+ xml,
208
+ hyperlinks
209
+ });
210
+ return marker;
211
+ }
212
+ get isEmpty() {
213
+ return this.registered.length === 0;
214
+ }
215
+ get entries() {
216
+ return this.registered;
217
+ }
218
+ };
219
+ function escapeRegExp(value) {
220
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
221
+ }
222
+ function replaceShapeId(xml, id, name) {
223
+ return xml.replace(/<p:cNvPr id="[^"]+" name="[^"]*"/, `<p:cNvPr id="${id}" name="${name}"`);
224
+ }
225
+ function xmlAttr(value) {
226
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
227
+ }
228
+ function slideRelsPath(slidePath) {
229
+ return `ppt/slides/_rels/${slidePath.split("/").at(-1)}.rels`;
230
+ }
231
+ var SlideRelationshipEditor = class {
232
+ xml;
233
+ nextId;
234
+ changed = false;
235
+ constructor(xml) {
236
+ this.xml = xml;
237
+ const ids = Array.from(xml.matchAll(/\bId="rId(\d+)"/g), (match) => Number(match[1]));
238
+ this.nextId = Math.max(0, ...ids) + 1;
239
+ }
240
+ addHyperlink(href) {
241
+ const id = `rId${this.nextId++}`;
242
+ const rel = `<Relationship Id="${id}" Type="${HYPERLINK_REL_TYPE}" Target="${xmlAttr(href)}" TargetMode="External"/>`;
243
+ this.xml = this.xml.replace("</Relationships>", `${rel}</Relationships>`);
244
+ this.changed = true;
245
+ return id;
246
+ }
247
+ get result() {
248
+ return {
249
+ xml: this.xml,
250
+ changed: this.changed
251
+ };
252
+ }
253
+ };
254
+ function createRelationshipEditor(xml) {
255
+ return new SlideRelationshipEditor(xml ?? "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"></Relationships>");
256
+ }
257
+ function withHyperlinkRelationships(xml, hyperlinks, addRelationship) {
258
+ if (!hyperlinks.some(Boolean)) return xml;
259
+ let index = 0;
260
+ return xml.replace(/<a:rPr\b([^>]*)>([\s\S]*?)<\/a:rPr>/g, (match, attrs, body) => {
261
+ const href = hyperlinks[index++];
262
+ if (!href) return match;
263
+ return `<a:rPr${attrs}>${body}<a:hlinkClick r:id="${addRelationship(href)}"/></a:rPr>`;
264
+ });
265
+ }
266
+ function applyGlimpseTextBoxesToXml(xml, registry, addRelationship) {
267
+ let result = xml;
268
+ for (const entry of registry.entries) {
269
+ const re = new RegExp(`<p:sp><p:nvSpPr><p:cNvPr id="([^"]+)" name="${escapeRegExp(entry.marker)}"[\\s\\S]*?</p:sp>`, "g");
270
+ result = result.replace(re, (_match, id) => {
271
+ return withHyperlinkRelationships(replaceShapeId(entry.xml, id, entry.name), entry.hyperlinks, addRelationship);
272
+ });
273
+ }
274
+ return result;
275
+ }
276
+ async function applyGlimpseTextBoxes(data, registry) {
277
+ const zip = await (await loadJSZip()).loadAsync(data);
278
+ const slidePaths = Object.keys(zip.files).filter((path) => /^ppt\/slides\/slide\d+\.xml$/.test(path));
279
+ for (const path of slidePaths) {
280
+ const file = zip.file(path);
281
+ if (!file) continue;
282
+ const original = await file.async("text");
283
+ const relsPath = slideRelsPath(path);
284
+ const relsFile = zip.file(relsPath);
285
+ const relationshipEditor = createRelationshipEditor(relsFile ? await relsFile.async("text") : void 0);
286
+ const replaced = applyGlimpseTextBoxesToXml(original, registry, (href) => relationshipEditor.addHyperlink(href));
287
+ if (replaced !== original) zip.file(path, replaced);
288
+ const relationships = relationshipEditor.result;
289
+ if (relationships.changed) zip.file(relsPath, relationships.xml);
290
+ }
291
+ return zip;
292
+ }
293
+ function patchPptxWriteForGlimpseTextBoxes(pptx, registry) {
294
+ if (registry.isEmpty) return;
295
+ const originalWrite = pptx.write.bind(pptx);
296
+ const patchedWrite = async (rawProps) => {
297
+ const props = typeof rawProps === "string" ? { outputType: rawProps } : rawProps;
298
+ const zip = await applyGlimpseTextBoxes(await originalWrite({ outputType: "uint8array" }), registry);
299
+ const outputType = props?.outputType;
300
+ if (outputType === "STREAM") return zip.generateAsync({
301
+ type: "nodebuffer",
302
+ compression: props?.compression ? "DEFLATE" : "STORE"
303
+ });
304
+ if (outputType) return zip.generateAsync({
305
+ type: outputType,
306
+ compression: props?.compression ? "DEFLATE" : "STORE"
307
+ });
308
+ return zip.generateAsync({
309
+ type: "blob",
310
+ compression: props?.compression ? "DEFLATE" : "STORE"
311
+ });
312
+ };
313
+ pptx.write = patchedWrite;
314
+ const patchedStream = async (props) => pptx.write({
315
+ outputType: "STREAM",
316
+ compression: props?.compression
317
+ });
318
+ pptx.stream = patchedStream;
319
+ const patchedWriteFile = async (rawProps) => {
320
+ const props = typeof rawProps === "string" ? { fileName: rawProps } : rawProps;
321
+ if (!(typeof process !== "undefined" && Boolean(process.versions?.node))) {
322
+ const browserWriter = pptx;
323
+ if (typeof browserWriter.writeFileToBrowser !== "function") throw new Error("pptx.writeFile browser download helper is unavailable; use pptx.write({ outputType: 'blob' }) instead");
324
+ const rawName = props?.fileName ?? "Presentation.pptx";
325
+ const fileName = rawName.toLowerCase().endsWith(".pptx") ? rawName : `${rawName}.pptx`;
326
+ const blob = await patchedWrite({
327
+ outputType: "blob",
328
+ compression: props?.compression
329
+ });
330
+ await browserWriter.writeFileToBrowser(fileName, blob);
331
+ return fileName;
332
+ }
333
+ const rawName = props?.fileName ?? "Presentation.pptx";
334
+ const fileName = rawName.toLowerCase().endsWith(".pptx") ? rawName : `${rawName}.pptx`;
335
+ const buffer = await patchedWrite({
336
+ outputType: "nodebuffer",
337
+ compression: props?.compression
338
+ });
339
+ await (await import("fs")).promises.writeFile(fileName, buffer);
340
+ return fileName;
341
+ };
342
+ pptx.writeFile = patchedWriteFile;
343
+ }
344
+ //#endregion
345
+ export { GlimpseTextBoxRegistry, patchPptxWriteForGlimpseTextBoxes };
346
+
347
+ //# sourceMappingURL=glimpseTextBoxes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glimpseTextBoxes.js","names":[],"sources":["../../src/renderPptx/glimpseTextBoxes.ts"],"sourcesContent":["/**\n * Text primitive の @pptx-glimpse/document writer への段階的 swap。\n *\n * 混在期間の合成方式は「pptxgenjs 出力 zip をベースに、swap 済み Text\n * primitive の shape XML だけを glimpse writer で生成して該当 marker shape と\n * 差し替える」方式を採用する。pptxgenjs 側にはまだ Shape / Image / Table /\n * Chart など未 swap primitive と slide master 生成が残っているため、既存 zip を\n * ベースにすると [Content_Types].xml / rels / media parts の管理を現行実装へ\n * 寄せられる。代替案として glimpse の package をベースに未 swap primitive を\n * pptxgenjs から取り込む方式も検討したが、初回スライス時点では pptxgenjs 側の\n * 非 text primitive と master 出力を XML part 単位で切り出す責務が増え、以降の\n * primitive swap より先に package 合成の複雑さが大きくなるため採用しない。\n *\n * marker shape は描画順を保持するためだけに pptxgenjs へ追加し、write 時に\n * glimpse の `<p:sp>` で丸ごと置換する。Text content 自体は pptxgenjs `addText`\n * を経由しない。\n */\nimport {\n addTextBox,\n asEmu,\n asHundredthPt,\n asOoxmlAngle,\n asOoxmlPercent,\n asPt,\n createPptx,\n type AddTextBoxGradientFillInput,\n type AddTextBoxInput,\n type AddTextBoxParagraphInput,\n type AddTextBoxRunPropertiesInput,\n type PptxSourceModelAddTextBoxEdit,\n} from \"@pptx-glimpse/document\";\nimport type {\n PositionedNode,\n TextGlow,\n TextOutline,\n Underline,\n} from \"../types.ts\";\nimport { parseLinearGradient } from \"../shared/gradient.ts\";\nimport { EMU_PER_IN, pxToEmu, pxToPt } from \"./units.ts\";\nimport { createTextOptions, resolveSubSup } from \"./textOptions.ts\";\n\ntype PptxGenJSInstance = import(\"pptxgenjs\").default;\ntype StreamProps = NonNullable<Parameters<PptxGenJSInstance[\"stream\"]>[0]>;\ntype WriteProps = NonNullable<Parameters<PptxGenJSInstance[\"write\"]>[0]>;\ntype WriteFileProps = NonNullable<\n Parameters<PptxGenJSInstance[\"writeFile\"]>[0]\n>;\ntype BrowserWritablePptx = PptxGenJSInstance & {\n writeFileToBrowser?: (fileName: string, blobContent: Blob) => Promise<string>;\n};\ntype TextPositionedNode = Extract<PositionedNode, { type: \"text\" }>;\n\nconst MARKER_PREFIX = \"pom-text:\";\nconst HYPERLINK_REL_TYPE =\n \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink\";\n\ninterface GlimpseTextRun {\n text: string;\n properties: AddTextBoxRunPropertiesInput;\n href?: string;\n}\n\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\nfunction cleanHex(color: string | undefined): string | undefined {\n return color?.replace(/^#/, \"\").toUpperCase();\n}\n\nfunction toColorInput(color: string | undefined) {\n const hex = cleanHex(color);\n return hex ? { kind: \"srgb\" as const, hex } : undefined;\n}\n\nfunction toUnderlineInput(underline: Underline | undefined) {\n if (underline === undefined || underline === false) return undefined;\n if (underline === true) return true;\n return {\n style: underline.style,\n color: toColorInput(underline.color),\n };\n}\n\nfunction toBaselineInput(\n subscript: boolean | undefined,\n superscript: boolean | undefined,\n) {\n if (subscript) return \"subscript\" as const;\n if (superscript) return \"superscript\" as const;\n return undefined;\n}\n\nfunction toGlowInput(glow: TextGlow | undefined) {\n if (!glow) return undefined;\n return {\n radius: asEmu(Math.round(pxToEmu(glow.size ?? 8))),\n color: toColorInput(glow.color ?? \"FFFFFF\")!,\n };\n}\n\nfunction toOutlineInput(outline: TextOutline | undefined) {\n if (!outline) return undefined;\n return {\n width: asEmu(Math.round(pxToEmu(outline.size ?? 1))),\n color: toColorInput(outline.color ?? \"FFFFFF\"),\n };\n}\n\nfunction toCharSpacing(letterSpacingPx: number | undefined) {\n if (letterSpacingPx === undefined) return undefined;\n return Math.round(pxToPt(letterSpacingPx) * 100);\n}\n\nfunction toTextGradientInput(\n value: string | undefined,\n): AddTextBoxGradientFillInput | undefined {\n if (!value) return undefined;\n const linear = parseLinearGradient(value);\n if (!linear) return undefined;\n const dmlAngle = (((linear.angle - 90) % 360) + 360) % 360;\n return {\n angle: asOoxmlAngle(Math.round(dmlAngle * 60000)),\n stops: linear.stops.map((stop) => ({\n position: asOoxmlPercent(Math.round(stop.position * 1000)),\n color: toColorInput(stop.color)!,\n })),\n };\n}\n\nfunction stripUndefined<T extends Record<string, unknown>>(value: T): T {\n return Object.fromEntries(\n Object.entries(value).filter(([, entry]) => entry !== undefined),\n ) as T;\n}\n\nfunction buildRunProperties(\n node: TextPositionedNode,\n run: NonNullable<TextPositionedNode[\"runs\"]>[number] | undefined,\n gradientFill: AddTextBoxGradientFillInput | undefined,\n): AddTextBoxRunPropertiesInput {\n const fontSizePx = run?.fontSize ?? node.fontSize ?? 24;\n const letterSpacingPx = run?.letterSpacing ?? node.letterSpacing;\n const subSup = run ? resolveSubSup(run, node) : node;\n const color = gradientFill\n ? undefined\n : toColorInput(run?.color ?? node.color);\n\n return stripUndefined({\n fontFace: run?.fontFamily ?? node.fontFamily ?? \"Noto Sans JP\",\n fontSize: asPt(pxToPt(fontSizePx)),\n color,\n gradientFill,\n bold: run?.bold ?? node.bold,\n italic: run?.italic ?? node.italic,\n underline: toUnderlineInput(resolveUnderline(node, run)),\n strike: run?.strike ?? node.strike,\n baseline: toBaselineInput(subSup.subscript, subSup.superscript),\n highlight: toColorInput(run?.highlight ?? node.highlight),\n glow: toGlowInput(node.glow),\n outline: toOutlineInput(node.outline),\n charSpacing: toCharSpacing(letterSpacingPx),\n });\n}\n\nfunction resolveUnderline(\n node: TextPositionedNode,\n run: NonNullable<TextPositionedNode[\"runs\"]>[number] | undefined,\n): Underline | undefined {\n if (run?.underline !== undefined) return run.underline;\n if (node.underline !== undefined) return node.underline;\n return run?.href ? true : undefined;\n}\n\nfunction createParagraphProperties(\n node: TextPositionedNode,\n): AddTextBoxParagraphInput[\"properties\"] {\n const lineHeight = node.lineHeight ?? 1.3;\n const fontSizePx = node.fontSize ?? 24;\n return stripUndefined({\n align: node.textAlign,\n lineSpacing: asHundredthPt(\n Math.round(pxToPt(fontSizePx * lineHeight) * 100),\n ),\n });\n}\n\nfunction buildParagraphs(node: TextPositionedNode): {\n paragraphs: readonly AddTextBoxParagraphInput[];\n hyperlinks: readonly (string | undefined)[];\n} {\n const gradientFill = toTextGradientInput(node.textGradient);\n const sourceRuns: GlimpseTextRun[] =\n node.runs && node.runs.length > 0\n ? node.runs.map((run) => ({\n text: run.text,\n properties: buildRunProperties(node, run, gradientFill),\n href: run.href,\n }))\n : [\n {\n text: node.text ?? \"\",\n properties: buildRunProperties(node, undefined, gradientFill),\n },\n ];\n const paragraphRuns: GlimpseTextRun[][] = [[]];\n for (const run of sourceRuns) {\n const lines = run.text.replace(/\\r*\\n/g, \"\\n\").split(\"\\n\");\n lines.forEach((line, index) => {\n if (index > 0) {\n paragraphRuns.push([]);\n }\n paragraphRuns[paragraphRuns.length - 1]?.push({\n ...run,\n text: line,\n });\n });\n }\n\n return {\n paragraphs: paragraphRuns.map((runs) => ({\n properties: createParagraphProperties(node),\n runs,\n })),\n hyperlinks: paragraphRuns.flatMap((runs) =>\n runs.map((run) => (run.text ? run.href : undefined)),\n ),\n };\n}\n\nfunction withGlowAlpha(xml: string, node: TextPositionedNode): string {\n const glow = node.glow;\n if (!glow) return xml;\n const alpha = Math.round((glow.opacity ?? 0.75) * 100000);\n const color = cleanHex(glow.color ?? \"FFFFFF\");\n const target = `<a:glow rad=\"${Math.round(pxToEmu(glow.size ?? 8))}\"><a:srgbClr val=\"${color}\"/></a:glow>`;\n const replacement = `<a:glow rad=\"${Math.round(pxToEmu(glow.size ?? 8))}\"><a:srgbClr val=\"${color}\"><a:alpha val=\"${alpha}\"/></a:srgbClr></a:glow>`;\n return xml.replaceAll(target, replacement);\n}\n\nfunction withPptxGenParagraphDefaults(xml: string): string {\n let result = xml.replaceAll('baseline=\"-25000\"', 'baseline=\"-40000\"');\n result = result.replace(/<a:bodyPr\\b([^>]*)\\/>/g, (_match, attrs) => {\n const attrText = attrs as string;\n const withRtl = /(?:^|\\s)rtlCol=/.test(attrText)\n ? attrText\n : `${attrText} rtlCol=\"0\"`;\n const withAnchor = /(?:^|\\s)anchor=/.test(withRtl)\n ? withRtl\n : `${withRtl} anchor=\"t\"`;\n return `<a:bodyPr${withAnchor}/>`;\n });\n result = result.replace(\n /<a:pPr([^>]*)>([\\s\\S]*?)<\\/a:pPr>/g,\n (match, attrs, body) => {\n const attrText = attrs as string;\n const nextAttrs = /(?:^|\\s)indent=/.test(attrText)\n ? attrText\n : `${attrText} indent=\"0\" marL=\"0\"`;\n const bodyText = body as string;\n const nextBody = bodyText.includes(\"<a:buNone/>\")\n ? bodyText\n : `${bodyText}<a:buNone/>`;\n return `<a:pPr${nextAttrs}>${nextBody}</a:pPr>`;\n },\n );\n result = result.replace(\n /<a:p>(?!<a:pPr)/g,\n '<a:p><a:pPr indent=\"0\" marL=\"0\"><a:buNone/></a:pPr>',\n );\n return result;\n}\n\nfunction createTextBoxXml(\n node: TextPositionedNode,\n name: string,\n): { xml: string; hyperlinks: readonly (string | undefined)[] } {\n const source = createPptx();\n const slideHandle = source.slides[0]?.handle;\n if (!slideHandle) {\n throw new Error(\"createPptx did not create an editable slide\");\n }\n\n const textOptions = createTextOptions(node);\n const { paragraphs, hyperlinks } = buildParagraphs(node);\n const input: AddTextBoxInput = {\n offsetX: asEmu(Math.round(textOptions.x * EMU_PER_IN)),\n offsetY: asEmu(Math.round(textOptions.y * EMU_PER_IN)),\n width: asEmu(Math.round(textOptions.w * EMU_PER_IN)),\n height: asEmu(Math.round(textOptions.h * EMU_PER_IN)),\n rotation:\n node.rotate !== undefined\n ? asOoxmlAngle(Math.round(node.rotate * 60000))\n : undefined,\n name,\n body: {\n marginLeft: asEmu(0),\n marginRight: asEmu(0),\n marginTop: asEmu(0),\n marginBottom: asEmu(0),\n },\n paragraphs,\n };\n const edited = addTextBox(source, slideHandle, input);\n const edit = edited.edits?.at(-1) as\n PptxSourceModelAddTextBoxEdit | undefined;\n if (edit?.kind !== \"addTextBox\") {\n throw new Error(\"addTextBox did not produce an addTextBox edit\");\n }\n return {\n xml: withPptxGenParagraphDefaults(withGlowAlpha(edit.xml, node)),\n hyperlinks,\n };\n}\n\ninterface RegisteredTextBox {\n marker: string;\n name: string;\n xml: string;\n hyperlinks: readonly (string | undefined)[];\n}\n\nexport class GlimpseTextBoxRegistry {\n private readonly registered: RegisteredTextBox[] = [];\n\n register(node: TextPositionedNode): string {\n const index = this.registered.length;\n const marker = `${MARKER_PREFIX}${index}`;\n const name = `Text ${index + 1}`;\n const { xml, hyperlinks } = createTextBoxXml(node, name);\n this.registered.push({ marker, name, xml, hyperlinks });\n return marker;\n }\n\n get isEmpty(): boolean {\n return this.registered.length === 0;\n }\n\n get entries(): readonly RegisteredTextBox[] {\n return this.registered;\n }\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction replaceShapeId(xml: string, id: string, name: string): string {\n return xml.replace(\n /<p:cNvPr id=\"[^\"]+\" name=\"[^\"]*\"/,\n `<p:cNvPr id=\"${id}\" name=\"${name}\"`,\n );\n}\n\nfunction xmlAttr(value: string): string {\n return value\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\");\n}\n\nfunction slideRelsPath(slidePath: string): string {\n const fileName = slidePath.split(\"/\").at(-1);\n return `ppt/slides/_rels/${fileName}.rels`;\n}\n\nclass SlideRelationshipEditor {\n private nextId: number;\n\n private changed = false;\n\n constructor(private xml: string) {\n const ids = Array.from(xml.matchAll(/\\bId=\"rId(\\d+)\"/g), (match) =>\n Number(match[1]),\n );\n this.nextId = Math.max(0, ...ids) + 1;\n }\n\n addHyperlink(href: string): string {\n const id = `rId${this.nextId++}`;\n const rel =\n `<Relationship Id=\"${id}\" Type=\"${HYPERLINK_REL_TYPE}\" ` +\n `Target=\"${xmlAttr(href)}\" TargetMode=\"External\"/>`;\n this.xml = this.xml.replace(\"</Relationships>\", `${rel}</Relationships>`);\n this.changed = true;\n return id;\n }\n\n get result(): { xml: string; changed: boolean } {\n return { xml: this.xml, changed: this.changed };\n }\n}\n\nfunction createRelationshipEditor(xml: string | undefined) {\n return new SlideRelationshipEditor(\n xml ??\n '<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>' +\n '<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\"></Relationships>',\n );\n}\n\nfunction withHyperlinkRelationships(\n xml: string,\n hyperlinks: readonly (string | undefined)[],\n addRelationship: (href: string) => string,\n): string {\n if (!hyperlinks.some(Boolean)) return xml;\n let index = 0;\n return xml.replace(\n /<a:rPr\\b([^>]*)>([\\s\\S]*?)<\\/a:rPr>/g,\n (match, attrs, body) => {\n const href = hyperlinks[index++];\n if (!href) return match;\n const id = addRelationship(href);\n return `<a:rPr${attrs as string}>${body as string}<a:hlinkClick r:id=\"${id}\"/></a:rPr>`;\n },\n );\n}\n\nfunction applyGlimpseTextBoxesToXml(\n xml: string,\n registry: GlimpseTextBoxRegistry,\n addRelationship: (href: string) => string,\n): string {\n let result = xml;\n for (const entry of registry.entries) {\n const re = new RegExp(\n `<p:sp><p:nvSpPr><p:cNvPr id=\"([^\"]+)\" name=\"${escapeRegExp(\n entry.marker,\n )}\"[\\\\s\\\\S]*?</p:sp>`,\n \"g\",\n );\n result = result.replace(re, (_match, id: string) => {\n const xmlWithIds = replaceShapeId(entry.xml, id, entry.name);\n return withHyperlinkRelationships(\n xmlWithIds,\n entry.hyperlinks,\n addRelationship,\n );\n });\n }\n return result;\n}\n\nasync function applyGlimpseTextBoxes(\n data: Uint8Array | ArrayBuffer,\n registry: GlimpseTextBoxRegistry,\n): Promise<import(\"jszip\")> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(data);\n\n const slidePaths = Object.keys(zip.files).filter((path) =>\n /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path),\n );\n for (const path of slidePaths) {\n const file = zip.file(path);\n if (!file) continue;\n const original = await file.async(\"text\");\n const relsPath = slideRelsPath(path);\n const relsFile = zip.file(relsPath);\n const relationshipEditor = createRelationshipEditor(\n relsFile ? await relsFile.async(\"text\") : undefined,\n );\n const replaced = applyGlimpseTextBoxesToXml(original, registry, (href) =>\n relationshipEditor.addHyperlink(href),\n );\n if (replaced !== original) {\n zip.file(path, replaced);\n }\n const relationships = relationshipEditor.result;\n if (relationships.changed) {\n zip.file(relsPath, relationships.xml);\n }\n }\n return zip;\n}\n\nexport function patchPptxWriteForGlimpseTextBoxes(\n pptx: PptxGenJSInstance,\n registry: GlimpseTextBoxRegistry,\n): void {\n if (registry.isEmpty) return;\n\n const originalWrite = pptx.write.bind(pptx);\n\n const patchedWrite = async (rawProps?: WriteProps | string) => {\n const props: WriteProps | undefined =\n typeof rawProps === \"string\"\n ? ({ outputType: rawProps } as WriteProps)\n : rawProps;\n const data = (await originalWrite({\n outputType: \"uint8array\",\n })) as Uint8Array;\n const zip = await applyGlimpseTextBoxes(data, registry);\n\n const outputType = props?.outputType;\n if (outputType === \"STREAM\") {\n return zip.generateAsync({\n type: \"nodebuffer\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n }\n if (outputType) {\n return zip.generateAsync({\n type: outputType,\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n }\n return zip.generateAsync({\n type: \"blob\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n };\n pptx.write = patchedWrite;\n\n const patchedStream = async (props?: StreamProps) =>\n pptx.write({\n outputType: \"STREAM\",\n compression: props?.compression,\n });\n pptx.stream = patchedStream;\n\n const patchedWriteFile = async (rawProps?: WriteFileProps | string) => {\n const props: WriteFileProps | undefined =\n typeof rawProps === \"string\" ? { fileName: rawProps } : rawProps;\n const isNode =\n typeof process !== \"undefined\" && Boolean(process.versions?.node);\n if (!isNode) {\n const browserWriter = pptx as BrowserWritablePptx;\n if (typeof browserWriter.writeFileToBrowser !== \"function\") {\n throw new Error(\n \"pptx.writeFile browser download helper is unavailable; use pptx.write({ outputType: 'blob' }) instead\",\n );\n }\n const rawName = props?.fileName ?? \"Presentation.pptx\";\n const fileName = rawName.toLowerCase().endsWith(\".pptx\")\n ? rawName\n : `${rawName}.pptx`;\n const blob = (await patchedWrite({\n outputType: \"blob\",\n compression: props?.compression,\n })) as Blob;\n await browserWriter.writeFileToBrowser(fileName, blob);\n return fileName;\n }\n const rawName = props?.fileName ?? \"Presentation.pptx\";\n const fileName = rawName.toLowerCase().endsWith(\".pptx\")\n ? rawName\n : `${rawName}.pptx`;\n const buffer = (await patchedWrite({\n outputType: \"nodebuffer\",\n compression: props?.compression,\n })) as Buffer;\n const fs = await import(\"fs\");\n await fs.promises.writeFile(fileName, buffer);\n return fileName;\n };\n pptx.writeFile = patchedWriteFile;\n}\n"],"mappings":"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"}
@@ -132,6 +132,11 @@ function patchPptxWriteForGlowEffects(pptx, registry) {
132
132
  });
133
133
  };
134
134
  pptx.write = patchedWrite;
135
+ const patchedStream = async (props) => pptx.write({
136
+ outputType: "STREAM",
137
+ compression: props?.compression
138
+ });
139
+ pptx.stream = patchedStream;
135
140
  const patchedWriteFile = async (rawProps) => {
136
141
  const props = typeof rawProps === "string" ? { fileName: rawProps } : rawProps;
137
142
  if (!(typeof process !== "undefined" && Boolean(process.versions?.node))) return originalWriteFile(props);
@@ -1 +1 @@
1
- {"version":3,"file":"glowEffects.js","names":[],"sources":["../../src/renderPptx/glowEffects.ts"],"sourcesContent":["/**\n * Shape / Icon の glow (光彩) 効果の実現\n *\n * pptxgenjs の Shape options には glow 相当のネイティブサポートがないため\n * (TextPropsOptions の glow は文字グリフ用)、以下の方式で実現する:\n *\n * 1. レンダリング時: glow 指定のある shape に一意なマーカー名を `objectName`\n * として埋め込み、レジストリに登録する。マーカー名は `<p:cNvPr name=\"...\"/>`\n * として PPTX 出力 XML に書き込まれる。\n * 2. 出力時: pptx.write() / writeFile() をラップし、出力 zip 内のスライド XML\n * の該当 `<p:sp>` の `<p:spPr>` 末尾に DrawingML ネイティブの\n * `<a:effectLst><a:glow>...</a:glow></a:effectLst>` を挿入する。\n *\n * 文字列置換採用理由 / 注意点は gradientFills.ts と同様。\n */\nimport type { TextGlow } from \"../types.ts\";\nimport { pxToEmu } from \"./units.ts\";\n\ntype PptxGenJSInstance = import(\"pptxgenjs\").default;\ntype WriteProps = NonNullable<Parameters<PptxGenJSInstance[\"write\"]>[0]>;\ntype WriteFileProps = NonNullable<\n Parameters<PptxGenJSInstance[\"writeFile\"]>[0]\n>;\n\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\ninterface RegisteredGlow {\n /** objectName に埋め込むマーカー文字列 */\n marker: string;\n /** size (px) — XML 出力時に EMU に変換する */\n sizePx: number;\n /** opacity 0-1 */\n opacity: number;\n /** 6桁 HEX (# なし、大文字) */\n color: string;\n}\n\nconst MARKER_PREFIX = \"pom-glow:\";\n\n/**\n * glow 仕様を一意なマーカー名にマップし、後処理で\n * `<a:effectLst><a:glow>` 要素として挿入できるようにするレジストリ\n */\nexport class GlowEffectRegistry {\n private readonly markerBySpec = new Map<string, string>();\n private readonly registered: RegisteredGlow[] = [];\n\n /**\n * glow を登録し、`objectName` に渡すマーカー文字列を返す。\n * 同じ仕様の glow は同じマーカーを返す。\n */\n register(glow: TextGlow): string {\n const sizePx = glow.size ?? 8;\n const opacity = glow.opacity ?? 0.75;\n const color = (glow.color ?? \"FFFFFF\").replace(/^#/, \"\").toUpperCase();\n const specKey = JSON.stringify({ sizePx, opacity, color });\n const existing = this.markerBySpec.get(specKey);\n if (existing) return existing;\n\n const index = this.registered.length;\n const marker = `${MARKER_PREFIX}${index}`;\n this.markerBySpec.set(specKey, marker);\n this.registered.push({ marker, sizePx, opacity, color });\n return marker;\n }\n\n get isEmpty(): boolean {\n return this.registered.length === 0;\n }\n\n get entries(): readonly RegisteredGlow[] {\n return this.registered;\n }\n}\n\n/**\n * DrawingML の `<a:glow>` 要素を構築する (effectLst で囲まない、内側だけ)\n *\n * - rad (光彩半径): px → EMU\n * - alpha (透明度): 0-1 → 1/1000 % (0-100000)\n */\nfunction buildGlowXml(entry: RegisteredGlow): string {\n const rad = Math.round(pxToEmu(entry.sizePx));\n const alpha = Math.round(entry.opacity * 100000);\n return `<a:glow rad=\"${rad}\"><a:srgbClr val=\"${entry.color}\"><a:alpha val=\"${alpha}\"/></a:srgbClr></a:glow>`;\n}\n\n/**\n * 1 つの slide XML を受け取り、登録された glow を該当 shape に適用した XML を返す\n *\n * `<p:cNvPr ... name=\"pom-glow:N\">` を含む `<p:sp>` ブロックを正規表現で検出し、\n * 以下のいずれかで `<a:glow>` を挿入する:\n *\n * 1. 既に `<a:effectLst>...</a:effectLst>` が存在する場合\n * (= pptxgenjs が shape の `shadow` 等で同要素を生成済み):\n * その `effectLst` 内側の末尾 (`</a:effectLst>` の直前) に `<a:glow>` を追加する。\n * OOXML スキーマ上 `<p:spPr>` 内の `<a:effectLst>` は最大 1 個までで、二重に\n * 並べると不正になるため。\n * 2. 既存 `effectLst` が無い場合:\n * `</p:spPr>` の直前に新規 `<a:effectLst><a:glow/></a:effectLst>` を挿入する。\n *\n * pptxgenjs は cNvPr を `<p:cNvPr.../>` (self-closing) と\n * `<p:cNvPr...></p:cNvPr>` (open+close) のどちらの形式でも出力し得るため、\n * cNvPr の閉じ形式に依存せず name 属性のあとから lazy に進める。\n */\nfunction applyGlowToXml(xml: string, registry: GlowEffectRegistry): string {\n let result = xml;\n for (const entry of registry.entries) {\n const glowXml = buildGlowXml(entry);\n const re = new RegExp(\n `(<p:cNvPr[^>]*name=\"${entry.marker}\"[\\\\s\\\\S]*?)(</p:spPr>)`,\n \"g\",\n );\n result = result.replace(re, (_match, prefix, suffix) => {\n const block = prefix as string;\n // shape ブロック内に既存の effectLst があるかを `</a:effectLst>` の有無で判定する。\n // 別 shape の effectLst を誤って書き換えないよう、検索範囲は prefix\n // (= 該当 cNvPr 〜 直後の </p:spPr> の手前まで) に限定している。\n const closingIdx = block.lastIndexOf(\"</a:effectLst>\");\n if (closingIdx >= 0) {\n const before = block.substring(0, closingIdx);\n const after = block.substring(closingIdx);\n return `${before}${glowXml}${after}${suffix as string}`;\n }\n return `${block}<a:effectLst>${glowXml}</a:effectLst>${suffix as string}`;\n });\n }\n return result;\n}\n\n/**\n * 出力 zip 内のスライド XML に glow 効果を挿入する\n */\nexport async function applyGlowEffects(\n data: Uint8Array | ArrayBuffer,\n registry: GlowEffectRegistry,\n): Promise<import(\"jszip\")> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(data);\n\n const slidePaths = Object.keys(zip.files).filter((path) =>\n /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path),\n );\n for (const path of slidePaths) {\n const file = zip.file(path);\n if (!file) continue;\n const original = await file.async(\"text\");\n const replaced = applyGlowToXml(original, registry);\n if (replaced !== original) {\n zip.file(path, replaced);\n }\n }\n return zip;\n}\n\n/**\n * pptx インスタンスの write / writeFile をラップし、出力時に glow 後処理を適用する。\n *\n * 既に他の後処理 (gradientFills 等) によりラップされている場合に備え、\n * 元の関数を保存してチェーン可能にしている。\n */\nexport function patchPptxWriteForGlowEffects(\n pptx: PptxGenJSInstance,\n registry: GlowEffectRegistry,\n): void {\n if (registry.isEmpty) return;\n\n const originalWrite = pptx.write.bind(pptx);\n const originalWriteFile = pptx.writeFile.bind(pptx);\n\n const patchedWrite = async (rawProps?: WriteProps | string) => {\n const props: WriteProps | undefined =\n typeof rawProps === \"string\"\n ? ({ outputType: rawProps } as WriteProps)\n : rawProps;\n const data = (await originalWrite({\n outputType: \"uint8array\",\n })) as Uint8Array;\n const zip = await applyGlowEffects(data, registry);\n\n const outputType = props?.outputType;\n if (outputType === \"STREAM\") {\n return zip.generateAsync({\n type: \"nodebuffer\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n }\n if (outputType) {\n return zip.generateAsync({ type: outputType });\n }\n return zip.generateAsync({\n type: \"blob\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n };\n pptx.write = patchedWrite;\n\n const patchedWriteFile = async (rawProps?: WriteFileProps | string) => {\n const props: WriteFileProps | undefined =\n typeof rawProps === \"string\" ? { fileName: rawProps } : rawProps;\n const isNode =\n typeof process !== \"undefined\" && Boolean(process.versions?.node);\n if (!isNode) {\n return originalWriteFile(props);\n }\n const rawName = props?.fileName ?? \"Presentation.pptx\";\n const fileName = rawName.toLowerCase().endsWith(\".pptx\")\n ? rawName\n : `${rawName}.pptx`;\n const buffer = (await patchedWrite({\n outputType: \"nodebuffer\",\n compression: props?.compression,\n })) as Buffer;\n const fs = await import(\"fs\");\n await fs.promises.writeFile(fileName, buffer);\n return fileName;\n };\n pptx.writeFile = patchedWriteFile;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"glowEffects.js","names":[],"sources":["../../src/renderPptx/glowEffects.ts"],"sourcesContent":["/**\n * Shape / Icon の glow (光彩) 効果の実現\n *\n * pptxgenjs の Shape options には glow 相当のネイティブサポートがないため\n * (TextPropsOptions の glow は文字グリフ用)、以下の方式で実現する:\n *\n * 1. レンダリング時: glow 指定のある shape に一意なマーカー名を `objectName`\n * として埋め込み、レジストリに登録する。マーカー名は `<p:cNvPr name=\"...\"/>`\n * として PPTX 出力 XML に書き込まれる。\n * 2. 出力時: pptx.write() / writeFile() をラップし、出力 zip 内のスライド XML\n * の該当 `<p:sp>` の `<p:spPr>` 末尾に DrawingML ネイティブの\n * `<a:effectLst><a:glow>...</a:glow></a:effectLst>` を挿入する。\n *\n * 文字列置換採用理由 / 注意点は gradientFills.ts と同様。\n */\nimport type { TextGlow } from \"../types.ts\";\nimport { pxToEmu } from \"./units.ts\";\n\ntype PptxGenJSInstance = import(\"pptxgenjs\").default;\ntype StreamProps = NonNullable<Parameters<PptxGenJSInstance[\"stream\"]>[0]>;\ntype WriteProps = NonNullable<Parameters<PptxGenJSInstance[\"write\"]>[0]>;\ntype WriteFileProps = NonNullable<\n Parameters<PptxGenJSInstance[\"writeFile\"]>[0]\n>;\n\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\ninterface RegisteredGlow {\n /** objectName に埋め込むマーカー文字列 */\n marker: string;\n /** size (px) — XML 出力時に EMU に変換する */\n sizePx: number;\n /** opacity 0-1 */\n opacity: number;\n /** 6桁 HEX (# なし、大文字) */\n color: string;\n}\n\nconst MARKER_PREFIX = \"pom-glow:\";\n\n/**\n * glow 仕様を一意なマーカー名にマップし、後処理で\n * `<a:effectLst><a:glow>` 要素として挿入できるようにするレジストリ\n */\nexport class GlowEffectRegistry {\n private readonly markerBySpec = new Map<string, string>();\n private readonly registered: RegisteredGlow[] = [];\n\n /**\n * glow を登録し、`objectName` に渡すマーカー文字列を返す。\n * 同じ仕様の glow は同じマーカーを返す。\n */\n register(glow: TextGlow): string {\n const sizePx = glow.size ?? 8;\n const opacity = glow.opacity ?? 0.75;\n const color = (glow.color ?? \"FFFFFF\").replace(/^#/, \"\").toUpperCase();\n const specKey = JSON.stringify({ sizePx, opacity, color });\n const existing = this.markerBySpec.get(specKey);\n if (existing) return existing;\n\n const index = this.registered.length;\n const marker = `${MARKER_PREFIX}${index}`;\n this.markerBySpec.set(specKey, marker);\n this.registered.push({ marker, sizePx, opacity, color });\n return marker;\n }\n\n get isEmpty(): boolean {\n return this.registered.length === 0;\n }\n\n get entries(): readonly RegisteredGlow[] {\n return this.registered;\n }\n}\n\n/**\n * DrawingML の `<a:glow>` 要素を構築する (effectLst で囲まない、内側だけ)\n *\n * - rad (光彩半径): px → EMU\n * - alpha (透明度): 0-1 → 1/1000 % (0-100000)\n */\nfunction buildGlowXml(entry: RegisteredGlow): string {\n const rad = Math.round(pxToEmu(entry.sizePx));\n const alpha = Math.round(entry.opacity * 100000);\n return `<a:glow rad=\"${rad}\"><a:srgbClr val=\"${entry.color}\"><a:alpha val=\"${alpha}\"/></a:srgbClr></a:glow>`;\n}\n\n/**\n * 1 つの slide XML を受け取り、登録された glow を該当 shape に適用した XML を返す\n *\n * `<p:cNvPr ... name=\"pom-glow:N\">` を含む `<p:sp>` ブロックを正規表現で検出し、\n * 以下のいずれかで `<a:glow>` を挿入する:\n *\n * 1. 既に `<a:effectLst>...</a:effectLst>` が存在する場合\n * (= pptxgenjs が shape の `shadow` 等で同要素を生成済み):\n * その `effectLst` 内側の末尾 (`</a:effectLst>` の直前) に `<a:glow>` を追加する。\n * OOXML スキーマ上 `<p:spPr>` 内の `<a:effectLst>` は最大 1 個までで、二重に\n * 並べると不正になるため。\n * 2. 既存 `effectLst` が無い場合:\n * `</p:spPr>` の直前に新規 `<a:effectLst><a:glow/></a:effectLst>` を挿入する。\n *\n * pptxgenjs は cNvPr を `<p:cNvPr.../>` (self-closing) と\n * `<p:cNvPr...></p:cNvPr>` (open+close) のどちらの形式でも出力し得るため、\n * cNvPr の閉じ形式に依存せず name 属性のあとから lazy に進める。\n */\nfunction applyGlowToXml(xml: string, registry: GlowEffectRegistry): string {\n let result = xml;\n for (const entry of registry.entries) {\n const glowXml = buildGlowXml(entry);\n const re = new RegExp(\n `(<p:cNvPr[^>]*name=\"${entry.marker}\"[\\\\s\\\\S]*?)(</p:spPr>)`,\n \"g\",\n );\n result = result.replace(re, (_match, prefix, suffix) => {\n const block = prefix as string;\n // shape ブロック内に既存の effectLst があるかを `</a:effectLst>` の有無で判定する。\n // 別 shape の effectLst を誤って書き換えないよう、検索範囲は prefix\n // (= 該当 cNvPr 〜 直後の </p:spPr> の手前まで) に限定している。\n const closingIdx = block.lastIndexOf(\"</a:effectLst>\");\n if (closingIdx >= 0) {\n const before = block.substring(0, closingIdx);\n const after = block.substring(closingIdx);\n return `${before}${glowXml}${after}${suffix as string}`;\n }\n return `${block}<a:effectLst>${glowXml}</a:effectLst>${suffix as string}`;\n });\n }\n return result;\n}\n\n/**\n * 出力 zip 内のスライド XML に glow 効果を挿入する\n */\nexport async function applyGlowEffects(\n data: Uint8Array | ArrayBuffer,\n registry: GlowEffectRegistry,\n): Promise<import(\"jszip\")> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(data);\n\n const slidePaths = Object.keys(zip.files).filter((path) =>\n /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path),\n );\n for (const path of slidePaths) {\n const file = zip.file(path);\n if (!file) continue;\n const original = await file.async(\"text\");\n const replaced = applyGlowToXml(original, registry);\n if (replaced !== original) {\n zip.file(path, replaced);\n }\n }\n return zip;\n}\n\n/**\n * pptx インスタンスの write / writeFile をラップし、出力時に glow 後処理を適用する。\n *\n * 既に他の後処理 (gradientFills 等) によりラップされている場合に備え、\n * 元の関数を保存してチェーン可能にしている。\n */\nexport function patchPptxWriteForGlowEffects(\n pptx: PptxGenJSInstance,\n registry: GlowEffectRegistry,\n): void {\n if (registry.isEmpty) return;\n\n const originalWrite = pptx.write.bind(pptx);\n const originalWriteFile = pptx.writeFile.bind(pptx);\n\n const patchedWrite = async (rawProps?: WriteProps | string) => {\n const props: WriteProps | undefined =\n typeof rawProps === \"string\"\n ? ({ outputType: rawProps } as WriteProps)\n : rawProps;\n const data = (await originalWrite({\n outputType: \"uint8array\",\n })) as Uint8Array;\n const zip = await applyGlowEffects(data, registry);\n\n const outputType = props?.outputType;\n if (outputType === \"STREAM\") {\n return zip.generateAsync({\n type: \"nodebuffer\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n }\n if (outputType) {\n return zip.generateAsync({ type: outputType });\n }\n return zip.generateAsync({\n type: \"blob\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n };\n pptx.write = patchedWrite;\n\n const patchedStream = async (props?: StreamProps) =>\n pptx.write({\n outputType: \"STREAM\",\n compression: props?.compression,\n });\n pptx.stream = patchedStream;\n\n const patchedWriteFile = async (rawProps?: WriteFileProps | string) => {\n const props: WriteFileProps | undefined =\n typeof rawProps === \"string\" ? { fileName: rawProps } : rawProps;\n const isNode =\n typeof process !== \"undefined\" && Boolean(process.versions?.node);\n if (!isNode) {\n return originalWriteFile(props);\n }\n const rawName = props?.fileName ?? \"Presentation.pptx\";\n const fileName = rawName.toLowerCase().endsWith(\".pptx\")\n ? rawName\n : `${rawName}.pptx`;\n const buffer = (await patchedWrite({\n outputType: \"nodebuffer\",\n compression: props?.compression,\n })) as Buffer;\n const fs = await import(\"fs\");\n await fs.promises.writeFile(fileName, buffer);\n return fileName;\n };\n pptx.writeFile = patchedWriteFile;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { parseGradient, parseLinearGradient } from "../shared/gradient.js";
1
+ import { parseGradient } from "../shared/gradient.js";
2
2
  //#region src/renderPptx/gradientFills.ts
3
3
  async function loadJSZip() {
4
4
  const mod = await import("jszip");
@@ -59,24 +59,6 @@ function registerBackgroundGradient(value, opacity, registry) {
59
59
  return registry.register(gradient, opacity);
60
60
  }
61
61
  /**
62
- * textGradient 属性値をパースしてレジストリに登録し、マーカー色を返す。
63
- * 戻り値のマーカー色を text run の color に渡すと、pptxgenjs が出力する
64
- * `<a:rPr><a:solidFill><a:srgbClr val="マーカー色"/></a:solidFill></a:rPr>` が
65
- * 後処理で gradFill に置換され、PowerPoint 上ネイティブの文字グラデーションとして
66
- * 表示・編集可能になる。
67
- *
68
- * textGradient は radial-gradient を受け付けない (linear-gradient のみ)。
69
- * パースできない場合 (スキーマ検証済みのため通常発生しない) は undefined を返す。
70
- */
71
- function registerTextGradient(value, registry) {
72
- const linear = parseLinearGradient(value);
73
- if (!linear) return void 0;
74
- return registry.register({
75
- kind: "linear",
76
- value: linear
77
- });
78
- }
79
- /**
80
62
  * Gradient を DrawingML の `<a:gradFill>` 要素に変換する
81
63
  *
82
64
  * - カラーストップ位置: % → 1/1000 % (0-100000)
@@ -147,6 +129,11 @@ function patchPptxWriteForGradientFills(pptx, registry) {
147
129
  });
148
130
  };
149
131
  pptx.write = patchedWrite;
132
+ const patchedStream = async (props) => pptx.write({
133
+ outputType: "STREAM",
134
+ compression: props?.compression
135
+ });
136
+ pptx.stream = patchedStream;
150
137
  const patchedWriteFile = async (rawProps) => {
151
138
  const props = typeof rawProps === "string" ? { fileName: rawProps } : rawProps;
152
139
  if (!(typeof process !== "undefined" && Boolean(process.versions?.node))) return originalWriteFile(props);
@@ -162,6 +149,6 @@ function patchPptxWriteForGradientFills(pptx, registry) {
162
149
  pptx.writeFile = patchedWriteFile;
163
150
  }
164
151
  //#endregion
165
- export { GradientFillRegistry, applyGradientFills, patchPptxWriteForGradientFills, registerBackgroundGradient, registerTextGradient };
152
+ export { GradientFillRegistry, applyGradientFills, patchPptxWriteForGradientFills, registerBackgroundGradient };
166
153
 
167
154
  //# sourceMappingURL=gradientFills.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gradientFills.js","names":[],"sources":["../../src/renderPptx/gradientFills.ts"],"sourcesContent":["/**\n * グラデーション塗りの実現\n *\n * pptxgenjs はグラデーション塗りを未サポート\n * (https://github.com/gitbrent/PptxGenJS/issues/102) のため、以下の方式で実現する:\n *\n * 1. レンダリング時: グラデーション指定のある shape をビルド内で一意なマーカー色の\n * 単色塗り (solidFill) として描画し、レジストリに登録する\n * 2. 出力時: pptx.write() / writeFile() をラップし、出力 zip 内のスライド XML の\n * `<a:solidFill><a:srgbClr val=\"マーカー色\"/></a:solidFill>` を\n * DrawingML ネイティブの `<a:gradFill>` に置換する\n *\n * 置換は pptxgenjs が生成する固定パターンに対する完全一致の文字列置換で行う。\n * スライド XML 全体をパーサーで往復させると無関係な要素の表現が変わり得るため、\n * 意図的に文字列置換を採用している。\n */\nimport type { Gradient } from \"../shared/gradient.ts\";\nimport { parseGradient, parseLinearGradient } from \"../shared/gradient.ts\";\n\ntype PptxGenJSInstance = import(\"pptxgenjs\").default;\ntype WriteProps = NonNullable<Parameters<PptxGenJSInstance[\"write\"]>[0]>;\ntype WriteFileProps = NonNullable<\n Parameters<PptxGenJSInstance[\"writeFile\"]>[0]\n>;\n\n// JSZip は CJS パッケージのため動的 import で読み込む\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\ninterface RegisteredGradientFill {\n /** マーカー色 (6桁大文字 HEX、# なし) */\n marker: string;\n gradient: Gradient;\n /** 0-1。指定時は各カラーストップに alpha として反映する */\n opacity?: number;\n}\n\n/** マーカー色の探索開始値。reserveColors で予約済みの色はスキップされる */\nconst MARKER_BASE = 0x0f7a3d;\n\nexport class GradientFillRegistry {\n private readonly reserved = new Set<string>();\n private readonly markerBySpec = new Map<string, string>();\n private readonly registered: RegisteredGradientFill[] = [];\n private nextCandidate = MARKER_BASE;\n\n /**\n * テキスト中に現れる 6桁 HEX をマーカー候補から除外する。\n * 入力 XML 由来のユーザー指定色とマーカーの衝突を防ぐため、\n * register より前に入力 XML 文字列を渡しておく。\n */\n reserveColors(text: string): void {\n for (const match of text.matchAll(/[0-9a-fA-F]{6}/g)) {\n this.reserved.add(match[0].toUpperCase());\n }\n }\n\n /** グラデーションを登録し、対応するマーカー色を返す */\n register(gradient: Gradient, opacity?: number): string {\n const specKey = JSON.stringify({ gradient, opacity });\n const existing = this.markerBySpec.get(specKey);\n if (existing) return existing;\n\n let marker: string;\n do {\n marker = this.nextCandidate.toString(16).toUpperCase().padStart(6, \"0\");\n this.nextCandidate = (this.nextCandidate + 1) % 0x1000000;\n } while (this.reserved.has(marker));\n this.reserved.add(marker);\n\n this.markerBySpec.set(specKey, marker);\n this.registered.push({ marker, gradient, opacity });\n return marker;\n }\n\n get isEmpty(): boolean {\n return this.registered.length === 0;\n }\n\n get entries(): readonly RegisteredGradientFill[] {\n return this.registered;\n }\n}\n\n/**\n * backgroundGradient 属性値をパースしてレジストリに登録し、マーカー色を返す。\n * linear-gradient / radial-gradient の両方に対応。\n * パースできない場合 (スキーマ検証済みのため通常発生しない) は undefined を返す。\n */\nexport function registerBackgroundGradient(\n value: string,\n opacity: number | undefined,\n registry: GradientFillRegistry,\n): string | undefined {\n const gradient = parseGradient(value);\n if (!gradient) return undefined;\n return registry.register(gradient, opacity);\n}\n\n/**\n * textGradient 属性値をパースしてレジストリに登録し、マーカー色を返す。\n * 戻り値のマーカー色を text run の color に渡すと、pptxgenjs が出力する\n * `<a:rPr><a:solidFill><a:srgbClr val=\"マーカー色\"/></a:solidFill></a:rPr>` が\n * 後処理で gradFill に置換され、PowerPoint 上ネイティブの文字グラデーションとして\n * 表示・編集可能になる。\n *\n * textGradient は radial-gradient を受け付けない (linear-gradient のみ)。\n * パースできない場合 (スキーマ検証済みのため通常発生しない) は undefined を返す。\n */\nexport function registerTextGradient(\n value: string,\n registry: GradientFillRegistry,\n): string | undefined {\n const linear = parseLinearGradient(value);\n if (!linear) return undefined;\n return registry.register({ kind: \"linear\", value: linear });\n}\n\n/**\n * Gradient を DrawingML の `<a:gradFill>` 要素に変換する\n *\n * - カラーストップ位置: % → 1/1000 % (0-100000)\n * - 角度 (linear): CSS 基準 (0deg = 上向き) → DrawingML 基準 (0 = 右向き、1/60000 度)\n * - 中心位置 (radial): CSS 風 % → DrawingML `<a:fillToRect l/t/r/b>` (1/1000 %)。\n * fillToRect は焦点を表す矩形で、中心位置 (cx, cy) に対し\n * l=cx*1000 / t=cy*1000 / r=(100-cx)*1000 / b=(100-cy)*1000 とする。\n * PowerPoint の radial fill は path=\"circle\" 1 種類で、shape (circle / ellipse) や\n * size キーワードを描画上区別しない。要素の縦横比に応じて自動で楕円状になる。\n */\nfunction buildGradFillXml(gradient: Gradient, opacity?: number): string {\n const alphaXml =\n opacity !== undefined\n ? `<a:alpha val=\"${Math.round(opacity * 100000)}\"/>`\n : \"\";\n const gsXml = gradient.value.stops\n .map((stop) => {\n const pos = Math.round(stop.position * 1000);\n const srgbClr = alphaXml\n ? `<a:srgbClr val=\"${stop.color}\">${alphaXml}</a:srgbClr>`\n : `<a:srgbClr val=\"${stop.color}\"/>`;\n return `<a:gs pos=\"${pos}\">${srgbClr}</a:gs>`;\n })\n .join(\"\");\n\n if (gradient.kind === \"linear\") {\n const dmlAngle = (((gradient.value.angle - 90) % 360) + 360) % 360;\n const ang = Math.round(dmlAngle * 60000);\n return `<a:gradFill flip=\"none\" rotWithShape=\"1\"><a:gsLst>${gsXml}</a:gsLst><a:lin ang=\"${ang}\" scaled=\"0\"/></a:gradFill>`;\n }\n\n const { centerX, centerY } = gradient.value;\n const l = Math.round(centerX * 1000);\n const t = Math.round(centerY * 1000);\n const r = Math.round((100 - centerX) * 1000);\n const b = Math.round((100 - centerY) * 1000);\n return `<a:gradFill flip=\"none\" rotWithShape=\"1\"><a:gsLst>${gsXml}</a:gsLst><a:path path=\"circle\"><a:fillToRect l=\"${l}\" t=\"${t}\" r=\"${r}\" b=\"${b}\"/></a:path></a:gradFill>`;\n}\n\n/**\n * 出力 zip 内のスライド XML のマーカー solidFill を gradFill に置換する\n */\nexport async function applyGradientFills(\n data: Uint8Array | ArrayBuffer,\n registry: GradientFillRegistry,\n): Promise<import(\"jszip\")> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(data);\n\n const slidePaths = Object.keys(zip.files).filter((path) =>\n /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path),\n );\n for (const path of slidePaths) {\n const file = zip.file(path);\n if (!file) continue;\n let xml = await file.async(\"text\");\n let replaced = false;\n for (const { marker, gradient, opacity } of registry.entries) {\n const target = `<a:solidFill><a:srgbClr val=\"${marker}\"/></a:solidFill>`;\n if (!xml.includes(target)) continue;\n xml = xml.replaceAll(target, buildGradFillXml(gradient, opacity));\n replaced = true;\n }\n if (replaced) {\n zip.file(path, xml);\n }\n }\n return zip;\n}\n\n/**\n * pptx インスタンスの write / writeFile をラップし、\n * 出力時にグラデーション後処理を適用する。\n *\n * pptxgenjs の write と同じ outputType / compression の挙動を再現する。\n * writeFile は Node 環境のみ後処理対象 (ブラウザでは pptxgenjs がダウンロード\n * 処理を行うため、元の実装にフォールバックする)。\n */\nexport function patchPptxWriteForGradientFills(\n pptx: PptxGenJSInstance,\n registry: GradientFillRegistry,\n): void {\n if (registry.isEmpty) return;\n\n const originalWrite = pptx.write.bind(pptx);\n const originalWriteFile = pptx.writeFile.bind(pptx);\n\n const patchedWrite = async (rawProps?: WriteProps | string) => {\n // DEPRECATED: pptxgenjs は write(outputType) の文字列 overload を\n // ランタイムでは今も受け付けるため、同様に正規化する\n const props: WriteProps | undefined =\n typeof rawProps === \"string\"\n ? ({ outputType: rawProps } as WriteProps)\n : rawProps;\n const data = (await originalWrite({\n outputType: \"uint8array\",\n })) as Uint8Array;\n const zip = await applyGradientFills(data, registry);\n\n const outputType = props?.outputType;\n if (outputType === \"STREAM\") {\n return zip.generateAsync({\n type: \"nodebuffer\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n }\n if (outputType) {\n return zip.generateAsync({ type: outputType });\n }\n return zip.generateAsync({\n type: \"blob\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n };\n pptx.write = patchedWrite;\n\n const patchedWriteFile = async (rawProps?: WriteFileProps | string) => {\n // DEPRECATED: pptxgenjs は writeFile(fileName) の文字列 overload を\n // ランタイムでは今も受け付けるため、同様に正規化する\n const props: WriteFileProps | undefined =\n typeof rawProps === \"string\" ? { fileName: rawProps } : rawProps;\n const isNode =\n typeof process !== \"undefined\" && Boolean(process.versions?.node);\n if (!isNode) {\n return originalWriteFile(props);\n }\n const rawName = props?.fileName ?? \"Presentation.pptx\";\n const fileName = rawName.toLowerCase().endsWith(\".pptx\")\n ? rawName\n : `${rawName}.pptx`;\n const buffer = (await patchedWrite({\n outputType: \"nodebuffer\",\n compression: props?.compression,\n })) as Buffer;\n const fs = await import(\"fs\");\n await fs.promises.writeFile(fileName, buffer);\n return fileName;\n };\n pptx.writeFile = patchedWriteFile;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gradientFills.js","names":[],"sources":["../../src/renderPptx/gradientFills.ts"],"sourcesContent":["/**\n * グラデーション塗りの実現\n *\n * pptxgenjs はグラデーション塗りを未サポート\n * (https://github.com/gitbrent/PptxGenJS/issues/102) のため、以下の方式で実現する:\n *\n * 1. レンダリング時: グラデーション指定のある shape をビルド内で一意なマーカー色の\n * 単色塗り (solidFill) として描画し、レジストリに登録する\n * 2. 出力時: pptx.write() / writeFile() をラップし、出力 zip 内のスライド XML の\n * `<a:solidFill><a:srgbClr val=\"マーカー色\"/></a:solidFill>` を\n * DrawingML ネイティブの `<a:gradFill>` に置換する\n *\n * Text の textGradient は glimpseTextBoxes.ts の TextBox native XML 生成へ移行済み。\n *\n * 置換は pptxgenjs が生成する固定パターンに対する完全一致の文字列置換で行う。\n * スライド XML 全体をパーサーで往復させると無関係な要素の表現が変わり得るため、\n * 意図的に文字列置換を採用している。\n */\nimport type { Gradient } from \"../shared/gradient.ts\";\nimport { parseGradient } from \"../shared/gradient.ts\";\n\ntype PptxGenJSInstance = import(\"pptxgenjs\").default;\ntype StreamProps = NonNullable<Parameters<PptxGenJSInstance[\"stream\"]>[0]>;\ntype WriteProps = NonNullable<Parameters<PptxGenJSInstance[\"write\"]>[0]>;\ntype WriteFileProps = NonNullable<\n Parameters<PptxGenJSInstance[\"writeFile\"]>[0]\n>;\n\n// JSZip は CJS パッケージのため動的 import で読み込む\nasync function loadJSZip(): Promise<typeof import(\"jszip\")> {\n const mod = await import(\"jszip\");\n /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n return (mod as any).default ?? mod;\n /* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return */\n}\n\ninterface RegisteredGradientFill {\n /** マーカー色 (6桁大文字 HEX、# なし) */\n marker: string;\n gradient: Gradient;\n /** 0-1。指定時は各カラーストップに alpha として反映する */\n opacity?: number;\n}\n\n/** マーカー色の探索開始値。reserveColors で予約済みの色はスキップされる */\nconst MARKER_BASE = 0x0f7a3d;\n\nexport class GradientFillRegistry {\n private readonly reserved = new Set<string>();\n private readonly markerBySpec = new Map<string, string>();\n private readonly registered: RegisteredGradientFill[] = [];\n private nextCandidate = MARKER_BASE;\n\n /**\n * テキスト中に現れる 6桁 HEX をマーカー候補から除外する。\n * 入力 XML 由来のユーザー指定色とマーカーの衝突を防ぐため、\n * register より前に入力 XML 文字列を渡しておく。\n */\n reserveColors(text: string): void {\n for (const match of text.matchAll(/[0-9a-fA-F]{6}/g)) {\n this.reserved.add(match[0].toUpperCase());\n }\n }\n\n /** グラデーションを登録し、対応するマーカー色を返す */\n register(gradient: Gradient, opacity?: number): string {\n const specKey = JSON.stringify({ gradient, opacity });\n const existing = this.markerBySpec.get(specKey);\n if (existing) return existing;\n\n let marker: string;\n do {\n marker = this.nextCandidate.toString(16).toUpperCase().padStart(6, \"0\");\n this.nextCandidate = (this.nextCandidate + 1) % 0x1000000;\n } while (this.reserved.has(marker));\n this.reserved.add(marker);\n\n this.markerBySpec.set(specKey, marker);\n this.registered.push({ marker, gradient, opacity });\n return marker;\n }\n\n get isEmpty(): boolean {\n return this.registered.length === 0;\n }\n\n get entries(): readonly RegisteredGradientFill[] {\n return this.registered;\n }\n}\n\n/**\n * backgroundGradient 属性値をパースしてレジストリに登録し、マーカー色を返す。\n * linear-gradient / radial-gradient の両方に対応。\n * パースできない場合 (スキーマ検証済みのため通常発生しない) は undefined を返す。\n */\nexport function registerBackgroundGradient(\n value: string,\n opacity: number | undefined,\n registry: GradientFillRegistry,\n): string | undefined {\n const gradient = parseGradient(value);\n if (!gradient) return undefined;\n return registry.register(gradient, opacity);\n}\n\n/**\n * Gradient を DrawingML の `<a:gradFill>` 要素に変換する\n *\n * - カラーストップ位置: % → 1/1000 % (0-100000)\n * - 角度 (linear): CSS 基準 (0deg = 上向き) → DrawingML 基準 (0 = 右向き、1/60000 度)\n * - 中心位置 (radial): CSS 風 % → DrawingML `<a:fillToRect l/t/r/b>` (1/1000 %)。\n * fillToRect は焦点を表す矩形で、中心位置 (cx, cy) に対し\n * l=cx*1000 / t=cy*1000 / r=(100-cx)*1000 / b=(100-cy)*1000 とする。\n * PowerPoint の radial fill は path=\"circle\" 1 種類で、shape (circle / ellipse) や\n * size キーワードを描画上区別しない。要素の縦横比に応じて自動で楕円状になる。\n */\nfunction buildGradFillXml(gradient: Gradient, opacity?: number): string {\n const alphaXml =\n opacity !== undefined\n ? `<a:alpha val=\"${Math.round(opacity * 100000)}\"/>`\n : \"\";\n const gsXml = gradient.value.stops\n .map((stop) => {\n const pos = Math.round(stop.position * 1000);\n const srgbClr = alphaXml\n ? `<a:srgbClr val=\"${stop.color}\">${alphaXml}</a:srgbClr>`\n : `<a:srgbClr val=\"${stop.color}\"/>`;\n return `<a:gs pos=\"${pos}\">${srgbClr}</a:gs>`;\n })\n .join(\"\");\n\n if (gradient.kind === \"linear\") {\n const dmlAngle = (((gradient.value.angle - 90) % 360) + 360) % 360;\n const ang = Math.round(dmlAngle * 60000);\n return `<a:gradFill flip=\"none\" rotWithShape=\"1\"><a:gsLst>${gsXml}</a:gsLst><a:lin ang=\"${ang}\" scaled=\"0\"/></a:gradFill>`;\n }\n\n const { centerX, centerY } = gradient.value;\n const l = Math.round(centerX * 1000);\n const t = Math.round(centerY * 1000);\n const r = Math.round((100 - centerX) * 1000);\n const b = Math.round((100 - centerY) * 1000);\n return `<a:gradFill flip=\"none\" rotWithShape=\"1\"><a:gsLst>${gsXml}</a:gsLst><a:path path=\"circle\"><a:fillToRect l=\"${l}\" t=\"${t}\" r=\"${r}\" b=\"${b}\"/></a:path></a:gradFill>`;\n}\n\n/**\n * 出力 zip 内のスライド XML のマーカー solidFill を gradFill に置換する\n */\nexport async function applyGradientFills(\n data: Uint8Array | ArrayBuffer,\n registry: GradientFillRegistry,\n): Promise<import(\"jszip\")> {\n const JSZip = await loadJSZip();\n const zip = await JSZip.loadAsync(data);\n\n const slidePaths = Object.keys(zip.files).filter((path) =>\n /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path),\n );\n for (const path of slidePaths) {\n const file = zip.file(path);\n if (!file) continue;\n let xml = await file.async(\"text\");\n let replaced = false;\n for (const { marker, gradient, opacity } of registry.entries) {\n const target = `<a:solidFill><a:srgbClr val=\"${marker}\"/></a:solidFill>`;\n if (!xml.includes(target)) continue;\n xml = xml.replaceAll(target, buildGradFillXml(gradient, opacity));\n replaced = true;\n }\n if (replaced) {\n zip.file(path, xml);\n }\n }\n return zip;\n}\n\n/**\n * pptx インスタンスの write / writeFile をラップし、\n * 出力時にグラデーション後処理を適用する。\n *\n * pptxgenjs の write と同じ outputType / compression の挙動を再現する。\n * writeFile は Node 環境のみ後処理対象 (ブラウザでは pptxgenjs がダウンロード\n * 処理を行うため、元の実装にフォールバックする)。\n */\nexport function patchPptxWriteForGradientFills(\n pptx: PptxGenJSInstance,\n registry: GradientFillRegistry,\n): void {\n if (registry.isEmpty) return;\n\n const originalWrite = pptx.write.bind(pptx);\n const originalWriteFile = pptx.writeFile.bind(pptx);\n\n const patchedWrite = async (rawProps?: WriteProps | string) => {\n // DEPRECATED: pptxgenjs は write(outputType) の文字列 overload を\n // ランタイムでは今も受け付けるため、同様に正規化する\n const props: WriteProps | undefined =\n typeof rawProps === \"string\"\n ? ({ outputType: rawProps } as WriteProps)\n : rawProps;\n const data = (await originalWrite({\n outputType: \"uint8array\",\n })) as Uint8Array;\n const zip = await applyGradientFills(data, registry);\n\n const outputType = props?.outputType;\n if (outputType === \"STREAM\") {\n return zip.generateAsync({\n type: \"nodebuffer\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n }\n if (outputType) {\n return zip.generateAsync({ type: outputType });\n }\n return zip.generateAsync({\n type: \"blob\",\n compression: props?.compression ? \"DEFLATE\" : \"STORE\",\n });\n };\n pptx.write = patchedWrite;\n\n const patchedStream = async (props?: StreamProps) =>\n pptx.write({\n outputType: \"STREAM\",\n compression: props?.compression,\n });\n pptx.stream = patchedStream;\n\n const patchedWriteFile = async (rawProps?: WriteFileProps | string) => {\n // DEPRECATED: pptxgenjs は writeFile(fileName) の文字列 overload を\n // ランタイムでは今も受け付けるため、同様に正規化する\n const props: WriteFileProps | undefined =\n typeof rawProps === \"string\" ? { fileName: rawProps } : rawProps;\n const isNode =\n typeof process !== \"undefined\" && Boolean(process.versions?.node);\n if (!isNode) {\n return originalWriteFile(props);\n }\n const rawName = props?.fileName ?? \"Presentation.pptx\";\n const fileName = rawName.toLowerCase().endsWith(\".pptx\")\n ? rawName\n : `${rawName}.pptx`;\n const buffer = (await patchedWrite({\n outputType: \"nodebuffer\",\n compression: props?.compression,\n })) as Buffer;\n const fs = await import(\"fs\");\n await fs.promises.writeFile(fileName, buffer);\n return fileName;\n };\n pptx.writeFile = patchedWriteFile;\n}\n"],"mappings":"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"}
@@ -1,51 +1,22 @@
1
- import { pxToPt } from "../units.js";
2
- import { convertGlow, convertOutline, convertStrike, convertUnderline, createTextOptions, resolveSubSup } from "../textOptions.js";
3
- import { registerTextGradient } from "../gradientFills.js";
1
+ import { createTextOptions } from "../textOptions.js";
4
2
  //#region src/renderPptx/nodes/text.ts
5
3
  function renderTextNode(node, ctx) {
6
4
  const textOptions = createTextOptions(node);
7
- const textGradientMarker = node.textGradient ? registerTextGradient(node.textGradient, ctx.buildContext.gradientFills) : void 0;
8
- if (node.runs && node.runs.length > 0) {
9
- const fontSizePx = node.fontSize ?? 24;
10
- const fontFamily = node.fontFamily ?? "Noto Sans JP";
11
- const textItems = node.runs.map((run) => {
12
- const letterSpacingPx = run.letterSpacing ?? node.letterSpacing;
13
- const runFontSizePx = run.fontSize ?? fontSizePx;
14
- const subSup = resolveSubSup(run, node);
15
- return {
16
- text: run.text,
17
- options: {
18
- fontSize: pxToPt(runFontSizePx),
19
- fontFace: run.fontFamily ?? fontFamily,
20
- color: textGradientMarker ?? run.color ?? node.color,
21
- bold: run.bold ?? node.bold,
22
- italic: run.italic ?? node.italic,
23
- underline: convertUnderline(run.underline ?? node.underline),
24
- strike: convertStrike(run.strike ?? node.strike),
25
- subscript: subSup.subscript,
26
- superscript: subSup.superscript,
27
- highlight: run.highlight ?? node.highlight,
28
- glow: convertGlow(node.glow),
29
- outline: convertOutline(node.outline),
30
- charSpacing: letterSpacingPx !== void 0 ? pxToPt(letterSpacingPx) : void 0,
31
- ...run.href ? { hyperlink: { url: run.href } } : {}
32
- }
33
- };
34
- });
35
- ctx.slide.addText(textItems, {
36
- x: textOptions.x,
37
- y: textOptions.y,
38
- w: textOptions.w,
39
- h: textOptions.h,
40
- rotate: textOptions.rotate,
41
- align: textOptions.align,
42
- valign: textOptions.valign,
43
- margin: textOptions.margin,
44
- lineSpacing: textOptions.lineSpacing
45
- });
46
- } else ctx.slide.addText(node.text ?? "", {
47
- ...textOptions,
48
- color: textGradientMarker ?? textOptions.color
5
+ const marker = ctx.buildContext.glimpseTextBoxes.register(node);
6
+ ctx.slide.addShape(ctx.pptx.ShapeType.rect, {
7
+ x: textOptions.x,
8
+ y: textOptions.y,
9
+ w: textOptions.w,
10
+ h: textOptions.h,
11
+ fill: {
12
+ color: "FFFFFF",
13
+ transparency: 100
14
+ },
15
+ line: {
16
+ color: "FFFFFF",
17
+ transparency: 100
18
+ },
19
+ objectName: marker
49
20
  });
50
21
  }
51
22
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","names":[],"sources":["../../../src/renderPptx/nodes/text.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport {\n createTextOptions,\n convertUnderline,\n convertStrike,\n convertGlow,\n convertOutline,\n resolveSubSup,\n} from \"../textOptions.ts\";\nimport { registerTextGradient } from \"../gradientFills.ts\";\nimport { pxToPt } from \"../units.ts\";\n\ntype TextPositionedNode = Extract<PositionedNode, { type: \"text\" }>;\n\nexport function renderTextNode(\n node: TextPositionedNode,\n ctx: RenderContext,\n): void {\n const textOptions = createTextOptions(node);\n\n // textGradient はマーカー色で text run color に適用し、\n // 出力時の後処理で gradFill に置換される (gradientFills.ts 参照)。\n // node 単位の指定として全 run の color を上書きする (run 単位指定はスコープ外)。\n const textGradientMarker = node.textGradient\n ? registerTextGradient(node.textGradient, ctx.buildContext.gradientFills)\n : undefined;\n\n if (node.runs && node.runs.length > 0) {\n const fontSizePx = node.fontSize ?? 24;\n const fontFamily = node.fontFamily ?? \"Noto Sans JP\";\n const textItems = node.runs.map((run) => {\n const letterSpacingPx = run.letterSpacing ?? node.letterSpacing;\n const runFontSizePx = run.fontSize ?? fontSizePx;\n const subSup = resolveSubSup(run, node);\n return {\n text: run.text,\n options: {\n fontSize: pxToPt(runFontSizePx),\n fontFace: run.fontFamily ?? fontFamily,\n color: textGradientMarker ?? run.color ?? node.color,\n bold: run.bold ?? node.bold,\n italic: run.italic ?? node.italic,\n underline: convertUnderline(run.underline ?? node.underline),\n strike: convertStrike(run.strike ?? node.strike),\n subscript: subSup.subscript,\n superscript: subSup.superscript,\n highlight: run.highlight ?? node.highlight,\n // glow / outline はノード単位指定のみ (run 単位はスコープ外)\n glow: convertGlow(node.glow),\n outline: convertOutline(node.outline),\n charSpacing:\n letterSpacingPx !== undefined ? pxToPt(letterSpacingPx) : undefined,\n ...(run.href ? { hyperlink: { url: run.href } } : {}),\n },\n };\n });\n ctx.slide.addText(textItems, {\n x: textOptions.x,\n y: textOptions.y,\n w: textOptions.w,\n h: textOptions.h,\n rotate: textOptions.rotate,\n align: textOptions.align,\n valign: textOptions.valign,\n margin: textOptions.margin,\n lineSpacing: textOptions.lineSpacing,\n });\n } else {\n ctx.slide.addText(node.text ?? \"\", {\n ...textOptions,\n color: textGradientMarker ?? textOptions.color,\n });\n }\n}\n"],"mappings":";;;;AAeA,SAAgB,eACd,MACA,KACM;CACN,MAAM,cAAc,kBAAkB,IAAI;CAK1C,MAAM,qBAAqB,KAAK,eAC5B,qBAAqB,KAAK,cAAc,IAAI,aAAa,aAAa,IACtE,KAAA;CAEJ,IAAI,KAAK,QAAQ,KAAK,KAAK,SAAS,GAAG;EACrC,MAAM,aAAa,KAAK,YAAY;EACpC,MAAM,aAAa,KAAK,cAAc;EACtC,MAAM,YAAY,KAAK,KAAK,KAAK,QAAQ;GACvC,MAAM,kBAAkB,IAAI,iBAAiB,KAAK;GAClD,MAAM,gBAAgB,IAAI,YAAY;GACtC,MAAM,SAAS,cAAc,KAAK,IAAI;GACtC,OAAO;IACL,MAAM,IAAI;IACV,SAAS;KACP,UAAU,OAAO,aAAa;KAC9B,UAAU,IAAI,cAAc;KAC5B,OAAO,sBAAsB,IAAI,SAAS,KAAK;KAC/C,MAAM,IAAI,QAAQ,KAAK;KACvB,QAAQ,IAAI,UAAU,KAAK;KAC3B,WAAW,iBAAiB,IAAI,aAAa,KAAK,SAAS;KAC3D,QAAQ,cAAc,IAAI,UAAU,KAAK,MAAM;KAC/C,WAAW,OAAO;KAClB,aAAa,OAAO;KACpB,WAAW,IAAI,aAAa,KAAK;KAEjC,MAAM,YAAY,KAAK,IAAI;KAC3B,SAAS,eAAe,KAAK,OAAO;KACpC,aACE,oBAAoB,KAAA,IAAY,OAAO,eAAe,IAAI,KAAA;KAC5D,GAAI,IAAI,OAAO,EAAE,WAAW,EAAE,KAAK,IAAI,KAAK,EAAE,IAAI,CAAC;IACrD;GACF;EACF,CAAC;EACD,IAAI,MAAM,QAAQ,WAAW;GAC3B,GAAG,YAAY;GACf,GAAG,YAAY;GACf,GAAG,YAAY;GACf,GAAG,YAAY;GACf,QAAQ,YAAY;GACpB,OAAO,YAAY;GACnB,QAAQ,YAAY;GACpB,QAAQ,YAAY;GACpB,aAAa,YAAY;EAC3B,CAAC;CACH,OACE,IAAI,MAAM,QAAQ,KAAK,QAAQ,IAAI;EACjC,GAAG;EACH,OAAO,sBAAsB,YAAY;CAC3C,CAAC;AAEL"}
1
+ {"version":3,"file":"text.js","names":[],"sources":["../../../src/renderPptx/nodes/text.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { createTextOptions } from \"../textOptions.ts\";\n\ntype TextPositionedNode = Extract<PositionedNode, { type: \"text\" }>;\n\nexport function renderTextNode(\n node: TextPositionedNode,\n ctx: RenderContext,\n): void {\n const textOptions = createTextOptions(node);\n const marker = ctx.buildContext.glimpseTextBoxes.register(node);\n\n ctx.slide.addShape(ctx.pptx.ShapeType.rect, {\n x: textOptions.x,\n y: textOptions.y,\n w: textOptions.w,\n h: textOptions.h,\n fill: { color: \"FFFFFF\", transparency: 100 },\n line: { color: \"FFFFFF\", transparency: 100 },\n objectName: marker,\n });\n}\n"],"mappings":";;AAMA,SAAgB,eACd,MACA,KACM;CACN,MAAM,cAAc,kBAAkB,IAAI;CAC1C,MAAM,SAAS,IAAI,aAAa,iBAAiB,SAAS,IAAI;CAE9D,IAAI,MAAM,SAAS,IAAI,KAAK,UAAU,MAAM;EAC1C,GAAG,YAAY;EACf,GAAG,YAAY;EACf,GAAG,YAAY;EACf,GAAG,YAAY;EACf,MAAM;GAAE,OAAO;GAAU,cAAc;EAAI;EAC3C,MAAM;GAAE,OAAO;GAAU,cAAc;EAAI;EAC3C,YAAY;CACd,CAAC;AACH"}
@@ -1,9 +1,9 @@
1
1
  import { pxToIn, pxToPt } from "../units.js";
2
2
  import { withContentBounds } from "../utils/contentArea.js";
3
- import { registerBackgroundGradient } from "../gradientFills.js";
4
3
  import { stripHash } from "../utils/visualStyle.js";
5
4
  import { measureTimeline } from "../../calcYogaLayout/measureCompositeNodes.js";
6
5
  import { resolveScaledContentArea } from "../utils/scaleToFit.js";
6
+ import { registerBackgroundGradient } from "../gradientFills.js";
7
7
  //#region src/renderPptx/nodes/timeline.ts
8
8
  function renderTimelineNode(node, ctx) {
9
9
  const direction = node.direction ?? "horizontal";
@@ -1,7 +1,7 @@
1
1
  import { getImageData } from "../../shared/measureImage.js";
2
2
  import { pxToIn, rectPxToIn } from "../units.js";
3
- import { registerBackgroundGradient } from "../gradientFills.js";
4
3
  import { BORDER_SIDES, convertBorderLine, convertShadow, hasVisibleBorder, resolveBackgroundFill, resolvePerSideBorders, resolveRectRadius } from "./visualStyle.js";
4
+ import { registerBackgroundGradient } from "../gradientFills.js";
5
5
  //#region src/renderPptx/utils/backgroundBorder.ts
6
6
  /**
7
7
  * ノードの背景色・背景画像・ボーダー・影を描画する
package/dist/types.js CHANGED
@@ -1,5 +1,5 @@
1
- import { parseGradient, parseLinearGradient } from "./shared/gradient.js";
2
1
  import { ICON_DATA } from "./icons/iconData.js";
2
+ import { parseGradient, parseLinearGradient } from "./shared/gradient.js";
3
3
  import { z } from "zod";
4
4
  //#region src/types.ts
5
5
  const FALLBACK_THEME_TOKENS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirokisakabe/pom",
3
- "version": "9.1.0",
3
+ "version": "9.1.1",
4
4
  "description": "AI-friendly PowerPoint generation with a Flexbox layout engine.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -53,8 +53,8 @@
53
53
  "@types/node": "^26.1.0",
54
54
  "@types/opentype.js": "^1.3.10",
55
55
  "@types/pngjs": "6.0.5",
56
- "@vitest/coverage-v8": "^4.1.9",
57
- "@vitest/ui": "^4.1.9",
56
+ "@vitest/coverage-v8": "^4.1.10",
57
+ "@vitest/ui": "^4.1.10",
58
58
  "dependency-cruiser": "^18.0.0",
59
59
  "lucide-static": "^1.23.0",
60
60
  "pixelmatch": "7.2.0",
@@ -64,7 +64,7 @@
64
64
  "tsx": "4.23.0"
65
65
  },
66
66
  "dependencies": {
67
- "@pptx-glimpse/document": "0.1.0",
67
+ "@pptx-glimpse/document": "0.5.0",
68
68
  "@resvg/resvg-wasm": "^2.6.2",
69
69
  "fast-xml-parser": "^5.9.3",
70
70
  "image-size": "2.0.2",