chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Background renderer — resolves and applies slide/layout/master backgrounds.
|
||||
*/
|
||||
|
||||
import type { RelEntry } from '../parser/rel-parser'
|
||||
import type { SafeXmlNode } from '../parser/xml-parser'
|
||||
import { hexToRgb } from '../utils/color'
|
||||
import { getOrCreateBlobUrl, resolveMediaPath } from '../utils/media'
|
||||
import type { RenderContext } from './render-context'
|
||||
import { resolveColor, resolveFill } from './style-resolver'
|
||||
|
||||
const COLOR_NODE_NAMES = new Set([
|
||||
'srgbClr',
|
||||
'schemeClr',
|
||||
'sysClr',
|
||||
'prstClr',
|
||||
'hslClr',
|
||||
'scrgbClr',
|
||||
])
|
||||
|
||||
/**
|
||||
* Check whether a node contains a supported OOXML color node.
|
||||
*/
|
||||
function hasColorNode(node: SafeXmlNode): boolean {
|
||||
if (COLOR_NODE_NAMES.has(node.localName)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return node.allChildren().some((child) => COLOR_NODE_NAMES.has(child.localName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite a semi-transparent color on white so the result is always opaque.
|
||||
* This prevents the slide background from becoming see-through when embedded
|
||||
* in containers with dark backgrounds (e.g. e2e-compare panels).
|
||||
*/
|
||||
function compositeOnWhite(r: number, g: number, b: number, a: number): string {
|
||||
const cr = Math.round(r * a + 255 * (1 - a))
|
||||
const cg = Math.round(g * a + 255 * (1 - a))
|
||||
const cb = Math.round(b * a + 255 * (1 - a))
|
||||
return `rgb(${cr},${cg},${cb})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the background for a slide onto the container element.
|
||||
*
|
||||
* Background priority: slide.background -> layout.background -> master.background.
|
||||
* The first found background is used.
|
||||
*/
|
||||
export function renderBackground(ctx: RenderContext, container: HTMLElement): void {
|
||||
// Find the first available background in the inheritance chain,
|
||||
// and track which rels map to use for resolving image references
|
||||
let bgNode: SafeXmlNode | undefined
|
||||
let bgRels: Map<string, RelEntry> = ctx.slide.rels
|
||||
|
||||
if (ctx.slide.background) {
|
||||
bgNode = ctx.slide.background
|
||||
bgRels = ctx.slide.rels
|
||||
} else if (ctx.layout.background) {
|
||||
bgNode = ctx.layout.background
|
||||
bgRels = ctx.layout.rels
|
||||
} else if (ctx.master.background) {
|
||||
bgNode = ctx.master.background
|
||||
bgRels = ctx.master.rels
|
||||
}
|
||||
|
||||
if (!bgNode) {
|
||||
container.style.backgroundColor = '#FFFFFF'
|
||||
return
|
||||
}
|
||||
|
||||
// Parse p:bg > p:bgPr
|
||||
const bgPr = bgNode.child('bgPr')
|
||||
if (bgPr.exists()) {
|
||||
renderBgPr(bgPr, ctx, container, bgRels)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse p:bg > p:bgRef (theme reference)
|
||||
const bgRef = bgNode.child('bgRef')
|
||||
if (bgRef.exists()) {
|
||||
renderBgRef(bgRef, ctx, container)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback
|
||||
container.style.backgroundColor = '#FFFFFF'
|
||||
}
|
||||
|
||||
/**
|
||||
* Render background from bgPr (background properties).
|
||||
* Contains direct fill definitions: solidFill, gradFill, blipFill, etc.
|
||||
*/
|
||||
function renderBgPr(
|
||||
bgPr: SafeXmlNode,
|
||||
ctx: RenderContext,
|
||||
container: HTMLElement,
|
||||
rels?: Map<string, RelEntry>
|
||||
): void {
|
||||
// solidFill
|
||||
const solidFill = bgPr.child('solidFill')
|
||||
if (solidFill.exists()) {
|
||||
const { color, alpha } = resolveColor(solidFill, ctx)
|
||||
const hex = color.startsWith('#') ? color : `#${color}`
|
||||
if (alpha < 1) {
|
||||
const { r, g, b } = hexToRgb(hex)
|
||||
container.style.backgroundColor = compositeOnWhite(r, g, b, alpha)
|
||||
} else {
|
||||
container.style.backgroundColor = hex
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// gradFill
|
||||
const gradFill = bgPr.child('gradFill')
|
||||
if (gradFill.exists()) {
|
||||
const css = resolveFill(bgPr, ctx)
|
||||
if (css) {
|
||||
container.style.background = css
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// blipFill (image background)
|
||||
const blipFill = bgPr.child('blipFill')
|
||||
if (blipFill.exists()) {
|
||||
renderBlipBackground(blipFill, ctx, container, rels)
|
||||
return
|
||||
}
|
||||
|
||||
// noFill — still render as white; the slide is a self-contained element
|
||||
// and transparent backgrounds break when embedded in dark containers
|
||||
const noFill = bgPr.child('noFill')
|
||||
if (noFill.exists()) {
|
||||
container.style.backgroundColor = '#FFFFFF'
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render background from bgRef (theme format scheme reference).
|
||||
* Simplified: just resolve the color from the reference.
|
||||
*/
|
||||
function renderBgRef(bgRef: SafeXmlNode, ctx: RenderContext, container: HTMLElement): void {
|
||||
// bgRef may contain a color child (schemeClr, srgbClr, etc.)
|
||||
if (!hasColorNode(bgRef)) {
|
||||
container.style.backgroundColor = '#FFFFFF'
|
||||
return
|
||||
}
|
||||
|
||||
const { color, alpha } = resolveColor(bgRef, ctx)
|
||||
const hex = color.startsWith('#') ? color : `#${color}`
|
||||
if (alpha < 1) {
|
||||
const { r, g, b } = hexToRgb(hex)
|
||||
container.style.backgroundColor = compositeOnWhite(r, g, b, alpha)
|
||||
} else {
|
||||
container.style.backgroundColor = hex
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a blip (image) fill as a CSS background.
|
||||
*/
|
||||
function renderBlipBackground(
|
||||
blipFill: SafeXmlNode,
|
||||
ctx: RenderContext,
|
||||
container: HTMLElement,
|
||||
rels?: Map<string, RelEntry>
|
||||
): void {
|
||||
const blip = blipFill.child('blip')
|
||||
const embedId = blip.attr('embed') ?? blip.attr('r:embed')
|
||||
|
||||
if (!embedId) return
|
||||
|
||||
// Resolve image from rels + media (use provided rels or fall back to slide rels)
|
||||
const relsMap = rels ?? ctx.slide.rels
|
||||
const rel = relsMap.get(embedId)
|
||||
if (!rel) return
|
||||
|
||||
const mediaPath = resolveMediaPath(rel.target)
|
||||
const data = ctx.presentation.media.get(mediaPath)
|
||||
if (!data) return
|
||||
|
||||
const url = getOrCreateBlobUrl(mediaPath, data, ctx.mediaUrlCache)
|
||||
|
||||
container.style.backgroundImage = `url("${url}")`
|
||||
|
||||
// Check for stretch or tile mode
|
||||
const stretch = blipFill.child('stretch')
|
||||
if (stretch.exists()) {
|
||||
container.style.backgroundSize = 'cover'
|
||||
container.style.backgroundPosition = 'center'
|
||||
container.style.backgroundRepeat = 'no-repeat'
|
||||
|
||||
// Parse fillRect for non-uniform stretch
|
||||
const fillRect = stretch.child('fillRect')
|
||||
if (fillRect.exists()) {
|
||||
// fillRect specifies insets — if all zero, it's a full stretch
|
||||
container.style.backgroundSize = '100% 100%'
|
||||
}
|
||||
}
|
||||
|
||||
const tile = blipFill.child('tile')
|
||||
if (tile.exists()) {
|
||||
container.style.backgroundRepeat = 'repeat'
|
||||
container.style.backgroundSize = 'auto'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user