# 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 `<a:ext>` | PowerPoint refuses to open |
| LINE shape from node A (right) to node B (left) | `cx < 0` in `<a:ext>` | PowerPoint refuses to open |
| LINE with equal source/target y | `cy = 0` in `<a:ext>` | PowerPoint refuses to open |
| OVAL at `x: -2, y: -2` (decorative, partially off-slide) | `x < 0, y < 0` in `<a:off>` | 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
```
