@hirokisakabe/pom 8.4.0 → 8.5.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.
- package/dist/buildPptx.d.ts.map +1 -1
- package/dist/buildPptx.js +3 -1
- package/dist/buildPptx.js.map +1 -1
- package/dist/calcYogaLayout/fontLoader.js +30 -1
- package/dist/calcYogaLayout/fontLoader.js.map +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/registry/definitions/text.js +1 -1
- package/dist/registry/definitions/text.js.map +1 -1
- package/dist/renderPptx/nodes/shape.js +19 -16
- package/dist/renderPptx/nodes/shape.js.map +1 -1
- package/dist/renderPptx/nodes/text.js +1 -1
- package/dist/renderPptx/nodes/text.js.map +1 -1
- package/dist/renderPptx/textOptions.js +22 -3
- package/dist/renderPptx/textOptions.js.map +1 -1
- package/dist/validatePositioned/validatePositioned.js +92 -0
- package/dist/validatePositioned/validatePositioned.js.map +1 -0
- package/package.json +1 -1
package/dist/buildPptx.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildPptx.d.ts","names":[],"sources":["../src/buildPptx.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"buildPptx.d.ts","names":[],"sources":["../src/buildPptx.ts"],"mappings":";;;;;UAmBiB,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
|
@@ -9,6 +9,7 @@ import { parseMasterPptx } from "./parseMasterPptx.js";
|
|
|
9
9
|
import { parseXml } from "./parseXml/parseXml.js";
|
|
10
10
|
import { renderPptx } from "./renderPptx/renderPptx.js";
|
|
11
11
|
import { toPositioned } from "./toPositioned/toPositioned.js";
|
|
12
|
+
import { validatePositioned } from "./validatePositioned/validatePositioned.js";
|
|
12
13
|
//#region src/buildPptx.ts
|
|
13
14
|
async function buildPptx(xml, slideSize, options) {
|
|
14
15
|
const ctx = createBuildContext(options?.textMeasurement ?? "auto");
|
|
@@ -16,12 +17,13 @@ async function buildPptx(xml, slideSize, options) {
|
|
|
16
17
|
if (options?.master) ctx.gradientFills.reserveColors(JSON.stringify(options.master));
|
|
17
18
|
const nodes = parseXml(xml);
|
|
18
19
|
const positionedPages = [];
|
|
19
|
-
for (const node of nodes) {
|
|
20
|
+
for (const [slideIndex, node] of nodes.entries()) {
|
|
20
21
|
let map;
|
|
21
22
|
try {
|
|
22
23
|
if (options?.autoFit !== false) map = await autoFitSlide(node, slideSize, ctx);
|
|
23
24
|
else map = await calcYogaLayout(node, slideSize, ctx);
|
|
24
25
|
const positioned = await toPositioned(node, ctx, extractLayoutResults(map));
|
|
26
|
+
validatePositioned(positioned, slideSize, ctx, slideIndex);
|
|
25
27
|
positionedPages.push(positioned);
|
|
26
28
|
} finally {
|
|
27
29
|
if (map) freeYogaTree(map);
|
package/dist/buildPptx.js.map
CHANGED
|
@@ -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 { 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\";\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 node of nodes) {\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 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 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":"
|
|
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 { 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 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":";;;;;;;;;;;;;AAwBA,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;CAEtD,MAAM,cAAc,IAAI,YAAY;CAEpC,IAAI,SAAS,UAAU,YAAY,SAAS,GAC1C,MAAM,IAAI,iBAAiB,WAAW;CAGxC,OAAO;EAAE;EAAM;CAAY;AAC7B"}
|
|
@@ -51,6 +51,35 @@ function measureTextWidth(text, fontSizePx, weight) {
|
|
|
51
51
|
return getFont(weight).getAdvanceWidth(text, fontSizePx, { kerning: true });
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
+
* フォントの縦方向メトリクスを fontSizePx に対する比率で取得する
|
|
55
|
+
*
|
|
56
|
+
* - typoAscender / typoDescender: グリフ ink のおおよその上端・下端
|
|
57
|
+
* (descender は正の値に符号反転して返す)
|
|
58
|
+
* - winDescent: レンダラが固定行送り (spcPts) のときに行下端から
|
|
59
|
+
* baseline までの距離として確保する descent
|
|
60
|
+
*
|
|
61
|
+
* バンドル外フォント使用時もバンドルフォント (Noto Sans JP) の値を
|
|
62
|
+
* 近似値として使う想定 (テキスト幅計測と同じ方針)。
|
|
63
|
+
*
|
|
64
|
+
* @param weight フォントウェイト
|
|
65
|
+
* @returns 各メトリクスの fontSizePx に対する比率
|
|
66
|
+
*/
|
|
67
|
+
function measureFontVerticalMetricsRatio(weight) {
|
|
68
|
+
const font = getFont(weight);
|
|
69
|
+
const upm = font.unitsPerEm;
|
|
70
|
+
const os2 = font.tables?.os2;
|
|
71
|
+
if (!os2) return {
|
|
72
|
+
typoAscender: .88,
|
|
73
|
+
typoDescender: .12,
|
|
74
|
+
winDescent: .288
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
typoAscender: os2.sTypoAscender / upm,
|
|
78
|
+
typoDescender: -os2.sTypoDescender / upm,
|
|
79
|
+
winDescent: os2.usWinDescent / upm
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
54
83
|
* フォントの自然な行高さ比率を取得する
|
|
55
84
|
*
|
|
56
85
|
* PowerPoint の lineHeight はフォントサイズではなく、
|
|
@@ -74,6 +103,6 @@ function measureFontLineHeightRatio(weight) {
|
|
|
74
103
|
return (os2.usWinAscent + os2.usWinDescent) / upm;
|
|
75
104
|
}
|
|
76
105
|
//#endregion
|
|
77
|
-
export { isBundledFont, measureFontLineHeightRatio, measureTextWidth };
|
|
106
|
+
export { isBundledFont, measureFontLineHeightRatio, measureFontVerticalMetricsRatio, measureTextWidth };
|
|
78
107
|
|
|
79
108
|
//# sourceMappingURL=fontLoader.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fontLoader.js","names":[],"sources":["../../src/calcYogaLayout/fontLoader.ts"],"sourcesContent":["/**\n * opentype.js を使用したフォント読み込みモジュール\n * Node.js とブラウザ両方で動作する\n */\n\nimport type { Font } from \"opentype.js\";\nimport * as opentypeModule from \"opentype.js\";\nimport { NOTO_SANS_JP_REGULAR_BASE64 } from \"./fonts/notoSansJPRegular.ts\";\nimport { NOTO_SANS_JP_BOLD_BASE64 } from \"./fonts/notoSansJPBold.ts\";\n\n// opentype.js 2.0 は ESM ビルドで named export のみを提供する一方、\n// CJS UMD ビルドでは module.exports = factory() の動的構造のため\n// Node ESM から取り込むと named exports が静的解析できない。\n// どちらの形でも動くよう default プロパティを優先して unwrap する。\nconst opentype =\n (opentypeModule as unknown as { default?: typeof opentypeModule }).default ??\n opentypeModule;\n\n// フォントキャッシュ\nconst fontCache = new Map<string, Font>();\n\n/**\n * Base64 文字列を ArrayBuffer に変換する\n * Node.js とブラウザ両方で動作する\n */\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\n // Node.js 環境\n if (typeof Buffer !== \"undefined\") {\n const buffer = Buffer.from(base64, \"base64\");\n return buffer.buffer.slice(\n buffer.byteOffset,\n buffer.byteOffset + buffer.byteLength,\n );\n }\n // ブラウザ環境\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * フォントを取得する(キャッシュ付き)\n * @param weight フォントウェイト (\"normal\" or \"bold\")\n * @returns opentype.js の Font オブジェクト\n */\nfunction getFont(weight: \"normal\" | \"bold\"): Font {\n const cacheKey = weight;\n\n // キャッシュがあればそれを返す\n const cached = fontCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Base64 データを選択\n const base64 =\n weight === \"bold\" ? NOTO_SANS_JP_BOLD_BASE64 : NOTO_SANS_JP_REGULAR_BASE64;\n\n // ArrayBuffer に変換してパース\n const buffer = base64ToArrayBuffer(base64);\n const font = opentype.parse(buffer);\n\n // キャッシュに保存\n fontCache.set(cacheKey, font);\n\n return font;\n}\n\n/** バンドル済みフォント名の一覧 */\nconst BUNDLED_FONT_NAMES = new Set([\"Noto Sans JP\"]);\n\n/**\n * 指定されたフォントがバンドル済みかどうかを判定する\n */\nexport function isBundledFont(fontFamily: string): boolean {\n return BUNDLED_FONT_NAMES.has(fontFamily);\n}\n\n/**\n * 指定したテキストの幅を計測する\n * @param text 計測するテキスト\n * @param fontSizePx フォントサイズ(ピクセル)\n * @param weight フォントウェイト\n * @returns テキスト幅(ピクセル)\n */\nexport function measureTextWidth(\n text: string,\n fontSizePx: number,\n weight: \"normal\" | \"bold\",\n): number {\n const font = getFont(weight);\n return font.getAdvanceWidth(text, fontSizePx, { kerning: true });\n}\n\n/**\n * フォントの自然な行高さ比率を取得する\n *\n * PowerPoint の lineHeight はフォントサイズではなく、\n * フォントメトリクス(ascent + descent)に対する倍率として適用される。\n * この関数は fontSizePx に対する自然な行高さの比率を返す。\n *\n * - USE_TYPO_METRICS (fsSelection bit 7) が設定されている場合:\n * sTypoAscender, sTypoDescender, sTypoLineGap を使用\n * - 設定されていない場合:\n * usWinAscent, usWinDescent を使用\n *\n * @param weight フォントウェイト\n * @returns fontSizePx に対する行高さの比率(例: 1.448)\n */\nexport function measureFontLineHeightRatio(weight: \"normal\" | \"bold\"): number {\n const font = getFont(weight);\n const upm = font.unitsPerEm;\n const os2 = font.tables?.os2;\n\n if (!os2) {\n return 1.0;\n }\n\n const useTypoMetrics = Boolean(os2.fsSelection & (1 << 7));\n\n if (useTypoMetrics) {\n return (os2.sTypoAscender - os2.sTypoDescender + os2.sTypoLineGap) / upm;\n }\n\n return (os2.usWinAscent + os2.usWinDescent) / upm;\n}\n"],"mappings":";;;;AAcA,MAAM,WACH,eAAkE,WACnE;AAGF,MAAM,4BAAY,IAAI,IAAkB;;;;;AAMxC,SAAS,oBAAoB,QAA6B;CAExD,IAAI,OAAO,WAAW,aAAa;EACjC,MAAM,SAAS,OAAO,KAAK,QAAQ,QAAQ;EAC3C,OAAO,OAAO,OAAO,MACnB,OAAO,YACP,OAAO,aAAa,OAAO,UAC7B;CACF;CAEA,MAAM,eAAe,KAAK,MAAM;CAChC,MAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;CAChD,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KACvC,MAAM,KAAK,aAAa,WAAW,CAAC;CAEtC,OAAO,MAAM;AACf;;;;;;AAOA,SAAS,QAAQ,QAAiC;CAChD,MAAM,WAAW;CAGjB,MAAM,SAAS,UAAU,IAAI,QAAQ;CACrC,IAAI,QACF,OAAO;CAQT,MAAM,SAAS,oBAHb,WAAW,SAAS,2BAA2B,2BAGR;CACzC,MAAM,OAAO,SAAS,MAAM,MAAM;CAGlC,UAAU,IAAI,UAAU,IAAI;CAE5B,OAAO;AACT;;AAGA,MAAM,qBAAqB,IAAI,IAAI,CAAC,cAAc,CAAC;;;;AAKnD,SAAgB,cAAc,YAA6B;CACzD,OAAO,mBAAmB,IAAI,UAAU;AAC1C;;;;;;;;AASA,SAAgB,iBACd,MACA,YACA,QACQ;CAER,OADa,QAAQ,MACX,CAAC,CAAC,gBAAgB,MAAM,YAAY,EAAE,SAAS,KAAK,CAAC;AACjE;;;;;;;;;;;;;;;;AAiBA,SAAgB,2BAA2B,QAAmC;CAC5E,MAAM,OAAO,QAAQ,MAAM;CAC3B,MAAM,MAAM,KAAK;CACjB,MAAM,MAAM,KAAK,QAAQ;CAEzB,IAAI,CAAC,KACH,OAAO;CAKT,IAFuB,QAAQ,IAAI,cAAe,GAEjC,GACf,QAAQ,IAAI,gBAAgB,IAAI,iBAAiB,IAAI,gBAAgB;CAGvE,QAAQ,IAAI,cAAc,IAAI,gBAAgB;AAChD"}
|
|
1
|
+
{"version":3,"file":"fontLoader.js","names":[],"sources":["../../src/calcYogaLayout/fontLoader.ts"],"sourcesContent":["/**\n * opentype.js を使用したフォント読み込みモジュール\n * Node.js とブラウザ両方で動作する\n */\n\nimport type { Font } from \"opentype.js\";\nimport * as opentypeModule from \"opentype.js\";\nimport { NOTO_SANS_JP_REGULAR_BASE64 } from \"./fonts/notoSansJPRegular.ts\";\nimport { NOTO_SANS_JP_BOLD_BASE64 } from \"./fonts/notoSansJPBold.ts\";\n\n// opentype.js 2.0 は ESM ビルドで named export のみを提供する一方、\n// CJS UMD ビルドでは module.exports = factory() の動的構造のため\n// Node ESM から取り込むと named exports が静的解析できない。\n// どちらの形でも動くよう default プロパティを優先して unwrap する。\nconst opentype =\n (opentypeModule as unknown as { default?: typeof opentypeModule }).default ??\n opentypeModule;\n\n// フォントキャッシュ\nconst fontCache = new Map<string, Font>();\n\n/**\n * Base64 文字列を ArrayBuffer に変換する\n * Node.js とブラウザ両方で動作する\n */\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\n // Node.js 環境\n if (typeof Buffer !== \"undefined\") {\n const buffer = Buffer.from(base64, \"base64\");\n return buffer.buffer.slice(\n buffer.byteOffset,\n buffer.byteOffset + buffer.byteLength,\n );\n }\n // ブラウザ環境\n const binaryString = atob(base64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * フォントを取得する(キャッシュ付き)\n * @param weight フォントウェイト (\"normal\" or \"bold\")\n * @returns opentype.js の Font オブジェクト\n */\nfunction getFont(weight: \"normal\" | \"bold\"): Font {\n const cacheKey = weight;\n\n // キャッシュがあればそれを返す\n const cached = fontCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Base64 データを選択\n const base64 =\n weight === \"bold\" ? NOTO_SANS_JP_BOLD_BASE64 : NOTO_SANS_JP_REGULAR_BASE64;\n\n // ArrayBuffer に変換してパース\n const buffer = base64ToArrayBuffer(base64);\n const font = opentype.parse(buffer);\n\n // キャッシュに保存\n fontCache.set(cacheKey, font);\n\n return font;\n}\n\n/** バンドル済みフォント名の一覧 */\nconst BUNDLED_FONT_NAMES = new Set([\"Noto Sans JP\"]);\n\n/**\n * 指定されたフォントがバンドル済みかどうかを判定する\n */\nexport function isBundledFont(fontFamily: string): boolean {\n return BUNDLED_FONT_NAMES.has(fontFamily);\n}\n\n/**\n * 指定したテキストの幅を計測する\n * @param text 計測するテキスト\n * @param fontSizePx フォントサイズ(ピクセル)\n * @param weight フォントウェイト\n * @returns テキスト幅(ピクセル)\n */\nexport function measureTextWidth(\n text: string,\n fontSizePx: number,\n weight: \"normal\" | \"bold\",\n): number {\n const font = getFont(weight);\n return font.getAdvanceWidth(text, fontSizePx, { kerning: true });\n}\n\n/**\n * フォントの縦方向メトリクスを fontSizePx に対する比率で取得する\n *\n * - typoAscender / typoDescender: グリフ ink のおおよその上端・下端\n * (descender は正の値に符号反転して返す)\n * - winDescent: レンダラが固定行送り (spcPts) のときに行下端から\n * baseline までの距離として確保する descent\n *\n * バンドル外フォント使用時もバンドルフォント (Noto Sans JP) の値を\n * 近似値として使う想定 (テキスト幅計測と同じ方針)。\n *\n * @param weight フォントウェイト\n * @returns 各メトリクスの fontSizePx に対する比率\n */\nexport function measureFontVerticalMetricsRatio(weight: \"normal\" | \"bold\"): {\n typoAscender: number;\n typoDescender: number;\n winDescent: number;\n} {\n const font = getFont(weight);\n const upm = font.unitsPerEm;\n const os2 = font.tables?.os2;\n\n if (!os2) {\n // メトリクスが取れない場合は Noto Sans JP 相当の値で近似する\n return { typoAscender: 0.88, typoDescender: 0.12, winDescent: 0.288 };\n }\n\n return {\n typoAscender: os2.sTypoAscender / upm,\n typoDescender: -os2.sTypoDescender / upm,\n winDescent: os2.usWinDescent / upm,\n };\n}\n\n/**\n * フォントの自然な行高さ比率を取得する\n *\n * PowerPoint の lineHeight はフォントサイズではなく、\n * フォントメトリクス(ascent + descent)に対する倍率として適用される。\n * この関数は fontSizePx に対する自然な行高さの比率を返す。\n *\n * - USE_TYPO_METRICS (fsSelection bit 7) が設定されている場合:\n * sTypoAscender, sTypoDescender, sTypoLineGap を使用\n * - 設定されていない場合:\n * usWinAscent, usWinDescent を使用\n *\n * @param weight フォントウェイト\n * @returns fontSizePx に対する行高さの比率(例: 1.448)\n */\nexport function measureFontLineHeightRatio(weight: \"normal\" | \"bold\"): number {\n const font = getFont(weight);\n const upm = font.unitsPerEm;\n const os2 = font.tables?.os2;\n\n if (!os2) {\n return 1.0;\n }\n\n const useTypoMetrics = Boolean(os2.fsSelection & (1 << 7));\n\n if (useTypoMetrics) {\n return (os2.sTypoAscender - os2.sTypoDescender + os2.sTypoLineGap) / upm;\n }\n\n return (os2.usWinAscent + os2.usWinDescent) / upm;\n}\n"],"mappings":";;;;AAcA,MAAM,WACH,eAAkE,WACnE;AAGF,MAAM,4BAAY,IAAI,IAAkB;;;;;AAMxC,SAAS,oBAAoB,QAA6B;CAExD,IAAI,OAAO,WAAW,aAAa;EACjC,MAAM,SAAS,OAAO,KAAK,QAAQ,QAAQ;EAC3C,OAAO,OAAO,OAAO,MACnB,OAAO,YACP,OAAO,aAAa,OAAO,UAC7B;CACF;CAEA,MAAM,eAAe,KAAK,MAAM;CAChC,MAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;CAChD,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KACvC,MAAM,KAAK,aAAa,WAAW,CAAC;CAEtC,OAAO,MAAM;AACf;;;;;;AAOA,SAAS,QAAQ,QAAiC;CAChD,MAAM,WAAW;CAGjB,MAAM,SAAS,UAAU,IAAI,QAAQ;CACrC,IAAI,QACF,OAAO;CAQT,MAAM,SAAS,oBAHb,WAAW,SAAS,2BAA2B,2BAGR;CACzC,MAAM,OAAO,SAAS,MAAM,MAAM;CAGlC,UAAU,IAAI,UAAU,IAAI;CAE5B,OAAO;AACT;;AAGA,MAAM,qBAAqB,IAAI,IAAI,CAAC,cAAc,CAAC;;;;AAKnD,SAAgB,cAAc,YAA6B;CACzD,OAAO,mBAAmB,IAAI,UAAU;AAC1C;;;;;;;;AASA,SAAgB,iBACd,MACA,YACA,QACQ;CAER,OADa,QAAQ,MACX,CAAC,CAAC,gBAAgB,MAAM,YAAY,EAAE,SAAS,KAAK,CAAC;AACjE;;;;;;;;;;;;;;;AAgBA,SAAgB,gCAAgC,QAI9C;CACA,MAAM,OAAO,QAAQ,MAAM;CAC3B,MAAM,MAAM,KAAK;CACjB,MAAM,MAAM,KAAK,QAAQ;CAEzB,IAAI,CAAC,KAEH,OAAO;EAAE,cAAc;EAAM,eAAe;EAAM,YAAY;CAAM;CAGtE,OAAO;EACL,cAAc,IAAI,gBAAgB;EAClC,eAAe,CAAC,IAAI,iBAAiB;EACrC,YAAY,IAAI,eAAe;CACjC;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,2BAA2B,QAAmC;CAC5E,MAAM,OAAO,QAAQ,MAAM;CAC3B,MAAM,MAAM,KAAK;CACjB,MAAM,MAAM,KAAK,QAAQ;CAEzB,IAAI,CAAC,KACH,OAAO;CAKT,IAFuB,QAAQ,IAAI,cAAe,GAEjC,GACf,QAAQ,IAAI,gBAAgB,IAAI,iBAAiB,IAAI,gBAAgB;CAGvE,QAAQ,IAAI,cAAc,IAAI,gBAAgB;AAChD"}
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/diagnostics.d.ts
|
|
2
|
-
type DiagnosticCode = "IMAGE_MEASURE_FAILED" | "IMAGE_NOT_PREFETCHED" | "AUTOFIT_OVERFLOW" | "SCALE_BELOW_THRESHOLD" | "MASTER_PPTX_PARSE_FAILED" | "ARROW_REF_NOT_FOUND" | "DUPLICATE_NODE_ID" | "PER_SIDE_BORDER_WITH_RADIUS";
|
|
2
|
+
type DiagnosticCode = "IMAGE_MEASURE_FAILED" | "IMAGE_NOT_PREFETCHED" | "AUTOFIT_OVERFLOW" | "SCALE_BELOW_THRESHOLD" | "MASTER_PPTX_PARSE_FAILED" | "ARROW_REF_NOT_FOUND" | "DUPLICATE_NODE_ID" | "PER_SIDE_BORDER_WITH_RADIUS" | "NODE_OUT_OF_BOUNDS" | "NODE_OVERLAP";
|
|
3
3
|
interface Diagnostic {
|
|
4
4
|
code: DiagnosticCode;
|
|
5
5
|
message: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.d.ts","names":[],"sources":["../src/diagnostics.ts"],"mappings":";KAAY,cAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","names":[],"sources":["../src/diagnostics.ts"],"mappings":";KAAY,cAAA;AAAA,UAYK,UAAA;EACf,IAAA,EAAM,cAAc;EACpB,OAAA;AAAA;AAAA,cAWW,gBAAA,SAAyB,KAAA;EAAA,SACR,WAAA,EAAa,UAAA;cAAb,WAAA,EAAa,UAAA;AAAA"}
|
package/dist/diagnostics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.js","names":[],"sources":["../src/diagnostics.ts"],"sourcesContent":["export type DiagnosticCode =\n | \"IMAGE_MEASURE_FAILED\"\n | \"IMAGE_NOT_PREFETCHED\"\n | \"AUTOFIT_OVERFLOW\"\n | \"SCALE_BELOW_THRESHOLD\"\n | \"MASTER_PPTX_PARSE_FAILED\"\n | \"ARROW_REF_NOT_FOUND\"\n | \"DUPLICATE_NODE_ID\"\n | \"PER_SIDE_BORDER_WITH_RADIUS\";\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n message: string;\n}\n\nexport class DiagnosticCollector {\n readonly items: Diagnostic[] = [];\n\n add(code: DiagnosticCode, message: string): void {\n this.items.push({ code, message });\n }\n}\n\nexport class DiagnosticsError extends Error {\n constructor(public readonly diagnostics: Diagnostic[]) {\n const summary = diagnostics\n .map((d) => `[${d.code}] ${d.message}`)\n .join(\"\\n\");\n super(`Build completed with diagnostics:\\n${summary}`);\n this.name = \"DiagnosticsError\";\n }\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"diagnostics.js","names":[],"sources":["../src/diagnostics.ts"],"sourcesContent":["export type DiagnosticCode =\n | \"IMAGE_MEASURE_FAILED\"\n | \"IMAGE_NOT_PREFETCHED\"\n | \"AUTOFIT_OVERFLOW\"\n | \"SCALE_BELOW_THRESHOLD\"\n | \"MASTER_PPTX_PARSE_FAILED\"\n | \"ARROW_REF_NOT_FOUND\"\n | \"DUPLICATE_NODE_ID\"\n | \"PER_SIDE_BORDER_WITH_RADIUS\"\n | \"NODE_OUT_OF_BOUNDS\"\n | \"NODE_OVERLAP\";\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n message: string;\n}\n\nexport class DiagnosticCollector {\n readonly items: Diagnostic[] = [];\n\n add(code: DiagnosticCode, message: string): void {\n this.items.push({ code, message });\n }\n}\n\nexport class DiagnosticsError extends Error {\n constructor(public readonly diagnostics: Diagnostic[]) {\n const summary = diagnostics\n .map((d) => `[${d.code}] ${d.message}`)\n .join(\"\\n\");\n super(`Build completed with diagnostics:\\n${summary}`);\n this.name = \"DiagnosticsError\";\n }\n}\n"],"mappings":";AAiBA,IAAa,sBAAb,MAAiC;CAC/B,QAA+B,CAAC;CAEhC,IAAI,MAAsB,SAAuB;EAC/C,KAAK,MAAM,KAAK;GAAE;GAAM;EAAQ,CAAC;CACnC;AACF;AAEA,IAAa,mBAAb,cAAsC,MAAM;CACd;CAA5B,YAAY,aAA2C;EACrD,MAAM,UAAU,YACb,KAAK,MAAM,IAAI,EAAE,KAAK,IAAI,EAAE,SAAS,CAAC,CACtC,KAAK,IAAI;EACZ,MAAM,sCAAsC,SAAS;EAJ3B,KAAA,cAAA;EAK1B,KAAK,OAAO;CACd;AACF"}
|
|
@@ -10,7 +10,7 @@ const textNodeDef = {
|
|
|
10
10
|
const fontSizePx = n.fontSize ?? 24;
|
|
11
11
|
const fontFamily = n.fontFamily ?? "Noto Sans JP";
|
|
12
12
|
const fontWeight = n.bold ? "bold" : "normal";
|
|
13
|
-
const lineHeight = 1.3;
|
|
13
|
+
const lineHeight = n.lineHeight ?? 1.3;
|
|
14
14
|
const letterSpacingPx = n.letterSpacing;
|
|
15
15
|
yn.setMeasureFunc((width, widthMode) => {
|
|
16
16
|
const { widthPx, heightPx } = measureText(text, (() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","names":[],"sources":["../../../src/registry/definitions/text.ts"],"sourcesContent":["import type { POMNode } from \"../../types.ts\";\nimport type { NodeDefinition, Yoga } from \"../types.ts\";\nimport type { Node as YogaNode } from \"yoga-layout\";\nimport { measureText } from \"../../calcYogaLayout/measureText.ts\";\nimport type { BuildContext } from \"../../buildContext.ts\";\nimport { renderTextNode } from \"../../renderPptx/nodes/text.ts\";\nimport { getNodeMetadata } from \"../nodeMetadata.ts\";\n\nexport const textNodeDef: NodeDefinition = {\n ...getNodeMetadata(\"text\"),\n applyYogaStyle(node: POMNode, yn: YogaNode, yoga: Yoga, ctx: BuildContext) {\n const n = node as Extract<POMNode, { type: \"text\" }>;\n const text = n.text;\n const fontSizePx = n.fontSize ?? 24;\n const fontFamily = n.fontFamily ?? \"Noto Sans JP\";\n const fontWeight = n.bold ? \"bold\" : \"normal\";\n const lineHeight = 1.3;\n const letterSpacingPx = n.letterSpacing;\n\n yn.setMeasureFunc((width, widthMode) => {\n const maxWidthPx = (() => {\n switch (widthMode) {\n case yoga.MEASURE_MODE_UNDEFINED:\n return Number.POSITIVE_INFINITY;\n case yoga.MEASURE_MODE_EXACTLY:\n case yoga.MEASURE_MODE_AT_MOST:\n return width;\n default:\n return Number.POSITIVE_INFINITY;\n }\n })();\n\n const { widthPx, heightPx } = measureText(\n text,\n maxWidthPx,\n {\n fontFamily,\n fontSizePx,\n lineHeight,\n fontWeight,\n letterSpacingPx,\n },\n ctx.textMeasurementMode,\n );\n\n return { width: widthPx, height: heightPx };\n });\n },\n render(node, ctx) {\n renderTextNode(node as Extract<typeof node, { type: \"text\" }>, ctx);\n },\n};\n"],"mappings":";;;;AAQA,MAAa,cAA8B;CACzC,GAAG,gBAAgB,MAAM;CACzB,eAAe,MAAe,IAAc,MAAY,KAAmB;EACzE,MAAM,IAAI;EACV,MAAM,OAAO,EAAE;EACf,MAAM,aAAa,EAAE,YAAY;EACjC,MAAM,aAAa,EAAE,cAAc;EACnC,MAAM,aAAa,EAAE,OAAO,SAAS;EACrC,MAAM,aAAa;
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../../src/registry/definitions/text.ts"],"sourcesContent":["import type { POMNode } from \"../../types.ts\";\nimport type { NodeDefinition, Yoga } from \"../types.ts\";\nimport type { Node as YogaNode } from \"yoga-layout\";\nimport { measureText } from \"../../calcYogaLayout/measureText.ts\";\nimport type { BuildContext } from \"../../buildContext.ts\";\nimport { renderTextNode } from \"../../renderPptx/nodes/text.ts\";\nimport { getNodeMetadata } from \"../nodeMetadata.ts\";\n\nexport const textNodeDef: NodeDefinition = {\n ...getNodeMetadata(\"text\"),\n applyYogaStyle(node: POMNode, yn: YogaNode, yoga: Yoga, ctx: BuildContext) {\n const n = node as Extract<POMNode, { type: \"text\" }>;\n const text = n.text;\n const fontSizePx = n.fontSize ?? 24;\n const fontFamily = n.fontFamily ?? \"Noto Sans JP\";\n const fontWeight = n.bold ? \"bold\" : \"normal\";\n const lineHeight = n.lineHeight ?? 1.3;\n const letterSpacingPx = n.letterSpacing;\n\n yn.setMeasureFunc((width, widthMode) => {\n const maxWidthPx = (() => {\n switch (widthMode) {\n case yoga.MEASURE_MODE_UNDEFINED:\n return Number.POSITIVE_INFINITY;\n case yoga.MEASURE_MODE_EXACTLY:\n case yoga.MEASURE_MODE_AT_MOST:\n return width;\n default:\n return Number.POSITIVE_INFINITY;\n }\n })();\n\n const { widthPx, heightPx } = measureText(\n text,\n maxWidthPx,\n {\n fontFamily,\n fontSizePx,\n lineHeight,\n fontWeight,\n letterSpacingPx,\n },\n ctx.textMeasurementMode,\n );\n\n return { width: widthPx, height: heightPx };\n });\n },\n render(node, ctx) {\n renderTextNode(node as Extract<typeof node, { type: \"text\" }>, ctx);\n },\n};\n"],"mappings":";;;;AAQA,MAAa,cAA8B;CACzC,GAAG,gBAAgB,MAAM;CACzB,eAAe,MAAe,IAAc,MAAY,KAAmB;EACzE,MAAM,IAAI;EACV,MAAM,OAAO,EAAE;EACf,MAAM,aAAa,EAAE,YAAY;EACjC,MAAM,aAAa,EAAE,cAAc;EACnC,MAAM,aAAa,EAAE,OAAO,SAAS;EACrC,MAAM,aAAa,EAAE,cAAc;EACnC,MAAM,kBAAkB,EAAE;EAE1B,GAAG,gBAAgB,OAAO,cAAc;GAatC,MAAM,EAAE,SAAS,aAAa,YAC5B,aAbwB;IACxB,QAAQ,WAAR;KACE,KAAK,KAAK,wBACR,OAAO,OAAO;KAChB,KAAK,KAAK;KACV,KAAK,KAAK,sBACR,OAAO;KACT,SACE,OAAO,OAAO;IAClB;GACF,EAAA,CAIW,GACT;IACE;IACA;IACA;IACA;IACA;GACF,GACA,IAAI,mBACN;GAEA,OAAO;IAAE,OAAO;IAAS,QAAQ;GAAS;EAC5C,CAAC;CACH;CACA,OAAO,MAAM,KAAK;EAChB,eAAe,MAAgD,GAAG;CACpE;AACF"}
|
|
@@ -14,22 +14,25 @@ function renderShapeNode(node, ctx) {
|
|
|
14
14
|
shadow: convertShadow(node.shadow),
|
|
15
15
|
rotate: node.rotate
|
|
16
16
|
};
|
|
17
|
-
if (node.text)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
if (node.text) {
|
|
18
|
+
const fontSizePx = node.fontSize ?? 24;
|
|
19
|
+
const lineHeight = node.lineHeight ?? 1.3;
|
|
20
|
+
ctx.slide.addText(node.text, {
|
|
21
|
+
...shapeOptions,
|
|
22
|
+
shape: node.shapeType,
|
|
23
|
+
fontSize: pxToPt(fontSizePx),
|
|
24
|
+
fontFace: node.fontFamily ?? "Noto Sans JP",
|
|
25
|
+
color: node.color,
|
|
26
|
+
bold: node.bold,
|
|
27
|
+
italic: node.italic,
|
|
28
|
+
underline: convertUnderline(node.underline),
|
|
29
|
+
strike: convertStrike(node.strike),
|
|
30
|
+
highlight: node.highlight,
|
|
31
|
+
align: node.textAlign ?? "center",
|
|
32
|
+
valign: "middle",
|
|
33
|
+
lineSpacing: pxToPt(fontSizePx * lineHeight)
|
|
34
|
+
});
|
|
35
|
+
} else ctx.slide.addShape(node.shapeType, shapeOptions);
|
|
33
36
|
}
|
|
34
37
|
//#endregion
|
|
35
38
|
export { renderShapeNode };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shape.js","names":[],"sources":["../../../src/renderPptx/nodes/shape.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { pxToPt } from \"../units.ts\";\nimport { convertUnderline, convertStrike } from \"../textOptions.ts\";\nimport { getContentAreaIn } from \"../utils/contentArea.ts\";\nimport { convertBorderLine, convertShadow } from \"../utils/visualStyle.ts\";\n\ntype ShapePositionedNode = Extract<PositionedNode, { type: \"shape\" }>;\n\nexport function renderShapeNode(\n node: ShapePositionedNode,\n ctx: RenderContext,\n): void {\n const shapeOptions = {\n ...getContentAreaIn(node),\n fill: node.fill\n ? {\n color: node.fill.color,\n transparency: node.fill.transparency,\n }\n : undefined,\n line: node.line ? convertBorderLine(node.line) : undefined,\n shadow: convertShadow(node.shadow),\n rotate: node.rotate,\n };\n\n if (node.text) {\n // テキストがある場合:addTextでshapeを指定\n ctx.slide.addText(node.text, {\n ...shapeOptions,\n shape: node.shapeType,\n fontSize: pxToPt(
|
|
1
|
+
{"version":3,"file":"shape.js","names":[],"sources":["../../../src/renderPptx/nodes/shape.ts"],"sourcesContent":["import type { PositionedNode } from \"../../types.ts\";\nimport type { RenderContext } from \"../types.ts\";\nimport { pxToPt } from \"../units.ts\";\nimport { convertUnderline, convertStrike } from \"../textOptions.ts\";\nimport { getContentAreaIn } from \"../utils/contentArea.ts\";\nimport { convertBorderLine, convertShadow } from \"../utils/visualStyle.ts\";\n\ntype ShapePositionedNode = Extract<PositionedNode, { type: \"shape\" }>;\n\nexport function renderShapeNode(\n node: ShapePositionedNode,\n ctx: RenderContext,\n): void {\n const shapeOptions = {\n ...getContentAreaIn(node),\n fill: node.fill\n ? {\n color: node.fill.color,\n transparency: node.fill.transparency,\n }\n : undefined,\n line: node.line ? convertBorderLine(node.line) : undefined,\n shadow: convertShadow(node.shadow),\n rotate: node.rotate,\n };\n\n if (node.text) {\n const fontSizePx = node.fontSize ?? 24;\n const lineHeight = node.lineHeight ?? 1.3;\n // テキストがある場合:addTextでshapeを指定\n ctx.slide.addText(node.text, {\n ...shapeOptions,\n shape: node.shapeType,\n fontSize: pxToPt(fontSizePx),\n fontFace: node.fontFamily ?? \"Noto Sans JP\",\n color: node.color,\n bold: node.bold,\n italic: node.italic,\n underline: convertUnderline(node.underline),\n strike: convertStrike(node.strike),\n highlight: node.highlight,\n align: node.textAlign ?? \"center\",\n valign: \"middle\" as const,\n // Text と同じく行送りを固定値 (spcPts) で指定し、計測高さ\n // (行数 × fontSize × lineHeight) と実描画の行高さを一致させる (#846)。\n // valign middle のためテキストブロックは枠内中央に配置され、\n // Text のような描画 y 補正は不要\n lineSpacing: pxToPt(fontSizePx * lineHeight),\n });\n } else {\n // テキストがない場合:addShapeを使用\n ctx.slide.addShape(node.shapeType, shapeOptions);\n }\n}\n"],"mappings":";;;;;AASA,SAAgB,gBACd,MACA,KACM;CACN,MAAM,eAAe;EACnB,GAAG,iBAAiB,IAAI;EACxB,MAAM,KAAK,OACP;GACE,OAAO,KAAK,KAAK;GACjB,cAAc,KAAK,KAAK;EAC1B,IACA,KAAA;EACJ,MAAM,KAAK,OAAO,kBAAkB,KAAK,IAAI,IAAI,KAAA;EACjD,QAAQ,cAAc,KAAK,MAAM;EACjC,QAAQ,KAAK;CACf;CAEA,IAAI,KAAK,MAAM;EACb,MAAM,aAAa,KAAK,YAAY;EACpC,MAAM,aAAa,KAAK,cAAc;EAEtC,IAAI,MAAM,QAAQ,KAAK,MAAM;GAC3B,GAAG;GACH,OAAO,KAAK;GACZ,UAAU,OAAO,UAAU;GAC3B,UAAU,KAAK,cAAc;GAC7B,OAAO,KAAK;GACZ,MAAM,KAAK;GACX,QAAQ,KAAK;GACb,WAAW,iBAAiB,KAAK,SAAS;GAC1C,QAAQ,cAAc,KAAK,MAAM;GACjC,WAAW,KAAK;GAChB,OAAO,KAAK,aAAa;GACzB,QAAQ;GAKR,aAAa,OAAO,aAAa,UAAU;EAC7C,CAAC;CACH,OAEE,IAAI,MAAM,SAAS,KAAK,WAAW,YAAY;AAEnD"}
|
|
@@ -35,7 +35,7 @@ function renderTextNode(node, ctx) {
|
|
|
35
35
|
align: textOptions.align,
|
|
36
36
|
valign: textOptions.valign,
|
|
37
37
|
margin: textOptions.margin,
|
|
38
|
-
|
|
38
|
+
lineSpacing: textOptions.lineSpacing
|
|
39
39
|
});
|
|
40
40
|
} else ctx.slide.addText(node.text ?? "", textOptions);
|
|
41
41
|
}
|
|
@@ -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} from \"../textOptions.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 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 return {\n text: run.text,\n options: {\n fontSize: pxToPt(fontSizePx),\n fontFace: run.fontFamily ?? fontFamily,\n color: 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 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
|
|
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} from \"../textOptions.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 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 return {\n text: run.text,\n options: {\n fontSize: pxToPt(fontSizePx),\n fontFace: run.fontFamily ?? fontFamily,\n color: 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 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 ?? \"\", textOptions);\n }\n}\n"],"mappings":";;;AAaA,SAAgB,eACd,MACA,KACM;CACN,MAAM,cAAc,kBAAkB,IAAI;CAE1C,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,OAAO;IACL,MAAM,IAAI;IACV,SAAS;KACP,UAAU,OAAO,UAAU;KAC3B,UAAU,IAAI,cAAc;KAC5B,OAAO,IAAI,SAAS,KAAK;KACzB,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,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,WAAW;AAElD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { measureFontVerticalMetricsRatio } from "../calcYogaLayout/fontLoader.js";
|
|
2
|
+
import { pxToIn, pxToPt } from "./units.js";
|
|
2
3
|
import { getContentAreaIn } from "./utils/contentArea.js";
|
|
3
4
|
//#region src/renderPptx/textOptions.ts
|
|
4
5
|
/**
|
|
@@ -44,18 +45,36 @@ function convertOutline(outline) {
|
|
|
44
45
|
color: outline.color ?? "FFFFFF"
|
|
45
46
|
};
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 行内でグリフ ink が上下中央に来るようにするための描画 y 補正量 (px)
|
|
50
|
+
*
|
|
51
|
+
* レンダラ (LibreOffice / PowerPoint) は固定行送り (spcPts) のとき
|
|
52
|
+
* baseline を「行下端 − winDescent × fontSize」に置くため、グリフ ink
|
|
53
|
+
* (typoAscender + typoDescender) は行内で下寄りになり、同じ gap でも
|
|
54
|
+
* テキスト上側の視覚余白が広く下側が狭く見える (#846)。
|
|
55
|
+
* baseline の実位置と「ink を行内中央に置いたときの baseline 位置」の
|
|
56
|
+
* 差分を返し、呼び出し側でテキストフレームをその分だけ上へずらす。
|
|
57
|
+
*/
|
|
58
|
+
function calcGlyphCenteringShiftPx(fontSizePx, lineHeight, fontWeight) {
|
|
59
|
+
const lineHeightPx = fontSizePx * lineHeight;
|
|
60
|
+
const metrics = measureFontVerticalMetricsRatio(fontWeight);
|
|
61
|
+
return lineHeightPx - metrics.winDescent * fontSizePx - ((lineHeightPx - (metrics.typoAscender + metrics.typoDescender) * fontSizePx) / 2 + metrics.typoAscender * fontSizePx);
|
|
62
|
+
}
|
|
47
63
|
function createTextOptions(node) {
|
|
48
64
|
const fontSizePx = node.fontSize ?? 24;
|
|
49
65
|
const fontFamily = node.fontFamily ?? "Noto Sans JP";
|
|
50
66
|
const lineHeight = node.lineHeight ?? 1.3;
|
|
67
|
+
const area = getContentAreaIn(node);
|
|
68
|
+
const glyphShiftPx = calcGlyphCenteringShiftPx(fontSizePx, lineHeight, node.bold ? "bold" : "normal");
|
|
51
69
|
return {
|
|
52
|
-
...
|
|
70
|
+
...area,
|
|
71
|
+
y: area.y - pxToIn(glyphShiftPx),
|
|
53
72
|
fontSize: pxToPt(fontSizePx),
|
|
54
73
|
fontFace: fontFamily,
|
|
55
74
|
align: node.textAlign ?? "left",
|
|
56
75
|
valign: "top",
|
|
57
76
|
margin: 0,
|
|
58
|
-
|
|
77
|
+
lineSpacing: pxToPt(fontSizePx * lineHeight),
|
|
59
78
|
rotate: node.rotate,
|
|
60
79
|
color: node.color,
|
|
61
80
|
bold: node.bold,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textOptions.js","names":[],"sources":["../../src/renderPptx/textOptions.ts"],"sourcesContent":["import type {\n PositionedNode,\n TextGlow,\n TextOutline,\n Underline,\n UnderlineStyle,\n} from \"../types.ts\";\nimport { pxToPt } from \"./units.ts\";\nimport { getContentAreaIn } from \"./utils/contentArea.ts\";\n\ntype TextNode = Extract<PositionedNode, { type: \"text\" }>;\n\n/**\n * underline プロパティを pptxgenjs 形式に変換する\n */\nexport function convertUnderline(\n underline: Underline | undefined,\n): { style?: UnderlineStyle; color?: string } | undefined {\n if (underline === undefined) return undefined;\n if (underline === false) return undefined;\n if (underline === true) return { style: \"sng\" };\n return {\n style: underline.style,\n color: underline.color,\n };\n}\n\n/**\n * strike プロパティを pptxgenjs 形式に変換する\n */\nexport function convertStrike(\n strike: boolean | undefined,\n): \"sngStrike\" | undefined {\n if (strike) return \"sngStrike\";\n return undefined;\n}\n\n/**\n * glow プロパティを pptxgenjs 形式に変換する\n * size はユーザー入力 px、pptxgenjs の glow.size は pt。\n * pptxgenjs は省略時デフォルトを Object.assign で合成するため undefined を\n * 渡すとデフォルトが消える。ここで pom 側のデフォルトを確定させる。\n */\nexport function convertGlow(\n glow: TextGlow | undefined,\n): { size: number; opacity: number; color: string } | undefined {\n if (glow === undefined) return undefined;\n return {\n size: pxToPt(glow.size ?? 8),\n opacity: glow.opacity ?? 0.75,\n color: glow.color ?? \"FFFFFF\",\n };\n}\n\n/**\n * outline プロパティを pptxgenjs 形式に変換する\n * size はユーザー入力 px、pptxgenjs の outline.size は pt\n */\nexport function convertOutline(\n outline: TextOutline | undefined,\n): { size: number; color: string } | undefined {\n if (outline === undefined) return undefined;\n return {\n size: pxToPt(outline.size ?? 1),\n color: outline.color ?? \"FFFFFF\",\n };\n}\n\nexport function createTextOptions(node: TextNode) {\n const fontSizePx = node.fontSize ?? 24;\n const fontFamily = node.fontFamily ?? \"Noto Sans JP\";\n const lineHeight = node.lineHeight ?? 1.3;\n\n return {\n ...
|
|
1
|
+
{"version":3,"file":"textOptions.js","names":[],"sources":["../../src/renderPptx/textOptions.ts"],"sourcesContent":["import type {\n PositionedNode,\n TextGlow,\n TextOutline,\n Underline,\n UnderlineStyle,\n} from \"../types.ts\";\nimport { pxToIn, pxToPt } from \"./units.ts\";\nimport { getContentAreaIn } from \"./utils/contentArea.ts\";\nimport { measureFontVerticalMetricsRatio } from \"../calcYogaLayout/fontLoader.ts\";\n\ntype TextNode = Extract<PositionedNode, { type: \"text\" }>;\n\n/**\n * underline プロパティを pptxgenjs 形式に変換する\n */\nexport function convertUnderline(\n underline: Underline | undefined,\n): { style?: UnderlineStyle; color?: string } | undefined {\n if (underline === undefined) return undefined;\n if (underline === false) return undefined;\n if (underline === true) return { style: \"sng\" };\n return {\n style: underline.style,\n color: underline.color,\n };\n}\n\n/**\n * strike プロパティを pptxgenjs 形式に変換する\n */\nexport function convertStrike(\n strike: boolean | undefined,\n): \"sngStrike\" | undefined {\n if (strike) return \"sngStrike\";\n return undefined;\n}\n\n/**\n * glow プロパティを pptxgenjs 形式に変換する\n * size はユーザー入力 px、pptxgenjs の glow.size は pt。\n * pptxgenjs は省略時デフォルトを Object.assign で合成するため undefined を\n * 渡すとデフォルトが消える。ここで pom 側のデフォルトを確定させる。\n */\nexport function convertGlow(\n glow: TextGlow | undefined,\n): { size: number; opacity: number; color: string } | undefined {\n if (glow === undefined) return undefined;\n return {\n size: pxToPt(glow.size ?? 8),\n opacity: glow.opacity ?? 0.75,\n color: glow.color ?? \"FFFFFF\",\n };\n}\n\n/**\n * outline プロパティを pptxgenjs 形式に変換する\n * size はユーザー入力 px、pptxgenjs の outline.size は pt\n */\nexport function convertOutline(\n outline: TextOutline | undefined,\n): { size: number; color: string } | undefined {\n if (outline === undefined) return undefined;\n return {\n size: pxToPt(outline.size ?? 1),\n color: outline.color ?? \"FFFFFF\",\n };\n}\n\n/**\n * 行内でグリフ ink が上下中央に来るようにするための描画 y 補正量 (px)\n *\n * レンダラ (LibreOffice / PowerPoint) は固定行送り (spcPts) のとき\n * baseline を「行下端 − winDescent × fontSize」に置くため、グリフ ink\n * (typoAscender + typoDescender) は行内で下寄りになり、同じ gap でも\n * テキスト上側の視覚余白が広く下側が狭く見える (#846)。\n * baseline の実位置と「ink を行内中央に置いたときの baseline 位置」の\n * 差分を返し、呼び出し側でテキストフレームをその分だけ上へずらす。\n */\nexport function calcGlyphCenteringShiftPx(\n fontSizePx: number,\n lineHeight: number,\n fontWeight: \"normal\" | \"bold\",\n): number {\n const lineHeightPx = fontSizePx * lineHeight;\n const metrics = measureFontVerticalMetricsRatio(fontWeight);\n const baselineFromTopPx = lineHeightPx - metrics.winDescent * fontSizePx;\n const inkHeightPx =\n (metrics.typoAscender + metrics.typoDescender) * fontSizePx;\n const centeredBaselineFromTopPx =\n (lineHeightPx - inkHeightPx) / 2 + metrics.typoAscender * fontSizePx;\n return baselineFromTopPx - centeredBaselineFromTopPx;\n}\n\nexport function createTextOptions(node: TextNode) {\n const fontSizePx = node.fontSize ?? 24;\n const fontFamily = node.fontFamily ?? \"Noto Sans JP\";\n const lineHeight = node.lineHeight ?? 1.3;\n\n const area = getContentAreaIn(node);\n const glyphShiftPx = calcGlyphCenteringShiftPx(\n fontSizePx,\n lineHeight,\n node.bold ? \"bold\" : \"normal\",\n );\n\n return {\n ...area,\n y: area.y - pxToIn(glyphShiftPx),\n fontSize: pxToPt(fontSizePx),\n fontFace: fontFamily,\n align: node.textAlign ?? \"left\",\n valign: \"top\" as const,\n margin: 0,\n // 行送りを固定値 (spcPts) で指定する。倍率指定 (spcPct) はレンダラがフォント\n // メトリクスに対する倍率として解釈するため、計測高さ (行数 × fontSize ×\n // lineHeight) と実描画の行高さが一致せず、グリフがボックスからはみ出して\n // 上下余白が非対称になる (#846)\n lineSpacing: pxToPt(fontSizePx * lineHeight),\n rotate: node.rotate,\n color: node.color,\n bold: node.bold,\n italic: node.italic,\n underline: convertUnderline(node.underline),\n strike: convertStrike(node.strike),\n highlight: node.highlight,\n glow: convertGlow(node.glow),\n outline: convertOutline(node.outline),\n // letterSpacing はユーザー入力 px、pptxgenjs の charSpacing は pt\n charSpacing:\n node.letterSpacing !== undefined ? pxToPt(node.letterSpacing) : undefined,\n };\n}\n"],"mappings":";;;;;;;AAgBA,SAAgB,iBACd,WACwD;CACxD,IAAI,cAAc,KAAA,GAAW,OAAO,KAAA;CACpC,IAAI,cAAc,OAAO,OAAO,KAAA;CAChC,IAAI,cAAc,MAAM,OAAO,EAAE,OAAO,MAAM;CAC9C,OAAO;EACL,OAAO,UAAU;EACjB,OAAO,UAAU;CACnB;AACF;;;;AAKA,SAAgB,cACd,QACyB;CACzB,IAAI,QAAQ,OAAO;AAErB;;;;;;;AAQA,SAAgB,YACd,MAC8D;CAC9D,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;CAC/B,OAAO;EACL,MAAM,OAAO,KAAK,QAAQ,CAAC;EAC3B,SAAS,KAAK,WAAW;EACzB,OAAO,KAAK,SAAS;CACvB;AACF;;;;;AAMA,SAAgB,eACd,SAC6C;CAC7C,IAAI,YAAY,KAAA,GAAW,OAAO,KAAA;CAClC,OAAO;EACL,MAAM,OAAO,QAAQ,QAAQ,CAAC;EAC9B,OAAO,QAAQ,SAAS;CAC1B;AACF;;;;;;;;;;;AAYA,SAAgB,0BACd,YACA,YACA,YACQ;CACR,MAAM,eAAe,aAAa;CAClC,MAAM,UAAU,gCAAgC,UAAU;CAM1D,OAL0B,eAAe,QAAQ,aAAa,eAI3D,gBAFA,QAAQ,eAAe,QAAQ,iBAAiB,cAElB,IAAI,QAAQ,eAAe;AAE9D;AAEA,SAAgB,kBAAkB,MAAgB;CAChD,MAAM,aAAa,KAAK,YAAY;CACpC,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,aAAa,KAAK,cAAc;CAEtC,MAAM,OAAO,iBAAiB,IAAI;CAClC,MAAM,eAAe,0BACnB,YACA,YACA,KAAK,OAAO,SAAS,QACvB;CAEA,OAAO;EACL,GAAG;EACH,GAAG,KAAK,IAAI,OAAO,YAAY;EAC/B,UAAU,OAAO,UAAU;EAC3B,UAAU;EACV,OAAO,KAAK,aAAa;EACzB,QAAQ;EACR,QAAQ;EAKR,aAAa,OAAO,aAAa,UAAU;EAC3C,QAAQ,KAAK;EACb,OAAO,KAAK;EACZ,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,WAAW,iBAAiB,KAAK,SAAS;EAC1C,QAAQ,cAAc,KAAK,MAAM;EACjC,WAAW,KAAK;EAChB,MAAM,YAAY,KAAK,IAAI;EAC3B,SAAS,eAAe,KAAK,OAAO;EAEpC,aACE,KAAK,kBAAkB,KAAA,IAAY,OAAO,KAAK,aAAa,IAAI,KAAA;CACpE;AACF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getNodeDef } from "../registry/nodeRegistry.js";
|
|
2
|
+
import "../registry/index.js";
|
|
3
|
+
//#region src/validatePositioned/validatePositioned.ts
|
|
4
|
+
/** 浮動小数点演算とサブピクセル丸めの揺れを誤検知しないための許容量 (px) */
|
|
5
|
+
const EPSILON = .5;
|
|
6
|
+
/**
|
|
7
|
+
* toPositioned 後の絶対座標ツリーを走査し、レイアウト上の問題を
|
|
8
|
+
* Diagnostic (警告) として報告する。ビルドは止めない。
|
|
9
|
+
*
|
|
10
|
+
* - NODE_OUT_OF_BOUNDS: ノードの矩形がスライド境界からはみ出している
|
|
11
|
+
* - NODE_OVERLAP: VStack / HStack 内の兄弟ノード同士が意図せず重なっている
|
|
12
|
+
*
|
|
13
|
+
* 意図的な重なり (Layer 配下・position="absolute"・負 margin / gap・
|
|
14
|
+
* zIndex 明示) は検出対象外とし、誤検知を避ける。
|
|
15
|
+
*/
|
|
16
|
+
function validatePositioned(node, slideSize, ctx, slideIndex) {
|
|
17
|
+
walk(node, describeNode(node));
|
|
18
|
+
function walk(n, path) {
|
|
19
|
+
let descendantOutOfBounds = false;
|
|
20
|
+
if (n.type === "vstack" || n.type === "hstack" || n.type === "layer") {
|
|
21
|
+
const children = n.children;
|
|
22
|
+
if (n.type !== "layer") reportSiblingOverlap(n, children, path);
|
|
23
|
+
children.forEach((child, i) => {
|
|
24
|
+
descendantOutOfBounds = walk(child, childPath(path, child, i)) || descendantOutOfBounds;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (descendantOutOfBounds) return true;
|
|
28
|
+
return reportOutOfBounds(n, path);
|
|
29
|
+
}
|
|
30
|
+
function reportOutOfBounds(n, path) {
|
|
31
|
+
if (n.type === "line" || n.type === "arrow") return false;
|
|
32
|
+
if ("rotate" in n && n.rotate) return false;
|
|
33
|
+
const over = [];
|
|
34
|
+
if (-n.x > EPSILON) over.push(`left by ${fmt(-n.x)}px`);
|
|
35
|
+
if (-n.y > EPSILON) over.push(`top by ${fmt(-n.y)}px`);
|
|
36
|
+
if (n.x + n.w - slideSize.w > EPSILON) over.push(`right by ${fmt(n.x + n.w - slideSize.w)}px`);
|
|
37
|
+
if (n.y + n.h - slideSize.h > EPSILON) over.push(`bottom by ${fmt(n.y + n.h - slideSize.h)}px`);
|
|
38
|
+
if (over.length === 0) return false;
|
|
39
|
+
ctx.diagnostics.add("NODE_OUT_OF_BOUNDS", `slide ${slideIndex + 1}: ${path} (${fmtRect(n)}) extends beyond the slide bounds (${slideSize.w}x${slideSize.h}): ${over.join(", ")}`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
function reportSiblingOverlap(n, children, path) {
|
|
43
|
+
if ((n.gap ?? 0) < 0) return;
|
|
44
|
+
const candidates = children.map((child, i) => ({
|
|
45
|
+
child,
|
|
46
|
+
i
|
|
47
|
+
})).filter(({ child }) => !isIntentionalOverlap(child));
|
|
48
|
+
for (let a = 0; a < candidates.length; a++) for (let b = a + 1; b < candidates.length; b++) {
|
|
49
|
+
const { child: ca, i: ia } = candidates[a];
|
|
50
|
+
const { child: cb, i: ib } = candidates[b];
|
|
51
|
+
const w = Math.min(ca.x + ca.w, cb.x + cb.w) - Math.max(ca.x, cb.x);
|
|
52
|
+
const h = Math.min(ca.y + ca.h, cb.y + cb.h) - Math.max(ca.y, cb.y);
|
|
53
|
+
if (w > EPSILON && h > EPSILON) ctx.diagnostics.add("NODE_OVERLAP", `slide ${slideIndex + 1}: ${path}: children ${describeChild(ca, ia)} (${fmtRect(ca)}) and ${describeChild(cb, ib)} (${fmtRect(cb)}) overlap by ${fmt(w)}x${fmt(h)}px`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** 意図的な重なりとして兄弟重なり検査から除外すべき子か */
|
|
58
|
+
function isIntentionalOverlap(n) {
|
|
59
|
+
if (n.position === "absolute") return true;
|
|
60
|
+
if (n.zIndex !== void 0) return true;
|
|
61
|
+
return hasNegativeMargin(n.margin);
|
|
62
|
+
}
|
|
63
|
+
function hasNegativeMargin(margin) {
|
|
64
|
+
if (margin === void 0) return false;
|
|
65
|
+
if (typeof margin === "number") return margin < 0;
|
|
66
|
+
return [
|
|
67
|
+
margin.top,
|
|
68
|
+
margin.right,
|
|
69
|
+
margin.bottom,
|
|
70
|
+
margin.left
|
|
71
|
+
].some((v) => (v ?? 0) < 0);
|
|
72
|
+
}
|
|
73
|
+
function describeNode(n) {
|
|
74
|
+
const tag = getNodeDef(n.type).tagName;
|
|
75
|
+
return n.id ? `<${tag} id="${n.id}">` : `<${tag}>`;
|
|
76
|
+
}
|
|
77
|
+
function describeChild(n, index) {
|
|
78
|
+
return `${describeNode(n)}[${index}]`;
|
|
79
|
+
}
|
|
80
|
+
function childPath(parentPath, child, index) {
|
|
81
|
+
return `${parentPath} > ${describeChild(child, index)}`;
|
|
82
|
+
}
|
|
83
|
+
function fmt(v) {
|
|
84
|
+
return Math.round(v * 10) / 10;
|
|
85
|
+
}
|
|
86
|
+
function fmtRect(n) {
|
|
87
|
+
return `x=${fmt(n.x)}, y=${fmt(n.y)}, w=${fmt(n.w)}, h=${fmt(n.h)}`;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { validatePositioned };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=validatePositioned.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validatePositioned.js","names":[],"sources":["../../src/validatePositioned/validatePositioned.ts"],"sourcesContent":["import type { BuildContext } from \"../buildContext.ts\";\nimport { getNodeDef } from \"../registry/index.ts\";\nimport type { PositionedNode } from \"../types.ts\";\n\n/** 浮動小数点演算とサブピクセル丸めの揺れを誤検知しないための許容量 (px) */\nconst EPSILON = 0.5;\n\n/**\n * toPositioned 後の絶対座標ツリーを走査し、レイアウト上の問題を\n * Diagnostic (警告) として報告する。ビルドは止めない。\n *\n * - NODE_OUT_OF_BOUNDS: ノードの矩形がスライド境界からはみ出している\n * - NODE_OVERLAP: VStack / HStack 内の兄弟ノード同士が意図せず重なっている\n *\n * 意図的な重なり (Layer 配下・position=\"absolute\"・負 margin / gap・\n * zIndex 明示) は検出対象外とし、誤検知を避ける。\n */\nexport function validatePositioned(\n node: PositionedNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n slideIndex: number,\n): void {\n walk(node, describeNode(node));\n\n function walk(n: PositionedNode, path: string): boolean {\n // はみ出しの原因に最も近いノードを特定できるよう、子孫がはみ出して\n // いる場合は親 (巻き添えではみ出すコンテナ) の報告を抑制する\n let descendantOutOfBounds = false;\n\n if (n.type === \"vstack\" || n.type === \"hstack\" || n.type === \"layer\") {\n const children: PositionedNode[] = n.children;\n if (n.type !== \"layer\") {\n reportSiblingOverlap(n, children, path);\n }\n children.forEach((child, i) => {\n descendantOutOfBounds =\n walk(child, childPath(path, child, i)) || descendantOutOfBounds;\n });\n }\n\n if (descendantOutOfBounds) return true;\n return reportOutOfBounds(n, path);\n }\n\n function reportOutOfBounds(n: PositionedNode, path: string): boolean {\n // Line は線分座標 (x1,y1-x2,y2)、Arrow は id 参照ベースのため矩形判定の対象外\n if (n.type === \"line\" || n.type === \"arrow\") return false;\n // rotate は renderPptx でのみ適用され、回転後の境界はここでは分からない\n if (\"rotate\" in n && n.rotate) return false;\n\n const over: string[] = [];\n if (-n.x > EPSILON) over.push(`left by ${fmt(-n.x)}px`);\n if (-n.y > EPSILON) over.push(`top by ${fmt(-n.y)}px`);\n if (n.x + n.w - slideSize.w > EPSILON) {\n over.push(`right by ${fmt(n.x + n.w - slideSize.w)}px`);\n }\n if (n.y + n.h - slideSize.h > EPSILON) {\n over.push(`bottom by ${fmt(n.y + n.h - slideSize.h)}px`);\n }\n if (over.length === 0) return false;\n\n ctx.diagnostics.add(\n \"NODE_OUT_OF_BOUNDS\",\n `slide ${slideIndex + 1}: ${path} (${fmtRect(n)}) extends beyond the slide bounds (${slideSize.w}x${slideSize.h}): ${over.join(\", \")}`,\n );\n return true;\n }\n\n function reportSiblingOverlap(\n n: Extract<PositionedNode, { type: \"vstack\" | \"hstack\" }>,\n children: PositionedNode[],\n path: string,\n ): void {\n // 負 gap は重ねるための明示指定 (例: ProcessArrow 風の表現) なので対象外\n if ((n.gap ?? 0) < 0) return;\n\n const candidates = children\n .map((child, i) => ({ child, i }))\n .filter(({ child }) => !isIntentionalOverlap(child));\n\n for (let a = 0; a < candidates.length; a++) {\n for (let b = a + 1; b < candidates.length; b++) {\n const { child: ca, i: ia } = candidates[a];\n const { child: cb, i: ib } = candidates[b];\n const w = Math.min(ca.x + ca.w, cb.x + cb.w) - Math.max(ca.x, cb.x);\n const h = Math.min(ca.y + ca.h, cb.y + cb.h) - Math.max(ca.y, cb.y);\n if (w > EPSILON && h > EPSILON) {\n ctx.diagnostics.add(\n \"NODE_OVERLAP\",\n `slide ${slideIndex + 1}: ${path}: children ${describeChild(ca, ia)} (${fmtRect(ca)}) and ${describeChild(cb, ib)} (${fmtRect(cb)}) overlap by ${fmt(w)}x${fmt(h)}px`,\n );\n }\n }\n }\n }\n}\n\n/** 意図的な重なりとして兄弟重なり検査から除外すべき子か */\nfunction isIntentionalOverlap(n: PositionedNode): boolean {\n if (n.position === \"absolute\") return true;\n if (n.zIndex !== undefined) return true;\n return hasNegativeMargin(n.margin);\n}\n\nfunction hasNegativeMargin(margin: PositionedNode[\"margin\"]): boolean {\n if (margin === undefined) return false;\n if (typeof margin === \"number\") return margin < 0;\n return [margin.top, margin.right, margin.bottom, margin.left].some(\n (v) => (v ?? 0) < 0,\n );\n}\n\nfunction describeNode(n: PositionedNode): string {\n const tag = getNodeDef(n.type).tagName;\n return n.id ? `<${tag} id=\"${n.id}\">` : `<${tag}>`;\n}\n\nfunction describeChild(n: PositionedNode, index: number): string {\n return `${describeNode(n)}[${index}]`;\n}\n\nfunction childPath(\n parentPath: string,\n child: PositionedNode,\n index: number,\n): string {\n return `${parentPath} > ${describeChild(child, index)}`;\n}\n\nfunction fmt(v: number): number {\n return Math.round(v * 10) / 10;\n}\n\nfunction fmtRect(n: { x: number; y: number; w: number; h: number }): string {\n return `x=${fmt(n.x)}, y=${fmt(n.y)}, w=${fmt(n.w)}, h=${fmt(n.h)}`;\n}\n"],"mappings":"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"}
|