Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0b271ef
fix(desktop): preserve layered PPTX backgrounds
Siri-Ray Aug 13, 2026
8cd874e
fix(desktop): constrain layered PPTX rasterization
Siri-Ray Aug 13, 2026
fad335c
fix(desktop): preserve layered panel edge cases
Siri-Ray Aug 13, 2026
d3d0b3b
fix(desktop): preserve pseudo and masked PPTX layers
Siri-Ray Aug 13, 2026
6b17766
fix(desktop): preserve composed PPTX background layers
Siri-Ray Aug 13, 2026
ad87f34
test(desktop): batch layered PPTX export probe
Siri-Ray Aug 13, 2026
bb0e6b7
test(desktop): consolidate layered PPTX probe slide
Siri-Ray Aug 13, 2026
7af0559
test(desktop): terminate layered PPTX probe cleanly
Siri-Ray Aug 13, 2026
4dac1ab
fix(desktop): preserve layered PPTX capture visibility and order
Siri-Ray Aug 13, 2026
1ea94ea
test(desktop): expose layered PPTX probe stage
Siri-Ray Aug 13, 2026
4eb36cb
test(desktop): paint layered PPTX probe window
Siri-Ray Aug 13, 2026
e689e85
fix(desktop): layer slide backgrounds below pseudos
Siri-Ray Aug 13, 2026
2ce1302
fix(desktop): keep layered PPTX captures background-only
Siri-Ray Aug 13, 2026
a2a0c4d
fix(desktop): flatten blended PPTX backgrounds
Siri-Ray Aug 13, 2026
a176a78
fix(desktop): preserve blended PPTX backdrops
Siri-Ray Aug 13, 2026
7af7560
fix(desktop): preserve composited PPTX capture fidelity
Siri-Ray Aug 13, 2026
4a87b3c
test(desktop): tolerate Chromium color rounding
Siri-Ray Aug 13, 2026
13f1f6d
fix(desktop): preserve nested blend backdrops
Siri-Ray Aug 13, 2026
daab515
fix(desktop): honor paint order and pseudo masks
Siri-Ray Aug 13, 2026
5a0cf83
fix(desktop): preserve layered PPTX compositing boundaries
Siri-Ray Aug 13, 2026
39d79a6
fix(desktop): preserve native PPTX foreground layers
Siri-Ray Aug 13, 2026
0e79094
fix(desktop): preserve PPTX backdrop and pseudo fills
Siri-Ray Aug 13, 2026
3c134e2
fix(desktop): preserve PPTX compositor root paint
Siri-Ray Aug 13, 2026
de99f23
fix(desktop): capture composited pseudo foregrounds
Siri-Ray Aug 13, 2026
19a24a3
test(desktop): normalize composited pseudo capture scale
Siri-Ray Aug 13, 2026
b1dff83
fix(desktop): preserve nested PPTX compositor paint
Siri-Ray Aug 13, 2026
5c5630d
fix(desktop): preserve whole-paint PPTX captures
Siri-Ray Aug 14, 2026
4b73cad
fix(desktop): capture unsupported PPTX foreground effects
Siri-Ray Aug 14, 2026
58c1341
fix(desktop): preserve layered PPTX capture fidelity
Siri-Ray Aug 14, 2026
f7e6686
fix(desktop): preserve PPTX compositor roots
Siri-Ray Aug 14, 2026
a330761
fix(desktop): bound layered PPTX paint groups
Siri-Ray Aug 14, 2026
9cfcd85
fix(desktop): preserve clipped PPTX blend backdrops
Siri-Ray Aug 14, 2026
c2836fa
fix(desktop): capture clipped PPTX foreground groups
Siri-Ray Aug 14, 2026
1781778
fix(desktop): recheck clipped PPTX backdrop order
Siri-Ray Aug 14, 2026
ab689ae
test(desktop): map PPTX captures to exported media
Siri-Ray Aug 14, 2026
4a05094
test(desktop): make PPTX probe scale DPR-aware
Siri-Ray Aug 14, 2026
d778871
fix(desktop): retain clipped PPTX backdrop order
Siri-Ray Aug 14, 2026
ddc7933
fix(desktop): retain ancestor-clipped PPTX backdrops
Siri-Ray Aug 14, 2026
2dbbf82
fix(desktop): preserve nested PPTX blend backdrops
Siri-Ray Aug 14, 2026
8d2d70e
Merge main into agent/fix-editable-pptx-layered-backgrounds
Siri-Ray Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions apps/desktop/src/main/deck-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,12 @@ export async function runDomToPptx(slideSelector: string): Promise<{ b64?: strin
function ensureExplicitSlideBackgrounds(slides: HTMLElement[]): void {
for (const slide of slides) {
slide.querySelectorAll(":scope > [data-od-pptx-bg]").forEach((el) => el.remove());
// preserveLayeredGradientBackgrounds owns supported layered backgrounds
// authored directly on a slide. Adding the usual fallback shim as well
// would export the same semi-transparent texture twice.
if (hasRasterizableLayeredGradientBackground(getComputedStyle(slide).backgroundImage || "")) {
continue;
}
const background = effectiveBackgroundStyle(slide);
if (!background) continue;

Expand Down Expand Up @@ -1096,6 +1102,147 @@ export async function runDomToPptx(slideSelector: string): Promise<{ b64?: strin
}
}

function splitCssBackgroundLayers(input: string): string[] {
const layers: string[] = [];
let current = "";
let depth = 0;
let quote = "";
let escaped = false;
for (const char of input) {
if (escaped) {
current += char;
escaped = false;
continue;
}
if (char === "\\") {
current += char;
escaped = true;
continue;
}
if (quote) {
current += char;
if (char === quote) quote = "";
continue;
}
if (char === '"' || char === "'") {
current += char;
quote = char;
continue;
}
if (char === "(") depth += 1;
else if (char === ")") depth = Math.max(0, depth - 1);
if (char === "," && depth === 0) {
if (current.trim()) layers.push(current.trim());
current = "";
} else {
current += char;
}
}
if (current.trim()) layers.push(current.trim());
return layers;
}

function hasRasterizableLayeredGradientBackground(input: string): boolean {
const layers = splitCssBackgroundLayers(input);
if (layers.length < 2) return false;
// Keep this allowlist aligned with html2canvas 1.4.1's
// SUPPORTED_IMAGE_FUNCTIONS. In particular, repeating and conic gradients
// are discarded by its clone parser and must remain on the authored node.
const supportedGradient =
/^(?:(?:-(?:moz|ms|o|webkit)-)?(?:linear|radial)-gradient|-webkit-gradient)\(/i;
return layers.every((layer) => supportedGradient.test(layer));
}

function hasTextBackgroundClip(input: string): boolean {
return splitCssBackgroundLayers(input).some((layer) => layer.toLowerCase() === "text");
}

function preserveLayeredGradientBackgrounds(slides: HTMLElement[]): void {
const slideElements = new Set(slides);
const elements = new Set<HTMLElement>();
for (const slide of slides) {
elements.add(slide);
slide.querySelectorAll<HTMLElement>("*").forEach((el) => elements.add(el));
Comment thread
Siri-Ray marked this conversation as resolved.
Comment thread
Siri-Ray marked this conversation as resolved.
}

for (const element of elements) {
const style = getComputedStyle(element);
if (
!hasRasterizableLayeredGradientBackground(style.backgroundImage || "") ||
hasTextBackgroundClip(style.backgroundClip || "") ||
hasTextBackgroundClip(style.webkitBackgroundClip || "")
) {
continue;
}
const isStaticNestedElement = style.position === "static" && !slideElements.has(element);

// dom-to-pptx's native gradient parser assumes one linear-gradient and
// greedily merges layered gradients into one invalid SVG. Its custom-
// element path already uses html2canvas, so move only the background to
// a background-only custom element. The converter rasterizes that layer
// with Chromium while continuing to emit the authored children as native
// editable text and shapes.
const background = document.createElement("od-pptx-layered-background");
background.setAttribute("data-od-pptx-layered-bg", "true");
background.setAttribute("aria-hidden", "true");
background.style.setProperty("position", "absolute", "important");
background.style.setProperty("inset", "0", "important");
background.style.setProperty("z-index", "0", "important");
background.style.setProperty("pointer-events", "none", "important");
background.style.setProperty("box-sizing", "border-box", "important");
background.style.setProperty(
"padding",
`${style.paddingTop || "0px"} ${style.paddingRight || "0px"} ${style.paddingBottom || "0px"} ${style.paddingLeft || "0px"}`,
"important",
);
background.style.setProperty(
"border-width",
`${style.borderTopWidth || "0px"} ${style.borderRightWidth || "0px"} ${style.borderBottomWidth || "0px"} ${style.borderLeftWidth || "0px"}`,
"important",
);
background.style.setProperty("border-style", "solid", "important");
background.style.setProperty("border-color", "transparent", "important");
background.style.setProperty("border-radius", style.borderRadius || "0px", "important");
background.style.setProperty("background-color", style.backgroundColor, "important");
background.style.setProperty("background-image", style.backgroundImage, "important");
background.style.setProperty("background-position", style.backgroundPosition, "important");
background.style.setProperty("background-size", style.backgroundSize, "important");
background.style.setProperty("background-repeat", style.backgroundRepeat, "important");
background.style.setProperty("background-origin", style.backgroundOrigin, "important");
background.style.setProperty("background-clip", style.backgroundClip, "important");
Comment thread
Siri-Ray marked this conversation as resolved.
Outdated
Comment thread
Siri-Ray marked this conversation as resolved.
Outdated

if (isStaticNestedElement) {
// A static panel and its absolutely positioned descendants share the
// same outer containing block. Anchor only the capture child to the
// panel's measured border box so the authored panel never becomes a
// new containing block.
background.style.setProperty("inset", "auto", "important");
background.style.setProperty("left", `${element.offsetLeft}px`, "important");
background.style.setProperty("top", `${element.offsetTop}px`, "important");
background.style.setProperty("width", `${element.offsetWidth}px`, "important");
background.style.setProperty("height", `${element.offsetHeight}px`, "important");
} else {
// ensureExplicitSlideBackgrounds already establishes this containing-
// block contract for slides; positioned authored elements already own
// the absolutely positioned capture child.
if (style.position === "static") element.style.setProperty("position", "relative", "important");
Array.from(element.children).forEach((child) => {
Comment thread
Siri-Ray marked this conversation as resolved.
Outdated
const childStyle = getComputedStyle(child);
const childElement = child as HTMLElement;
if (childStyle.position === "static") {
childElement.style.setProperty("position", "relative", "important");
}
if (childStyle.zIndex === "auto") {
childElement.style.setProperty("z-index", "1", "important");
}
});
}

element.style.setProperty("background-image", "none", "important");
element.prepend(background);
}
}

function stabilizeLargeSingleLineText(slides: HTMLElement[]): void {
for (const slide of slides) {
slide.querySelectorAll<HTMLElement>("*").forEach((el) => {
Expand Down Expand Up @@ -1171,6 +1318,7 @@ export async function runDomToPptx(slideSelector: string): Promise<{ b64?: strin
.filter((el) => !(el as HTMLElement).closest(".mini-slide, .overview, .notes-overlay, .thumb"));
if (slides.length === 0) return { error: "no slides to export" };
ensureExplicitSlideBackgrounds(slides as HTMLElement[]);
preserveLayeredGradientBackgrounds(slides as HTMLElement[]);
stabilizeLargeSingleLineText(slides as HTMLElement[]);
promoteCjkTypefaces(slides as HTMLElement[]);
// dom-to-pptx assumes `node.className` is a string, but SVG elements expose
Expand Down
Loading
Loading