Files
simstudioai--sim/apps/sim/lib/pptx-renderer/model/layout.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

187 lines
5.8 KiB
TypeScript

/**
* 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<string, string>
background?: SafeXmlNode
placeholders: PlaceholderEntry[]
spTree: SafeXmlNode
rels: Map<string, import('../parser/rel-parser').RelEntry>
/** 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<string, string> | 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,
}
}