Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ snapshot commit; the development history before it is summarised here.

## Unreleased

- Slide master and layout editing (production feedback round 3, G5), measured in PowerPoint
for Mac 16. `deck.masters` / `deck.master()` give `Master` objects (`id` `M0`, `name`,
`part_path`, `layouts`, `slides`, `shapes`); `Layout` gets `id` (`L2`), `master`, `type`,
`slides`, `placeholder("body")`, `rename`, `duplicate` and `delete`. `Layout.shapes` and
`Master.shapes` are now the editable `Shape` a slide has (they were read-only
`LayoutShape` summaries, still importable), addressed `L<n>.<id>` / `M<n>.<id>` where `n`
is the `sldLayoutId` / `sldMasterId` less 2147483648 -- ids PowerPoint keeps on save, as it
keeps a slide's `sldId`; `deck.shape`, `deck.resolve` and the text API take them. Text,
run and paragraph formatting, fill, position, delete, replace_image, and `add_shape`,
`add_textbox`, `add_picture` (SVG too), `add_connector`, `add_table` on layouts and
masters. `shape.set_prompt(text)` / `shape.prompt`: a layout placeholder's prompt, the
first level written and `hasCustomPrompt="1"` set, as typing in Slide Master view does
(typing any text into a layout placeholder sets it too; formatting does not).
`deck.find_text(..., scope="slides" | "layouts" | "masters" | "all")` and
`outline_blocks(scope=)` (`TextBlock.part`; a placeholder holding only a field is left
out). `deck.set_footer(text, slide_number=, date="auto" | text | None, slides=,
skip_title=)` as Insert > Header & Footer writes it (Apply to All and Apply), and
`add_slide` then gives a new slide the layout's footers, as New Slide does.
`add_layout`, `duplicate_layout`, `rename_layout`, `delete_layout(reassign_to=)` as Slide
Master view does them (a layout in use is refused, as PowerPoint disables it).
`shape.inherited_by()` lists what inherits from a layout or master placeholder; deleting
one that something inherits from is refused. `deck.affected_slides(target)`.
- Tools: `ppt_set_text`, `ppt_format_text`, `ppt_set_shape`, `ppt_arrange` take `L2.5` /
`M0.7`, and the add tools `slide: L2` / `M0`; every result lists the slides an edit
`affects` (`checks.affects`) and checks them. `find_text` / `replace_text` take decks'
`stories`: `layouts`, `masters`, `all` (ooxml-edit 0.16.0); a replace that finds nothing on
the slides says when the text is on a layout or master. `describe` lists the masters, the
layouts' ids, their placeholders' ids and custom prompts, and the text on them that is not
a placeholder. The deferred `ppt_edit_master` (`set_prompt`, `set_footer`, `add_layout`,
`duplicate_layout`, `rename_layout`, `delete_layout`). Undo by slide (`scope`) finds a
change to the slide's layout or master and refuses it as `entangled`
(`DocumentFormat.undo_shared`, ooxml-edit 0.16.0). Every definition 11,779 estimated
(guard 11,800), the core 3,440 (about 5,091 counted); the strict set unchanged. Golden p17:
a template made ready for 2027 in Dutch.
- SVG pictures (production feedback round 3, I1): `slide.add_picture` takes an SVG and
stores it as PowerPoint 2016 and later store an inserted SVG, measured in PowerPoint for
Mac 16: the PNG in `a:blip`, the SVG behind its `asvg:svgBlip` extension (`xmlns:asvg` on
Expand Down
36 changes: 33 additions & 3 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,14 +752,44 @@ print(body.type, body.idx, body.sizes[:3]) # -> None 1 (28.0, 24.0, 20.0
slide = deck.add_slide(layout)
content = next(s for s in slide.shapes if s.placeholder == (None, 1))
print(content.placeholder) # -> Placeholder(type=None, idx=1)
print([shape.kind for shape in layout.shapes][:2]) # -> ['shape', 'shape']
print([shape.id for shape in layout.shapes][:2]) # -> ['L2.2', 'L2.3']
```

`layout.placeholders` lists what a slide made from the layout gets: each placeholder's
`type` (`None` is a content placeholder, `"obj"`), `idx` (what a slide's placeholder matches
it by), `name`, `bounds` (`left, top, width, height` in EMU) and the default text size of
each level. `shape.placeholder` is the same `(type, idx)` named tuple. `layout.shapes` are
read-only: layouts are edited in PowerPoint's slide master view.
each level. `shape.placeholder` is the same `(type, idx)` named tuple.

### Slide masters and layouts

`layout.shapes` and `deck.masters[0].shapes` are the same editable `Shape` objects a slide
has, addressed `L2.3` (a layout's `sldLayoutId` less 2147483648) and `M0.7` (the master's
`sldMasterId` likewise) -- ids PowerPoint keeps when it saves. An edit shows on every slide
built on the layout or master (`layout.slides`, `deck.affected_slides("M0.7")`).

```python
from pptx_agent import Document

deck = Document.new()
deck.add_slide("Title and Content").shapes[0].set_text("Findings")
legal = deck.masters[0].add_textbox(838200, 6721475, 7315200, 136525)
legal.set_text("\u00a9 2026 Example Corp")
for block in deck.find_text("2026", scope="all"): # slides, layouts and masters
deck.resolve(block.address).text = block.text.replace("2026", "2027")
print(deck.shape(legal.id).text, legal.id) # -> 漏 2027 Example Corp M0.7
deck.layout("Title and Content").placeholder("body").set_prompt("Typ hier uw bevinding")
juridisch = deck.add_layout("Juridisch", after="Title and Content") # Insert Layout
dutch = deck.duplicate_layout("Title and Content", name="Inhoud NL")
deck.delete_layout("Comparison") # refused while a slide uses it (reassign_to=)
deck.set_footer("Example Corp B.V.", slide_number=True, date=None) # Header & Footer, Apply to All
print([s.placeholder.type for s in deck.slides[0].shapes]) # -> ['title', None, 'ftr', 'sldNum']
```

`set_prompt` writes the first level and sets `hasCustomPrompt`, as typing in Slide Master
view does; `set_footer` writes what Insert > Header & Footer writes (the master's and every
layout's placeholders, the master's `<p:hf>`, each slide's date, footer and number
placeholders; `slides=` for Apply). Deleting a placeholder slides or layouts inherit from
is refused with their list (`shape.inherited_by()`).

### Theme colours, fonts and formatting as drawn

Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ classifiers = [
# them (office.user_font_dirs, OOXML_FONT_DIRS). 0.14.0: Call.unlocked (render and check
# on a snapshot, the lock let go) and slide ids in their slides. 0.15.0: chart formatting
# (axis scales, colours, markers, combination charts, chart text, point labels, bubble and
# filled radar charts) and edit_chart's actions for it.
dependencies = ["lxml>=4.9", "ooxml-common>=0.8.0", "ooxml-edit>=0.15.0", "markdown-it-py>=3.0",
# filled radar charts) and edit_chart's actions for it. 0.16.0: DocumentFormat.undo_shared
# (undo by slide refuses a layout or master change) and decks' stories on find/replace.
dependencies = ["lxml>=4.9", "ooxml-common>=0.8.0", "ooxml-edit>=0.16.0", "markdown-it-py>=3.0",
"mdit-py-plugins>=0.4"]

[project.optional-dependencies]
Expand Down
3 changes: 2 additions & 1 deletion src/pptx_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
measure_text)
from .edit.ids import ShapeId
from .edit.labels import LabelError
from .edit.slides import Layout, LayoutPlaceholder, LayoutShape, Placeholder
from .edit.slides import Layout, LayoutPlaceholder, LayoutShape, Master, Placeholder
from .edit.table import Region, Table, TableCell
from .edit.theme import Theme, ThemeFonts, ThemeRoles, Tint
from .edit.presets import PRESETS
Expand Down Expand Up @@ -137,6 +137,7 @@
"Layout",
"LayoutPlaceholder",
"LayoutShape",
"Master",
"LineFormat",
"MarkdownEscapeWarning",
"NotFullStateSvg",
Expand Down
Loading
Loading