# Corporate Template Adoption Workflow for extracting a corporate design system from a `.pptx` template and applying it to a deck built with pptxgenjs. ## 1. Extract Theme Colors & Fonts python-pptx's theme API (`slide_master.element.find(...)`) can raise `AttributeError`. Parse the XML directly via `zipfile` instead: ```python import zipfile, xml.etree.ElementTree as ET with zipfile.ZipFile('template.pptx', 'r') as z: theme_files = [f for f in z.namelist() if 'theme' in f and f.endswith('.xml')] for tf in theme_files: xml = z.read(tf) root = ET.fromstring(xml) ns = {'a': 'http://schemas.openxmlformats.org/drawingml/2006/main'} # Colors for clr in root.findall('.//a:srgbClr', ns): print(f"Color: #{clr.get('val')}") # Fonts for latin in root.findall('.//a:latin', ns): print(f"Font: {latin.get('typeface')}") ``` ## 2. Extract Embedded Images (Logos, Icons) ```python import zipfile, os with zipfile.ZipFile('template.pptx', 'r') as z: images = [f for f in z.namelist() if f.startswith('ppt/media/') and f.endswith(('.png', '.jpg', '.jpeg'))] for img in images: data = z.read(img) fname = os.path.basename(img) with open(f'/tmp/{fname}', 'wb') as f: f.write(data) ``` Inspect each with `vision_analyze`. Logos may be white-on-transparent and invisible on white backgrounds — if so, build a text+shape logo instead. ## 3. Render Template Slides for Visual Reference ```bash python3 ~/.hermes/skills/productivity/powerpoint/scripts/office/soffice.py \ --headless --convert-to pdf template.pptx mkdir -p tpl_qa pdftoppm -jpeg -r 100 -f 1 -l 5 template.pdf tpl_qa/tplate pdftoppm -jpeg -r 100 -f 32 -l 39 template.pdf tpl_qa/tplate ``` Use `vision_analyze` on selected template slides to understand: - Background color (white, dark, gradient?) - Card style (dark/light, shadows, rounded/sharp corners) - Accent bar style (vertical/horizontal, thickness, position) - Logo placement (usually top right) - Typography (font family, sizes, weights) - Decorative elements (hexagons, circles, patterns) ## 4. Define Custom Layout Dimensions If the template uses non-standard dimensions (e.g., 13.33" × 7.50"), define a custom layout in pptxgenjs **before adding any slides**: ```javascript pres.defineLayout({ name: "noris_wide", width: 13.33, height: 7.5 }); pres.layout = "noris_wide"; // Must set AFTER defineLayout ``` Common corporate template dimensions: - 13.33" × 7.50" — widescreen 16:9 (noris, many corporate templates) - 10" × 5.625" — LAYOUT_16x9 (pptxgenjs default) All coordinate values in helper functions (logo position, card widths, etc.) must be recalculated for the new dimensions — a logo at `x: 8.5` on a 10"-wide slide needs `x: 11.13` on a 13.33"-wide slide. ## 5. Build Design-System Constants File Create a `helpers.js` with the extracted colors, fonts, and reusable functions: ```javascript const C = { white: "FFFFFF", bgLight: "F2F2F2", // Card background petrol: "234D5B", // Primary brand color accent: "118291", // Accent teal burgundy: "9E1946", // Red accent text: "212427", // Body text textMid: "5F7D86", // Secondary text greyLt: "D3D3D3", greyMd: "A6A6A6", }; const FONT = "Open Sans"; ``` Include helpers: `addLogo()`, `slideNumber()`, `card()`, `accentBar()`, `shieldBadge()`, `title()`, `subtitle()`, `footerBar()`. ## 6. Logo Extraction — Find and Use the REAL Logo **Do not fall back to a text+shape logo approximation until you have exhausted all extraction options.** Users expect the actual corporate logo from the template, not a reconstruction. Only build a text-based logo (section 6c below) if no usable image exists after trying all techniques in sections 6a–6b. ### 6a. Identify the Logo Among Hundreds of Media Files Templates may contain 100+ images. Find the logo systematically: **Step 1 — Count image references across slides:** ```python import zipfile, xml.etree.ElementTree as ET, collections with zipfile.ZipFile('template.pptx', 'r') as z: rel_counts = collections.Counter() slide_rels = sorted([f for f in z.namelist() if f.startswith('ppt/slides/_rels/') and f.endswith('.xml.rels')]) for sf in slide_rels: tree = ET.parse(z.open(sf)) ns = {'r': 'http://schemas.openxmlformats.org/package/2006/relationships'} for rel in tree.findall('.//r:Relationship', ns): target = rel.get('Target', '') if 'media/' in target: rel_counts[target.split('/')[-1]] += 1 for img, cnt in rel_counts.most_common(10): print(f'{img:30s} appears on {cnt:3d} slides') ``` The logo typically appears on 20+ slides. Decorative elements appear on 1-4. **Step 2 — Check format and usability of top candidates:** ```python from PIL import Image import os for img_name in top_candidates: path = f'/tmp/{img_name}' if img_name.endswith('.emf'): print(f'{img_name}: EMF format — may not render in LibreOffice/pptxgenjs') # EMF files are often tiny (100-200 bytes) and unusable continue if os.path.exists(path): im = Image.open(path) print(f'{img_name}: {im.size} mode={im.mode}') ``` **Pitfall — EMF images:** The most-referenced image may be an EMF (Windows Enhanced Metafile) that LibreOffice cannot convert and pptxgenjs cannot embed. Check for PNG/JPG alternatives among the top candidates. In the noris template, `image1.emf` appeared on 26 slides but was only 188 bytes and unusable; the actual logo was `image2.png` (2268×336 RGBA), which was NOT the most-referenced image but was the largest PNG with the right aspect ratio for a logo. **Step 3 — Inspect candidate images with `vision_analyze`:** Send each top candidate to `vision_analyze` with: > "Is this a company logo? Describe the text, colors, and layout. Is it > suitable for white backgrounds? Dark backgrounds?" ### 6b. Multi-Part Logo Processing Some logos are single images containing multiple color variants side-by-side (e.g., "noris" in white-on-dark-box next to "network." in dark-on-transparent). To use these: **Find the split point by scanning pixel alpha:** ```python from PIL import Image img = Image.open('logo.png') w, h = img.size px = img.load() # Scan a row above the text (y=10) for the transition split_x = None found_opaque = False for x in range(w): r, g, b, a = px[x, 10] if a == 255 and r < 100: # opaque dark pixel (bg of left part) found_opaque = True elif found_opaque and a == 0: # transition to transparent split_x = x break ``` **Crop and save variants:** ```python # For white slides: use as-is (left=dark box with white text, right=dark text) img.save('logo_white_bg.png') # For dark slides: invert the RIGHT part only (dark text → white text) # Keep the LEFT part as-is (white text on dark box is already visible on dark bg) dark_ver = img.copy() dv_px = dark_ver.load() for x in range(w): for y in range(h): r, g, b, a = px[x, y] if x >= split_x and a > 0: dv_px[x, y] = (255-r, 255-g, 255-b, a) # invert right part dark_ver.save('logo_dark_bg.png') ``` **Use in pptxgenjs with correct aspect ratio:** ```javascript function addLogo(slide, w = 1.8, dark = false) { const logoPath = dark ? "logo_dark_bg.png" : "logo_white_bg.png"; const h = w / 6.74; // original aspect ratio (width/height) slide.addImage({ path: logoPath, x: 13.33 - w - 0.4, y: 0.25, w, h, }); } ``` ### 6c. Text-Based Logo (last resort only) Only when no usable logo image can be extracted after all attempts above: ```javascript function addLogo(slide, w = 1.8) { const h = 0.32; const x = 13.33 - w - 0.4; const y = 0.25; const boxW = w * 0.35; slide.addShape("roundRect", { x, y, w: boxW, h, fill: { color: C.petrol }, line: { type: "none" }, rectRadius: 0.03, }); slide.addText("noris", { x, y, w: boxW, h, fontSize: 13, fontFace: FONT, bold: true, color: C.white, align: "center", valign: "middle", margin: 0, }); slide.addText("network.", { x: x + boxW + 0.05, y, w: w - boxW - 0.05, h, fontSize: 13, fontFace: FONT, bold: true, color: C.petrol, align: "left", valign: "middle", margin: 0, }); } ``` ## 7. Non-ASCII Quote Pitfall German curly quotes (`„…"` / `»…«`) cause `SyntaxError: Unexpected identifier` in Node.js string literals. The parser treats `"` as a string terminator and the following text as a bare identifier. **Wrong:** ```javascript { text: "Milliarden von Parametern speichern das „Wissen" des Modells", ... } ``` **Right** — use Unicode escapes: ```javascript { text: "Milliarden von Parametern speichern das \u201EWissen\u201C des Modells", ... } ``` The `write_file` linter catches this on save. Always check the lint output after writing JS files with German text. ## 8. Design-Conformance QA After rebuilding all slides, verify each slide matches the template's design language. Use a design-specific QA prompt: ``` Check this slide for: 1) [company] corporate design (white bg, light grey cards, petrol #XXXXX accents, [Font] font) 2) logo visible top right 3) no text overlaps 4) no layout issues 5) readable text. Reply CLEAN or list ISSUES. ``` Run all slides through this check — a slide can be layout-clean but design-nonconformant (wrong background, missing logo, wrong card style). ## 9. PowerPoint Compatibility Validation (Post-Generation) **Critical:** LibreOffice and PowerPoint have different tolerance levels for malformed OOXML. A PPTX that opens perfectly in LibreOffice (and therefore passes PDF→JPEG visual QA) may be refused by PowerPoint entirely. The most common cause is **negative shape dimensions or positions** that pptxgenjs silently generates: | Source Pattern | XML Result | Effect | |---|---|---| | LINE shape from node A (low y) to node B (high y) | `cy < 0` in `` | PowerPoint refuses to open | | LINE shape from node A (right) to node B (left) | `cx < 0` in `` | PowerPoint refuses to open | | LINE with equal source/target y | `cy = 0` in `` | PowerPoint refuses to open | | OVAL at `x: -2, y: -2` (decorative, partially off-slide) | `x < 0, y < 0` in `` | PowerPoint refuses to open | ### Fix in Source Code **Option A — Fix coordinates (when the shape is meaningful):** ```javascript // LINE shapes: always use Math.abs/min const dx = dstX - srcX; const dy = dstY - srcY; s.addShape(pres.shapes.LINE, { x: Math.min(srcX, dstX), y: Math.min(srcY, dstY), w: Math.abs(dx), h: Math.max(Math.abs(dy), 1), // min 1 EMU line: { color: "CCCCCC", width: 1 }, }); // Decorative shapes: clamp to non-negative positions s.addShape(pres.shapes.OVAL, { x: 0, y: 0, w: 3, h: 3, // was x: -2, y: -2 fill: { color: "F2F2F2" }, }); ``` **Option B — Remove purely decorative shapes (often the better choice):** When decorative shapes (background circles, hexagon patterns, ambient ovals) cause negative coordinates, consider removing them entirely rather than repositioning. They serve no informational purpose and their absence improves compatibility without affecting the design. In the noris deck, decorative OVALs at `x: -2, y: -2` on the title and closing slides were deleted outright — the slides looked cleaner without them and the compatibility issue vanished. ```javascript // Simply delete the decorative shape block — no replacement needed // s.addShape(pres.shapes.OVAL, { x: -2, y: -2, ... }); // REMOVED ``` ### Validate Before Delivery After `node main.js`, run the validation script: ```bash python3 ~/.hermes/skills/productivity/visual-qa/scripts/validate_pptx_compat.py \ output.pptx ``` This checks for negative extents/offsets, malformed XML, missing required parts, broken relationship references, and missing Content_Type overrides. Exit code 0 means clean; 1 means issues found. **Add this to your standard build pipeline:** ```bash node main.js && \ python3 scripts/validate_pptx_compat.py AI_101.pptx && \ soffice --headless --convert-to pdf AI_101.pptx && \ pdftoppm -jpeg -r 150 AI_101.pdf qa/slide ``` The validation step takes <1 second and catches the #1 cause of "PowerPoint can't open the file" complaints before they happen. ## 10. Inserting New Slides into an Existing Deck When a user asks to add new content slides to an existing pptxgenjs-built deck (e.g. "ergänze Folien zu Tokens, KV-Cache, aktiven Parametern"), follow this workflow: ### Step 1: Create a New Module File Put new slide functions in a separate file (e.g. `slides_extra.js`) to avoid rewriting existing modules: ```javascript const { C, FONT, addLogo, slideNumber, card, accentBar, /* ... */ } = require("./helpers"); function slide_tokens(pres) { const s = pres.addSlide(); s.background = { color: C.white }; addLogo(s, 1.5); slideNumber(s, 13); // ← set to the FINAL slide number after insertion title(s, "Was ist ein Token?"); // ... } module.exports = { slide_tokens, /* other new slides */ }; ``` ### Step 2: Update main.js — Insert at the Right Position ```javascript const se = require("./slides_extra"); // ... s10.slide11_finetuning(pres); se.slide_transition_llm(pres); // ← new slide inserted here se.slide_tokens(pres); // ← new slide s10.slide12_llm_no_search(pres); // ← existing slide, now shifted // ... ``` ### Step 3: Renumber ALL slideNumber() Calls Every `slideNumber(s, N)` in EVERY module file must be updated to reflect the new ordering. A single off-by-one makes the deck look unprofessional. Track the mapping explicitly: ``` Old 1-11 → unchanged (1-11) New 12 → slide_transition_llm (NEW) New 13 → slide_tokens (NEW) Old 12 → slide12_llm_no_search → now 14 Old 13 → slide13_not_intelligent → now 15 Old 14 → slide14_moe → now 16 New 17 → slide_active_params (NEW) New 18 → slide_kv_cache (NEW) Old 15 → slide15_noris → now 19 Old 16 → slide16_summary → now 20 Old 17 → slide17_thanks → now 21 (no slideNumber, it's the closing slide) ``` ### Step 4: Update Agenda and Summary Slides - **Agenda**: add new entries, adjust numbering, reduce font size / spacing if the list grows beyond 9 items (12 items needs `fontSize: 16`, `y_step: 0.49`) - **Summary/Takeaways**: add new key points, compress spacing similarly ### Step 5: Rebuild + Validate + QA ```bash node main.js && \ python3 scripts/validate_pptx_compat.py output.pptx && \ soffice --headless --convert-to pdf output.pptx && \ pdftoppm -jpeg -r 150 output.pdf qa/slide ``` Then run visual QA on at least the new slides + agenda + summary. ### Pitfall: Forgetting to Renumber The most common mistake is updating `main.js` insertion order but forgetting to update `slideNumber(s, N)` inside existing slide functions. The slides will appear in the right order but show wrong page numbers. Always grep for all `slideNumber` calls after restructuring: ```bash grep -rn "slideNumber" slides_*.js ```