diff --git a/.changeset/grpfill-inheritance.md b/.changeset/grpfill-inheritance.md
new file mode 100644
index 0000000..39a1e88
--- /dev/null
+++ b/.changeset/grpfill-inheritance.md
@@ -0,0 +1,17 @@
+---
+"@textcortex/slidewise": patch
+---
+
+fix(pptx): render `` shapes by inheriting the group's fill
+
+Decorative line-art (e.g. the swoosh graphic on a title slide) is often
+authored as many `` segments inside a single ``, with every
+segment declaring `` so they share the group's one translucent
+colour. `parsePptx` had no `grpFill` branch, so each segment fell through to
+`transparent` and the entire graphic disappeared on import — present in the
+source but invisible in the deck.
+
+The group's resolved fill is now threaded down to descendants, and a shape with
+`` paints with it (walking up the group chain when an inner group
+defines no fill of its own). Geometry was already preserved; only the paint was
+being dropped.
diff --git a/packages/slidewise/src/lib/pptx/__tests__/grpfill.test.ts b/packages/slidewise/src/lib/pptx/__tests__/grpfill.test.ts
new file mode 100644
index 0000000..3316bdd
--- /dev/null
+++ b/packages/slidewise/src/lib/pptx/__tests__/grpfill.test.ts
@@ -0,0 +1,77 @@
+import { describe, it, expect } from "vitest";
+import JSZip from "jszip";
+import { parsePptx } from "../index";
+import type { SlideElement } from "@/lib/types";
+
+/**
+ * `` means "paint with the enclosing group's fill". Decorative
+ * line-art (e.g. the swoosh graphic on a title slide) is commonly authored as
+ * many `` segments inside one ``, with every segment
+ * declaring `` so they share the group's single translucent
+ * colour. If the importer doesn't resolve grpFill the segments fall through to
+ * transparent and the whole graphic disappears. This guards that inheritance.
+ */
+function baseZip(): JSZip {
+ const zip = new JSZip();
+ zip.file(
+ "[Content_Types].xml",
+ `
+
+
+
+
+
+`
+ );
+ zip.file(
+ "_rels/.rels",
+ ``
+ );
+ zip.file(
+ "ppt/presentation.xml",
+ ``
+ );
+ zip.file(
+ "ppt/_rels/presentation.xml.rels",
+ ``
+ );
+ zip.file(
+ "ppt/slides/_rels/slide1.xml.rels",
+ ``
+ );
+ return zip;
+}
+
+function flatten(els: SlideElement[]): SlideElement[] {
+ return els.flatMap((e) =>
+ e.type === "group" ? [e, ...flatten(e.children)] : [e]
+ );
+}
+
+const NS =
+ `xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"`;
+
+describe("grpFill inheritance", () => {
+ it("a shape inherits its group's solid fill", async () => {
+ const zip = baseZip();
+ // Group carries a red solid fill; its custGeom child declares
+ // and no fill of its own, so it must render red.
+ zip.file(
+ "ppt/slides/slide1.xml",
+ ``
+ );
+
+ const buffer = await zip.generateAsync({ type: "arraybuffer" });
+ const deck = await parsePptx(buffer);
+ const shape = flatten(deck.slides[0].elements).find(
+ (e) => e.type === "shape"
+ );
+ expect(shape).toBeTruthy();
+ if (shape && shape.type === "shape") {
+ // Inherited the group's red fill rather than collapsing to transparent.
+ expect(String(shape.fill).toUpperCase()).toContain("FF0000");
+ // The custGeom silhouette is preserved so it draws as line-art, not a box.
+ expect(shape.path).toBeTruthy();
+ }
+ });
+});
diff --git a/packages/slidewise/src/lib/pptx/pptxToDeck.ts b/packages/slidewise/src/lib/pptx/pptxToDeck.ts
index 49ffec4..0704557 100644
--- a/packages/slidewise/src/lib/pptx/pptxToDeck.ts
+++ b/packages/slidewise/src/lib/pptx/pptxToDeck.ts
@@ -975,6 +975,13 @@ interface GroupTransform {
b: number;
c: number;
d: number;
+ /**
+ * The enclosing ``'s resolved fill, threaded down so descendant
+ * shapes that declare `` ("inherit my fill from the group") can
+ * paint with it. Undefined at the slide root and inside groups that define
+ * no fill of their own. See the `p:grpSp` branch in {@link parseSpTree}.
+ */
+ groupFill?: string;
}
function identityTransform(): GroupTransform {
@@ -1046,7 +1053,13 @@ async function parseSpTree(
}
} else if (tag === "p:grpSp") {
const inner = composeGroupTransform(node, outer);
- const children = await parseSpTree(node, ctx, inner);
+ // Resolve this group's own fill so descendant shapes that use
+ // `` (inherit-from-group) can paint with it. A group that
+ // omits a fill inherits the enclosing group's, matching PowerPoint's
+ // walk up the group chain.
+ const groupFill =
+ extractShapeFill(node?.["p:grpSpPr"], ctx.theme) ?? outer.groupFill;
+ const children = await parseSpTree(node, ctx, { ...inner, groupFill });
if (!children.length) continue;
const group = buildGroupElement(node, children, ctx, outer);
// Register the whole `` so an unedited group round-trips
@@ -1367,9 +1380,16 @@ async function parseSpOrText(
// it carries the actual art. Resolved to a url("data:…") the renderer
// paints into the shape (clipped to its custGeom path when present).
const blipFill = await extractShapeBlipFill(spPr, ctx);
+ // `` means "paint with the enclosing group's fill". The group's
+ // resolved fill is threaded in via `outer.groupFill`; without this the shape
+ // falls through to transparent and disappears (e.g. decorative custGeom
+ // line-art whose every segment inherits one translucent group colour).
+ const grpFill =
+ spPr?.["a:grpFill"] !== undefined ? outer.groupFill : undefined;
const fillColor =
blipFill
?? extractShapeFill(spPr, ctx.theme)
+ ?? grpFill
?? (phSpPr ? extractShapeFill(phSpPr, ctx.theme) : undefined)
?? resolveStyleFillRef(sp, ctx)
?? "transparent";