/** * Slide layout parser — extracts color map override, background, * and placeholder shapes from a p:sldLayout XML. */ import { emuToPx } from '../parser/units' import type { SafeXmlNode } from '../parser/xml-parser' import { isPlaceholder, parseAllAttributes } from './xml-helpers' interface PlaceholderXfrm { position: { x: number; y: number } size: { w: number; h: number } } export interface PlaceholderEntry { node: SafeXmlNode /** When placeholder is inside a group, position/size in slide space (px). */ absoluteXfrm?: PlaceholderXfrm } export interface LayoutData { colorMapOverride?: Map background?: SafeXmlNode placeholders: PlaceholderEntry[] spTree: SafeXmlNode rels: Map /** When false, shapes from the slide master should NOT be rendered on this layout. */ showMasterSp: boolean } function getShapeXfrmInEmu( node: SafeXmlNode ): { offX: number; offY: number; cx: number; cy: number } | null { const spPr = node.child('spPr') if (!spPr.exists()) return null const xfrm = spPr.child('xfrm') if (!xfrm.exists()) return null const off = xfrm.child('off') const ext = xfrm.child('ext') const offX = off.numAttr('x') ?? 0 const offY = off.numAttr('y') ?? 0 const cx = ext.numAttr('cx') ?? 0 const cy = ext.numAttr('cy') ?? 0 return { offX, offY, cx, cy } } function getGroupXfrmInEmu(grpSp: SafeXmlNode): { offX: number offY: number cx: number cy: number chOffX: number chOffY: number chExtCx: number chExtCy: number } | null { const grpSpPr = grpSp.child('grpSpPr') if (!grpSpPr.exists()) return null const xfrm = grpSpPr.child('xfrm') if (!xfrm.exists()) return null const off = xfrm.child('off') const ext = xfrm.child('ext') const chOff = xfrm.child('chOff') const chExt = xfrm.child('chExt') const offX = off.numAttr('x') ?? 0 const offY = off.numAttr('y') ?? 0 const cx = ext.numAttr('cx') ?? 0 const cy = ext.numAttr('cy') ?? 0 // OOXML: when chOff/chExt omitted, child box equals group box (chOff=0,0 and chExt=ext) const chOffX = chOff.exists() ? (chOff.numAttr('x') ?? 0) : 0 const chOffY = chOff.exists() ? (chOff.numAttr('y') ?? 0) : 0 const chExtCx = chExt.exists() ? (chExt.numAttr('cx') ?? cx) : cx const chExtCy = chExt.exists() ? (chExt.numAttr('cy') ?? cy) : cy return { offX, offY, cx, cy, chOffX, chOffY, chExtCx: chExtCx > 0 ? chExtCx : 1, chExtCy: chExtCy > 0 ? chExtCy : 1, } } /** * Recursively collect placeholders; when inside a group, compute position/size in slide space. */ function extractPlaceholdersRecursive( spTree: SafeXmlNode, groupTransform: { offX: number; offY: number; scaleX: number; scaleY: number } | null ): PlaceholderEntry[] { const out: PlaceholderEntry[] = [] for (const child of spTree.allChildren()) { if (child.localName === 'grpSp') { const gx = getGroupXfrmInEmu(child) if (gx && gx.chExtCx > 0 && gx.chExtCy > 0) { const scaleX = gx.cx / gx.chExtCx const scaleY = gx.cy / gx.chExtCy const baseOffX = gx.offX - gx.chOffX * scaleX const baseOffY = gx.offY - gx.chOffY * scaleY const nextTransform = groupTransform ? { offX: groupTransform.offX + baseOffX * groupTransform.scaleX, offY: groupTransform.offY + baseOffY * groupTransform.scaleY, scaleX: groupTransform.scaleX * scaleX, scaleY: groupTransform.scaleY * scaleY, } : { offX: baseOffX, offY: baseOffY, scaleX, scaleY } const nested = extractPlaceholdersRecursive(child, nextTransform) out.push(...nested) } else { out.push(...extractPlaceholdersRecursive(child, groupTransform)) } continue } if (!isPlaceholder(child)) continue const sx = getShapeXfrmInEmu(child) if (!sx) { out.push({ node: child }) continue } if (groupTransform) { const absOffX = groupTransform.offX + sx.offX * groupTransform.scaleX const absOffY = groupTransform.offY + sx.offY * groupTransform.scaleY const absCx = sx.cx * groupTransform.scaleX const absCy = sx.cy * groupTransform.scaleY out.push({ node: child, absoluteXfrm: { position: { x: emuToPx(absOffX), y: emuToPx(absOffY) }, size: { w: emuToPx(absCx), h: emuToPx(absCy) }, }, }) } else { out.push({ node: child, absoluteXfrm: { position: { x: emuToPx(sx.offX), y: emuToPx(sx.offY) }, size: { w: emuToPx(sx.cx), h: emuToPx(sx.cy) }, }, }) } } return out } /** * Parse a slide layout XML root (`p:sldLayout`) into LayoutData. */ export function parseLayout(root: SafeXmlNode): LayoutData { const cSld = root.child('cSld') // --- Background --- const bg = cSld.child('bg') const background = bg.exists() ? bg : undefined // --- Shape tree --- const spTree = cSld.child('spTree') // --- Color map override --- let colorMapOverride: Map | undefined const clrMapOvr = root.child('clrMapOvr') if (clrMapOvr.exists()) { const overrideMapping = clrMapOvr.child('overrideClrMapping') if (overrideMapping.exists()) { colorMapOverride = parseAllAttributes(overrideMapping) } } // --- Placeholders (recursive so we find title/body inside grpSp; resolve position in slide space) --- const placeholders = extractPlaceholdersRecursive(spTree, null) // --- showMasterSp: if "0", master shapes should not be rendered for this layout --- const showMasterSpAttr = root.attr('showMasterSp') const showMasterSp = showMasterSpAttr !== '0' return { colorMapOverride, background, placeholders, spTree, rels: new Map(), // populated later by buildPresentation showMasterSp, } }