|
| 1 | +import { describe, it, expect } from "vitest"; |
| 2 | +import JSZip from "jszip"; |
| 3 | +import { parsePptx } from "../index"; |
| 4 | +import type { SlideElement } from "@/lib/types"; |
| 5 | + |
| 6 | +/** |
| 7 | + * `<a:grpFill/>` means "paint with the enclosing group's fill". Decorative |
| 8 | + * line-art (e.g. the swoosh graphic on a title slide) is commonly authored as |
| 9 | + * many `<a:custGeom>` segments inside one `<p:grpSp>`, with every segment |
| 10 | + * declaring `<a:grpFill/>` so they share the group's single translucent |
| 11 | + * colour. If the importer doesn't resolve grpFill the segments fall through to |
| 12 | + * transparent and the whole graphic disappears. This guards that inheritance. |
| 13 | + */ |
| 14 | +function baseZip(): JSZip { |
| 15 | + const zip = new JSZip(); |
| 16 | + zip.file( |
| 17 | + "[Content_Types].xml", |
| 18 | + `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
| 19 | +<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> |
| 20 | + <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/> |
| 21 | + <Default Extension="xml" ContentType="application/xml"/> |
| 22 | + <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/> |
| 23 | + <Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/> |
| 24 | +</Types>` |
| 25 | + ); |
| 26 | + zip.file( |
| 27 | + "_rels/.rels", |
| 28 | + `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/></Relationships>` |
| 29 | + ); |
| 30 | + zip.file( |
| 31 | + "ppt/presentation.xml", |
| 32 | + `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst><p:sldSz cx="12192000" cy="6858000"/></p:presentation>` |
| 33 | + ); |
| 34 | + zip.file( |
| 35 | + "ppt/_rels/presentation.xml.rels", |
| 36 | + `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>` |
| 37 | + ); |
| 38 | + zip.file( |
| 39 | + "ppt/slides/_rels/slide1.xml.rels", |
| 40 | + `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>` |
| 41 | + ); |
| 42 | + return zip; |
| 43 | +} |
| 44 | + |
| 45 | +function flatten(els: SlideElement[]): SlideElement[] { |
| 46 | + return els.flatMap((e) => |
| 47 | + e.type === "group" ? [e, ...flatten(e.children)] : [e] |
| 48 | + ); |
| 49 | +} |
| 50 | + |
| 51 | +const NS = |
| 52 | + `xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"`; |
| 53 | + |
| 54 | +describe("grpFill inheritance", () => { |
| 55 | + it("a <a:grpFill/> shape inherits its group's solid fill", async () => { |
| 56 | + const zip = baseZip(); |
| 57 | + // Group carries a red solid fill; its custGeom child declares <a:grpFill/> |
| 58 | + // and no fill of its own, so it must render red. |
| 59 | + zip.file( |
| 60 | + "ppt/slides/slide1.xml", |
| 61 | + `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><p:sld ${NS}><p:cSld><p:spTree><p:grpSp><p:nvGrpSpPr><p:cNvPr id="2" name="g"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="2000000" cy="2000000"/><a:chOff x="0" y="0"/><a:chExt cx="2000000" cy="2000000"/></a:xfrm><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="3" name="seg"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="1000000" cy="1000000"/></a:xfrm><a:custGeom><a:avLst/><a:gdLst/><a:rect l="0" t="0" r="0" b="0"/><a:pathLst><a:path w="1000000" h="1000000"><a:moveTo><a:pt x="0" y="0"/></a:moveTo><a:lnTo><a:pt x="1000000" y="0"/></a:lnTo><a:lnTo><a:pt x="1000000" y="1000000"/></a:lnTo><a:close/></a:path></a:pathLst></a:custGeom><a:grpFill/><a:ln><a:noFill/></a:ln></p:spPr></p:sp></p:grpSp></p:spTree></p:cSld></p:sld>` |
| 62 | + ); |
| 63 | + |
| 64 | + const buffer = await zip.generateAsync({ type: "arraybuffer" }); |
| 65 | + const deck = await parsePptx(buffer); |
| 66 | + const shape = flatten(deck.slides[0].elements).find( |
| 67 | + (e) => e.type === "shape" |
| 68 | + ); |
| 69 | + expect(shape).toBeTruthy(); |
| 70 | + if (shape && shape.type === "shape") { |
| 71 | + // Inherited the group's red fill rather than collapsing to transparent. |
| 72 | + expect(String(shape.fill).toUpperCase()).toContain("FF0000"); |
| 73 | + // The custGeom silhouette is preserved so it draws as line-art, not a box. |
| 74 | + expect(shape.path).toBeTruthy(); |
| 75 | + } |
| 76 | + }); |
| 77 | +}); |
0 commit comments