151 lines
5.6 KiB
Markdown
151 lines
5.6 KiB
Markdown
# Reference-Deck Analysis Patterns
|
||
|
||
It describes how to approach PPTX extraction and style analysis with `python-pptx`, using short illustrative
|
||
snippets — not a packaged module to copy wholesale.
|
||
|
||
- Do not treat these snippets as bundled runtime modules.
|
||
- Do not import from this file or recreate `.py` files under this skill as packaged resources.
|
||
- When a task needs extraction or style analysis, write task-local code with `python-pptx`, using these patterns as reference.
|
||
|
||
## Shared constants
|
||
|
||
Most helpers depend on two values: the EMU-per-inch conversion factor and the
|
||
DrawingML namespace used when reading raw OOXML.
|
||
|
||
```python
|
||
EMU_PER_INCH = 914400
|
||
DRAWING_NS = "{http://schemas.openxmlformats.org/drawingml/2006/main}"
|
||
|
||
|
||
def _inches(value: int) -> float:
|
||
return round(int(value or 0) / EMU_PER_INCH, 4)
|
||
```
|
||
|
||
## Style master: compact design context
|
||
|
||
**Goal:** summarize a reference deck into a compact "style master" (colors,
|
||
fonts, font sizes, shape styles, layout/region usage) suitable for prompting a
|
||
generator. Counters drive a `most_common` ranking; results are truncated by
|
||
`max_items`.
|
||
|
||
### Approach
|
||
|
||
1. Open with `Presentation(path)`; read slide size in inches.
|
||
2. Pull theme tokens (colors/fonts) directly from `ppt/theme/theme1.xml` via `zipfile`.
|
||
3. Walk each slide's shapes (recursing into groups), tallying tokens into `Counter`s.
|
||
4. Emit `styles`, `brands`, `template`, and `layout` sections from the top-N tallies.
|
||
|
||
### Illustrative snippet
|
||
|
||
```python
|
||
from collections import Counter
|
||
|
||
def analyze(presentation) -> dict:
|
||
colors: Counter[str] = Counter()
|
||
fonts: Counter[str] = Counter()
|
||
for slide in presentation.slides:
|
||
for shape in _iter_shapes(slide.shapes):
|
||
colors.update(_shape_colors(shape).values())
|
||
fonts.update(_text_styles(shape)["fonts"])
|
||
return {
|
||
"colors": [{"value": v, "count": c} for v, c in colors.most_common(10)],
|
||
"fonts": [{"value": v, "count": c} for v, c in fonts.most_common(10)],
|
||
}
|
||
```
|
||
|
||
### Key helper patterns
|
||
|
||
- **Recursive shape walk** — yield each shape, then recurse when it exposes `.shapes`:
|
||
|
||
```python
|
||
def _iter_shapes(shapes):
|
||
for shape in shapes:
|
||
yield shape
|
||
if hasattr(shape, "shapes"):
|
||
yield from _iter_shapes(shape.shapes)
|
||
```
|
||
|
||
- **Color normalization** — read `color.rgb`, fall back to `theme_color`, and
|
||
normalize to `#RRGGBB` or `theme:<token>`.
|
||
- **Region/flow classification** — bucket each shape's bbox center into
|
||
`top/middle/bottom` × `left/center/right`, and infer `row` / `column` / `grid`
|
||
from the spread of centers.
|
||
- **Neutral filtering** — treat colors with low channel spread (`max-min <= 18`)
|
||
as neutrals so brand accents rank above grays.
|
||
|
||
## Extractor: structured deck capture
|
||
|
||
**Goal:** turn a deck into a structured tree of slides → groups → objects, with
|
||
optional media extraction and a parallel list of raw OOXML render elements.
|
||
|
||
### Approach
|
||
|
||
1. For each slide, build a root group bbox covering the full slide.
|
||
2. Walk shapes recursively; groups become nested groups, leaf shapes become objects.
|
||
3. Classify each object by `kind` (`text`, `table`, `image`, `chart`, `connector`, …)
|
||
and capture kind-specific `content` plus `style`.
|
||
4. Optionally write media to an asset dir, or base64-embed when no dir is given.
|
||
5. Read speaker notes by resolving slide `_rels` to their `notesSlide` parts.
|
||
|
||
### Illustrative snippet
|
||
|
||
```python
|
||
def _bbox(shape) -> dict:
|
||
return {
|
||
"x": _inches(getattr(shape, "left", 0)),
|
||
"y": _inches(getattr(shape, "top", 0)),
|
||
"width": _inches(getattr(shape, "width", 0)),
|
||
"height": _inches(getattr(shape, "height", 0)),
|
||
}
|
||
|
||
def _kind(shape, shape_type: str) -> str:
|
||
if getattr(shape, "has_table", False):
|
||
return "table"
|
||
if getattr(shape, "has_chart", False):
|
||
return "chart"
|
||
if "picture" in shape_type or getattr(shape, "image", None):
|
||
return "image"
|
||
if getattr(shape, "has_text_frame", False) and shape.text.strip():
|
||
return "text"
|
||
return "shape"
|
||
```
|
||
|
||
### Kind-specific content notes
|
||
|
||
- **text** — capture `text` plus rich paragraphs (runs with font size, bold,
|
||
italic, color, hyperlink, paragraph alignment/level).
|
||
- **table** — emit `rows`, plus row/col counts, widths/heights, banding flags,
|
||
and merged-cell origins (`span_rows` / `span_cols`).
|
||
- **image** — record alt text, crop fractions, and either a written asset path
|
||
or base64 blob; flag `missing_embedded_image` when the blob is absent.
|
||
- **chart** — record `chart_type`, optional title, categories, and series values.
|
||
- **line / connector** — derive endpoints from the bbox plus flip flags, and read
|
||
arrow head/tail types from the `<a:ln>` element.
|
||
|
||
### Media & notes helpers
|
||
|
||
- **Embedded relationship ids** — iterate the shape element and collect attributes
|
||
ending in `}embed`, then resolve via `shape.part.related_part(rid)`.
|
||
- **Package media counts** — open the `.pptx` with `zipfile` and count names under
|
||
`ppt/media/` and `ppt/embeddings/`.
|
||
- **Notes** — parse each `ppt/slides/_rels/slideN.xml.rels`, follow the
|
||
`notesSlide` relationship, and join `<a:t>` text nodes.
|
||
|
||
## Safe attribute access
|
||
|
||
`python-pptx` raises on many optional properties (fills, colors, line styles).
|
||
Wrap reads in a small guard so extraction degrades gracefully:
|
||
|
||
```python
|
||
def _safe_attr(value, name):
|
||
if value is None:
|
||
return None
|
||
try:
|
||
return getattr(value, name)
|
||
except (AttributeError, TypeError, ValueError):
|
||
return None
|
||
```
|
||
|
||
Apply the same defensive pattern around `fill.type`, `line.width`, `color.rgb`,
|
||
`shape.image`, and chart/table accessors, since any of them can fail on real-world decks.
|