diff --git a/.claude/skills/power-pptx/SKILL.md b/.claude/skills/power-pptx/SKILL.md new file mode 100644 index 000000000..9350aca3a --- /dev/null +++ b/.claude/skills/power-pptx/SKILL.md @@ -0,0 +1,197 @@ +--- +name: power-pptx +description: Build PowerPoint (.pptx) decks from Python with the power-pptx library — the actively-maintained fork of python-pptx. Use this skill whenever the user wants to generate, mutate, lint, theme, animate, or render PowerPoint decks programmatically. The headline reason this fork exists is **space-awareness**: text that doesn't overflow its box and shapes that don't slide off the edges of the slide. Reach for it especially when generation is dynamic (LLM, DB, CLI, JSON spec) and the deck has to look right without manual cleanup. Other post-fork features include visual effects, animations, transitions, theme writer, design tokens, slide recipes, slide thumbnails, chart palettes, SVG embedding, 3D, and SmartArt text substitution. +--- + +# power-pptx + +`power-pptx` is the actively-maintained fork of `python-pptx`, +distributed on PyPI as `power-pptx` but imported as `import pptx` +(drop-in compatible). Use it for every PowerPoint generation / +mutation task. + +## The headline: space-aware authoring + +The single biggest reason this fork exists is to make programmatic +decks **physically correct**: text doesn't overflow its container, +shapes don't sit off the slide, and elements that overlap do so on +purpose. Three layered tools — used together — catch ~all real-world +issues: + +1. **`TextFrame.fit_text(...)`** measures with Pillow font metrics + and bakes a fitting size into the XML *before* save. +2. **`text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`** lets + PowerPoint shrink at render time as a fallback. +3. **`slide.lint()`** catches what slipped through; `auto_fix()` + nudges off-slide shapes back inside. + +**Read `references/space-aware-authoring.md` first** if the user is +generating decks from any dynamic input. It's the reason this skill +exists. + +The whole upstream 1.0.2 API still works — the rest of this skill +focuses on the post-fork additions because they're what's most often +missed by snippets pulled from the wider internet. + +## When to use this skill + +- The user wants to **generate a deck** from Python or a JSON / dict spec +- The user is concerned about **text overflow** or **layout correctness** + in generated decks (lead with `space-aware-authoring.md`) +- The user wants to **add visual effects** (shadow, glow, soft edges, + blur, reflection, alpha) to shapes +- The user wants **animations**, **transitions**, or **motion paths** +- The user wants to **read or write a theme** (palette + fonts), or + apply one from a `.potx` +- The user wants to **lint / auto-fix** geometry issues +- The user wants to **import a slide** between decks or **apply a template** +- The user wants a **design system** (tokens, recipes, Grid/Stack layout) +- The user wants **chart palettes**, **quick layouts**, or per-series + gradient/pattern fills +- The user wants **slide thumbnails** rendered to PNG +- The user wants **3D** primitives (bevels / extrusion) or **SmartArt + text substitution** +- The user wants **native SVG embedding** with PNG fallback + +## Install + +```bash +pip install power-pptx +``` + +The `cairosvg` dependency is optional — install only if you want +`add_svg_picture(...)` to auto-rasterise the PNG fallback. `pyyaml` is +optional too — install only if you want `DesignTokens.from_yaml`. + +## Reference snippets + +This skill ships a `references/` directory with focused recipe +collections. Read just the file you need — they're self-contained. + +| File | What it covers | +|---|---| +| `references/space-aware-authoring.md` | **READ THIS FIRST.** Pre-flight measurement (`fit_text`, `TextFitter.best_fit_font_size`), `auto_size` flags, the linter, and a robust layout pattern. **Phase 2 + Phase 6 text-fit estimator.** | +| `references/lint.md` | Detail on `slide.lint()`, issue types, `auto_fix`, and the `from_spec(..., lint="raise")` hook. **Phase 2.** | +| `references/design.md` | `DesignTokens`, `shape.style` facade, `Grid` / `Stack` layout primitives (geometry-safe placement), slide recipes (`title_slide`, `bullet_slide`, `kpi_slide`, `quote_slide`, `image_hero_slide`), starter pack. **Phase 9.** | +| `references/basics.md` | The 1.0.2 surface: `Presentation`, slides, placeholders, shapes, textboxes, tables, pictures, charts. Quick-reference cheatsheet. | +| `references/effects.md` | Shadow, glow, soft edges, blur, reflection, alpha-tinted colors, gradient fills (linear / radial / rectangular / shape), line ends/caps/joins/compound. **Phase 3 + Phase 6.** | +| `references/animations.md` | `Entrance` / `Exit` / `Emphasis` presets, triggers, by-paragraph reveal, sequencing context manager, motion paths. **Phase 5.** | +| `references/transitions.md` | Per-slide and deck-wide transitions including Morph and other `p14:` extensions. **Phase 4.** | +| `references/compose.md` | `from_spec` (JSON authoring with built-in lint), `import_slide`, `apply_template`. **Phase 2 + Phase 7.** | +| `references/theme.md` | Reading + writing the theme palette and fonts; `theme.apply(...)`; theme-aware color resolution via `pptx.inherit.resolve_color`. **Phase 6 + Phase 7.** | +| `references/picture-effects.md` | Picture transparency / brightness / contrast / recolor (grayscale / sepia / washout / duotone) and SVG embedding. **Phase 6.** | +| `references/charts.md` | Chart palettes, quick layouts, per-series gradient/pattern fills, plus the inherited chart API. **Phase 10.** | +| `references/render.md` | Slide thumbnails via LibreOffice. **Phase 10.** | +| `references/three-d.md` | Bevels and extrusion via `shape.three_d`. **Phase 8.** | +| `references/smart-art.md` | Text substitution inside an existing template's SmartArt. **Phase 8.** | +| `references/tables.md` | The inherited table API, plus `Cell.borders`. **Phase 4.** | +| `references/end-to-end-deck.md` | A complete worked example: tokens, recipes, animations, transitions, charts, **and a lint pass before save**. | + +## House rules for code you write + +1. **Always `from pptx import Presentation`** — never invent another + import path. +2. **Default to space-aware patterns** for any text the user controls + at runtime: `fit_text` *or* `auto_size = TEXT_TO_FIT_SHAPE`, plus a + `slide.lint()` pass before save. +3. **Reads should not mutate.** All effect / color / line proxies in + power-pptx return `None` for unset properties; assign `None` to + clear. +4. **Use EMU through helpers**: `Inches`, `Pt`, `Emu`, `Cm` from + `pptx.util`. Never write raw EMU integers when a helper exists. +5. **Use `Grid` / `Stack` for placement** when you have more than two + shapes on a slide — they compute geometry from the slide's real + dimensions, so you can't accidentally walk off the right edge. +6. **Prefer recipes for whole-slide layouts** when the user wants a + "good enough" pitch deck; drop down to direct `add_shape` / + `add_textbox` only when the recipes don't fit. +7. **Save once at the end** — build the deck in memory, then call + `prs.save(...)`. Don't open and re-save inside loops. +8. **For released-version constraints**: this fork is + `power-pptx>=1.1.0`. Pin that in any requirements file you generate. + +## A space-aware mini-template + +The pattern you'll reach for most often: + +```python +from pptx import Presentation +from pptx.enum.text import MSO_AUTO_SIZE +from pptx.util import Inches + +prs = Presentation() +slide = prs.slides.add_slide(prs.slide_layouts[5]) +slide.shapes.title.text = "Q4 Review" + +# Body box that has to swallow runtime-supplied text +box = slide.shapes.add_textbox(Inches(0.6), Inches(1.6), + Inches(12), Inches(5)) +tf = box.text_frame +tf.word_wrap = True +tf.text = USER_SUPPLIED_BODY + +# Belt: pick a determined size now using Pillow font metrics +tf.fit_text(font_family="Inter", max_size=24) + +# Braces: let PowerPoint shrink on the way down if a user later edits +tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE + +# Catch anything that slipped through. auto_fix() mutates the slide +# (currently: nudges OffSlide shapes back in), so we re-lint to see +# the residual issues. +slide.lint().auto_fix() +report = slide.lint() +errors = [i for i in report.issues if i.severity.value == "error"] +if errors: + raise RuntimeError("\n".join(str(e) for e in errors)) + +prs.save("out.pptx") +``` + +## Common pitfalls + +- **Calling `shape.shadow.inherit`** raises `DeprecationWarning`. Read + individual properties (`blur_radius`, `distance`, `direction`, + `color`) and check for `None` instead. +- **Bare-int sizes in `DesignTokens` typography** are interpreted as + **EMU**, not points. Use floats (`44.0`) or `Pt(44)` to mean + 44-point font. +- **Recipes use the Blank layout**, so `slide.shapes.title` is `None`. + Address shapes by index (`slide.shapes[0]`, `slide.shapes[1]`, …). +- **`add_svg_picture` without `cairosvg` and without a `png_fallback`** + raises `CairoSvgUnavailable`. Either install cairosvg or supply a + pre-rasterised PNG. +- **`TextOverflow` is reported but not auto-fixed**. The current + `report.auto_fix()` only handles `OffSlide`. For overflow, use + `tf.fit_text(...)` or `auto_size = TEXT_TO_FIT_SHAPE`. +- **Slide thumbnails require `soffice` on PATH** (LibreOffice). + Otherwise you get `ThumbnailRendererUnavailable`. +- **`MSO_PATTERN_TYPE.ERCENT_40`** is the upstream typo and emits a + `DeprecationWarning`. Use `PERCENT_40`. + +## Where to look in the project + +If the user has the `power-pptx` repo checked out alongside this +skill, these paths are useful for source-of-truth lookup: + +- `src/pptx/lint.py` — `SlideLintReport`, `TextOverflow`, `OffSlide`, + `ShapeCollision`, `LintSeverity`. +- `src/pptx/text/text.py`, `src/pptx/text/layout.py` — `fit_text`, + `TextFitter`, `_best_fit_font_size`. +- `src/pptx/animation.py` — `Entrance`, `Exit`, `Emphasis`, + `MotionPath`, `SlideAnimations`. +- `src/pptx/compose/` — `from_spec`, plus the `import_slide` / + `apply_template` re-exports. +- `src/pptx/theme.py`, `src/pptx/inherit.py` — theme reader/writer and + `resolve_color`. +- `src/pptx/dml/effect.py`, `src/pptx/dml/picture.py`, + `src/pptx/dml/line.py` — Phase 3/6 visual effects, picture filters, + line-end formatting. +- `src/pptx/design/` — `tokens`, `style`, `layout`, `recipes`. +- `src/pptx/chart/palettes.py`, `src/pptx/chart/quick_layouts.py`. +- `src/pptx/render.py` — slide-thumbnail renderer. +- `src/pptx/smart_art.py`, `src/pptx/_svg.py`. +- `examples/starter_pack/` — three example token sets and a build script. + +The user-facing Sphinx documentation under `docs/user/` mirrors the +sections in this skill and is a good source of additional prose. diff --git a/.claude/skills/power-pptx/references/animations.md b/.claude/skills/power-pptx/references/animations.md new file mode 100644 index 000000000..cbefedf7c --- /dev/null +++ b/.claude/skills/power-pptx/references/animations.md @@ -0,0 +1,147 @@ +# Animations (Phase 5) + +`pptx.animation` ships a preset-only API that maps directly onto +PowerPoint's built-in animation library. All generated XML is valid +OOXML and round-trips through PowerPoint without loss. + +## Imports + +```python +from pptx.animation import Entrance, Exit, Emphasis, MotionPath, Trigger +from pptx.util import Inches, Pt +``` + +`Trigger` is an alias for `pptx.enum.animation.PP_ANIM_TRIGGER`. + +## Triggers and delay + +Every preset accepts an optional `trigger` and `delay` (milliseconds): + +```python +Entrance.fade(slide, shape) # default: ON_CLICK +Entrance.fly_in(slide, shape, trigger=Trigger.WITH_PREVIOUS) +Entrance.zoom(slide, shape, trigger=Trigger.AFTER_PREVIOUS, delay=500) +``` + +## Entrance presets + +```python +Entrance.appear(slide, shape) +Entrance.fade(slide, shape) +Entrance.fly_in(slide, shape, direction="bottom") # also "top", "left", "right" +Entrance.float_in(slide, shape) +Entrance.wipe(slide, shape) +Entrance.zoom(slide, shape) +Entrance.wheel(slide, shape) +Entrance.random_bars(slide, shape) +``` + +## Exit presets + +```python +Exit.disappear(slide, shape) +Exit.fade(slide, shape) +Exit.fly_out(slide, shape) +Exit.float_out(slide, shape) +Exit.wipe(slide, shape) +Exit.zoom(slide, shape) +``` + +## Emphasis presets + +```python +Emphasis.pulse(slide, shape) +Emphasis.spin(slide, shape) +Emphasis.teeter(slide, shape) +``` + +## Per-paragraph reveal + +Reveal a text frame one paragraph at a time, fired by a single click: + +```python +body = slide.placeholders[1].text_frame +Entrance.fade(slide, body, by_paragraph=True) +``` + +Supported presets for `by_paragraph=True`: `appear`, `fade`, `wipe`, +`zoom`, `wheel`, `random_bars`. The first paragraph fires on the +caller-supplied trigger (or `ON_CLICK`); subsequent paragraphs default +to `Trigger.AFTER_PREVIOUS`. + +## Sequencing — chain effects from one click + +```python +with slide.animations.sequence(): + Entrance.fade(slide, title_shape) + Entrance.fly_in(slide, body_shape) + Emphasis.pulse(slide, badge_shape) +``` + +Inside the `with` block: +- The first effect fires on `Trigger.ON_CLICK` (or whatever `start=` is + passed to `sequence(start=...)`). +- Every subsequent effect defaults to `Trigger.AFTER_PREVIOUS`. +- Explicit per-call triggers still win. + +Sequences cannot be nested. + +## Motion paths + +```python +MotionPath.line(slide, shape, dx=Inches(2), dy=Inches(1)) +MotionPath.diagonal(slide, shape, dx=Inches(3), dy=Inches(2)) +MotionPath.circle(slide, shape, radius=Inches(1), clockwise=True) +MotionPath.arc(slide, shape, dx=Inches(3), dy=Inches(0), height=0.4) +MotionPath.zigzag(slide, shape, dx=Inches(4), dy=Inches(0), + segments=6, amplitude=0.2) +MotionPath.spiral(slide, shape, radius=Inches(2), + turns=2.5, clockwise=True) + +# Pass a raw OOXML motion-path expression +MotionPath.custom(slide, shape, "M 0 0 L 0.5 0.5 L 1 0") +``` + +All preset constructors normalise EMU inputs against the slide +dimensions before emitting the path attribute, so the *absolute* travel +distance is preserved across slide sizes. + +## Round-trip safety + +Animations authored in PowerPoint survive a read–modify–write cycle. +Generated effects are appended to the existing `` timing tree +without touching pre-existing `` nodes: + +```python +prs = Presentation("hand-authored.pptx") +slide = prs.slides[0] +Entrance.fade(slide, slide.shapes[0]) # adds, doesn't disturb +prs.save("with-extra-fade.pptx") +``` + +## End-to-end example + +```python +from pptx import Presentation +from pptx.animation import Entrance, Emphasis, MotionPath, Trigger +from pptx.util import Inches + +prs = Presentation() +slide = prs.slides.add_slide(prs.slide_layouts[5]) +slide.shapes.title.text = "Animated demo" + +box = slide.shapes.add_textbox(Inches(1), Inches(2), Inches(8), Inches(1)) +box.text_frame.text = "Click anywhere to animate" + +# A 3-step click-driven sequence +with slide.animations.sequence(): + Entrance.fade(slide, slide.shapes.title) + Entrance.fly_in(slide, box, direction="left") + Emphasis.pulse(slide, box) + +# Extra effect after the sequence: a motion path on the box +MotionPath.arc(slide, box, dx=Inches(2), dy=Inches(0), height=0.3, + trigger=Trigger.AFTER_PREVIOUS) + +prs.save("animated.pptx") +``` diff --git a/.claude/skills/power-pptx/references/basics.md b/.claude/skills/power-pptx/references/basics.md new file mode 100644 index 000000000..38cbbc8db --- /dev/null +++ b/.claude/skills/power-pptx/references/basics.md @@ -0,0 +1,170 @@ +# Basics — the inherited 1.0.2 surface + +Everything in this file works the same as upstream `python-pptx 1.0.2`. +It's here so you don't have to leave the skill for boring boilerplate. + +## Open / create / save + +```python +from pptx import Presentation + +prs = Presentation() # blank deck, default 16:9 +prs = Presentation("template.pptx") # open existing +prs.save("out.pptx") +``` + +`Presentation(...)` also accepts a binary file-like object — useful for +HTTP responses or in-memory generation: + +```python +import io +buf = io.BytesIO() +prs.save(buf) +buf.seek(0) +return buf.getvalue() +``` + +## Slide size + +```python +from pptx.util import Inches + +prs.slide_width = Inches(13.333) # 16:9 widescreen +prs.slide_height = Inches(7.5) +``` + +## Adding slides + +```python +title_layout = prs.slide_layouts[0] # 0 = Title, 1 = Title+Content, +blank_layout = prs.slide_layouts[6] # 5 = Title only, 6 = Blank, ... + +slide = prs.slides.add_slide(title_layout) +slide.shapes.title.text = "Q4 Review" +slide.placeholders[1].text = "April 2026" +``` + +Layouts are master-dependent; use `prs.slide_master.slide_layouts` if you +want to be explicit, or iterate `for L in prs.slide_layouts: print(L.name)` +to discover what the template ships. + +## Text boxes + +```python +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor + +box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1)) +tf = box.text_frame +tf.word_wrap = True + +p = tf.paragraphs[0] +p.text = "Hello world" +p.font.name = "Inter" +p.font.size = Pt(36) +p.font.bold = True +p.font.color.rgb = RGBColor(0x1F, 0x29, 0x37) + +p2 = tf.add_paragraph() +p2.text = "Subtitle goes here" +p2.font.size = Pt(18) +``` + +## Auto shapes + +```python +from pptx.enum.shapes import MSO_SHAPE + +card = slide.shapes.add_shape( + MSO_SHAPE.ROUNDED_RECTANGLE, + left=Inches(1), top=Inches(2), + width=Inches(4), height=Inches(2.5), +) +card.fill.solid() +card.fill.fore_color.rgb = RGBColor(0xF8, 0xFA, 0xFC) +card.line.color.rgb = RGBColor(0xE5, 0xE7, 0xEB) +card.line.width = Pt(1) +``` + +## Pictures + +```python +pic = slide.shapes.add_picture( + "hero.jpg", + left=Inches(0), top=Inches(0), + width=prs.slide_width, height=prs.slide_height, +) +``` + +## Tables + +```python +table_shape = slide.shapes.add_table( + rows=4, cols=3, + left=Inches(1), top=Inches(2), + width=Inches(8), height=Inches(3), +) +table = table_shape.table + +# Header +for i, label in enumerate(("Metric", "Value", "Δ QoQ")): + cell = table.cell(0, i) + cell.text = label + cell.text_frame.paragraphs[0].font.bold = True + +# Body +for row, (k, v, d) in enumerate([("ARR", "$182M", "+27%"), + ("NDR", "131%", "+3%"), + ("CAC payback", "8 mo", "−1 mo")], start=1): + table.cell(row, 0).text = k + table.cell(row, 1).text = v + table.cell(row, 2).text = d +``` + +(See `tables.md` for `Cell.borders`, the post-fork addition.) + +## Charts + +```python +from pptx.chart.data import CategoryChartData +from pptx.enum.chart import XL_CHART_TYPE + +data = CategoryChartData() +data.categories = ["Q1", "Q2", "Q3", "Q4"] +data.add_series("ARR", (100, 130, 155, 182)) + +chart_shape = slide.shapes.add_chart( + XL_CHART_TYPE.COLUMN_CLUSTERED, + Inches(1), Inches(2), Inches(8), Inches(4.5), + data, +) +chart = chart_shape.chart +chart.has_title = True +chart.chart_title.text_frame.text = "ARR ($M)" +``` + +(See `charts.md` for chart palettes, quick layouts, and per-series fills.) + +## Iterating an existing deck + +```python +prs = Presentation("input.pptx") +for slide in prs.slides: + for shape in slide.shapes: + if shape.has_text_frame: + for para in shape.text_frame.paragraphs: + for run in para.runs: + print(run.text) +``` + +## Common units + +```python +from pptx.util import Inches, Pt, Cm, Emu, Mm + +Inches(1) # 914400 EMU +Pt(12) # 152400 EMU +Cm(2.54) # ≈ Inches(1) +``` + +Use these everywhere — never write the EMU integers directly. diff --git a/.claude/skills/power-pptx/references/charts.md b/.claude/skills/power-pptx/references/charts.md new file mode 100644 index 000000000..1fa57f9ff --- /dev/null +++ b/.claude/skills/power-pptx/references/charts.md @@ -0,0 +1,145 @@ +# Charts: palettes, quick layouts, per-series fills (Phase 10) + +The chart helpers below stack on top of the existing chart API; nothing +here replaces `chart_style` or the underlying series formatting — they +just make common operations one line each. + +## A baseline chart + +```python +from pptx import Presentation +from pptx.chart.data import CategoryChartData +from pptx.enum.chart import XL_CHART_TYPE +from pptx.util import Inches + +prs = Presentation() +slide = prs.slides.add_slide(prs.slide_layouts[5]) +slide.shapes.title.text = "Run-rate metrics" + +data = CategoryChartData() +data.categories = ["Q1", "Q2", "Q3", "Q4"] +data.add_series("ARR", (100, 130, 155, 182)) +data.add_series("NDR (%)", (115, 118, 124, 131)) + +chart_shape = slide.shapes.add_chart( + XL_CHART_TYPE.COLUMN_CLUSTERED, + Inches(1), Inches(2), Inches(11), Inches(5), + data, +) +chart = chart_shape.chart +``` + +## Chart palettes + +`Chart.apply_palette(palette)` recolors every series in declaration +order from a named built-in or an iterable of color-likes. Palettes +wrap when the chart has more series than colors: + +```python +chart.apply_palette("modern") # built-in +chart.apply_palette(["#4F9DFF", "#7FCFA1", "#F7B500"]) + +# Mix and match — any color-like works +from pptx.dml.color import RGBColor +chart.apply_palette([ + RGBColor(0x4F, 0x9D, 0xFF), + "#7FCFA1", + (247, 181, 0), +]) +``` + +Six built-ins ship in `pptx.chart.palettes`: + +- `modern` +- `classic` +- `editorial` +- `vibrant` +- `monochrome_blue` +- `monochrome_warm` + +```python +from pptx.chart.palettes import ( + CHART_PALETTES, + palette_names, + resolve_palette, +) + +print(palette_names()) # → ['modern', 'classic', ...] +colors = resolve_palette("editorial") # → list[RGBColor] +``` + +`resolve_palette` is also handy for sharing the same colors with +non-chart shapes. + +The `chart_style` integer is left untouched, so the palette overrides +only the per-series fill without rewriting the rest of the style. + +## Quick layouts + +`Chart.apply_quick_layout(layout)` toggles title / legend / axis-title +/ gridline visibility in opinionated combinations. Ten built-in +presets ship in `pptx.chart.quick_layouts`: + +```python +chart.apply_quick_layout("title_legend_right") +chart.apply_quick_layout("title_legend_bottom") +chart.apply_quick_layout("title_legend_top") +chart.apply_quick_layout("title_legend_left") +chart.apply_quick_layout("title_no_legend") +chart.apply_quick_layout("no_title_no_legend") +chart.apply_quick_layout("title_axes_legend_right") +chart.apply_quick_layout("title_axes_legend_bottom") +chart.apply_quick_layout("minimal") +chart.apply_quick_layout("dense") +``` + +Custom layouts can be supplied as a dict spec: + +```python +chart.apply_quick_layout({ + "has_title": True, + "title_text": "ARR ($M)", + "has_legend": True, + "legend_position": "bottom", + "category_axis": {"has_major_gridlines": False}, + "value_axis": {"has_major_gridlines": True, + "tick_labels": True}, +}) +``` + +Missing keys leave the chart untouched so layouts compose cleanly. +Charts without category/value axes (e.g. pie) silently skip the +corresponding keys. + +## Per-series gradient and pattern fills + +`chart.series[i].format.fill` is a regular `FillFormat`, so all four +gradient kinds and `MSO_PATTERN_TYPE` patterns work per-series with no +chart-specific shim: + +```python +fill = chart.series[0].format.fill +fill.gradient(kind="linear") +fill.gradient_stops.replace([ + (0.0, "#0F2D6B"), + (1.0, "#4F9DFF"), +]) + +# Patterned fill on the second series +from pptx.enum.dml import MSO_PATTERN_TYPE +pat = chart.series[1].format.fill +pat.patterned() +pat.pattern = MSO_PATTERN_TYPE.WIDE_DOWNWARD_DIAGONAL +pat.fore_color.rgb = (0x10, 0xB9, 0x81) +pat.back_color.rgb = (0xFF, 0xFF, 0xFF) +``` + +## End-to-end: branded chart + +```python +chart.apply_palette("modern") +chart.apply_quick_layout("title_axes_legend_bottom") + +# Override the title text +chart.chart_title.text_frame.text = "ARR & NDR ($M / %)" +``` diff --git a/.claude/skills/power-pptx/references/compose.md b/.claude/skills/power-pptx/references/compose.md new file mode 100644 index 000000000..5a1afa470 --- /dev/null +++ b/.claude/skills/power-pptx/references/compose.md @@ -0,0 +1,143 @@ +# Composition: from_spec, import_slide, apply_template (Phase 2 + 7) + +The `pptx.compose` package collects entry points for higher-level +authoring and cross-presentation operations. + +## JSON authoring with `from_spec` + +The single entry point for generator scripts (LLM or otherwise). The +spec dict is validated for known keys and value shapes before +construction (no JSON Schema is involved): + +```python +from pptx.compose import from_spec + +prs = from_spec({ + "theme": {"palette": "modern_blue", "fonts": "inter"}, + "slides": [ + { + "layout": "title", + "title": "Q4 Review", + "subtitle": "April 2026", + "transition": "morph", + }, + { + "layout": "kpi_grid", + "title": "Run-rate metrics", + "kpis": [ + {"label": "ARR", "value": "$182M", "delta": +0.27}, + {"label": "NDR", "value": "131%", "delta": +0.03}, + ], + }, + { + "layout": "bullets", + "title": "Customer impact", + "bullets": [ + "Two flagship customers shipped this week.", + "NPS improved 8 points QoQ.", + ], + }, + ], + "lint": "raise", # fail loudly on bad output +}) + +prs.save("q4-review.pptx") +``` + +Layout names map either to Phase-9 design recipes (where supplied) or +to a small built-in set of layouts using the host presentation's +master. + +The `lint` field accepts `"off"`, `"warn"`, or `"raise"`: + +- ``"off"`` (default) — no lint pass. +- ``"warn"`` — log every issue through the stdlib ``logging`` module. +- ``"raise"`` — raise ``pptx.exc.LintError`` if any error-severity + issue is found. + +`from_spec` runs the lint pass internally; outside of `from_spec`, +iterate the slides yourself (see `lint.md`). + +## Cross-presentation operations + +```python +from pptx import Presentation +from pptx.compose import import_slide, apply_template +``` + +### Importing a slide + +```python +src = Presentation("source.pptx") +dst = Presentation("destination.pptx") + +# Clone src.slides[3] into dst, including its layout reference. +import_slide(dst, src.slides[3], merge_master="dedupe") +``` + +Image-rename collisions, layout references, and master/theme parts are +handled automatically. Two strategies for masters: + +- `merge_master="dedupe"` (default-ish, recommended) reuses an + equivalent master in the destination if one matches. +- `merge_master="clone"` always brings a fresh copy of the source + master alongside. + +### Applying a template + +```python +apply_template(dst, "brand-template.potx") +``` + +Re-points every slide's layout/master/theme at masters from the +`.potx` (or `.pptx`). Slide content is preserved. Layout matching: +name → type → first layout. Unreferenced old masters / layouts / +themes are dropped from the saved package. + +## End-to-end pipeline + +A typical "we have a master deck and need to bolt on N report slides" +script: + +```python +from pptx import Presentation +from pptx.compose import import_slide, apply_template, from_spec + +# 1. Generate the body slides from data +body = from_spec({ + "slides": [ + {"layout": "kpi_grid", "title": team["name"], "kpis": team["kpis"]} + for team in teams + ], +}) + +# 2. Open the cover deck and append the body slides +deck = Presentation("cover.pptx") +for slide in body.slides: + import_slide(deck, slide, merge_master="dedupe") + +# 3. Re-skin everything against the latest brand template +apply_template(deck, "brand-2026.potx") + +# 4. Lint and save (no Presentation-level lint hook in 1.1; iterate) +from pptx.exc import LintError + +errors = [] +for slide in deck.slides: + slide.lint().auto_fix() + errors.extend( + i for i in slide.lint().issues if i.severity.value == "error" + ) +if errors: + raise LintError("; ".join(str(e) for e in errors)) + +deck.save("final.pptx") +``` + +## When NOT to use `from_spec` + +`from_spec` is intentionally bounded — small built-in layouts plus the +recipes from `pptx.design.recipes`. If you need something the recipe +library doesn't ship, drop down to direct shape construction (or write +a recipe and contribute it back). Don't try to express arbitrary +geometry through the spec dict. diff --git a/.claude/skills/power-pptx/references/design.md b/.claude/skills/power-pptx/references/design.md new file mode 100644 index 000000000..cd0f7c272 --- /dev/null +++ b/.claude/skills/power-pptx/references/design.md @@ -0,0 +1,238 @@ +# Design system layer (Phase 9) + +The `pptx.design` package turns the low-level API into something where +the *default* output looks good. Nothing here adds new XML — it's all +built on top of the foundations from earlier phases. + +## Design tokens + +`DesignTokens` is a source-agnostic container for brand tokens: +palette, typography, radii, shadows, spacings. + +```python +from pptx.design.tokens import DesignTokens + +tokens = DesignTokens.from_dict({ + "palette": { + "primary": "#4F9DFF", + "neutral": "#1F2937", + "background": "#FFFFFF", + "positive": "#10B981", + "negative": "#EF4444", + "on_primary": "#FFFFFF", + }, + "typography": { + # Recipes look up the keys "heading" and "body". Other keys are + # available for your own use. Bare floats are treated as POINTS; + # bare ints are EMU. Use floats unless you know what you're doing. + "heading": {"family": "Inter", "size": 44.0, "bold": True}, + "body": {"family": "Inter", "size": 18.0}, + "caption": {"family": "Inter", "size": 12.0, "italic": True}, + }, + "shadows": { + # 'blur' / 'distance' are bare-float points too. + "card": {"blur": 18.0, "distance": 4.0, "alpha": 0.18}, + }, + "radii": {"card": 12.0, "button": 6.0}, + "spacings": {"sm": 8.0, "md": 16.0, "lg": 32.0}, +}) +``` + +### Other constructors + +```python +# Optional pyyaml dependency +tokens = DesignTokens.from_yaml("brand.yml") + +# Extracts the six accent slots, dk1/dk2/lt1/lt2, hyperlink slots, and +# major/minor fonts from a deck or template +tokens = DesignTokens.from_pptx("template.pptx") + +# Layer brand-spec overrides on top of a template-extracted base +tokens = DesignTokens.from_pptx("template.pptx").merge( + DesignTokens.from_dict({"palette": {"accent": "#FF6600"}}) +) +``` + +## Token-resolving shape style + +Every shape exposes a `ShapeStyle` facade. Setters fan assignments out +to the low-level proxies: + +```python +shape.style.fill = tokens.palette["primary"] +shape.style.line = tokens.palette["primary"] +shape.style.shadow = tokens.shadows["card"] +shape.style.text_color = tokens.palette["on_primary"] +shape.style.font = tokens.typography["body"] +``` + +Partial `ShadowToken` assignments leave unset fields untouched, so +overrides are non-destructive. To clear an effect entirely: + +```python +shape.style.shadow = None +``` + +## Layout primitives + +Pure build-time geometry — no XML is read or mutated until `place()`. + +### Grid + +```python +from pptx.design.layout import Grid +from pptx.util import Pt + +grid = Grid(slide, cols=12, rows=6, gutter=Pt(12), margin=Pt(48)) + +# Place a shape that spans columns 0..5, rows 0..3 +grid.place(card1, col=0, row=0, col_span=6, row_span=4) +grid.place(card2, col=6, row=0, col_span=6, row_span=4) + +# Or compute a Box without placing +box = grid.cell(col=0, row=4, col_span=12, row_span=2) +``` + +### Stack + +```python +from pptx.design.layout import Stack + +stack = Stack(direction="vertical", gap=Pt(8), + left=Pt(48), top=Pt(48), width=Pt(600)) + +stack.place(title, height=Pt(64)) +stack.place(subtitle, height=Pt(28)) +stack.place(body, height=Pt(280)) + +stack.reset() # rewind cursor +``` + +`direction="horizontal"` walks left-to-right with `gap` between items. + +## Slide recipes + +Opinionated parameterized slide constructors. Each takes the host +`Presentation`, recipe-specific kwargs, an optional `DesignTokens`, +and an optional `transition=` name: + +```python +from pptx.design.recipes import ( + title_slide, bullet_slide, kpi_slide, + quote_slide, image_hero_slide, +) + +title_slide( + prs, + title="Q4 Review", + subtitle="April 2026", + tokens=tokens, + transition="morph", +) + +bullet_slide( + prs, + title="Customer impact", + bullets=[ + "Two flagship customers shipped this week.", + "NPS improved 8 points QoQ.", + "EU expansion ahead of plan.", + ], + tokens=tokens, +) + +kpi_slide( + prs, + title="Run-rate metrics", + kpis=[ + {"label": "ARR", "value": "$182M", "delta": +0.27}, + {"label": "NDR", "value": "131%", "delta": +0.03}, + {"label": "CAC payback", "value": "8 mo", "delta": -0.10}, + ], + tokens=tokens, +) + +quote_slide( + prs, + quote="The new dashboards saved my team a week per sprint.", + attribution="Director of Eng, Flagship Customer", + tokens=tokens, +) + +image_hero_slide( + prs, + title="Q4 2026", + image="hero.jpg", # path or binary file-like + tokens=tokens, +) +``` + +Recipes use the `Blank` layout and place every shape themselves so the +rendered geometry doesn't depend on the host template's master. + +`kpi_slide` honours `palette["positive"]` / `palette["negative"]` when +tinting deltas (falls back to green/red when unset). It applies +`tokens.shadows["card"]` to each card when present. + +`image_hero_slide` uses `palette["on_primary"]` for overlay text and +tints the bottom band with `palette["primary"]` at 55% alpha. + +## Starter pack + +`examples/starter_pack/` ships three example token sets — `modern`, +`classic`, and `editorial` — each exporting both a raw `SPEC` dict and +a ready-to-use `TOKENS`: + +```python +from examples.starter_pack import modern, classic, editorial + +prs = Presentation() +title_slide(prs, title="Hello", subtitle="World", tokens=modern.TOKENS) +prs.save("modern.pptx") +``` + +Run `python -m examples.starter_pack.build_preview` to render one +preview deck per set into `examples/starter_pack/_out/`. + +## End-to-end branded deck + +```python +from pptx import Presentation +from pptx.design.tokens import DesignTokens +from pptx.design.recipes import ( + title_slide, bullet_slide, kpi_slide, quote_slide, +) + +tokens = DesignTokens.from_dict({ + "palette": { + "primary": "#4F9DFF", + "neutral": "#1F2937", + "positive": "#10B981", + "negative": "#EF4444", + "on_primary": "#FFFFFF", + }, + "typography": { + # Recipes look up "heading" and "body". Floats = points, ints = EMU. + "heading": {"family": "Inter", "size": 44.0, "bold": True}, + "body": {"family": "Inter", "size": 18.0}, + }, + "shadows": {"card": {"blur": 18.0, "distance": 4.0, "alpha": 0.18}}, +}) + +prs = Presentation() +title_slide(prs, title="Q4 Review", subtitle="April 2026", + tokens=tokens, transition="morph") +kpi_slide(prs, title="Run-rate metrics", kpis=[ + {"label": "ARR", "value": "$182M", "delta": +0.27}, + {"label": "NDR", "value": "131%", "delta": +0.03}, +], tokens=tokens) +bullet_slide(prs, title="Customer impact", bullets=[ + "Two flagship customers shipped this week.", + "NPS improved 8 points QoQ.", +], tokens=tokens) +quote_slide(prs, quote="The new dashboards saved my team a week per sprint.", + attribution="Director of Eng", tokens=tokens) + +prs.save("q4-review.pptx") +``` diff --git a/.claude/skills/power-pptx/references/effects.md b/.claude/skills/power-pptx/references/effects.md new file mode 100644 index 000000000..fd6db47ad --- /dev/null +++ b/.claude/skills/power-pptx/references/effects.md @@ -0,0 +1,155 @@ +# Visual effects (Phase 3 + Phase 6) + +Every shape in `power-pptx` exposes non-mutating effect proxies. Reads +return `None` when nothing is set; writes lazily create the underlying +`` / `` element. + +## Outer shadow + +```python +from pptx.util import Pt +from pptx.dml.color import RGBColor + +shadow = card.shadow +shadow.blur_radius = Pt(8) +shadow.distance = Pt(4) +shadow.direction = 90.0 # degrees, 90 = down +shadow.color.rgb = RGBColor(0, 0, 0) +shadow.color.alpha = 0.35 # 35% opacity +``` + +To clear, assign `None` to each property — the `` element +is dropped when the last attribute goes away, restoring inheritance. + +> ⚠ `shadow.inherit` (read or write) emits a `DeprecationWarning` in +> 1.1+. Read individual properties for `None` instead. + +## Glow + +```python +card.glow.radius = Pt(6) +card.glow.color.rgb = RGBColor(0x4F, 0x9D, 0xFF) +``` + +## Soft edges + +```python +card.soft_edges.radius = Pt(3) +``` + +## Blur + +```python +card.blur.radius = Pt(4) +card.blur.grow = True # grow with the shape +``` + +## Reflection + +```python +card.reflection.blur_radius = Pt(2) +card.reflection.distance = Pt(1) +card.reflection.start_alpha = 0.5 +card.reflection.end_alpha = 0.0 +``` + +## Combining for a "card" look + +```python +from pptx.enum.shapes import MSO_SHAPE +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor + +card = slide.shapes.add_shape( + MSO_SHAPE.ROUNDED_RECTANGLE, + Inches(1), Inches(1.5), Inches(4), Inches(2.5), +) +card.fill.solid() +card.fill.fore_color.rgb = RGBColor(0xFF, 0xFF, 0xFF) +card.line.fill.background() # no border + +card.shadow.blur_radius = Pt(18) +card.shadow.distance = Pt(4) +card.shadow.direction = 90.0 +card.shadow.color.rgb = RGBColor(0, 0, 0) +card.shadow.color.alpha = 0.18 + +card.soft_edges.radius = Pt(1) +``` + +## Alpha-tinted fills + +```python +card.fill.solid() +card.fill.fore_color.rgb = RGBColor(0x4F, 0x9D, 0xFF) +card.fill.fore_color.alpha = 0.55 # glassy +``` + +`alpha` is also available on the lazy proxy returned by `Font.color` +and `LineFormat.color`: + +```python +title_run.font.color.rgb = RGBColor(0x1F, 0x29, 0x37) +title_run.font.color.alpha = 0.9 +``` + +## Gradient fills with kinds and mutable stops + +```python +fill = card.fill +fill.gradient(kind="radial") # also "linear", "rectangular", "shape" +fill.gradient_kind # → "radial" + +stops = fill.gradient_stops +stops.replace([ + (0.0, "#0F2D6B"), # hex with or without leading '#' + (0.55, RGBColor(0x4F, 0x9D, 0xFF)), + (1.0, (255, 255, 255)), # plain RGB tuple also accepted +]) + +# Add or remove individual stops +stops.append(0.85, "#A8C0FF") +del stops[1] +``` + +OOXML enforces a 2-stop minimum; the helper raises if you try to drop +below that. + +## Line ends, caps, joins, compound lines + +```python +from pptx.enum.dml import ( + MSO_LINE_CAP_STYLE, + MSO_LINE_COMPOUND_STYLE, + MSO_LINE_JOIN_STYLE, + MSO_LINE_END_TYPE, + MSO_LINE_END_SIZE, +) + +line = arrow.line +line.head_end.type = MSO_LINE_END_TYPE.TRIANGLE +line.head_end.width = MSO_LINE_END_SIZE.MEDIUM +line.head_end.length = MSO_LINE_END_SIZE.LARGE +line.tail_end.type = MSO_LINE_END_TYPE.OVAL +line.cap = MSO_LINE_CAP_STYLE.ROUND +line.compound = MSO_LINE_COMPOUND_STYLE.DOUBLE +line.join = MSO_LINE_JOIN_STYLE.BEVEL +``` + +Reads on an unset attribute return `None` — assigning `None` clears +just that attribute. When the last attribute on a head/tail end goes +away the `` / `` element is dropped so theme +inheritance is preserved. + +## Reading effects without mutating + +Always safe to inspect: + +```python +if card.shadow.blur_radius is None: + print("no explicit shadow") +else: + print("blur:", card.shadow.blur_radius.pt) +``` + +No `` is written by the read. diff --git a/.claude/skills/power-pptx/references/end-to-end-deck.md b/.claude/skills/power-pptx/references/end-to-end-deck.md new file mode 100644 index 000000000..d834ae9c1 --- /dev/null +++ b/.claude/skills/power-pptx/references/end-to-end-deck.md @@ -0,0 +1,230 @@ +# End-to-end: a complete branded deck + +A worked example that exercises most of the post-fork features in one +script: design tokens, recipes, transitions, animations, a chart with +a custom palette, a layout pass through the linter, and an optional +thumbnail render. + +```python +""" +Build a branded Q4 review deck. + +Demonstrates: DesignTokens, recipes, deck-wide transitions, +sequenced animations, chart palette, lint-on-save, thumbnails. +""" +from __future__ import annotations + +from pathlib import Path + +from pptx import Presentation +from pptx.animation import Emphasis, Entrance, Trigger +from pptx.chart.data import CategoryChartData +from pptx.design.recipes import ( + bullet_slide, + image_hero_slide, + kpi_slide, + quote_slide, + title_slide, +) +from pptx.design.tokens import DesignTokens +from pptx.dml.color import RGBColor +from pptx.enum.presentation import MSO_TRANSITION_TYPE +from pptx.enum.chart import XL_CHART_TYPE +from pptx.util import Inches, Pt + + +# ---- Tokens ------------------------------------------------------------------ + +TOKENS = DesignTokens.from_dict( + { + "palette": { + "primary": "#4F9DFF", + "neutral": "#1F2937", + "background": "#FFFFFF", + "positive": "#10B981", + "negative": "#EF4444", + "on_primary": "#FFFFFF", + }, + "typography": { + # NB: recipes look up the keys "heading" and "body". Bare + # floats are treated as POINTS; bare ints are EMU. + "heading": {"family": "Inter", "size": 44.0, "bold": True}, + "body": {"family": "Inter", "size": 18.0}, + }, + "shadows": { + "card": {"blur": 18.0, "distance": 4.0, "alpha": 0.18}, + }, + "radii": {"card": 12.0}, + "spacings": {"sm": 8.0, "md": 16.0, "lg": 32.0}, + } +) + + +# ---- Build the deck ---------------------------------------------------------- + +def build(out_path: str | Path) -> Presentation: + prs = Presentation() + prs.slide_width = Inches(13.333) + prs.slide_height = Inches(7.5) + + # Cover + title_slide( + prs, + title="Q4 2026 Review", + subtitle="April 2026", + tokens=TOKENS, + ) + + # KPIs + kpi_slide( + prs, + title="Run-rate metrics", + kpis=[ + {"label": "ARR", "value": "$182M", "delta": +0.27}, + {"label": "NDR", "value": "131%", "delta": +0.03}, + {"label": "CAC payback", "value": "8 mo", "delta": -0.10}, + ], + tokens=TOKENS, + ) + + # Bullets — annotated with a sequenced paragraph reveal. + # bullet_slide adds the title textbox first and the body textbox + # second, so shapes[1] is reliably the body. + bs = bullet_slide( + prs, + title="Customer impact", + bullets=[ + "Two flagship customers shipped this week.", + "NPS improved 8 points QoQ.", + "EU expansion ahead of plan.", + ], + tokens=TOKENS, + ) + body_tf = bs.shapes[1].text_frame + Entrance.fade(bs, body_tf, by_paragraph=True) + + # Custom chart slide — chart palette + quick layout + cs = prs.slides.add_slide(prs.slide_layouts[5]) + cs.shapes.title.text = "ARR by segment ($M)" + data = CategoryChartData() + data.categories = ["Enterprise", "Mid-market", "SMB", "Self-serve"] + data.add_series("FY25", (62, 41, 18, 9)) + data.add_series("FY26", (94, 55, 23, 10)) + chart_shape = cs.shapes.add_chart( + XL_CHART_TYPE.BAR_CLUSTERED, + Inches(1), Inches(1.8), Inches(11), Inches(5.0), + data, + ) + chart = chart_shape.chart + chart.apply_palette("modern") + chart.apply_quick_layout("title_axes_legend_bottom") + chart.chart_title.text_frame.text = "ARR by segment ($M)" + + # Quote + # Recipes use the Blank layout, so slide.shapes.title is None; + # quote_slide adds the quote textbox first (shapes[0]). + qs = quote_slide( + prs, + quote="The new dashboards saved my team a week per sprint.", + attribution="Director of Eng, Flagship Customer", + tokens=TOKENS, + ) + Emphasis.pulse(qs, qs.shapes[0], trigger=Trigger.AFTER_PREVIOUS) + + # Hero closer (supply your own image path) + image_hero_slide( + prs, + title="Thank you", + image="assets/closer.jpg", + tokens=TOKENS, + ) + + # Deck-wide fade transition, then upgrade the cover to Morph + prs.set_transition(kind=MSO_TRANSITION_TYPE.FADE, duration=400) + prs.slides[0].transition.kind = MSO_TRANSITION_TYPE.MORPH + prs.slides[0].transition.duration = 1500 + + # Space-aware safety net: lint every slide, auto-fix off-slide + # shapes, and bail loudly if anything is still error-severity. + _lint_or_die(prs) + + prs.save(out_path) + return prs + + +def _lint_or_die(prs: Presentation) -> None: + from pptx.exc import LintError + + # Pass 1: nudge off-slide shapes inside the slide bounds + for slide in prs.slides: + slide.lint().auto_fix() + + # Pass 2: collect anything still failing + errors = [] + for i, slide in enumerate(prs.slides): + for issue in slide.lint().issues: + if issue.severity.value == "error": + errors.append(f"slide {i + 1}: {issue}") + + if errors: + raise LintError("\n".join(errors)) + + +# ---- Optional: rasterise thumbnails ------------------------------------------ + +def render_thumbnails(prs: Presentation, out_dir: str | Path) -> list[Path]: + """Best-effort thumbnail render. Skips gracefully if soffice is missing.""" + from pptx.render import ThumbnailRendererUnavailable + + try: + return prs.render_thumbnails(out_dir=out_dir) + except ThumbnailRendererUnavailable as exc: + print(f"thumbnail render skipped: {exc}") + return [] + + +if __name__ == "__main__": + deck = build("q4-review.pptx") + render_thumbnails(deck, "thumbs") +``` + +## What this exercises + +- **Phase 9** — `DesignTokens.from_dict`, four recipes (`title_slide`, + `kpi_slide`, `bullet_slide`, `quote_slide`, `image_hero_slide`), + shape-style fan-out via `tokens=`. +- **Phase 5** — `Entrance.fade(..., by_paragraph=True)` for a reveal, + `Emphasis.pulse(..., trigger=Trigger.AFTER_PREVIOUS)` chained off + the previous click. +- **Phase 4** — `Slide.transition` for the per-slide Morph, + `Presentation.set_transition` for the deck-wide fade. +- **Phase 10** — `Chart.apply_palette("modern")` and + `Chart.apply_quick_layout("title_axes_legend_bottom")`. +- **Phase 2** — `_lint_or_die(...)` as a generation safety net: + `slide.lint().auto_fix()` on every slide to nudge off-slide shapes + back inside, then a second pass that raises `LintError` on any + remaining error-severity issue (text overflow, residual off-slide, + etc). +- **Phase 10** — optional `render_thumbnails(...)` for downstream + tooling, with graceful fall-through when LibreOffice isn't + installed. + +## Adapting for production + +- Persist `TOKENS` separately (YAML or `.pptx`) and load with + `DesignTokens.from_yaml(...)` / `DesignTokens.from_pptx(...)`. That + way design and code evolve independently. +- In production, run `_lint_or_die(...)` (or its equivalent) explicitly + before save. Use `slide.lint().auto_fix()` to repair what can be + fixed automatically (currently: nudge `OffSlide` shapes back inside + the slide bounds), then decide what to do with the residual issues — + log warning-severity issues but ship the deck, raise on + error-severity issues. Keep the stricter "raise on any error" + behaviour in CI. +- There is no `Presentation`-level lint hook in 1.1; iterate + `prs.slides` and call `slide.lint()` per slide, or use + `pptx.compose.from_spec(..., lint="raise")` if you can express the + deck as a spec. +- If you want the chart palette to align with brand tokens rather than + the built-in `"modern"`, pass an explicit list: + `chart.apply_palette([TOKENS.palette["primary"], ...])`. diff --git a/.claude/skills/power-pptx/references/lint.md b/.claude/skills/power-pptx/references/lint.md new file mode 100644 index 000000000..35d1ca619 --- /dev/null +++ b/.claude/skills/power-pptx/references/lint.md @@ -0,0 +1,107 @@ +# Layout linter (Phase 2) + +Programmatic decks tend to ship the same handful of bugs over and +over: text spilling out of its container, shapes off-slide, layered +elements that aren't intended overlaps. The linter is built for +exactly that use case — it's especially useful when feeding decks +generated from LLM output or arbitrary user input. + +## Run on a slide + +```python +report = slide.lint() +report.issues # list[LintIssue] +report.has_errors # bool +print(report.summary()) +``` + +For a whole deck, iterate the slides yourself: + +```python +all_issues = [] +for slide in prs.slides: + all_issues.extend(slide.lint().issues) +``` + +`from_spec` (see `compose.md`) accepts a deck-level +``"lint": "warn" | "raise"`` field that walks every slide for you. + +## Issue types + +```python +from pptx.lint import TextOverflow, OffSlide, ShapeCollision + +for issue in report.issues: + if isinstance(issue, TextOverflow): + print("overflow", issue.shapes[0].name, "ratio", issue.ratio) + elif isinstance(issue, OffSlide): + print("off-slide", issue.shapes[0].name, "side", issue.side) + elif isinstance(issue, ShapeCollision): + a, b = issue.shapes + print("collision", a.name, b.name, + "intersection_pct", issue.intersection_pct) +``` + +Every issue carries a `severity` (`LintSeverity.ERROR` / `WARNING` / +`INFO`), a `code` string, a `message`, and a `shapes` tuple of the +shapes it implicates. + +`TextOverflow` uses Pillow font metrics and respects margins, vertical +anchor, line spacing, and `auto_size`. + +## Auto-fix + +```python +fixes = report.auto_fix() # mutates; returns list[str] +preview = report.auto_fix(dry_run=True) # no mutation; returns list[str] +``` + +What's currently fixable: + +- **`OffSlide`** → translates the shape so it sits inside the slide + bounds. Returns a one-line description of each nudge. +- **`TextOverflow`** → reported only. Auto-fitting requires designer + judgment on font size vs content; do it manually with + ``text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE``. +- **`ShapeCollision`** → reported only. Auto-nudging would almost + always break the design. + +## Save-time hooks (via `from_spec`) + +If you build the deck through `pptx.compose.from_spec`, the spec dict +accepts a top-level ``"lint"`` field: + +```python +from pptx.compose import from_spec + +prs = from_spec({ + "slides": [...], + "lint": "raise", # also "warn" or "off" (default) +}) +``` + +`"warn"` logs every issue through stdlib `logging`; `"raise"` raises +`pptx.exc.LintError` if any error-severity issue is found. + +## Recommended pattern for generators + +```python +from pptx.exc import LintError + +prs = build_deck_from_user_input(...) + +# 1. Auto-fix what we can, slide by slide +for slide in prs.slides: + report = slide.lint() + report.auto_fix() + +# 2. Re-run and bail on any remaining errors +remaining = [] +for slide in prs.slides: + remaining.extend(i for i in slide.lint().issues + if getattr(i, "severity", None) == "error") +if remaining: + raise LintError("; ".join(str(i) for i in remaining)) + +prs.save("out.pptx") +``` diff --git a/.claude/skills/power-pptx/references/picture-effects.md b/.claude/skills/power-pptx/references/picture-effects.md new file mode 100644 index 000000000..2b715646d --- /dev/null +++ b/.claude/skills/power-pptx/references/picture-effects.md @@ -0,0 +1,119 @@ +# Picture effects + native SVG (Phase 6) + +Pictures gain a dedicated `effects` accessor that wraps the OOXML +`` filters, plus native SVG support with PNG fallback. + +## Picture filters + +```python +from pptx import Presentation +from pptx.util import Inches +from pptx.dml.color import RGBColor + +prs = Presentation() +slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank + +pic = slide.shapes.add_picture( + "hero.jpg", Inches(0), Inches(0), + width=prs.slide_width, height=prs.slide_height, +) + +# Continuous adjustments — all in [-1.0, 1.0] (or [0.0, 1.0] for transparency) +pic.effects.transparency = 0.3 # 30% see-through +pic.effects.brightness = 0.10 +pic.effects.contrast = 0.05 +``` + +## Recolor presets + +```python +pic.effects.recolor("grayscale") +pic.effects.recolor("sepia") +pic.effects.recolor("washout") # PowerPoint's "Washout" +pic.effects.recolor("black_and_white") +``` + +## Duotone + +```python +pic.effects.set_duotone( + RGBColor(0x12, 0x1E, 0x4D), # shadow color + "#A8C0FF", # highlight color (hex with or without '#') +) + +# Plain RGB tuples are also accepted +pic.effects.set_duotone((18, 30, 77), (168, 192, 255)) +``` + +To clear: + +```python +pic.effects.clear_recolor() # drops any duotone / grayscale / etc. +``` + +## Native SVG with PNG fallback + +`add_svg_picture` embeds both an SVG and a PNG fallback inside the +same `` so PowerPoint and earlier viewers each render the +right one. + +```python +# Auto-rasterise via the optional `cairosvg` dependency +slide.shapes.add_svg_picture("logo.svg", Inches(0.5), Inches(0.5)) + +# Bring your own fallback PNG +slide.shapes.add_svg_picture( + "logo.svg", + Inches(0.5), Inches(0.5), + width=Inches(1.5), height=Inches(1.5), + png_fallback="logo.png", +) +``` + +If `cairosvg` isn't installed and you don't pass `png_fallback`, the +call raises `pptx._svg.CairoSvgUnavailable` with a clear install hint. + +The `image/svg+xml` content type is registered with the package so +SVG parts authored elsewhere round-trip through PowerPoint untouched. + +## End-to-end: tinted photo with overlay text + +```python +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.shapes import MSO_SHAPE + +prs = Presentation() +slide = prs.slides.add_slide(prs.slide_layouts[6]) + +# Full-bleed hero image, duotoned to brand colors +pic = slide.shapes.add_picture( + "hero.jpg", 0, 0, + width=prs.slide_width, height=prs.slide_height, +) +pic.effects.set_duotone(RGBColor(0x12, 0x1E, 0x4D), "#A8C0FF") + +# Bottom band with overlay text +band = slide.shapes.add_shape( + MSO_SHAPE.RECTANGLE, + 0, prs.slide_height - Inches(1.5), + prs.slide_width, Inches(1.5), +) +band.fill.solid() +band.fill.fore_color.rgb = RGBColor(0x12, 0x1E, 0x4D) +band.fill.fore_color.alpha = 0.55 +band.line.fill.background() + +box = slide.shapes.add_textbox( + Inches(0.6), prs.slide_height - Inches(1.2), + prs.slide_width - Inches(1.2), Inches(0.9), +) +p = box.text_frame.paragraphs[0] +p.text = "Q4 2026" +p.font.size = Pt(40) +p.font.bold = True +p.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + +prs.save("hero.pptx") +``` diff --git a/.claude/skills/power-pptx/references/render.md b/.claude/skills/power-pptx/references/render.md new file mode 100644 index 000000000..022974812 --- /dev/null +++ b/.claude/skills/power-pptx/references/render.md @@ -0,0 +1,122 @@ +# Slide thumbnails (Phase 10) + +`pptx.render` shells out to LibreOffice to rasterise slides as PNGs. +This is for review tooling, dashboards, and CI artifacts — it does not +require Microsoft PowerPoint or an Office license, but `soffice` must +be on `$PATH` (or you can point at a custom binary). + +## Convenience methods + +```python +# All slides → ./thumbs/.png +paths = prs.render_thumbnails(out_dir="thumbs") + +# Single slide as bytes +png = slide.render_thumbnail(return_bytes=True) + +# Single slide written to a specific path +slide.render_thumbnail(out_path="cover.png") +``` + +## Module-level entry points + +```python +from pptx.render import ( + render_slide_thumbnails, + render_slide_thumbnail, +) + +paths = render_slide_thumbnails( + prs, + out_dir="thumbs", + slide_indexes=[0, 3, 7], # only these slides + soffice_bin="/opt/libreoffice/program/soffice", + timeout=60, # seconds +) + +png = render_slide_thumbnail(slide, return_bytes=True) +``` + +The output resolution is whatever LibreOffice's headless PNG +converter chooses — there's no `width=` knob. If you need a specific +size, post-process with Pillow (``Image.open(...).resize(...)``). + +## Pointing at a custom binary + +Three ways to choose `soffice`, in priority order: + +1. The `soffice_bin=` keyword argument +2. The `POWER_PPTX_SOFFICE` environment variable +3. The first `soffice` (or `libreoffice`) on `$PATH` + +```python +import os +os.environ["POWER_PPTX_SOFFICE"] = "/opt/libreoffice/program/soffice" +prs.render_thumbnails(out_dir="thumbs") +``` + +## Errors + +```python +from pptx.render import ( + ThumbnailRendererUnavailable, + ThumbnailRendererError, +) + +try: + paths = prs.render_thumbnails(out_dir="thumbs") +except ThumbnailRendererUnavailable as e: + # soffice not on PATH — message includes an install hint + print(e) +except ThumbnailRendererError as e: + # soffice ran but produced no PNG / exited non-zero / timed out + print(e) +``` + +## Patterns + +### Generate review images for an HTML preview + +```python +import base64 + +prs.save("deck.pptx") +images = [] +for i in range(len(prs.slides)): + png = prs.slides[i].render_thumbnail(return_bytes=True) + images.append(base64.b64encode(png).decode("ascii")) + +html = "\n".join( + f'' + for b64 in images +) +``` + +### CI artefacts + +```python +# In tests/conftest.py or similar +from pathlib import Path + +def attach_deck_thumbs(prs, out: Path): + out.mkdir(exist_ok=True) + return prs.render_thumbnails(out_dir=out) +``` + +### Skip on dev machines without LibreOffice + +```python +import shutil +import pytest + +requires_soffice = pytest.mark.skipif( + shutil.which("soffice") is None and shutil.which("libreoffice") is None, + reason="LibreOffice not installed", +) + +@requires_soffice +def test_renders_thumbnails(tmp_path): + prs = build_demo_deck() + paths = prs.render_thumbnails(out_dir=tmp_path) + assert len(paths) == len(prs.slides) +``` diff --git a/.claude/skills/power-pptx/references/smart-art.md b/.claude/skills/power-pptx/references/smart-art.md new file mode 100644 index 000000000..ebc7efd39 --- /dev/null +++ b/.claude/skills/power-pptx/references/smart-art.md @@ -0,0 +1,75 @@ +# SmartArt text substitution (Phase 8) + +Full SmartArt creation is intentionally **out of scope** — the layout +algorithms are proprietary and non-trivial to reverse-engineer. + +What `power-pptx` *does* support is text substitution inside an +*existing* template's SmartArt. The classic use case: a corporate +org-chart template whose names need refreshing every quarter. + +## Iterating SmartArt on a slide + +```python +prs = Presentation("org-chart-template.pptx") +slide = prs.slides[0] + +for sa in slide.smart_art: + print("nodes:", sa.texts) +``` + +`slide.smart_art` is a `SmartArtCollection`. Each item is a +`SmartArtShape` with: + +- `texts` — ordered list of node text strings +- `set_text(values, *, strict=True)` — replaces node text in document + order without touching layout, style, or color parts + +## Replacing names + +```python +slide.smart_art[0].set_text(["Alex", "Priya", "Sam", "Lin", "Jordan"]) +``` + +By default `set_text` is `strict=True` and raises if `len(values)` +doesn't match the number of nodes in the diagram. Pass +`strict=False` to truncate / pad with the existing text instead: + +```python +slide.smart_art[0].set_text(["Alex", "Priya"], strict=False) +``` + +## Round-trip + +`DiagramDataPart` and its sibling part classes are registered so the +SmartArt `diagrams/data#.xml`, `layout#`, `quickStyle#`, and `colors#` +parts are handled as typed `XmlPart` subclasses. Reads never mutate. + +## What this is not + +- **No creation.** You can't build a new SmartArt graphic from + scratch. Author it in PowerPoint as a template, then use this API to + refresh it. +- **No structural edits.** Adding/removing nodes is not supported. The + list you pass to `set_text` must align with the existing nodes. +- **No styling changes.** Color and quick-style parts are left alone. + +## End-to-end: refresh a quarterly org chart + +```python +from pptx import Presentation + +prs = Presentation("org-chart-template.pptx") +slide = prs.slides[0] + +names = [ + "Alex Halwell", # CEO + "Priya Shah", # COO + "Sam Tucker", # CFO + "Lin Chen", # CTO + "Jordan Reyes", # CRO + "Morgan Patel", # CMO +] +slide.smart_art[0].set_text(names) + +prs.save("org-chart-2026q2.pptx") +``` diff --git a/.claude/skills/power-pptx/references/space-aware-authoring.md b/.claude/skills/power-pptx/references/space-aware-authoring.md new file mode 100644 index 000000000..5f6219c87 --- /dev/null +++ b/.claude/skills/power-pptx/references/space-aware-authoring.md @@ -0,0 +1,201 @@ +# Space-aware authoring + +This is the **headline reason `power-pptx` exists**. Generated decks +break in two predictable ways: + +1. Text overflows its container. +2. Boxes sit off the slide. + +The library gives you three layered tools to prevent both — used in +this order, they catch ~all real-world cases: + +1. **Pre-flight measurement** — choose a font size that fits *before* + committing the text. +2. **Auto-fit on the text frame** — let PowerPoint shrink the font on + the way down if the text is dynamic. +3. **The linter** — catch what slipped through, before save. + +Use all three. They compose. None of them require Microsoft PowerPoint +to be installed. + +## 1. Pre-flight measurement: pick the right size up front + +`TextFrame.fit_text(...)` measures with Pillow font metrics and sets +the largest whole-point font size that fits the box: + +```python +from pptx.util import Inches, Pt + +box = slide.shapes.add_textbox(Inches(1), Inches(2), Inches(8), Inches(1.5)) +tf = box.text_frame +tf.text = dynamic_title + +# Largest whole-point size ≤ max_size that fits in the box's extents +tf.fit_text(font_family="Inter", max_size=44, bold=True) +``` + +`fit_text` also sets `auto_size = MSO_AUTO_SIZE.NONE`, so PowerPoint +won't second-guess the size at render time. On Linux / serverless +environments without the requested font installed, it falls back to +Pillow's bundled default — you still get a usable size, no exception. + +For finer control (e.g. you want to size text *for* a known box but +leave styling to a recipe), use the underlying fitter directly: + +```python +from pptx.text.layout import TextFitter + +best_pt = TextFitter.best_fit_font_size( + text="Q4 2026 Customer Outcomes Review", + extents=(Inches(8), Inches(1.5)), + max_size=44, + font_file="/usr/share/fonts/truetype/inter/Inter-Bold.ttf", +) +``` + +`best_fit_font_size` returns an int point size; the caller decides what +to do with it. + +## 2. Auto-fit: let PowerPoint shrink at render time + +When the text isn't fully known at authoring time (or you want +PowerPoint to adapt as the user edits the deck), set +`text_frame.auto_size`: + +```python +from pptx.enum.text import MSO_AUTO_SIZE + +# Shrink the text to fit +tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE + +# Or grow the shape to fit the text +tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT + +# Or do nothing (the default — overflowing text is just clipped) +tf.auto_size = MSO_AUTO_SIZE.NONE +``` + +`TEXT_TO_FIT_SHAPE` is the right default for headline / KPI / bullet +cards where the box geometry is fixed and the text is dynamic. +`SHAPE_TO_FIT_TEXT` is the right default for body copy where the box +should grow vertically. + +> ⚠ `auto_size` is rendered by PowerPoint itself — `power-pptx` only +> writes the flag. If you want determinism (CI screenshots, PDF +> export pipelines), prefer `fit_text` so the size is baked into the +> XML. + +## 3. The linter: catch what slipped through + +Run `slide.lint()` before save. It uses Pillow font metrics so it +catches overflow even on auto-fit text frames, and it knows the slide's +real dimensions so off-slide shapes are caught regardless of slide +size: + +```python +from pptx.lint import OffSlide, TextOverflow, ShapeCollision +from pptx.exc import LintError + +errors = [] +for slide in prs.slides: + report = slide.lint() + + # Cheap auto-fix first (currently nudges off-slide shapes back in) + report.auto_fix() + + # Re-collect what's left + for issue in slide.lint().issues: + if issue.severity.value == "error": + errors.append(issue) + +if errors: + raise LintError("; ".join(str(e) for e in errors)) + +prs.save("out.pptx") +``` + +For decks built through `pptx.compose.from_spec(...)`, fold the linter +into the spec itself: + +```python +prs = from_spec({ + "slides": [...], + "lint": "raise", # also "warn", "off" +}) +``` + +## Putting it together: a robust headline + +```python +from pptx import Presentation +from pptx.enum.text import MSO_AUTO_SIZE +from pptx.util import Inches + +def add_headline(prs, slide, text): + box = slide.shapes.add_textbox( + Inches(0.6), Inches(0.4), + Inches(prs.slide_width.inches - 1.2), Inches(1.2), + ) + tf = box.text_frame + tf.word_wrap = True + tf.text = text + + # 1. Pre-flight size pass — bakes a determined size into the XML + tf.fit_text(font_family="Inter", max_size=44, bold=True) + + # 2. Belt-and-braces: if the user later types more, PowerPoint + # will shrink rather than overflow. + tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE + + return box + +# 3. Linter as the safety net at save time +for slide in prs.slides: + slide.lint().auto_fix() + +prs.save("headline.pptx") +``` + +## Geometry helpers — never hand-place EMUs + +Off-slide shapes nearly always come from arithmetic mistakes when +positioning. Use the design layer's `Grid` and `Stack` instead of +adding `Inches(...)`s by hand: + +```python +from pptx.design.layout import Grid, Stack +from pptx.util import Pt + +# 12-column grid with a uniform gutter and outer margin +grid = Grid(slide, cols=12, rows=6, gutter=Pt(12), margin=Pt(48)) +grid.place(card1, col=0, row=0, col_span=6, row_span=4) +grid.place(card2, col=6, row=0, col_span=6, row_span=4) + +# Vertical cursor with a known total width +stack = Stack(direction="vertical", gap=Pt(8), + left=Pt(48), top=Pt(48), width=Pt(600)) +stack.place(title, height=Pt(64)) +stack.place(body, height=Pt(280)) +``` + +Both compute geometry from the slide's actual dimensions, so you can't +accidentally walk off the right edge — and they're pure arithmetic +(no XML reads or writes) until `place()` is called. + +## Why not just slap `auto_size = SHAPE_TO_FIT_TEXT` on everything? + +It's tempting, but it fights with the design. A "Customer impact" +title that grows to two lines pushes the body content down, which +might collide with a chart, which the linter then flags. The chain +keeps moving the failure further from the cause. + +The robust pattern is: + +- **Fixed geometry, fixed font size** for branded slides where the + designer made a deliberate choice. Use `fit_text` to *verify* the + size still fits when content is dynamic. +- **`TEXT_TO_FIT_SHAPE`** as the catch-all for headlines / KPI cards. +- **`SHAPE_TO_FIT_TEXT`** only when the slide is a "wall of text" type + where vertical growth is acceptable. +- **Linter at the end**, always — it's the only thing that sees the + *whole* slide instead of one shape at a time. diff --git a/.claude/skills/power-pptx/references/tables.md b/.claude/skills/power-pptx/references/tables.md new file mode 100644 index 000000000..9f69fe0e0 --- /dev/null +++ b/.claude/skills/power-pptx/references/tables.md @@ -0,0 +1,121 @@ +# Tables + +Most of the table API is unchanged from upstream `python-pptx`. The +post-fork addition is `Cell.borders` — see the bottom of this file. + +## Adding a table + +```python +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor + +shape = slide.shapes.add_table( + rows=4, cols=3, + left=Inches(1), top=Inches(2), + width=Inches(8), height=Inches(3), +) +table = shape.table +``` + +## Headers and cell text + +```python +HEADERS = ["Metric", "Value", "Δ QoQ"] +for col, label in enumerate(HEADERS): + cell = table.cell(0, col) + cell.text = label + cell.text_frame.paragraphs[0].font.bold = True + cell.text_frame.paragraphs[0].font.size = Pt(14) + +ROWS = [ + ("ARR", "$182M", "+27%"), + ("NDR", "131%", "+3%"), + ("CAC payback", "8 mo", "−1 mo"), +] +for r, row in enumerate(ROWS, start=1): + for c, value in enumerate(row): + table.cell(r, c).text = value +``` + +## Column widths and row heights + +```python +table.columns[0].width = Inches(3.5) +table.columns[1].width = Inches(2.5) +table.columns[2].width = Inches(2.0) + +table.rows[0].height = Inches(0.6) +for r in range(1, len(table.rows)): + table.rows[r].height = Inches(0.5) +``` + +## Cell fill + +```python +cell = table.cell(0, 0) +cell.fill.solid() +cell.fill.fore_color.rgb = RGBColor(0x1F, 0x29, 0x37) +cell.text_frame.paragraphs[0].font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) +``` + +## Vertical anchor + +```python +from pptx.enum.text import MSO_VERTICAL_ANCHOR + +cell.vertical_anchor = MSO_VERTICAL_ANCHOR.MIDDLE +``` + +## Cell borders (Phase 4 — post-fork addition) + +`cell.borders` exposes per-edge `LineFormat` proxies plus convenience +helpers. Backed by the OOXML `a:lnL/lnR/lnT/lnB/lnTlToBr/lnBlToTr` +children of `a:tcPr`. + +### Per-edge + +```python +cell.borders.left.color.rgb = RGBColor(0xE5, 0xE7, 0xEB) +cell.borders.left.width = Pt(0.5) +cell.borders.bottom.color.rgb = RGBColor(0x1F, 0x29, 0x37) +cell.borders.bottom.width = Pt(1.5) +cell.borders.diagonal_down.color.rgb = RGBColor(0xEF, 0x44, 0x44) +``` + +### All edges in one call + +```python +cell.borders.all(width=Pt(0.5), color=RGBColor(0xE5, 0xE7, 0xEB)) +cell.borders.outer(width=Pt(1.0), color=RGBColor(0x1F, 0x29, 0x37)) +cell.borders.none() # clears every edge +``` + +### Zebra-striped borders pattern + +```python +LIGHT = RGBColor(0xE5, 0xE7, 0xEB) +DARK = RGBColor(0x1F, 0x29, 0x37) + +# Header row — bottom edge dark +for col in range(len(HEADERS)): + table.cell(0, col).borders.bottom.color.rgb = DARK + table.cell(0, col).borders.bottom.width = Pt(1.5) + +# Body rows — light row separator +for r in range(1, len(table.rows)): + for c in range(len(HEADERS)): + cell = table.cell(r, c) + cell.borders.bottom.color.rgb = LIGHT + cell.borders.bottom.width = Pt(0.5) +``` + +## Reading borders + +Reads on an unset edge return a `LineFormat` whose properties read as +`None` — matching the rest of the library's "reads don't mutate" +contract: + +```python +if cell.borders.bottom.width is None: + print("inherits border from style") +``` diff --git a/.claude/skills/power-pptx/references/theme.md b/.claude/skills/power-pptx/references/theme.md new file mode 100644 index 000000000..9fd21a760 --- /dev/null +++ b/.claude/skills/power-pptx/references/theme.md @@ -0,0 +1,105 @@ +# Themes (Phase 6 + 7) + +`Presentation.theme` returns a `Theme` proxy that's both readable and +writable. Theme parts are loaded as a typed `ThemePart(XmlPart)` so +writes round-trip on save. + +## Reading the palette + +```python +from pptx.enum.dml import MSO_THEME_COLOR + +accent1 = prs.theme.colors[MSO_THEME_COLOR.ACCENT_1] # → RGBColor +accent2 = prs.theme.colors[MSO_THEME_COLOR.ACCENT_2] +bg1 = prs.theme.colors[MSO_THEME_COLOR.BACKGROUND_1] # canonical lt1 +text1 = prs.theme.colors[MSO_THEME_COLOR.TEXT_1] # canonical dk1 +hyper = prs.theme.colors[MSO_THEME_COLOR.HYPERLINK] +follow = prs.theme.colors[MSO_THEME_COLOR.FOLLOWED_HYPERLINK] +``` + +The six accent slots, the dk1/dk2/lt1/lt2 background slots, and the +hyperlink slots are addressable. + +## Reading fonts + +```python +major = prs.theme.fonts.major # heading font (str) +minor = prs.theme.fonts.minor # body font (str) +``` + +## Writing the palette + +```python +from pptx.dml.color import RGBColor + +prs.theme.colors[MSO_THEME_COLOR.ACCENT_1] = RGBColor(0x4F, 0x9D, 0xFF) +prs.theme.colors[MSO_THEME_COLOR.ACCENT_2] = RGBColor(0x10, 0xB9, 0x81) +``` + +Alias slots (`BACKGROUND_1` / `BACKGROUND_2` / `TEXT_1` / `TEXT_2`) +resolve to their canonical `lt1` / `lt2` / `dk1` / `dk2` target. + +## Writing fonts + +```python +prs.theme.fonts.major = "Inter" +prs.theme.fonts.minor = "Inter" +``` + +Rewrites the `//` +typeface. + +## Bulk-copy from another theme + +```python +brand = Presentation("brand.potx") +prs.theme.apply(brand.theme) # copies palette + major/minor fonts +``` + +## Theme-aware color resolution + +`pptx.inherit.resolve_color` returns the effective `RGBColor` for any +`ColorFormat` (or the lazy proxy on `Font.color` / `LineFormat.color`). +Explicit RGB values are returned as-is, scheme colors resolve through +the theme, and unset colors return `None` without mutating XML: + +```python +from pptx.inherit import resolve_color + +rgb = resolve_color(run.font.color, theme=prs.theme) +if rgb is None: + print("inherits from layout/master") +else: + print("effective color:", rgb) +``` + +`brightness` is honoured by blending toward white or black, mirroring +PowerPoint's `lumMod` / `lumOff` model. + +> ⚠ Full placeholder-walking (`slide → layout → master`) is *not* +> implemented; this resolver covers the 80% case (theme-color lookup) +> without touching XML. + +## End-to-end: rebrand a deck + +```python +from pptx import Presentation +from pptx.dml.color import RGBColor +from pptx.enum.dml import MSO_THEME_COLOR + +prs = Presentation("input.pptx") + +# Punchy palette +prs.theme.colors[MSO_THEME_COLOR.ACCENT_1] = RGBColor(0xFF, 0x66, 0x00) +prs.theme.colors[MSO_THEME_COLOR.ACCENT_2] = RGBColor(0x12, 0x1E, 0x4D) +prs.theme.colors[MSO_THEME_COLOR.HYPERLINK] = RGBColor(0x12, 0x1E, 0x4D) + +# Inter everywhere +prs.theme.fonts.major = "Inter" +prs.theme.fonts.minor = "Inter" + +prs.save("rebranded.pptx") +``` + +Anything in the deck that referenced `accent1` / `accent2` / +`majorFont` / `minorFont` will pick up the new values automatically. diff --git a/.claude/skills/power-pptx/references/three-d.md b/.claude/skills/power-pptx/references/three-d.md new file mode 100644 index 000000000..73b4d4b5a --- /dev/null +++ b/.claude/skills/power-pptx/references/three-d.md @@ -0,0 +1,98 @@ +# 3D primitives (Phase 8) + +`shape.three_d` exposes bevels (`` / ``) and +extrusion (``), backed by `CT_Shape3D` and `CT_Scene3D` element +classes in `pptx.oxml.dml.three_d`. + +## Bevels + +```python +from pptx.util import Pt +from pptx.enum.dml import BevelPreset + +three_d = card.three_d + +# Top bevel +three_d.bevel_top.preset = BevelPreset.SOFT_ROUND +three_d.bevel_top.width = Pt(6) +three_d.bevel_top.height = Pt(3) + +# Bottom bevel (less common) +three_d.bevel_bottom.preset = BevelPreset.ANGLE +three_d.bevel_bottom.width = Pt(2) +three_d.bevel_bottom.height = Pt(1) +``` + +`BevelPreset` covers the standard PowerPoint set: `RELAXED_INSET`, +`CIRCLE`, `SLOPE`, `CROSS`, `ANGLE`, `SOFT_ROUND`, `CONVEX`, `COOL_SLANT`, +`DIVOT`, `RIBLET`, `HARD_EDGE`, `ART_DECO`. + +## Extrusion + +```python +from pptx.dml.color import RGBColor + +three_d.extrusion_height = Pt(20) +three_d.extrusion_color = RGBColor(0x12, 0x1E, 0x4D) +``` + +## Contour + +```python +three_d.contour_width = Pt(1) +three_d.contour_color = RGBColor(0xFF, 0xFF, 0xFF) +``` + +## Material preset + +Material affects how the surface reacts to scene lighting: + +```python +from pptx.enum.dml import PresetMaterial + +three_d.preset_material = PresetMaterial.METAL +# Other options: MATTE, PLASTIC, METAL, WARM_MATTE, TRANSLUCENT_POWDER, +# POWDER, DARK_EDGE, SOFT_EDGE, CLEAR, FLAT, SOFT_METAL +``` + +## End-to-end: a beveled badge + +```python +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.enum.shapes import MSO_SHAPE +from pptx.enum.dml import BevelPreset, PresetMaterial +from pptx.dml.color import RGBColor + +prs = Presentation() +slide = prs.slides.add_slide(prs.slide_layouts[6]) + +badge = slide.shapes.add_shape( + MSO_SHAPE.OVAL, + Inches(5.5), Inches(3.0), Inches(2.0), Inches(2.0), +) +badge.fill.solid() +badge.fill.fore_color.rgb = RGBColor(0xFF, 0xC1, 0x07) +badge.line.fill.background() + +td = badge.three_d +td.bevel_top.preset = BevelPreset.SOFT_ROUND +td.bevel_top.width = Pt(8) +td.bevel_top.height = Pt(4) +td.preset_material = PresetMaterial.METAL + +# Combine with a soft shadow for depth +badge.shadow.blur_radius = Pt(12) +badge.shadow.distance = Pt(4) +badge.shadow.direction = 90.0 +badge.shadow.color.alpha = 0.3 + +prs.save("badge.pptx") +``` + +## Round-trip + +The `` and `` slots were already reserved in the +upstream `oxml/shapes/shared.py`; this proxy just gives them a public +read/write face. PowerPoint-authored 3D round-trips cleanly even if +you don't touch the proxy. diff --git a/.claude/skills/power-pptx/references/transitions.md b/.claude/skills/power-pptx/references/transitions.md new file mode 100644 index 000000000..e444c4a7d --- /dev/null +++ b/.claude/skills/power-pptx/references/transitions.md @@ -0,0 +1,100 @@ +# Slide transitions (Phase 4) + +Each slide exposes a `transition` proxy backed by ``. +Reads on an unset transition return `None` and never mutate XML, +keeping theme inheritance intact. + +## Per-slide + +```python +from pptx.enum.presentation import MSO_TRANSITION_TYPE + +slide.transition.kind = MSO_TRANSITION_TYPE.MORPH +slide.transition.duration = 1500 # milliseconds +slide.transition.advance_on_click = True +slide.transition.advance_after = 5000 # 5-second auto-advance +``` + +To remove a transition entirely: + +```python +slide.transition.clear() +``` + +Reads without explicit settings: + +```python +if slide.transition.kind is None: + print("inherits from theme") +``` + +## Supported kinds + +`MSO_TRANSITION_TYPE` covers 25+ kinds including Office 2010+ +extension transitions on the `p14:` namespace: + +- Classics: `FADE`, `PUSH`, `WIPE`, `SPLIT`, `REVEAL`, `RANDOM_BARS`, + `SHAPE`, `UNCOVER`, `COVER`, `CUT`, `DISSOLVE`, `ZOOM` +- Office 2010+ (p14): `MORPH`, `VORTEX`, `CONVEYOR`, `SWITCH`, + `GALLERY`, `FLY_THROUGH`, `RIPPLE`, `HONEYCOMB`, `GLITTER`, `ORBIT`, + `PAN`, `WARP`, `WIND` + +Direction modifiers (`fromLeft`, `fromTop`, etc.) are not yet +exposed by the high-level API — they round-trip but you have to set +them through the underlying element. + +## Deck-wide helper + +`Presentation.set_transition(...)` applies the same transition (or a +partial update) to every slide in one call. Unspecified kwargs leave +each slide's existing setting untouched: + +```python +prs.set_transition(kind=MSO_TRANSITION_TYPE.FADE, duration=750) + +# Bump the duration on every slide without changing the kind +prs.set_transition(duration=1200) + +# Turn on auto-advance everywhere without disturbing kind or duration +prs.set_transition(advance_on_click=True, advance_after=8000) + +# Remove the transition element on every slide +prs.set_transition(kind=None) +``` + +## End-to-end example + +```python +from pptx import Presentation +from pptx.enum.presentation import MSO_TRANSITION_TYPE +from pptx.util import Inches + +prs = Presentation() + +# Slide 1 — title +slide1 = prs.slides.add_slide(prs.slide_layouts[0]) +slide1.shapes.title.text = "Q4 Review" +slide1.placeholders[1].text = "April 2026" + +# Slide 2 — content +slide2 = prs.slides.add_slide(prs.slide_layouts[5]) +slide2.shapes.title.text = "Run-rate metrics" + +# Use Morph between the two title slides +slide1.transition.kind = MSO_TRANSITION_TYPE.MORPH +slide1.transition.duration = 1500 + +# Default everything else to a quick fade +prs.set_transition(kind=MSO_TRANSITION_TYPE.FADE, duration=400) +# (set_transition won't overwrite slide1's already-set kind unless the +# kind kwarg is provided — but we passed kind here, so for slide1 it +# WILL be overwritten. To preserve slide1, set it AFTER the deck-wide +# call instead.) + +# Correct order: +prs.set_transition(kind=MSO_TRANSITION_TYPE.FADE, duration=400) +slide1.transition.kind = MSO_TRANSITION_TYPE.MORPH +slide1.transition.duration = 1500 + +prs.save("with-transitions.pptx") +``` diff --git a/.gitignore b/.gitignore index 49183d6c7..cfb2f3b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.pyc /dist/ /docs/.build +/docs/_build/ out.txt _scratch/ /spec/gen_spec/spec*.db diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 125538586..fa8114f04 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,7 +10,7 @@ build: sphinx: configuration: docs/conf.py # -- fail on all warnings to avoid broken references -- - # fail_on_warning: true + fail_on_warning: true # -- package versions required to build your documentation -- # -- see https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -- diff --git a/HISTORY.rst b/HISTORY.rst index fd7f5a315..66993dfd4 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,11 +12,27 @@ PyPI; the importable package name (``pptx``) is unchanged. .. _`scanny/python-pptx`: https://github.com/scanny/python-pptx -1.9.0 (unreleased) -+++++++++++++++++++ +1.1.0 (2026-04-28) +++++++++++++++++++ + +This is the inaugural release under the ``power-pptx`` distribution name +on PyPI. It is a drop-in replacement for ``python-pptx`` 1.0.2: +``import pptx`` continues to work and existing user code is unaffected. +It bundles every feature from Phases 1 through 10 of the fork's roadmap — +visual effects, animations, transitions, theme reader/writer, JSON +authoring, the layout linter, design tokens and slide recipes, chart +palettes and quick layouts, slide thumbnails, and more. + +The Sphinx documentation has also been rebuilt: every new module ships +with a user-guide chapter and an API-reference page, the substitution +table covers every public class added by the fork, and Read-the-Docs +builds now fail on Sphinx warnings. -New API — Phase 9 (design-system layer, partial) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The full per-phase changelog follows; the project changes summary is +collected near the end under "Project changes". + +Phase 9 — design-system layer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ``pptx.design.tokens.DesignTokens``: source-agnostic container for brand tokens — ``palette`` (str → ``RGBColor``), ``typography`` @@ -175,11 +191,8 @@ Phase 6 — text-fit estimator on Linux / minimal runtimes directory scan are skipped silently. -1.8.0 (unreleased) -+++++++++++++++++++ - -New API — Phase 8 (3D primitives and SmartArt text substitution) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Phase 8 — 3D primitives and SmartArt text substitution +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ``shape.three_d`` accessor: ``ThreeDFormat`` facade exposing ``bevel_top``/``bevel_bottom`` (``_BevelFormat`` with ``preset``, @@ -202,11 +215,8 @@ New API — Phase 8 (3D primitives and SmartArt text substitution) parts are handled as typed ``XmlPart`` subclasses. -1.7.0 (unreleased) -+++++++++++++++++++ - -New API — Phase 7 (slide composition) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Phase 7 — slide composition +~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ``Presentation.import_slide(source_slide, merge_master='dedupe'|'clone')``: clones a slide from any ``Presentation`` into the receiver. Copies @@ -223,19 +233,12 @@ New API — Phase 7 (slide composition) from the saved package. -1.1.0.dev0 (unreleased) -+++++++++++++++++++++++ - -This is the first release under the ``power-pptx`` name. It is a -drop-in replacement for ``python-pptx`` 1.0.2: ``import pptx`` continues -to work and existing user code is unaffected. - Project changes ~~~~~~~~~~~~~~~ - Renamed the PyPI distribution from ``python-pptx-next`` to ``power-pptx``. The importable package remains ``pptx``. -- Repository moved to ``codehalwell/python-pptx``. +- Repository moved to ``codehalwell/power-pptx``. - Original ``LICENSE`` (MIT, Steve Canny, 2013) preserved verbatim; fork copyright added on a second line per MIT requirements. - Dropped the vestigial ``pyparsing`` line from ``requirements.txt``; @@ -245,6 +248,41 @@ Project changes - Dropped Python 3.8 (EOL October 2024). Minimum supported version is now 3.9, matching ``pyright``'s configured ``pythonVersion``. +Documentation +~~~~~~~~~~~~~ + +- Sphinx config rebuilt for ``power-pptx``: switched to the + ``sphinx-rtd-theme``, removed dead upstream-specific hacks, refreshed + the substitution table, and turned on ``fail_on_warning`` for + Read-the-Docs builds. +- New user-guide chapters: visual effects, animations, slide + transitions, layout linter, JSON authoring + cross-presentation + composition, themes, design-system layer, advanced charts (palettes + / quick layouts / per-series fills), and slide thumbnails. +- New API reference pages: ``pptx.animation``, ``pptx.lint``, + ``pptx.compose``, ``pptx.theme`` (plus ``pptx.inherit.resolve_color``), + ``pptx.design`` (tokens, style, layout, recipes), ``pptx.render``, + ``pptx.smart_art``, plus enum pages for ``MSO_LINE_CAP_STYLE``, + ``MSO_LINE_COMPOUND_STYLE``, ``MSO_LINE_JOIN_STYLE``, + ``MSO_LINE_END_TYPE``, ``MSO_LINE_END_SIZE``, ``MSO_TRANSITION_TYPE``, + and ``PP_ANIM_TRIGGER``. +- ``ShadowFormat`` and the ``DrawingML`` reference page surface the + full Phase 3/6 effect family (``GlowFormat``, ``SoftEdgeFormat``, + ``BlurFormat``, ``ReflectionFormat``, ``LineEndFormat``, + ``PictureEffects``). + +Deprecations (scheduled for removal in 2.0) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- ``ShadowFormat.inherit`` now emits a ``DeprecationWarning`` on both + read and write. Read individual properties (``blur_radius``, + ``distance``, ``direction``, ``color``) for ``None`` instead. The + ``inherit`` property is scheduled for removal in 2.0. +- ``MSO_PATTERN_TYPE.ERCENT_40`` is now an aliased member of + ``PERCENT_40`` and emits a ``DeprecationWarning`` on access. +- ``shapes.turbo_add_enabled`` setter remains a no-op and emits a + ``DeprecationWarning`` (shape-id allocation is now always O(1)). + New API ~~~~~~~ diff --git a/README.rst b/README.rst index 3de4ec841..daede22e6 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,50 @@ It can also be used to analyze PowerPoint files from a corpus, perhaps to extract search-indexing text and images, or simply to automate the production of a slide or two that would be tedious to get right by hand. +What's new in the fork +---------------------- + +The fork extends the 1.0.2 surface with features the upstream roadmap did +not cover. All additions are drop-in compatible — existing scripts keep +working — and every new feature ships with a round-trip regression test. + +* **Visual effects** — outer shadow, glow, soft edges, blur, and reflection + exposed as non-mutating proxies on every shape; alpha-tinted colors + (``RGBColor.alpha``); gradient fills with ``linear`` / ``radial`` / + ``rectangular`` / ``shape`` kinds and mutable stops; line ends, caps, + joins, and compound lines. +* **Animations and transitions** — preset entrance, exit, and emphasis + effects; motion-path presets (line, diagonal, circle, arc, zigzag, + spiral); per-paragraph reveal; sequencing context manager; + per-slide and deck-wide transitions including Morph and the other + ``p14:`` extension transitions. +* **Layout linter** — ``slide.lint()`` reports text overflow, off-slide + shapes, and undeclared collisions, with optional ``auto_fix()`` and + save-time hooks. +* **JSON authoring** — ``pptx.compose.from_spec(...)`` builds a deck from + a JSON-shaped spec; ``import_slide`` and ``apply_template`` cover + cross-presentation operations. +* **Theme reader and writer** — read theme colors and fonts; write fresh + ```` values into the clrScheme; apply a theme imported from + a ``.potx``. +* **Picture effects** — transparency, brightness, contrast, recolor + (grayscale, sepia, washout, duotone); native SVG embedding with PNG + fallback. +* **Design-system layer** — ``DesignTokens`` (palette, typography, + shadows, radii, spacings) loadable from a dict, YAML, or a ``.pptx``; + a token-resolving ``shape.style`` facade; ``Grid`` / ``Stack`` layout + primitives; opinionated slide recipes (``title``, ``bullet``, ``kpi``, + ``quote``, ``image_hero``); a starter pack of three example token sets. +* **Charting** — chart palette presets independent of ``chart_style``; + ten quick-layout presets; full per-series gradient and pattern fills. +* **3D primitives and SmartArt text substitution** — bevel and extrusion + via ``shape.three_d``; ``slide.smart_art[i].set_text([...])``. +* **Slide thumbnails** — ``Presentation.render_thumbnails()`` shells out + to LibreOffice for PNG previews. + +See ``HISTORY.rst`` for the full changelog and ``ROADMAP.md`` for the +broader plan. + Attribution ----------- @@ -41,12 +85,9 @@ descriptively to identify the file format the library reads and writes. Documentation ------------- -More information is available in the `python-pptx documentation`_ (note: -the hosted docs currently still reflect the upstream 1.0 API; new APIs -introduced in this fork are documented in their respective release notes -in ``HISTORY.rst`` until the docs site is rebuilt). - -Browse `examples with screenshots`_ to get a quick idea what you can do. +The Sphinx documentation lives under ``docs/`` and covers both the +inherited 1.0.2 API and every feature added by the fork. Browse +`examples with screenshots`_ to get a quick idea what you can do. .. _`python-pptx`: https://github.com/scanny/python-pptx @@ -54,7 +95,5 @@ Browse `examples with screenshots`_ to get a quick idea what you can do. https://github.com/scanny/python-pptx .. _`Steve Canny`: https://github.com/scanny -.. _`python-pptx documentation`: - https://python-pptx.readthedocs.org/en/latest/ .. _`examples with screenshots`: https://python-pptx.readthedocs.org/en/latest/user/quickstart.html diff --git a/ROADMAP.md b/ROADMAP.md index af2d8b3cd..7dc7019eb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -612,10 +612,21 @@ Items that are valuable but not on the critical path: and a configurable timeout. Raises `ThumbnailRendererUnavailable` with an install hint when `soffice` isn't on PATH and `ThumbnailRendererError` on conversion failure. -- **Documentation site rebuild.** Deferred — large dedicated effort, - outside the per-feature shipping cadence the rest of the roadmap is - built around. Will be picked up once the API has stabilised on - the 1.x line. +- [x] **Documentation site rebuild.** Sphinx config rewritten for the + fork (modern theme, refreshed substitution table, ``fail_on_warning`` + on Read-the-Docs); ``README.rst`` rewritten around the new feature + set; ``docs/index.rst`` reorganised; new user-guide chapters added + for visual effects, animations, transitions, the layout linter, + composition, themes, the design-system layer, advanced charts, and + slide thumbnails; new API-reference pages added for ``pptx.animation``, + ``pptx.lint``, ``pptx.compose``, ``pptx.theme`` (plus + ``pptx.inherit.resolve_color``), ``pptx.design`` (tokens, style, + layout, recipes), ``pptx.render``, and ``pptx.smart_art``; the + ``DrawingML`` reference page now covers the full Phase 3/6 effect + family; new enum pages cover ``MSO_LINE_CAP_STYLE``, + ``MSO_LINE_COMPOUND_STYLE``, ``MSO_LINE_JOIN_STYLE``, + ``MSO_LINE_END_TYPE``, ``MSO_LINE_END_SIZE``, + ``MSO_TRANSITION_TYPE``, and ``PP_ANIM_TRIGGER``. --- @@ -640,6 +651,25 @@ they cannot be checked off on the 1.x line: No new features in 2.0; it's a cleanup release. New features land in 2.1. +### 1.x readiness for the 2.0 cut (shipped in 1.1.0) + +These are the 1.x-line preparations that make the 2.0 removals +mechanical when the time comes: + +- [x] `ShadowFormat.inherit` emits a `DeprecationWarning` on both read + and write, with a message pointing callers at the per-property + reads. Tests assert the warning fires. +- [x] `MSO_PATTERN_TYPE.ERCENT_40` is wired through the + `_DeprecatingEnumMeta` machinery and emits a `DeprecationWarning` + on access, with `PERCENT_40` as the canonical name. +- [x] `shapes.turbo_add_enabled` setter emits a `DeprecationWarning` + noting that shape-id allocation is now always O(1). +- [x] `Font.color` was switched to a non-mutating `_LazyColorFormat` + proxy in Phase 1; the upstream behavior of inserting an empty + `` on read is gone in 1.x, so the 2.0 removal is just + documentation cleanup (no flag is needed). +- [x] `RGBColor.from_hex` already shipped (Phase 11 row above). + --- ## How to follow / contribute diff --git a/docs/api/animation.rst b/docs/api/animation.rst new file mode 100644 index 000000000..04823393c --- /dev/null +++ b/docs/api/animation.rst @@ -0,0 +1,43 @@ +.. _animation_api: + +Animations +========== + +.. currentmodule:: pptx.animation + + +|SlideAnimations| objects +------------------------- + +.. autoclass:: SlideAnimations + + +Entrance presets +---------------- + +.. autoclass:: Entrance + + +Exit presets +------------ + +.. autoclass:: Exit + + +Emphasis presets +---------------- + +.. autoclass:: Emphasis + + +Motion paths +------------ + +.. autoclass:: MotionPath + + +Triggers +-------- + +See :ref:`PpAnimTrigger`. ``Trigger`` is exported from :mod:`pptx.animation` +as an alias for :class:`pptx.enum.animation.PP_ANIM_TRIGGER`. diff --git a/docs/api/compose.rst b/docs/api/compose.rst new file mode 100644 index 000000000..7dc329ede --- /dev/null +++ b/docs/api/compose.rst @@ -0,0 +1,13 @@ +.. _compose_api: + +Composition +=========== + +.. currentmodule:: pptx.compose + + +.. autofunction:: from_spec + +.. autofunction:: import_slide + +.. autofunction:: apply_template diff --git a/docs/api/design.rst b/docs/api/design.rst new file mode 100644 index 000000000..c9ecfb9a7 --- /dev/null +++ b/docs/api/design.rst @@ -0,0 +1,66 @@ +.. _design_api: + +Design system +============= + + +Tokens +------ + +.. currentmodule:: pptx.design.tokens + +.. autoclass:: DesignTokens + :members: + :undoc-members: + + +.. autoclass:: TypographyToken + :members: + :undoc-members: + + +.. autoclass:: ShadowToken + :members: + :undoc-members: + + +Style facade +------------ + +.. currentmodule:: pptx.design.style + +.. autoclass:: ShapeStyle + :members: + :undoc-members: + + +Layout primitives +----------------- + +.. currentmodule:: pptx.design.layout + +.. autoclass:: Box + :members: + :undoc-members: + + +.. autoclass:: Grid + :members: + :undoc-members: + + +.. autoclass:: Stack + :members: + :undoc-members: + + +Recipes +------- + +.. currentmodule:: pptx.design.recipes + +.. autofunction:: title_slide +.. autofunction:: bullet_slide +.. autofunction:: kpi_slide +.. autofunction:: quote_slide +.. autofunction:: image_hero_slide diff --git a/docs/api/dml.rst b/docs/api/dml.rst index d96485216..a6e4e135a 100644 --- a/docs/api/dml.rst +++ b/docs/api/dml.rst @@ -31,11 +31,19 @@ various aspects of shapes. :undoc-members: +|LineFormat| line ends +~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: pptx.dml.line.LineEndFormat + :members: + :undoc-members: + + |ColorFormat| objects --------------------- .. autoclass:: pptx.dml.color.ColorFormat - :members: brightness, rgb, theme_color, type + :members: brightness, rgb, theme_color, type, alpha :undoc-members: @@ -43,13 +51,44 @@ various aspects of shapes. ------------------ .. autoclass:: pptx.dml.color.RGBColor - :members: from_string + :members: from_string, from_hex :undoc-members: +Effect proxies +-------------- + |ShadowFormat| objects ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: pptx.dml.effect.ShadowFormat :members: :undoc-members: + + +.. autoclass:: pptx.dml.effect.GlowFormat + :members: + :undoc-members: + + +.. autoclass:: pptx.dml.effect.SoftEdgeFormat + :members: + :undoc-members: + + +.. autoclass:: pptx.dml.effect.BlurFormat + :members: + :undoc-members: + + +.. autoclass:: pptx.dml.effect.ReflectionFormat + :members: + :undoc-members: + + +Picture effects +--------------- + +.. autoclass:: pptx.dml.picture.PictureEffects + :members: + :undoc-members: diff --git a/docs/api/enum/MsoLineCapStyle.rst b/docs/api/enum/MsoLineCapStyle.rst new file mode 100644 index 000000000..fc720d38b --- /dev/null +++ b/docs/api/enum/MsoLineCapStyle.rst @@ -0,0 +1,8 @@ +.. _MsoLineCapStyle: + +``MSO_LINE_CAP_STYLE`` +====================== + +.. autoclass:: pptx.enum.dml.MSO_LINE_CAP_STYLE + :members: + :undoc-members: diff --git a/docs/api/enum/MsoLineCompoundStyle.rst b/docs/api/enum/MsoLineCompoundStyle.rst new file mode 100644 index 000000000..865a03d77 --- /dev/null +++ b/docs/api/enum/MsoLineCompoundStyle.rst @@ -0,0 +1,8 @@ +.. _MsoLineCompoundStyle: + +``MSO_LINE_COMPOUND_STYLE`` +=========================== + +.. autoclass:: pptx.enum.dml.MSO_LINE_COMPOUND_STYLE + :members: + :undoc-members: diff --git a/docs/api/enum/MsoLineEndSize.rst b/docs/api/enum/MsoLineEndSize.rst new file mode 100644 index 000000000..f11c4ec45 --- /dev/null +++ b/docs/api/enum/MsoLineEndSize.rst @@ -0,0 +1,8 @@ +.. _MsoLineEndSize: + +``MSO_LINE_END_SIZE`` +===================== + +.. autoclass:: pptx.enum.dml.MSO_LINE_END_SIZE + :members: + :undoc-members: diff --git a/docs/api/enum/MsoLineEndType.rst b/docs/api/enum/MsoLineEndType.rst new file mode 100644 index 000000000..810364f91 --- /dev/null +++ b/docs/api/enum/MsoLineEndType.rst @@ -0,0 +1,8 @@ +.. _MsoLineEndType: + +``MSO_LINE_END_TYPE`` +===================== + +.. autoclass:: pptx.enum.dml.MSO_LINE_END_TYPE + :members: + :undoc-members: diff --git a/docs/api/enum/MsoLineJoinStyle.rst b/docs/api/enum/MsoLineJoinStyle.rst new file mode 100644 index 000000000..88ca30ec1 --- /dev/null +++ b/docs/api/enum/MsoLineJoinStyle.rst @@ -0,0 +1,8 @@ +.. _MsoLineJoinStyle: + +``MSO_LINE_JOIN_STYLE`` +======================= + +.. autoclass:: pptx.enum.dml.MSO_LINE_JOIN_STYLE + :members: + :undoc-members: diff --git a/docs/api/enum/MsoTransitionType.rst b/docs/api/enum/MsoTransitionType.rst new file mode 100644 index 000000000..1e17f3b6f --- /dev/null +++ b/docs/api/enum/MsoTransitionType.rst @@ -0,0 +1,8 @@ +.. _MsoTransitionType: + +``MSO_TRANSITION_TYPE`` +======================= + +.. autoclass:: pptx.enum.presentation.MSO_TRANSITION_TYPE + :members: + :undoc-members: diff --git a/docs/api/enum/PpAnimTrigger.rst b/docs/api/enum/PpAnimTrigger.rst new file mode 100644 index 000000000..42e716894 --- /dev/null +++ b/docs/api/enum/PpAnimTrigger.rst @@ -0,0 +1,10 @@ +.. _PpAnimTrigger: + +``PP_ANIM_TRIGGER`` +=================== + +.. autoclass:: pptx.enum.animation.PP_ANIM_TRIGGER + :members: + :undoc-members: + +Re-exported from :mod:`pptx.animation` as ``Trigger`` for convenience. diff --git a/docs/api/enum/index.rst b/docs/api/enum/index.rst index e3c465c75..befc48113 100644 --- a/docs/api/enum/index.rst +++ b/docs/api/enum/index.rst @@ -14,14 +14,21 @@ can be found here: MsoConnectorType MsoFillType MsoLanguageId + MsoLineCapStyle + MsoLineCompoundStyle MsoLineDashStyle + MsoLineEndSize + MsoLineEndType + MsoLineJoinStyle MsoPatternType MsoShapeType MsoTextUnderlineType MsoThemeColorIndex + MsoTransitionType MsoVerticalAnchor PpActionType + PpAnimTrigger PpMediaType PpParagraphAlignment PpPlaceholderType diff --git a/docs/api/lint.rst b/docs/api/lint.rst new file mode 100644 index 000000000..7521d38ae --- /dev/null +++ b/docs/api/lint.rst @@ -0,0 +1,36 @@ +.. _lint_api: + +Linter +====== + +.. currentmodule:: pptx.lint + + +.. autoclass:: SlideLintReport + :members: + :undoc-members: + + +.. autoclass:: LintIssue + :members: + :undoc-members: + + +.. autoclass:: TextOverflow + :members: + :show-inheritance: + + +.. autoclass:: OffSlide + :members: + :show-inheritance: + + +.. autoclass:: ShapeCollision + :members: + :show-inheritance: + + +.. autoclass:: LintSeverity + :members: + :undoc-members: diff --git a/docs/api/render.rst b/docs/api/render.rst new file mode 100644 index 000000000..9d72dad3a --- /dev/null +++ b/docs/api/render.rst @@ -0,0 +1,19 @@ +.. _render_api: + +Slide rendering +=============== + +.. currentmodule:: pptx.render + + +.. autofunction:: render_slide_thumbnails + +.. autofunction:: render_slide_thumbnail + + +Exceptions +---------- + +.. autoexception:: ThumbnailRendererUnavailable + +.. autoexception:: ThumbnailRendererError diff --git a/docs/api/slides.rst b/docs/api/slides.rst index 1909ecd08..93f0372cd 100644 --- a/docs/api/slides.rst +++ b/docs/api/slides.rst @@ -108,3 +108,16 @@ This class is not intended to be constructed directly. :members: :exclude-members: clone_master_placeholders :inherited-members: + + +|SlideTransition| objects +------------------------- + +The :attr:`~pptx.slide.Slide.transition` property of |Slide| returns a +|SlideTransition| proxy backed by ````. Reads on an unset +transition return |None| and never mutate XML, keeping theme inheritance +intact. + +.. autoclass:: pptx.slide.SlideTransition + :members: + :undoc-members: diff --git a/docs/api/smart_art.rst b/docs/api/smart_art.rst new file mode 100644 index 000000000..d719fce2e --- /dev/null +++ b/docs/api/smart_art.rst @@ -0,0 +1,20 @@ +.. _smart_art_api: + +SmartArt +======== + +|pp| supports text substitution inside an existing template's SmartArt. +Full SmartArt creation is intentionally out of scope — see ``ROADMAP.md``. + +.. currentmodule:: pptx.smart_art + + +.. autoclass:: SmartArtCollection + :members: + :special-members: __len__, __getitem__, __iter__ + :undoc-members: + + +.. autoclass:: SmartArtShape + :members: + :undoc-members: diff --git a/docs/api/theme.rst b/docs/api/theme.rst new file mode 100644 index 000000000..0c04924fb --- /dev/null +++ b/docs/api/theme.rst @@ -0,0 +1,33 @@ +.. _theme_api: + +Theme +===== + +.. currentmodule:: pptx.theme + + +|Theme| objects +--------------- + +.. autoclass:: Theme + :members: + :undoc-members: + + +.. autoclass:: ThemeColors + :members: + :special-members: __getitem__, __setitem__ + :undoc-members: + + +.. autoclass:: ThemeFonts + :members: + :undoc-members: + + +Inheritance helpers +------------------- + +.. currentmodule:: pptx.inherit + +.. autofunction:: resolve_color diff --git a/docs/conf.py b/docs/conf.py index cf9911544..f863552da 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,575 +1,264 @@ -# -*- coding: utf-8 -*- +# Configuration file for the Sphinx documentation builder. # -# python-pptx documentation build configuration file, created by -# sphinx-quickstart on Thu Nov 29 13:59:35 2012. -# -# This file is execfile()d with the current directory set to its containing -# dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# Originally created for python-pptx by Steve Canny in 2012; updated for +# the power-pptx fork in 2026. + +from __future__ import annotations import os import sys -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) from pptx import __version__ # noqa: E402 +# -- Project information ----------------------------------------------------- -# -- Allow nonlocal image URI's to accommodate travis-ci status image ------- - -import sphinx.environment # noqa: E402 -from docutils.utils import get_source_line # noqa: E402 - - -def _warn_node(self, msg, node, **kwargs): - if not msg.startswith("nonlocal image URI found:"): - self._warnfunc(msg, "%s:%s" % get_source_line(node), **kwargs) - - -sphinx.environment.BuildEnvironment.warn_node = _warn_node +project = "power-pptx" +copyright = "2012, 2013, Steve Canny; 2026, Daniel Halwell" +author = "Daniel Halwell" +version = __version__ +release = __version__ -# -- General configuration -------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' +# -- General configuration --------------------------------------------------- -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", - "sphinx.ext.inheritance_diagram", "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", "sphinx.ext.todo", "sphinx.ext.coverage", - "sphinx.ext.ifconfig", - "sphinx.ext.viewcode", ] -# Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] - -# The suffix of source filenames. source_suffix = ".rst" +master_doc = "index" +exclude_patterns = [".build", "_build"] +pygments_style = "sphinx" -# The encoding of source files. -# source_encoding = 'utf-8-sig' +# -- Autodoc options --------------------------------------------------------- -# The master toctree document. -master_doc = "index" +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, +} +autodoc_member_order = "bysource" +autodoc_typehints = "description" -# General information about the project. -project = u"power-pptx" -copyright = u"2012, 2013, Steve Canny; 2026, Daniel Halwell" +# -- Intersphinx ------------------------------------------------------------- -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = __version__ -# The full version, including alpha/beta/rc tags. -release = __version__ +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), +} + +# -- Substitutions used across the documentation ----------------------------- -# A string of reStructuredText that will be included at the end of every source -# file that is read. This is the right place to add substitutions that should -# be available in every file. rst_epilog = """ -.. |ActionSetting| replace:: :class:`.ActionSetting` +.. |pp| replace:: power-pptx -.. |Adjustment| replace:: :class:`.Adjustment` +.. |str| replace:: :class:`str` +.. |int| replace:: :class:`int` +.. |float| replace:: :class:`float` +.. |list| replace:: :class:`list` +.. |bool| replace:: :class:`bool` +.. |bytes| replace:: :class:`bytes` +.. |True| replace:: :class:`True` +.. |False| replace:: :class:`False` +.. |None| replace:: :class:`None` +.. |datetime| replace:: :class:`datetime.datetime` -.. |AdjustmentCollection| replace:: :class:`.AdjustmentCollection` +.. |AttributeError| replace:: :exc:`AttributeError` +.. |KeyError| replace:: :exc:`KeyError` +.. |TypeError| replace:: :exc:`TypeError` +.. |ValueError| replace:: :exc:`ValueError` +.. |NotImplementedError| replace:: :exc:`NotImplementedError` +.. |InvalidXmlError| replace:: :exc:`InvalidXmlError` +.. |ActionSetting| replace:: :class:`.ActionSetting` +.. |Adjustment| replace:: :class:`.Adjustment` +.. |AdjustmentCollection| replace:: :class:`.AdjustmentCollection` .. |AreaSeries| replace:: :class:`.AreaSeries` - -.. |AttributeError| replace:: :exc:`.AttributeError` - .. |Axis| replace:: :class:`.Axis` - .. |AxisTitle| replace:: :class:`.AxisTitle` - .. |_Background| replace:: :class:`._Background` - .. |BarPlot| replace:: :class:`.BarPlot` - .. |BarSeries| replace:: :class:`.BarSeries` - -.. |BaseFileSystem| replace:: :class:`BaseFileSystem` - .. |_BaseMaster| replace:: :class:`._BaseMaster` - .. |BasePlaceholder| replace:: :class:`.BasePlaceholder` - .. |_BasePlot| replace:: :class:`._BasePlot` - +.. |BaseFileSystem| replace:: :class:`BaseFileSystem` .. |BaseShape| replace:: :class:`.BaseShape` - .. |BaseSlidePart| replace:: :class:`.BaseSlidePart` - +.. |BlurFormat| replace:: :class:`.BlurFormat` +.. |Borders| replace:: :class:`.Borders` +.. |_Borders| replace:: :class:`._Borders` .. |BubbleChartData| replace:: :class:`.BubbleChartData` - .. |BubblePlot| replace:: :class:`.BubblePlot` - .. |BubblePoints| replace:: :class:`.BubblePoints` - .. |BubbleSeries| replace:: :class:`.BubbleSeries` - .. |BubbleSeriesData| replace:: :class:`.BubbleSeriesData` - .. |category.Categories| replace:: :class:`~.category.Categories` - .. |data.Categories| replace:: :class:`~.data.Categories` - .. |category.Category| replace:: :class:`~.category.Category` - .. |data.Category| replace:: :class:`~.data.Category` - .. |CategoryAxis| replace:: :class:`.CategoryAxis` - .. |CategoryChartData| replace:: :class:`.CategoryChartData` - .. |CategoryLevel| replace:: :class:`.CategoryLevel` - .. |CategoryPoints| replace:: :class:`.CategoryPoints` - .. |_Cell| replace:: :class:`_Cell` - .. |Chart| replace:: :class:`.Chart` - .. |ChartData| replace:: :class:`.ChartData` - .. |ChartFormat| replace:: :class:`.ChartFormat` - .. |ChartPart| replace:: :class:`.ChartPart` - .. |ChartTitle| replace:: :class:`.ChartTitle` - .. |ChartXmlWriter| replace:: :class:`.ChartXmlWriter` - -.. |_Close| replace:: :class:`_Close` - -.. |Collection| replace:: :class:`Collection` - .. |ColorFormat| replace:: :class:`.ColorFormat` - .. |_Column| replace:: :class:`_Column` - .. |_ColumnCollection| replace:: :class:`_ColumnCollection` - .. |Connector| replace:: :class:`.Connector` - .. |CoreProperties| replace:: :class:`.CoreProperties` - .. |DataLabel| replace:: :class:`.DataLabel` - .. |DataLabels| replace:: :class:`.DataLabels` - .. |DateAxis| replace:: :class:`.DateAxis` - -.. |datetime| replace:: :class:`datetime.datetime` - -.. |DirectoryFileSystem| replace:: :class:`DirectoryFileSystem` - -.. |DrawingOperations| replace:: :class:`.DrawingOperations` - +.. |DesignTokens| replace:: :class:`.DesignTokens` .. |Emu| replace:: :class:`.Emu` - -.. |False| replace:: :class:`False` - -.. |FileSystem| replace:: :class:`FileSystem` - .. |FillFormat| replace:: :class:`.FillFormat` - -.. |float| replace:: :class:`float` - .. |Font| replace:: :class:`.Font` - .. |FreeformBuilder| replace:: :class:`.FreeformBuilder` - +.. |GlowFormat| replace:: :class:`.GlowFormat` .. |GradientStops| replace:: :class:`.GradientStops` - .. |GraphicFrame| replace:: :class:`.GraphicFrame` - .. |GroupShape| replace:: :class:`.GroupShape` - .. |GroupShapes| replace:: :class:`.GroupShapes` - .. |_Hyperlink| replace:: :class:`._Hyperlink` - .. |Hyperlink| replace:: :class:`.Hyperlink` - .. |Image| replace:: :class:`.Image` - .. |ImagePart| replace:: :class:`.ImagePart` - .. |Inches| replace:: :class:`.Inches` - -.. |int| replace:: :class:`int` - -.. |InvalidXmlError| replace:: :exc:`InvalidXmlError` - -.. |KeyError| replace:: :exc:`KeyError` - .. |LayoutPlaceholder| replace:: :class:`.LayoutPlaceholder` - .. |LayoutPlaceholders| replace:: :class:`.LayoutPlaceholders` - .. |LayoutShapes| replace:: :class:`.LayoutShapes` - .. |Legend| replace:: :class:`.Legend` - .. |Length| replace:: :class:`.Length` - .. |LineFormat| replace:: :class:`.LineFormat` - -.. |_LineSegment| replace:: :class:`._LineSegment` - +.. |LineEndFormat| replace:: :class:`.LineEndFormat` .. |LineSeries| replace:: :class:`.LineSeries` - -.. |list| replace:: :class:`list` - +.. |_LineSegment| replace:: :class:`._LineSegment` .. |MajorGridlines| replace:: :class:`.MajorGridlines` - .. |Marker| replace:: :class:`.Marker` - .. |MasterPlaceholder| replace:: :class:`.MasterPlaceholder` - .. |MasterPlaceholders| replace:: :class:`.MasterPlaceholders` - .. |MasterShapes| replace:: :class:`.MasterShapes` - .. |_MediaFormat| replace:: :class:`._MediaFormat` - -.. |None| replace:: :class:`None` - .. |NotesMaster| replace:: :class:`.NotesMaster` - .. |NotesSlide| replace:: :class:`.NotesSlide` - .. |NotesSlidePlaceholders| replace:: :class:`.NotesSlidePlaceholders` - .. |NotesSlideShapes| replace:: :class:`.NotesSlideShapes` - -.. |NotImplementedError| replace:: :exc:`NotImplementedError` - +.. |_Paragraph| replace:: :class:`_Paragraph` .. |OpcPackage| replace:: :class:`.OpcPackage` - .. |Package| replace:: :class:`Package` - .. |PackURI| replace:: :class:`.PackURI` - -.. |_Paragraph| replace:: :class:`_Paragraph` - .. |Part| replace:: :class:`Part` - .. |PartTypeSpec| replace:: :class:`PartTypeSpec` - .. |Picture| replace:: :class:`.Picture` - +.. |PictureEffects| replace:: :class:`.PictureEffects` .. |PieSeries| replace:: :class:`.PieSeries` - .. |_PlaceholderFormat| replace:: :class:`._PlaceholderFormat` - .. |PlaceholderGraphicFrame| replace:: :class:`.PlaceholderGraphicFrame` - .. |PlaceholderPicture| replace:: :class:`.PlaceholderPicture` - .. |Plots| replace:: :class:`.Plots` - .. |Point| replace:: :class:`.Point` - -.. |pp| replace:: `python-pptx` - .. |Presentation| replace:: :class:`~pptx.presentation.Presentation` - -.. |PresentationPart| replace:: :class:`.PresentationPart` - .. |Pt| replace:: :class:`.Pt` - .. |RadarSeries| replace:: :class:`.RadarSeries` - +.. |ReflectionFormat| replace:: :class:`.ReflectionFormat` .. |_Relationship| replace:: :class:`._Relationship` - .. |_Relationships| replace:: :class:`_Relationships` - .. |RGBColor| replace:: :class:`.RGBColor` - .. |_Row| replace:: :class:`_Row` - .. |_RowCollection| replace:: :class:`_RowCollection` - .. |_Run| replace:: :class:`_Run` - .. |Series| replace:: :class:`.Series` - .. |SeriesCollection| replace:: :class:`.SeriesCollection` - .. |ShadowFormat| replace:: :class:`.ShadowFormat` - .. |Shape| replace:: :class:`.Shape` - .. |ShapeCollection| replace:: :class:`.ShapeCollection` - +.. |ShapeStyle| replace:: :class:`.ShapeStyle` .. |Slide| replace:: :class:`.Slide` - +.. |SlideLintReport| replace:: :class:`.SlideLintReport` +.. |SmartArtShape| replace:: :class:`.SmartArtShape` +.. |SlideShapes| replace:: :class:`.SlideShapes` +.. |SlidePlaceholders| replace:: :class:`.SlidePlaceholders` +.. |SlideMasterPart| replace:: :class:`.SlideMasterPart` +.. |SlideLayoutPart| replace:: :class:`.SlideLayoutPart` +.. |SoftEdgeFormat| replace:: :class:`.SoftEdgeFormat` .. |Slides| replace:: :class:`.Slides` - +.. |SlideAnimations| replace:: :class:`.SlideAnimations` .. |SlideLayout| replace:: :class:`.SlideLayout` - .. |SlideLayouts| replace:: :class:`.SlideLayouts` - -.. |SlideLayoutPart| replace:: :class:`.SlideLayoutPart` - .. |SlideMaster| replace:: :class:`.SlideMaster` - .. |SlideMasters| replace:: :class:`.SlideMasters` - -.. |SlideMasterPart| replace:: :class:`.SlideMasterPart` - -.. |SlidePlaceholders| replace:: :class:`.SlidePlaceholders` - -.. |SlideShapes| replace:: :class:`.SlideShapes` - -.. |str| replace:: :class:`str` - +.. |SlideTransition| replace:: :class:`.SlideTransition` +.. |SmartArtCollection| replace:: :class:`.SmartArtCollection` .. |Table| replace:: :class:`Table` - .. |TextFrame| replace:: :class:`.TextFrame` - +.. |Theme| replace:: :class:`.Theme` +.. |ThreeDFormat| replace:: :class:`.ThreeDFormat` .. |TickLabels| replace:: :class:`.TickLabels` - -.. |True| replace:: :class:`True` - -.. |TypeError| replace:: :exc:`TypeError` - .. |ValueAxis| replace:: :class:`.ValueAxis` - -.. |ValueError| replace:: :exc:`ValueError` - -.. |WorkbookWriter| replace:: :class:`.WorkbookWriter` - .. |XyChartData| replace:: :class:`.XyChartData` - .. |XyPoints| replace:: :class:`.XyPoints` - .. |XySeries| replace:: :class:`.XySeries` - .. |XySeriesData| replace:: :class:`.XySeriesData` - +.. |WorkbookWriter| replace:: :class:`.WorkbookWriter` .. |ZipFileSystem| replace:: :class:`ZipFileSystem` +.. |DirectoryFileSystem| replace:: :class:`DirectoryFileSystem` +.. |FileSystem| replace:: :class:`FileSystem` +.. |Collection| replace:: :class:`Collection` +.. |DrawingOperations| replace:: :class:`.DrawingOperations` """ -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [".build"] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - - -# -- Options for HTML output ------------------------------------------------ - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "armstrong" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [".themes"] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None +# -- HTML output ------------------------------------------------------------- -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -html_sidebars = { - "**": ["localtoc.html", "relations.html", "sidebarlinks.html", "searchbox.html"] +html_theme = "sphinx_rtd_theme" +html_theme_options = { + "navigation_depth": 3, + "collapse_navigation": False, + "style_external_links": True, } - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. +html_static_path = ["_static"] +html_title = f"power-pptx {release}" +html_short_title = "power-pptx" htmlhelp_basename = "power-pptxdoc" -# -- Options for LaTeX output ----------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # 'preamble': '', -} +# -- LaTeX output ------------------------------------------------------------ -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, -# documentclass [howto/manual]). latex_documents = [ - ( - "index", - "power-pptx.tex", - u"power-pptx Documentation", - u"Steve Canny", - "manual", - ), + ("index", "power-pptx.tex", "power-pptx Documentation", author, "manual"), ] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True +# -- Manual page output ------------------------------------------------------ -# -- Options for manual page output ----------------------------------------- +man_pages = [("index", "power-pptx", "power-pptx Documentation", [author], 1)] -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ("index", "power-pptx", u"power-pptx Documentation", [u"Steve Canny"], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False +# -- Texinfo output ---------------------------------------------------------- -# -- Options for Texinfo output --------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) texinfo_documents = [ ( "index", "power-pptx", - u"power-pptx Documentation", - u"Steve Canny", + "power-pptx Documentation", + author, "power-pptx", - "One line description of project.", + "Create, read, and update PowerPoint 2007+ (.pptx) files from Python.", "Miscellaneous", ), ] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - - -# Example configuration for intersphinx: refer to the Python standard library. -# intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/docs/index.rst b/docs/index.rst index 79ad6c369..8e556f3a0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,5 @@ - -python-pptx -=========== +power-pptx +========== Release v\ |version| (:ref:`Installation `) @@ -10,40 +9,44 @@ Release v\ |version| (:ref:`Installation `) Philosophy ---------- -|pp| aims to broadly support the PowerPoint format (PPTX, PowerPoint 2007 and later), -but its primary commitment is to be *industrial-grade*, that is, suitable for use in a -commercial setting. Maintaining this robustness requires a high engineering standard -which includes a comprehensive two-level (e2e + unit) testing regimen. This discipline -comes at a cost in development effort/time, but we consider reliability to be an -essential requirement. +|pp| aims to broadly support the PowerPoint format (PPTX, PowerPoint 2007 and +later), but its primary commitment is to be *industrial-grade*, that is, +suitable for use in a commercial setting. Maintaining this robustness requires +a high engineering standard which includes a comprehensive two-level (e2e + +unit) testing regimen and a round-trip regression harness that locks in every +new feature. -Feature Support +Feature support --------------- |pp| has the following capabilities: * Round-trip any Open XML presentation (.pptx file) including all its elements -* Add slides -* Populate text placeholders, for example to create a bullet slide -* Add image to slide at arbitrary position and size -* Add textbox to a slide; manipulate text font size and bold -* Add table to a slide -* Add auto shapes (e.g. polygons, flowchart shapes, etc.) to a slide -* Add and manipulate column, bar, line, and pie charts +* Add slides, populate text placeholders, add images, textboxes, tables, auto + shapes (polygons, flowchart shapes, etc.), and column / bar / line / pie + charts at arbitrary positions and sizes * Access and change core document properties such as title and subject -* And many others ... - -Even with all |pp| does, the PowerPoint document format is very rich and there are still -features |pp| does not support. - - -New features/releases ---------------------- - -New features are generally added via sponsorship. If there's a new feature you need for -your use case, feel free to reach out at the email address on the github.com/scanny -profile page. Many of the most used features such as charts were added this way. +* Apply visual effects (shadows, glow, soft edges, blur, reflection) and + alpha-tinted colors via non-mutating proxies on every shape +* Author entrance, exit, emphasis, and motion-path animations using a small + preset library; apply per-slide and deck-wide transitions including Morph +* Read and write the active theme's color scheme and major/minor fonts; + apply a theme imported from a ``.potx`` +* Compose presentations from a JSON spec, import slides between decks, and + apply a template to existing slides +* Run a layout linter on each slide to detect text overflow, off-slide + shapes, and shape collisions; auto-fix nudges off-slide shapes back + inside the slide bounds (text-overflow auto-fix is on the roadmap) +* Build with a design-token system, opinionated slide recipes, and ``Grid`` / + ``Stack`` layout primitives +* Recolor charts from named palettes and toggle title / legend / axis-label + / gridline visibility through quick-layout presets +* Render slide thumbnails through LibreOffice when one is on ``$PATH`` + +Even with all that, the PowerPoint document format is very rich and there are +still features |pp| does not support — see ``ROADMAP.md`` for what is +planned and what is intentionally out of scope. User Guide @@ -67,6 +70,15 @@ User Guide user/notes user/use-cases user/concepts + user/effects + user/animation + user/transitions + user/lint + user/compose + user/theme + user/design + user/charts-advanced + user/render Community Guide @@ -101,6 +113,13 @@ API Documentation api/image api/exc api/util + api/animation + api/lint + api/compose + api/theme + api/design + api/render + api/smart_art api/enum/index diff --git a/docs/user/animation.rst b/docs/user/animation.rst new file mode 100644 index 000000000..669ac4aeb --- /dev/null +++ b/docs/user/animation.rst @@ -0,0 +1,83 @@ +.. _animation: + +Animations +========== + +|pp| ships a preset-only animation API that maps directly onto +PowerPoint's built-in animation library. All generated XML is valid OOXML +and round-trips through PowerPoint without loss. Animations authored in +the desktop UI survive a read–modify–write cycle untouched. + +Triggers +-------- + +Every preset accepts an optional ``trigger`` and ``delay``:: + + from pptx.animation import Entrance, Trigger + + Entrance.fade(slide, shape) # ON_CLICK + Entrance.fly_in(slide, shape, trigger=Trigger.WITH_PREVIOUS) + Entrance.zoom(slide, shape, trigger=Trigger.AFTER_PREVIOUS, + delay=500) + +Entrance / exit / emphasis presets +---------------------------------- + +:: + + Entrance.appear(slide, shape) + Entrance.fade(slide, shape) + Entrance.fly_in(slide, shape, direction="bottom") + Entrance.float_in(slide, shape) + Entrance.wipe(slide, shape) + Entrance.zoom(slide, shape) + Entrance.wheel(slide, shape) + Entrance.random_bars(slide, shape) + + Exit.disappear(slide, shape) + Exit.fade(slide, shape) + Exit.fly_out(slide, shape) + + Emphasis.pulse(slide, shape) + Emphasis.spin(slide, shape) + Emphasis.teeter(slide, shape) + +Per-paragraph reveal +-------------------- + +Pass ``by_paragraph=True`` to fade, wipe, zoom, wheel, or appear a text +frame in one paragraph at a time. Each paragraph fires +``AFTER_PREVIOUS`` so the whole sequence plays from a single click:: + + Entrance.fade(slide, body_text_frame, by_paragraph=True) + +Sequencing +---------- + +A context manager defaults the first effect inside the block to the +caller-supplied (or ``ON_CLICK``) trigger and chains the rest with +``AFTER_PREVIOUS``:: + + with slide.animations.sequence(): + Entrance.fade(slide, title) + Entrance.fly_in(slide, body) + Emphasis.pulse(slide, badge) + +Sequences are not nestable; explicit per-call triggers still win. + +Motion paths +------------ + +:: + + from pptx.animation import MotionPath + from pptx.util import Inches + + MotionPath.line(slide, shape, Inches(2), Inches(1)) + MotionPath.diagonal(slide, shape, Inches(3), Inches(2)) + MotionPath.circle(slide, shape, radius=Inches(1), clockwise=True) + MotionPath.arc(slide, shape, Inches(3), Inches(0), height=0.4) + MotionPath.zigzag(slide, shape, Inches(4), Inches(0), + segments=6, amplitude=0.2) + MotionPath.spiral(slide, shape, Inches(2), turns=2.5) + MotionPath.custom(slide, shape, "M 0 0 L 0.5 0.5") # OOXML expr diff --git a/docs/user/charts-advanced.rst b/docs/user/charts-advanced.rst new file mode 100644 index 000000000..8b12d2bb7 --- /dev/null +++ b/docs/user/charts-advanced.rst @@ -0,0 +1,58 @@ +.. _charts-advanced: + +Charts: palettes, quick layouts, per-series fills +================================================== + +The chart helpers below stack on top of the existing chart API; nothing +here replaces ``chart_style`` or the underlying series formatting — they +just make common operations one line each. + +Chart palettes +-------------- + +``Chart.apply_palette(palette)`` recolors every series in declaration +order from a named built-in or an iterable of color-likes (``RGBColor``, +hex strings with or without ``#``, or ``(r, g, b)`` triples). Palettes +wrap when the chart has more series than colors:: + + chart.apply_palette("modern") + chart.apply_palette(["#4F9DFF", "#7FCFA1", "#F7B500"]) + +Six built-ins ship in ``pptx.chart.palettes``: +``modern``, ``classic``, ``editorial``, ``vibrant``, +``monochrome_blue``, and ``monochrome_warm``. ``palette_names()`` and +``resolve_palette()`` are also exported for callers that want to share +the same color set with non-chart shapes. + +The ``chart_style`` integer is left untouched, so the palette overrides +only the per-series fill without rewriting the rest of the style. + +Quick layouts +------------- + +``Chart.apply_quick_layout(layout)`` toggles title / legend / axis-title +/ gridline visibility in opinionated combinations. Ten built-in +presets ship in ``pptx.chart.quick_layouts``:: + + chart.apply_quick_layout("title_legend_right") + chart.apply_quick_layout("title_legend_bottom") + chart.apply_quick_layout("title_axes_legend_right") + chart.apply_quick_layout("minimal") + +Custom layouts can be supplied as a dict spec. Missing keys leave the +chart untouched so layouts compose cleanly, and charts without +category/value axes (e.g. pie) silently skip the corresponding keys. + +Per-series gradient and pattern fills +------------------------------------- + +``chart.series[i].format.fill`` is a regular |FillFormat|, so all four +gradient kinds and ``MSO_PATTERN_TYPE`` patterns work per-series with no +chart-specific shim:: + + fill = chart.series[0].format.fill + fill.gradient(kind="linear") + fill.gradient_stops.replace([(0.0, "#0F2D6B"), (1.0, "#4F9DFF")]) + + chart.series[1].format.fill.patterned() + chart.series[1].format.fill.pattern = MSO_PATTERN_TYPE.WIDE_DOWNWARD_DIAGONAL diff --git a/docs/user/compose.rst b/docs/user/compose.rst new file mode 100644 index 000000000..84c7dac92 --- /dev/null +++ b/docs/user/compose.rst @@ -0,0 +1,59 @@ +.. _compose: + +Composition: from_spec, import_slide, apply_template +===================================================== + +The :mod:`pptx.compose` package collects entry points for higher-level +authoring and cross-presentation operations. + +JSON authoring +-------------- + +``from_spec`` is a single entry point for generator scripts (LLM or +otherwise). The spec dict is validated for known keys and value +shapes before construction (no JSON Schema is involved):: + + from pptx.compose import from_spec + + prs = from_spec({ + "theme": {"palette": "modern_blue", "fonts": "inter"}, + "slides": [ + {"layout": "title", "title": "Q4 Review", + "subtitle": "April 2026", "transition": "morph"}, + {"layout": "kpi_grid", "title": "Run-rate metrics", + "kpis": [ + {"label": "ARR", "value": "$182M", "delta": +0.27}, + {"label": "NDR", "value": "131%", "delta": +0.03}, + ]}, + {"layout": "bullets", "title": "Customer impact", + "bullets": [ + "Two flagship customers shipped this week.", + "NPS improved 8 points QoQ.", + ]}, + ], + "lint": "raise", + }) + +Layout names map either to Phase-9 design recipes (where supplied) or to +a small built-in set of layouts using the host presentation's master. + +Cross-presentation operations +----------------------------- + +:: + + from pptx import Presentation + from pptx.compose import import_slide, apply_template + + src = Presentation("source.pptx") + dst = Presentation("destination.pptx") + + # Clone a slide with its layout reference, deduping the master. + import_slide(dst, src.slides[3], merge_master="dedupe") + + # Re-point existing slides at masters/layouts from a .potx. + apply_template(dst, "brand-template.potx") + +``merge_master="clone"`` keeps a fresh copy of the source master alongside +existing masters; ``"dedupe"`` reuses an equivalent master in the +destination when one is available. diff --git a/docs/user/design.rst b/docs/user/design.rst new file mode 100644 index 000000000..cfa4be61b --- /dev/null +++ b/docs/user/design.rst @@ -0,0 +1,115 @@ +.. _design: + +Design system layer +=================== + +The :mod:`pptx.design` package turns the low-level API into something +where the *default* output looks good. Nothing here adds new XML — it's +all built on top of the foundations from earlier phases. + +Design tokens +------------- + +:class:`pptx.design.tokens.DesignTokens` is a source-agnostic container +for brand tokens — palette, typography, radii, shadows, and spacings:: + + from pptx.design.tokens import DesignTokens + + tokens = DesignTokens.from_dict({ + "palette": { + "primary": "#4F9DFF", + "neutral": "#1F2937", + "positive": "#10B981", + "negative": "#EF4444", + "on_primary": "#FFFFFF", + }, + "typography": { + "title": {"family": "Inter", "size": 44, "bold": True}, + "body": {"family": "Inter", "size": 18}, + }, + "shadows": { + "card": {"blur": 18, "distance": 4, "alpha": 0.18}, + }, + }) + +Other constructors: + +* :py:meth:`DesignTokens.from_yaml('brand.yml') ` + — optional ``pyyaml`` dependency. +* :py:meth:`DesignTokens.from_pptx('template.pptx') ` + — extracts the six accent slots, ``dk1`` / ``dk2`` / ``lt1`` / ``lt2``, + the hyperlink slots, and major/minor fonts. +* ``tokens.merge(other_tokens)`` layers an override set on top of a base. + +Token-resolving shape style +--------------------------- + +Every shape exposes a :class:`.ShapeStyle` facade that fans assignments +out to the low-level proxies:: + + shape.style.fill = tokens.palette["primary"] + shape.style.line = tokens.palette["primary"] + shape.style.shadow = tokens.shadows["card"] + shape.style.text_color = tokens.palette["on_primary"] + shape.style.font = tokens.typography["body"] + +Partial ``ShadowToken`` assignments leave unset fields untouched, so +overrides are non-destructive; ``shape.style.shadow = None`` clears the +effect. + +Layout primitives +----------------- + +``pptx.design.layout`` provides build-time geometry helpers — no XML is +read or mutated until you call ``place()``:: + + from pptx.design.layout import Grid, Stack + from pptx.util import Pt + + grid = Grid(slide, cols=12, rows=6, gutter=Pt(12)) + grid.place(card1, col=0, row=0, col_span=6, row_span=4) + grid.place(card2, col=6, row=0, col_span=6, row_span=4) + + stack = Stack(direction="vertical", gap=Pt(8), + left=Pt(48), top=Pt(48)) + stack.place(title, width=Pt(600), height=Pt(64)) + stack.place(body, width=Pt(600), height=Pt(200)) + +Slide recipes +------------- + +``pptx.design.recipes`` ships opinionated parameterized slide +constructors. Each takes the host |Presentation|, the recipe-specific +content kwargs, an optional |DesignTokens|, and an optional +``transition=`` name:: + + from pptx.design.recipes import ( + title_slide, bullet_slide, kpi_slide, + quote_slide, image_hero_slide, + ) + + title_slide(prs, title="Q4 Review", subtitle="April 2026", + tokens=tokens, transition="morph") + bullet_slide(prs, title="Customer impact", + bullets=["Two flagship customers shipped this week.", + "NPS improved 8 points QoQ."], + tokens=tokens) + kpi_slide(prs, title="Run-rate metrics", + kpis=[{"label": "ARR", "value": "$182M", "delta": +0.27}, + {"label": "NDR", "value": "131%", "delta": +0.03}], + tokens=tokens) + +Recipes use the ``Blank`` layout and place every shape themselves so the +rendered geometry doesn't depend on the host template's master. + +Starter pack +------------ + +``examples/starter_pack/`` ships three example token sets — *modern*, +*classic*, and *editorial* — each exporting both a raw ``SPEC`` dict and +a ready-to-use ``TOKENS``. Run:: + + python -m examples.starter_pack.build_preview + +to render one preview deck per set under +``examples/starter_pack/_out/``. diff --git a/docs/user/effects.rst b/docs/user/effects.rst new file mode 100644 index 000000000..0ff971168 --- /dev/null +++ b/docs/user/effects.rst @@ -0,0 +1,102 @@ +.. _effects: + +Visual effects +============== + +Every shape in |pp| exposes a small family of effect proxies that read and +write the underlying ```` and related elements. Reads never +mutate the XML — accessing an unset property returns |None| so theme +inheritance is preserved. + +Shadow, glow, soft edges, blur, reflection +------------------------------------------ + +:: + + from pptx.util import Pt + from pptx.dml.color import RGBColor + + shadow = shape.shadow + shadow.blur_radius = Pt(8) + shadow.distance = Pt(4) + shadow.direction = 90.0 # degrees, pointing down + shadow.color.rgb = RGBColor(0x00, 0x00, 0x00) + shadow.color.alpha = 0.35 # 35% opacity + + shape.glow.radius = Pt(6) + shape.glow.color.rgb = RGBColor(0x4F, 0x9D, 0xFF) + + shape.soft_edges.radius = Pt(3) + + shape.blur.radius = Pt(4) + shape.blur.grow = True + + shape.reflection.blur_radius = Pt(2) + shape.reflection.distance = Pt(1) + shape.reflection.start_alpha = 0.5 + shape.reflection.end_alpha = 0.0 + +Setting every explicit property to |None| drops the corresponding XML +element again so the shape inherits the master/theme value. + +Alpha and gradient fills +------------------------ + +``ColorFormat.alpha`` is a read/write float in ``[0.0, 1.0]`` and is also +available on the lazy-color proxy returned by ``Font.color`` and +``LineFormat.color``. + +The gradient fill helper accepts a kind argument and exposes mutable +stops:: + + fill = shape.fill + fill.gradient(kind="radial") + fill.gradient_kind # → "radial" + + stops = fill.gradient_stops + stops.replace([ + (0.0, "#0F2D6B"), + (0.55, RGBColor(0x4F, 0x9D, 0xFF)), + (1.0, (255, 255, 255)), + ]) + +Picture effects +--------------- + +Pictures gain a dedicated ``effects`` accessor that wraps the OOXML +```` filters:: + + pic = slide.shapes.add_picture("hero.jpg", Inches(0), Inches(0)) + pic.effects.transparency = 0.2 + pic.effects.brightness = 0.1 + pic.effects.contrast = 0.05 + pic.effects.set_duotone(RGBColor(0x12, 0x1E, 0x4D), "#A8C0FF") + +``set_duotone`` accepts |RGBColor|, hex strings (with or without ``#``), +or RGB 3-tuples. + +Native SVG +---------- + +``slide.shapes.add_svg_picture(path, left, top)`` embeds both the SVG and +a PNG fallback inside the same ````. Provide ``png_fallback=`` to +supply a hand-rasterised file, or install ``cairosvg`` to have it +generated automatically. + +Line ends, caps, joins, compound lines +-------------------------------------- + +:: + + from pptx.enum.dml import ( + MSO_LINE_CAP_STYLE, + MSO_LINE_COMPOUND_STYLE, + MSO_LINE_JOIN_STYLE, + ) + + line = shape.line + line.head_end.type = "TRIANGLE" + line.tail_end.length = "LARGE" + line.cap = MSO_LINE_CAP_STYLE.ROUND + line.compound = MSO_LINE_COMPOUND_STYLE.DOUBLE + line.join = MSO_LINE_JOIN_STYLE.BEVEL diff --git a/docs/user/lint.rst b/docs/user/lint.rst new file mode 100644 index 000000000..f30194fb8 --- /dev/null +++ b/docs/user/lint.rst @@ -0,0 +1,100 @@ +.. _lint: + +Layout linter +============= + +|pp| includes a read-only inspector that reports geometric and typographic +issues on a slide. It is designed for scripts that generate slides +programmatically — most usefully for LLM-driven generators that +occasionally produce overflowing text or off-slide shapes. + +Running the linter +------------------ + +:: + + report = slide.lint() + report.issues # list[LintIssue] + report.has_errors # bool + print(report.summary()) + +For a whole deck, iterate the slides yourself:: + + all_issues = [] + for slide in prs.slides: + all_issues.extend(slide.lint().issues) + +The :func:`pptx.compose.from_spec` entry point also accepts a +deck-level ``"lint": "warn" | "raise"`` field that walks every slide +and surfaces issues for you. + +Issue types +----------- + +* :class:`pptx.lint.TextOverflow` — estimated text extent exceeds the + text-frame extent. The current 1.1 implementation uses a fast + character/line-count heuristic (default character width of + ``0.55 × pt``, line height of ``1.2 × pt``) and respects text-frame + margins; shapes with ``auto_size`` set to ``TEXT_TO_FIT_SHAPE`` or + ``SHAPE_TO_FIT_TEXT`` are skipped because they cannot overflow by + definition. A Pillow-driven measurement pass is on the roadmap. +* :class:`pptx.lint.OffSlide` — a shape is wholly or partly outside the + slide bounds. +* :class:`pptx.lint.ShapeCollision` — two shapes' bounding boxes overlap + significantly. + +Each issue carries a ``severity`` (:class:`~pptx.lint.LintSeverity`), +a ``code`` string, a human-readable ``message``, and a ``shapes`` +tuple of the shapes it implicates. + +Auto-fix +-------- + +Some issues can be repaired without designer judgment:: + + fixes = report.auto_fix() # mutates; returns list[str] + preview = report.auto_fix(dry_run=True) + +Currently auto-fixable: + +* ``OffSlide`` — translates the shape so it sits inside the slide + bounds. + +Not auto-fixable in 1.1: + +* ``TextOverflow`` — requires designer judgment on font size vs + content. Use ``text_frame.fit_text(...)`` (which measures with + Pillow font metrics and bakes a fitting size into the XML) or set + ``text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE`` to let + PowerPoint shrink at render time. +* ``ShapeCollision`` — auto-nudging shapes apart almost always breaks + the design. + +Recommended pattern for generators +---------------------------------- + +:: + + from pptx.exc import LintError + + prs = build_deck_from_user_input(...) + + # 1. Auto-fix what we can (currently: nudge OffSlide shapes back in) + for slide in prs.slides: + slide.lint().auto_fix() + + # 2. Re-run and bail on any remaining errors + remaining: list = [] + for slide in prs.slides: + remaining.extend( + i for i in slide.lint().issues + if i.severity.value == "error" + ) + if remaining: + raise LintError("; ".join(str(i) for i in remaining)) + + prs.save("out.pptx") + +When building through :func:`pptx.compose.from_spec`, the +``"lint": "raise"`` field on the spec dict does the same thing in +fewer lines. diff --git a/docs/user/render.rst b/docs/user/render.rst new file mode 100644 index 000000000..659356eca --- /dev/null +++ b/docs/user/render.rst @@ -0,0 +1,54 @@ +.. _render: + +Slide thumbnails +================ + +|pp| can render slide thumbnails by shelling out to LibreOffice. +This is a convenience for review tooling, dashboards, and CI artifacts +— it does not require Microsoft PowerPoint or an Office license, but +``soffice`` must be on ``$PATH`` (or you can point at a custom binary). + +Convenience methods +------------------- + +:: + + paths = prs.render_thumbnails(out_dir="thumbs") + png = slide.render_thumbnail(return_bytes=True) + +Module-level entry points +------------------------- + +:: + + from pptx.render import ( + render_slide_thumbnails, + render_slide_thumbnail, + ) + + paths = render_slide_thumbnails( + prs, + out_dir="thumbs", + slide_indexes=[0, 3, 7], + soffice_bin="/opt/libreoffice/program/soffice", + timeout=60, + ) + +The output resolution is whatever LibreOffice's headless PNG converter +chooses — there is no ``width=`` knob. Post-process with Pillow if you +need a specific size. + +Set the ``POWER_PPTX_SOFFICE`` environment variable to override the +binary path globally; ``return_bytes=True`` returns each image as raw +PNG bytes instead of writing files. + +Errors +------ + +Two exceptions surface failure modes: + +* :class:`pptx.render.ThumbnailRendererUnavailable` — ``soffice`` is not + on ``$PATH``. The error message includes an install hint. +* :class:`pptx.render.ThumbnailRendererError` — conversion failed (the + underlying ``soffice`` invocation produced no PNG, exited non-zero, + or timed out). diff --git a/docs/user/theme.rst b/docs/user/theme.rst new file mode 100644 index 000000000..09c5f228e --- /dev/null +++ b/docs/user/theme.rst @@ -0,0 +1,47 @@ +.. _theme: + +Themes +====== + +Reading +------- + +``Presentation.theme`` returns a :class:`pptx.theme.Theme` proxy. The +six accent slots (and the dk1/dk2/lt1/lt2 background and hyperlink +slots) are addressable by ``MSO_THEME_COLOR``:: + + from pptx.enum.dml import MSO_THEME_COLOR + + accent1 = prs.theme.colors[MSO_THEME_COLOR.ACCENT_1] # → RGBColor + major = prs.theme.fonts.major # → str + minor = prs.theme.fonts.minor + +Theme-aware color resolution +---------------------------- + +``pptx.inherit.resolve_color`` returns the effective |RGBColor| for any +``ColorFormat`` (or the lazy proxy on ``Font.color`` / +``LineFormat.color``). Explicit RGB values are returned as-is, scheme +colors resolve through the theme, and unset colors return |None| without +mutating XML:: + + from pptx.inherit import resolve_color + + rgb = resolve_color(run.font.color, theme=prs.theme) + +``brightness`` is applied by blending toward white or black, mirroring +PowerPoint's ``lumMod`` / ``lumOff`` model. + +Writing +------- + +Assignment through the same proxy writes a fresh ```` into +the requested slot (alias slots like ``BACKGROUND_1`` resolve to their +canonical ``lt1`` / ``lt2`` / ``dk1`` / ``dk2`` target):: + + prs.theme.colors[MSO_THEME_COLOR.ACCENT_1] = RGBColor(0x4F, 0x9D, 0xFF) + prs.theme.fonts.major = "Inter" + prs.theme.fonts.minor = "Inter" + + # Bulk-copy the palette and font pair from another deck's theme. + prs.theme.apply(other_prs.theme) diff --git a/docs/user/transitions.rst b/docs/user/transitions.rst new file mode 100644 index 000000000..1bef0372a --- /dev/null +++ b/docs/user/transitions.rst @@ -0,0 +1,36 @@ +.. _transitions: + +Slide transitions +================= + +Each slide exposes a ``transition`` proxy backed by ````. +Reads on an unset transition return |None| and never mutate XML, keeping +theme inheritance intact. + +Per-slide +--------- + +:: + + from pptx.enum.presentation import MSO_TRANSITION_TYPE + + slide.transition.kind = MSO_TRANSITION_TYPE.MORPH + slide.transition.duration = 1500 # milliseconds + slide.transition.advance_on_click = True + slide.transition.advance_after = 5000 # auto-advance after 5s + + slide.transition.clear() # remove the element + +The supported set covers 25+ kinds, including Morph, Vortex, Conveyor, +Switch, Gallery, and Fly Through. Direction modifiers are not yet +exposed. + +Deck-wide +--------- + +``Presentation.set_transition(...)`` applies the same transition (or a +partial update) to every slide in one call. Unspecified kwargs leave +each slide's existing setting untouched:: + + prs.set_transition(kind=MSO_TRANSITION_TYPE.FADE, duration=750) + prs.set_transition(advance_on_click=True) diff --git a/pyproject.toml b/pyproject.toml index 92bce9d60..18b4dc6ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,11 +36,12 @@ readme = "README.rst" requires-python = ">=3.9" [project.urls] -Changelog = "https://github.com/codehalwell/python-pptx/blob/main/HISTORY.rst" -Documentation = "https://github.com/codehalwell/python-pptx/blob/main/HISTORY.rst" -Homepage = "https://github.com/codehalwell/python-pptx" -Issues = "https://github.com/codehalwell/python-pptx/issues" -Repository = "https://github.com/codehalwell/python-pptx" +Changelog = "https://github.com/codehalwell/power-pptx/blob/main/HISTORY.rst" +Documentation = "https://power-pptx.readthedocs.io/en/latest/" +Homepage = "https://github.com/codehalwell/power-pptx" +Issues = "https://github.com/codehalwell/power-pptx/issues" +Repository = "https://github.com/codehalwell/power-pptx" +Roadmap = "https://github.com/codehalwell/power-pptx/blob/main/ROADMAP.md" Upstream = "https://github.com/scanny/python-pptx" UpstreamDocumentation = "https://python-pptx.readthedocs.io/en/latest/" diff --git a/requirements-docs.txt b/requirements-docs.txt index 90edd8e31..d237b92f7 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,3 @@ -Sphinx==1.8.6 -Jinja2==2.11.3 -MarkupSafe==0.23 -alabaster<0.7.14 +Sphinx>=7.0,<9 +sphinx-rtd-theme>=2.0 -e . diff --git a/src/pptx/__init__.py b/src/pptx/__init__.py index 239635246..1bfc2929e 100644 --- a/src/pptx/__init__.py +++ b/src/pptx/__init__.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: from pptx.opc.package import Part -__version__ = "1.1.0.dev0" +__version__ = "1.1.0" sys.modules["pptx.exceptions"] = exceptions del sys diff --git a/src/pptx/dml/effect.py b/src/pptx/dml/effect.py index 63c039939..afbdf7a09 100644 --- a/src/pptx/dml/effect.py +++ b/src/pptx/dml/effect.py @@ -2,6 +2,7 @@ from __future__ import annotations +import warnings from typing import TYPE_CHECKING, Callable from pptx.dml.color import ColorFormat @@ -133,11 +134,28 @@ def inherit(self) -> bool: Assigning True removes any explicit `` (restoring inheritance for *all* effects). Assigning False ensures the element is present but leaves it empty (no visible effect). + + .. deprecated:: 1.1 + Read individual properties (``shadow.blur_radius`` etc.) for + ``None`` instead. ``inherit`` is scheduled for removal in 2.0. """ + warnings.warn( + "ShadowFormat.inherit is deprecated; read individual properties " + "(blur_radius, distance, direction, color) for None instead. " + "Will be removed in power-pptx 2.0.", + DeprecationWarning, + stacklevel=2, + ) return self._element.effectLst is None @inherit.setter def inherit(self, value: bool): + warnings.warn( + "ShadowFormat.inherit is deprecated; assign individual properties " + "to None to clear them. Will be removed in power-pptx 2.0.", + DeprecationWarning, + stacklevel=2, + ) if bool(value): self._element._remove_effectLst() # pyright: ignore[reportPrivateUsage] else: diff --git a/tests/dml/test_effect.py b/tests/dml/test_effect.py index ec7c056a4..7db2ecf22 100644 --- a/tests/dml/test_effect.py +++ b/tests/dml/test_effect.py @@ -13,14 +13,21 @@ class DescribeShadowFormat(object): def it_knows_whether_it_inherits(self, inherit_get_fixture): shadow, expected_value = inherit_get_fixture - inherit = shadow.inherit + with pytest.warns(DeprecationWarning, match="ShadowFormat.inherit"): + inherit = shadow.inherit assert inherit is expected_value def it_can_change_whether_it_inherits(self, inherit_set_fixture): shadow, value, expected_xml = inherit_set_fixture - shadow.inherit = value + with pytest.warns(DeprecationWarning, match="ShadowFormat.inherit"): + shadow.inherit = value assert shadow._element.xml == expected_xml + def it_emits_deprecation_warning_on_inherit_access(self): + shadow = ShadowFormat(element("p:spPr")) + with pytest.warns(DeprecationWarning, match="ShadowFormat.inherit"): + _ = shadow.inherit + # fixtures ------------------------------------------------------- @pytest.fixture(