Conversation
The rpptx facade already merges and splits table cells, reports merge origins and spans, and reads and writes cell fills and margins, but the Python Cell exposed only its text. A deck pass that merged a header or shaded a cell had to fall back to python-pptx for that step. Cell now has python-pptx's merge(other_cell), split(), is_merge_origin, is_spanned, span_height and span_width over the native staged table operations. They keep the rectangular grid, so no handle goes stale and the revision does not advance. A cell of another table raises ValueError, and the native refusals of an overlapping merge or of a split on a cell that is not a merge origin raise RpptxError with the table unchanged. Cell.fill is a FillFormat through a new table cell fill target, and margin_left, margin_right, margin_top and margin_bottom read the tcPr margins as Length, or None when absent, with the write rules of the text frame margins. GitHub issue tensorbee#169.
A table row's stored height and a cell's lnL, lnR, lnT and lnB border lines were parsed and written by oxml-drawing, but the rpptx facade had no accessor for either, and Python had no rows collection at all. A deck pass could not size a row or draw a cell edge without python-pptx and raw XML edits. TableRef and TableMut now read row_height, and TableMut::set_row_height stages the change like set_column_width and keeps the graphic frame height equal to the sum of the rows. Both setters share one checked extent helper. The new CellBorder enum names an edge, and TableCellRef and TableCellMut read border(edge) while set_border replaces or clears it. The writer already emits the four lines in schema order before the cell fill. In Python, Table.rows is a RowCollection of Row handles whose height reads and writes through the facade. python-pptx has no border API, so Cell.border_left, border_right, border_top and border_bottom are LineFormat views over a cell border target, reusing the width, colour and fill logic of shape lines. Reading a border never creates one, and none of these writes advances the revision. GitHub issue tensorbee#169.
A picture's a:srcRect is modelled as BlipFill.source_rect, written in schema order and honoured by the renderer, but no facade method reached it, so neither Rust nor Python callers could read or change a crop. Only replace_image kept an existing one. ShapeRef::crop returns the four insets as Percent1000, now re-exported from rpptx, with zero for an absent edge and None for other shape kinds. ShapeMut::set_crop writes them on a picture and rejects other kinds. It keeps the stored attribute of an unchanged edge, drops a changed edge of zero, and adds no element for four zero insets on a picture without one, so a no-op write leaves the XML as it was. Python Shape gains python-pptx's crop_left, crop_top, crop_right and crop_bottom floats. A write rounds half to even like ST_Percentage, rejects a value that is not finite or out of range, changes nothing when the value is unchanged, and does not advance the revision. GitHub issue tensorbee#169.
New shapes land on top of a slide's shape tree, and nothing in rpptx-oxml, the facade or the binding could bring one forward or send it back. CT_ShapeTree only appended and removed children. CT_ShapeTree::move_child(from, to) serializes the tree, moves the child's bytes past exactly the children between the two indices, and reparses, as remove_child_by_id already does, so unmodelled members such as p:contentPart and a schema-final p:extLst keep their bytes and their place. Both now share one finder that returns the byte range of every typed child in order. Presentation::move_shape moves an immediate slide child with final-index semantics like move_slide and rejects an index past the last child without change. Animations, connector glue and relationships name shape ids, so they need no rewrite. Python gains ShapeCollection.move(from_, to), mirroring SlideCollection.move: negative indices, one revision bump, and a ValueError on the read-only nested collections. GitHub issue tensorbee#169.
A run's a:hlinkClick is modelled with only its relationship id, and nothing public resolved, added or retargeted the slide's hyperlink relationship, so neither Rust nor Python callers could read a link's address or point a run at a URL. Validation also reports a hyperlink relationship that nothing references, so a relationship-level helper would have left a window where the deck does not validate. Presentation::hyperlink_address resolves the r:id an a:hlinkClick stores, like python-pptx's address. Presentation::set_run_hyperlink does the write in one staged step: it finds the ordinary shape by its unique id, inside groups too, gives the regular run a fresh a:hlinkClick naming the slide's external hyperlink relationship to the address, reuses that relationship when it exists, and removes the old one once nothing on the slide names it, so repeated retargeting does not grow the part. None removes the link, the current address is a no-op, and a rejected address or run leaves the slide and its relationships as they were. Python gains Run.hyperlink, a Hyperlink view whose address getter and setter follow python-pptx, with None or an empty string removing the link. The write stays in place and does not advance the revision. GitHub issue tensorbee#169.
The table, crop, z-order and hyperlink changes add public methods and their documentation to rpptx and a shape tree move to rpptx-oxml, so both packaged archives grew. The measurements in scripts/readme_doctests.py and the archive rows of both READMEs now match cargo package, which the Docs job checks. 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
One commit per checklist item of #169, then the archive re-record.
Cellgains python-pptx'smerge(other_cell),split(),is_merge_origin,is_spanned,span_heightandspan_widthover the facade's stagedmerge_toandsplit, plusfill(aFillFormatthrough a new table cell fill target)and
margin_left,margin_right,margin_topandmargin_bottom. Mergeand split keep the rectangular grid, so no handle goes stale and the
revision does not advance.
TableRefandTableMutreadrow_height, andTableMut::set_row_heightstages the change likeset_column_widthand keeps the graphic frame height equal to the sum of therows. Both setters now share one checked extent helper. The new
CellBorderenum names an edge,TableCellRef::borderandTableCellMut::borderread it, andTableCellMut::set_borderreplaces orclears it. Python gains
Table.rows(aRowCollectionofRowhandles witha writable
height) andCell.border_left,border_right,border_topand
border_bottomasLineFormatviews, reusing the width, colour andfill logic of shape lines.
ShapeRef::cropandShapeMut::set_cropread andwrite a picture's
a:srcRectas fourPercent1000insets, now re-exportedfrom
rpptx. PythonShapegains python-pptx'scrop_left,crop_top,crop_rightandcrop_bottomfloats. Nooxml-drawingchange, so nooverlap with Paint slide backgrounds in PDF output and open gradients without ang or path #175's
fill.rswork.CT_ShapeTree::move_child(from, to)inrpptx-oxmlmoves achild's bytes past exactly the children between its two indices and
reparses, as
remove_child_by_iddoes, so unmodelled members such asp:contentPartand a schema-finalp:extLstkeep their bytes and place.Both now share one range finder.
Presentation::move_shape(slide_index, from_index, to_index)exposes it withmove_slide's final-index semantics,and Python gains
ShapeCollection.move(from_, to).Presentation::hyperlink_address(slide_index, relationship_id)resolves ther:idof ana:hlinkClick, andPresentation::set_run_hyperlink(slide_index, shape_id, paragraph_index, run_index, address)adds, retargets or removes a run's click hyperlink inone staged step, reusing the slide's relationship to the same address and
pruning the old one once nothing on the slide names it. Python gains
Run.hyperlink, aHyperlinkview whoseaddressfollows python-pptx.rpptxandrpptx-oxmlre-recorded inscripts/readme_doctests.pyand their READMEs.Every new Python name is in
_rpptx.pyiandtyping_smoke.py. HLD 06describes the new facade methods and HLD 10 the new binding surface. The
rpptx-pyREADME capability list gains one line per feature.Part of #169: checklist items "Table cells: merge_to / split, set_fill,
set_margins", "Table rows and columns: row height, cell borders", "Z-order",
"Picture crop" and the run half of "Hyperlinks on runs and shapes". Still open
after this PR and #181 (items A to E):
oxml-drawingtable change),row still hits),
hyperlink_addressand therelationship reuse and pruning of
set_run_hyperlink,proposed as out of scope for this round.
Why
These are the #169 items the facade either had and Python did not bind (F),
or that the model parsed but no public API reached (H1, J, L1), or that did
not exist at all (I). A deck pass that merged a header, sized a row, drew a
cell edge, cropped a picture, brought a shape forward or linked a run had to
keep a python-pptx step for it.
Notes
Nonewhen absent,borders are per-edge
LineFormatviews, z-order isshapes.move(from_, to)with one revision bump as a structural change, and an unreferenced hyperlink
relationship, and any relationship only the old hyperlink named such as an
a:sndclick sound, is pruned on retarget or removal.is relationship-level primitives (
relate_hyperlink, then write therun, then prune). While testing them I found that
validate()reports anunreferenced hyperlink relationship as
DanglingRelationship, andto_bytesdebug-asserts a clean validation. A public relate-then-write APIwould therefore leave a window where the deck does not validate, and a
caller who related an address without using it would keep an invalid deck.
set_run_hyperlinkdoes the relationship and run changes together on astaged slide clone and relationship list, and a rejected call changes
nothing. It addresses the run by shape id, like
picture_imageandreplace_picture_image, and refuses an id that another slide child shares,so a deck with duplicate ids (which
validate()already reports) raisesrather than editing the wrong shape.
hyperlink_addressstays a read-onlyprimitive that L2 can reuse for shape click hyperlinks.
RpptxError, not theValueErrorpython-pptx raises, for an overlappingmerge, a split of a cell that is not a merge origin, a row height that is not
positive, and a hyperlink address with a control character. This matches the
existing
Column.widthsetter. Binding-side checks (a cell of another table,a crop value out of range, a margin outside 32 bits, a non-picture crop)
raise
ValueError.Row.heightalso updatesthe graphic frame height, as
set_column_widthalready updates its width.python-pptx changes only
a:tr/@h.set_cropkeeps the stored attribute of anunchanged edge, drops a changed edge of zero, and adds no
a:srcRectforfour zero insets on a picture without one (python-pptx would write an empty
a:srcRect). Assigning a run's current hyperlink address changes nothing andkeeps any tooltip, and any other assignment writes a fresh
a:hlinkClickwith only
r:id, as python-pptx does. Relationships the old element namedand nothing else does, its click sound included, are removed. Reading a cell border or a cell fill
never creates
a:tcPrchildren, and clearing an absent margin leaves thebytes unchanged.
rpptxgainsCellBorder,the
Percent1000re-export,TableRef::row_height,TableMut::row_height/set_row_height,TableCellRef::border,TableCellMut::border/set_border,ShapeRef::crop,ShapeMut::set_crop,Presentation::move_shape,hyperlink_addressandset_run_hyperlink.rpptx-oxmlgainsCT_ShapeTree::move_child. Two behaviour-preservingrefactors:
set_column_widthuses the shared extent helper (same checks andmessages, covered by the existing table tests) and
remove_child_by_idusesthe shared range finder (covered by the removal tests).
Row.cells(python-pptx has it, rpptx Python bindings: what is left for a production deck chain, as one checklist #169 did not ask for it),hyperlinks on table cell runs (the Python text surface covers shape text
frames only) and
a:hlinkMouseOver.(table cells and rows after the fill and line test, crop and z-order after
the picture test, hyperlinks after the text property oracle test), away from
the insertion points of Keep rpptx handles valid when Python sets a run's text #173 and Bind the missing rpptx facade methods of the deck workflow in Python #181.
git merge-treeagainst Keep rpptx handles valid when Python sets a run's text #173, End the CLIs cleanly on a closed pipe and refuse silent overwrites #174,Paint slide backgrounds in PDF output and open gradients without ang or path #175, Expose comparison options as keywords of Python Document.compare #176, Make the body read walkers reach content controls #177, Save documents and presentations atomically, keeping links and modes #178, Align split_run, bookmarks and story comments on the direct body index #179, Keep every child of a text box that replacement rewrites #180 and Bind the missing rpptx facade methods of the deck workflow in Python #181 is clean on the feature commits.
The archive commit touches the
rpptxrow that Paint slide backgrounds in PDF output and open gradients without ang or path #175 and Save documents and presentations atomically, keeping links and modes #178 alsore-record, so whichever lands second re-runs the measurement.
Tests
Added to
crates/rpptx/tests/integration.rs:row_heights_and_cell_borders_round_trip_with_the_frame_height_in_step:row height and frame height after
set_row_height, rejected heights leavethe table unchanged, borders set, cleared and reopened, and
lnL,lnBbefore the cell fill in the XML.
picture_crop_round_trips_and_rewrites_only_the_changed_edges: four zeroinsets add nothing, a non-picture is refused, the reopened crop,
a:srcRectbetween the blip and the fill mode, and the edge rules on a hand-written
a:srcRect.moving_a_shape_changes_the_draw_order_and_keeps_every_shape_id: the pixelunder two overlapping rectangles changes colour in the rendered PNG, ids
keep their values, bad indices are refused, and the reopened deck validates.
run_hyperlinks_reuse_resolve_and_prune_their_relationships: rejectedaddresses and runs change nothing, one relationship per address, retargeting
prunes, removal prunes only when unused, the layout relationship is never
pruned, a run inside a group, and a shared shape id is refused.
#[ignore]pinned python-pptx 1.0.2 oracles in the style ofadd_table_matches_pinned_python_pptx_table_semantics, through one newhelper
python_pptx_1_0_2_reads: row heights and borders, crop, z-ordernames and run hyperlink addresses as python-pptx reads them.
Added to
rpptx-oxml's existingshape_treeunit test module:moving_a_child_passes_only_the_children_between_its_indices, with a rawmember between children and a schema-final
p:extLst.Added to
crates/rpptx-py/tests/test_documented_examples.py, each checkedagainst python-pptx 1.0.2 in both directions:
test_table_cells_merge_split_fill_and_margins_like_python_pptxtest_table_row_heights_and_cell_borders_write_what_python_pptx_readstest_picture_crop_matches_python_pptx_in_both_directions(a two-colourpicture renders differently once cropped)
test_shapes_move_changes_the_z_order_and_stales_handles_oncetest_run_hyperlink_address_reads_writes_and_prunes_like_python_pptx(repeated retargeting keeps one relationship per live address, and the PDF
carries the new URI)
typing_smoke.pygains one small function per feature.Run on this branch (macOS arm64, Python 3.12.14, python-pptx 1.0.2):
cargo fmt --all --check: pass.cargo clippy -p rpptx-oxml -p rpptx -p rpptx-py --all-targets --all-features -- -D warnings: pass, also on each commit alone.cargo test -p rpptx: 84 lib and 238 integration tests pass.cargo test -p rpptx --test integration -- --include-ignoredon the eightnew tests, the four python-pptx oracles included: pass.
cargo test -p rpptx-oxml: pass (16 unit, 155 integration).cargo test -p rpptx-pywithDYLD_LIBRARY_PATHset to the libpython thetest binary links: pass, 0 Rust tests.
cargo test -p rpptx-clipasses exceptvalidate_rejects_corruption_and_accepts_the_pinned_corpus(missing/corpus/pptx, environment only).cargo check --target wasm32-unknown-unknown -p rpptx-wasm: pass.maturin develop --lockedthenpytest crates/rpptx-py/tests: 45 passed, 0skipped. Each commit alone: 41, 42, 43, 44 and 45 passed.
mypy --strict crates/rpptx-py/tests/typing_smoke.py crates/rpptx-py/python/rpptx: no issues.python -m mypy.stubtest rpptx:no issues.
python3 scripts/hash_harness.py --check: 49 entries match.python3 scripts/prose_check.py: 0 violations.python3 scripts/readme_doctests.py: pass after the re-record.python3 scripts/sync_agent_skills.py --check: in sync.workflow_pptx.pyfrom #158 onfixture-deck.pptxgives the same rows asmain: the geometry, duplicate, replacement and comment rows belong to G andto #181, and
validateneeds the CLI onPATH. A probe on the same fixturemerges two cells of the slide 4 table, sets a row height, a bordered, filled
and inset cell, crops the slide 1 picture, moves it to the back and links a
run on slide 2 twice. The result reopens in rpptx,
rpptx validatepasses,the PDF carries the second URI, and python-pptx reads the span, the row
height, the margin, the crop, the new order and the link.
Environment-only failures seen: the missing-corpus test above, and the known
libpython load path of the
rpptx-pyRust test binary withoutDYLD_LIBRARY_PATH.