rpptx: effective placeholder geometry, geometry setters that keep it, and changing a slide's layout - #192
Open
hadim wants to merge 3 commits into
Open
rpptx: effective placeholder geometry, geometry setters that keep it, and changing a slide's layout#192hadim wants to merge 3 commits into
hadim wants to merge 3 commits into
Conversation
A placeholder without its own a:xfrm, the normal case in a deck made in PowerPoint, reported None for left, top, width and height, although rendering already placed it from its layout and master. Setting one coordinate or the rotation then wrote an a:xfrm with a zero partner or with only a rotation, and no a:ext, which the PDF, the PNGs and text_layout skip, so the placeholder vanished. rpptx-layout now exposes inherited_xfrm, the layout and master part of the transform chain, and the resolver falls back to it, so rendering is unchanged. The facade adds Presentation::effective_geometry, which reports what rendering places, and materialize_geometry, which copies the missing parts of the inherited transform onto a placeholder. The Python Shape gains effective_geometry(), and its left, top, width, height and rotation setters call materialize_geometry first, so one assignment keeps the other values. The explicit getters still report only the shape's own values. GitHub issue tensorbee#169.
Only the read side existed: slide_layout_index and the Python Slide.slide_layout getter followed the slide's layout relationship, and nothing could point it at another layout. Presentation::set_slide_layout retargets that relationship to any layout the masters reach, as a staged change that publishes only after the package reopens. Placeholders re-inherit from the new layout, while a placeholder without its own transform that the new layout chain does not place first receives the transform it inherited, so it stays where it was drawn instead of vanishing. Placeholders of the new layout that the slide lacks are not added. A layout of another master is accepted, because the slide only relates to its layout and follows the new master's theme and text styles, as it does in PowerPoint. The Python Slide.slide_layout becomes assignable and advances the revision once. GitHub issue tensorbee#169.
The inherited_xfrm function and its unit test grow the rpptx-layout package, and the effective geometry, geometry materialization and layout change methods with their integration tests grow the rpptx package, so the README archive rows of both crates and their ARCHIVE_MEASUREMENTS entries are re-measured. GitHub issue tensorbee#169.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
rpptx-layout,rpptx,rpptx-py: inherited placeholder geometry (rpptx Python bindings: what is left for a production deck chain, as one checklist #169,item "Inherited placeholder geometry").
rpptx-layoutexposesinherited_xfrm(placeholder, layout, master), thelayout and master part of the transform chain.
ResolveCtx::effective_xfrmand
effective_picture_xfrmnow fall back to it, so there is still oneplaceholder matching rule and rendering is unchanged.
rpptxaddsPresentation::effective_geometry(slide_index, shape_path),which returns
(left, top, width, height)as rendering places the shape:its own transform, or for a placeholder without one, the transform it
inherits from its layout placeholder, then that placeholder's master
counterpart. It also adds
Presentation::materialize_geometry(slide_index, shape_path), which copies the missing offset or extent of the inheritedtransform onto a placeholder, or the whole transform, rotation and flips
included, when it has none.
Shape.effective_geometry()returns a tuple of fourLengthvalues, or
Nonewhen the resolved transform has no extent. Theleft,top,width,heightandrotationsetters callmaterialize_geometryfirst, so one assignment on a placeholder keepsthe values it does not assign, and the placeholder stays in the PDF, the
PNGs and
text_layout(). The four getters are unchanged and still reportthe shape's own values.
effective_geometry() == (457200, 274638, 8229600, 1143000), aspython-pptx reports, and all 10 placeholders of the deck agree with
python-pptx.
rpptx,rpptx-py: changing the layout a slide uses (rpptx Python bindings: what is left for a production deck chain, as one checklist #169, item "Changingthe layout a slide uses").
Presentation::set_slide_layout(slide_index, layout_index)retargets the slide's layout relationship as a staged change.Placeholders re-inherit from the new layout, while a placeholder without its
own transform that the new layout chain does not place first receives the
transform it inherited, so it stays where it was drawn. Python
Slide.slide_layoutbecomes assignable and advances the revision once.rpptxandrpptx-layoutre-recorded.Part of #169. This closes its two items "Inherited placeholder geometry" and
"Changing the layout a slide uses". The other items of the checklist are in
#181 and in the rpptx-py tables branch, or still open.
Why
A placeholder without its own
a:xfrmis the normal case in a deck made inPowerPoint, and every placeholder
add_slidecreates has none. Itsleft,top,widthandheightreadNone, while the renderer already placed itthrough
ResolveCtx::effective_xfrm. Worse,shape.left = xwrote ana:xfrmwithy="0"and noa:ext, by the zero-partner rule of HLD 10. Theresolver takes the slide's transform whole and skips a shape without an
extent, so the placeholder silently disappeared from
to_pdf(), the PNGs andtext_layout(). I reproduced this onmainwith the fixture deck: aftertitle.left = 548640the title of slide 2 read(548640, 0, None, None)andshape id 2 was gone from
text_layout().shape.rotation = 30did the same,because it wrote an
a:xfrmthat carries onlyrot.Changing a slide's layout had only a read side (
slide_layout_index, thePython getter).
Notes
Getters stay explicit. I kept the explicit-values rule of PR #149 (and
inspect --jsonof PR #151):leftand the others still readNoneon aninheriting placeholder, and the effective values come from the separate
accessor, one of the two answers #169 accepts. As a consequence the "shape
moved and resized" step of
workflow_pptx.pystill fails as written, becauseit computes
sh.left + EMU // 10. Witheffective_geometry()it passes (seeTests). If you prefer python-pptx parity for the getters instead, the binding
could fall back to
effective_geometry()in the four getters only. Irecommend the accessor, which keeps
Nonemeaning "not set on this shape"everywhere.
What materializing does. Only placeholders are touched, and only when
their own transform lacks an offset or an extent and the layout chain
supplies a transform. A placeholder without a transform copies the inherited
one whole. A partial transform keeps what it has and takes the missing part,
which is also how python-pptx reads a partial
a:xfrmit wrote itself.The
rotationsetter materializes too, so rotating an inheritingplaceholder keeps its offset and extent, and python-pptx reads the same four
values and the new angle back. Shapes that are not placeholders keep the
zero-partner rule, as in python-pptx, and the existing test for it is
unchanged. HLD 10 now states both rules.
Rendering is unchanged. The resolver refactor is a pure move: every
existing
rpptx-layout,rpptx-renderandrpptxrender test passes as itwas, and the docx hash harness is unaffected. I did not make
effective_xfrmfall back per field, so a deck that already carries a partial transform
written by python-pptx or by an older rpptx still does not draw that
placeholder until it is edited again. That would change the pptx render
output of such decks, so I left it for a separate decision.
shape_path. The two geometry methods name a shape by its index in theslide tree, then its index in each enclosing group, the same path a Python
Shapehandle carries. I preferred it to a shape id, which real deckssometimes duplicate. A placeholder inside a group inherits as the renderer
resolves it. An empty path or one that names no shape is
Error::InvalidShapeMutation, for the readeffective_geometryas well.That follows
picture_image, a read that already reports a missing picturewith that variant, and the doc comment says so. If you would rather give
reads their own variant before this API is released, the integration test
that pins it is the one place to change. I recommend keeping the existing
convention.
Layout change policy. Retarget, and keep the geometry of placeholders the
new layout does not place. Placeholders of the new layout that the slide lacks
are not added, and empty orphan placeholders are not removed, unlike
PowerPoint's own layout switch. Only the geometry is kept for an unplaced
placeholder: its text keeps the master's title, body or other text style by
type, but no longer gets the insets, anchor, autofit or list style of its old
layout and master placeholders. I recommend keeping this scope and adding
placeholder creation later as an option if needed. A layout of another master
is accepted: the slide relates only to its layout, and it then follows the new
master's theme, colour map and text styles, which is what choosing that layout
does in PowerPoint. A test builds a second master and checks the title
re-inherits from it,
validate()is clean and the deck renders. If you wouldrather refuse other masters, it is one comparison of the two layouts' master
parts.
Feature gate. The three facade methods need
render, liketext_layout,because the placeholder matching rule lives in
rpptx-layout, an optionaldependency of
rpptx.rpptx-wasmbuilds withoutrenderand is unaffected.Moving the matching rule to
rpptx-oxmlwould lift the gate, at the cost of athird crate in this PR.
API additions, no breaking change.
rpptx_layout::inherited_xfrminrpptx-layout,Presentation::effective_geometry,materialize_geometryand
set_slide_layoutinrpptx, and PythonShape.effective_geometry()and the
Slide.slide_layoutsetter. Stub,typing_smoke.py, HLD 06, HLD 07 andHLD 10 updated. The Python geometry setters change behaviour on placeholders
only, as described above. Suggested release note line: "Setting one
coordinate or the rotation of a placeholder keeps its other values at the
ones it inherited, and
Shape.effective_geometry()reports them."Conflicts to expect.
crates/rpptx/src/lib.rs(new methods afterslide_layout_index), the rpptx-py stub,test_documented_examples.pyandtyping_smoke.pywith #173, #181 and the rpptx-py tables branch (my additionssit next to the geometry and slide layout entries), HLD 10, and the
rpptxarchive row, which the maintainer re-records at merge. The measurement dates
are left as recorded, as in the other PRs of this round.
Tests
Added:
crates/rpptx-layout/src/context.rs:inherited_transform_skips_the_slide_shape_and_follows_the_layout_key(the slide's own transform is ignored, the layout key selects the master
placeholder, latent footers match across indices, a master match alone is
not inherited, and
effective_xfrmagrees).crates/rpptx/tests/integration.rs, right after thetext_layoutgroup:effective_geometry_follows_the_placeholder_chain_as_rendering_does(masterand layout inheritance on the bundled template, the drawn text frames equal
the effective geometry, slide number with a different index, picture
placeholder, unmatched placeholder, non-placeholder, partial transforms,
a placeholder in a group, path and slide errors),
materialized_placeholder_geometry_keeps_the_shape_drawn_after_one_coordinate_changes(the bug without materializing, then idempotence, a moved title still laid
out, rotation and flips copied, partial transforms completed, non-placeholder
and unmatched shapes unchanged), and
changing_a_slide_layout_retargets_it_and_keeps_unplaced_placeholders_in_place(Title and Content to Title Only and to Title Slide, relative relationship
target,
validate(), both frames still laid out, no-op on the same layout,errors leave the bytes unchanged, and a layout of a second master).
crates/rpptx-py/tests/test_documented_examples.py:test_placeholder_effective_geometry_matches_python_pptx_and_one_setter_keeps_the_rest(every placeholder of four python-pptx layouts against python-pptx, then a
moved title, a resized body and a rotated title, which keeps its effective
geometry and its
text_layout()frame, all read back by python-pptx) nextto the geometry setter test, and
test_slide_layout_assignment_retargets_the_slide_and_keeps_unplaced_placeholders(one revision bump, python-pptx reads the new layout and the kept body
geometry, foreign layout and wrong type refused) next to the slide layout
test.
Run on macOS arm64 at the final branch head:
cargo fmt --all --check,python3 scripts/prose_check.py,python3 scripts/sync_agent_skills.py --check: clean.cargo clippy -p rpptx-layout -p rpptx -p rpptx-py -p rpptx-render -p rpptx-cli -p rpptx-wasm -p rdocx --all-targets --all-features -- -D warnings: clean.cargo test -p rpptx-layout -p rpptx -p rpptx-render -p rpptx-cli -p rpptx-wasm --all-features --no-fail-fast:rpptxlib 84 passed andintegration 246 passed,
rpptx-render100 passed,rpptx-wasm4 passed.rpptx-layout130 passed with the 3 known corpus tests failing for lack of/corpus/pptx, andrpptx-cli24 passed withvalidate_rejects_corruption_and_accepts_the_pinned_corpusfailing for thesame reason.
cargo test -p rdocx --no-fail-fast(consumer ofrpptx): lib 465 passed,integration 314 passed, regression 561 passed, doctests 2 passed. The 7
failures are the known pinned-tool ones
(
large_word_and_presentation_pdfs_preserve_logical_reading_order,word_and_powerpoint_chart_pixels_are_identical,odt_reader_matches_pinned_libreoffice_structure,public_authored_theme_and_fonts_match_pinned_word_resolution,sanitized_public_authoring_fixture_passes_every_conformance_stage,section_page_semantics_match_pinned_libreoffice_render,every_conditional_table_region_matches_word).cargo test -p rpptx-pywithDYLD_LIBRARY_PATHset: passes.cargo check --target wasm32-unknown-unknown -p rpptx-wasm: passes.python3 scripts/hash_harness.py --check: 49 entries match.python3 scripts/readme_doctests.py: passes with the re-recorded rows.pytest crates/rpptx-py/tests42 passed,mypy --strictontyping_smoke.pyand the package clean,mypy.stubtest rpptxclean. The rotation part of the new geometry testfails when the
rotationsetter does not materialize, as expected.workflow_pptx.pyfrom #158 against this build, with a debugrpptxCLI forthe validate step. As written, the geometry step still fails because the
explicit getters stay
None:The three steps that call
duplicate,replace_textandresolve_commentfail as on
mainand are covered by #181. With the geometry step changed toread the accessor, it passes, and the moved title keeps its inherited top and
height:
Rotating the title of slide 2 of the same deck with
t.rotation = 30.0keepseffective_geometry()at(457200, 274638, 8229600, 1143000), the explicitgetters now read those four values, shape id 2 stays in
text_layout(), andpython-pptx reads the saved deck back as
(457200, 274638, 8229600, 1143000)at 30 degrees.