Skip to content

Render shapes held in mc:AlternateContent - #20

Merged
mantissaman merged 1 commit into
mainfrom
fix/shapes-in-alternate-content
Jul 29, 2026
Merged

Render shapes held in mc:AlternateContent#20
mantissaman merged 1 commit into
mainfrom
fix/shapes-in-alternate-content

Conversation

@mantissaman

Copy link
Copy Markdown
Contributor

Closes #11. With this, every symptom reported in #3 is addressed.

The bug

Word writes a shape as a compatibility block, the modern DrawingML in mc:Choice and a VML fallback beside it:

<mc:AlternateContent>
  <mc:Choice Requires="wps"><w:drawing><wp:anchor>...<wps:wsp>...</mc:Choice>
  <mc:Fallback><w:pict><v:shape .../></w:pict></mc:Fallback>
</mc:AlternateContent>

The run parser did not know mc:AlternateContent, so the block fell into the unknown-element branch and was kept as raw XML. It survived a write, but layout never saw it, so nothing was drawn and no space was reserved.

Keeping fidelity while making it visible

The block is still captured verbatim. The DrawingML is read out of it separately into an alt_drawings field that is never serialised, so the raw copy remains the single source for output and the mc:Fallback is not lost.

That is the main risk in this change, so there is a test asserting the block comes back exactly once and the fallback survives. Checked against the real file from #3 as well: mc:AlternateContent 5 in and 5 out, w:drawing 6 and 6, w:pict 5 and 5, wps:wsp 5 and 5, a:blip 1 and 1.

Two things that are easy to get wrong

Fill against outline. Both are written as a:srgbClr. The outline sits inside a:ln, so the fill is read from a captured spPr with anything at or below an a:ln skipped. Without that, a shape picks up its border colour as its fill.

A picture also has an spPr. pic:pic carries pic:spPr, so parsed shape properties do not make a drawing a shape. An embed id is what makes it a picture and that takes precedence. I caught this because the anchored image from #3 stopped rendering once shapes were wired up.

What renders

Only what the layout output can already express, so no new primitives.

Text boxes are laid out in the engine, because breaking them into lines needs the font manager. The paginator renders them at the left margin and translates them onto the shape, which keeps justification and indent handling in one place rather than duplicating it.

Result on the file from #3

All five shapes now appear: the two blue boxes with their labels inside them, and the connector. The anchored image still renders in the right place alongside them, and the list fixes from #16 are unaffected.

Checks

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings and the full suite all pass. 354 tests, up from 347.

python3 scripts/hash_harness.py --check passes with 28 entries matching. As with #19, worth being explicit about why: none of the harness fixtures contains a shape, so the baselines are genuinely untouched. This does change output for any document that has one.

One thing to decide

This adds a public field to CT_R, a public field to CT_Anchor, and two new public types in rdocx-layout. AnchoredDrawing::embed_id also becomes AnchoredDrawing::content. So it is a breaking change and wants a 0.4.0 rather than a 0.3.x.

Word writes a shape as a compatibility block: the modern DrawingML in
mc:Choice and a VML fallback beside it. The run parser did not know
mc:AlternateContent, so the whole block fell into the unknown element
branch and was kept as raw XML. It survived a write but layout never saw
it, so nothing was drawn and no space was reserved.

The parser now reads the DrawingML out of mc:Choice as well as keeping
the block verbatim. The parsed form is held in a separate alt_drawings
field that is never serialised, so the raw copy stays the single source
for output and the mc:Fallback is not lost. A test asserts the block
comes back exactly once and that the fallback survives.

A wps:wsp is modelled as a preset geometry, an optional solid fill and
the paragraphs of its text box. Two details are easy to get wrong:

- The fill and the outline are both written as a:srgbClr, and the
  outline sits inside a:ln, so the fill is read from a captured spPr
  with anything at or below an a:ln skipped.
- A picture also carries a pic:spPr, so the presence of shape
  properties does not make a drawing a shape. An embed id is what makes
  it a picture and that takes precedence. Without this the anchored
  image in the file from #3 stopped rendering.

Rendering covers what the layout output can already express. A rect
with a fill becomes a FilledRect and a line becomes a Line. A shape
with a:noFill draws no body, which is correct rather than a gap: Word
uses unfilled rectangles as plain text boxes, and two of the five
shapes in the #3 file are exactly that. An unrecognised preset draws no
body but still draws its text.

Text boxes are laid out in the engine, because breaking them into lines
needs the font manager. The paginator renders them at the left margin
and translates them onto the shape, which keeps justification and
indent handling in one place.

Closes #11.
@mantissaman
mantissaman merged commit 21bb5b3 into main Jul 29, 2026
7 checks passed
@mantissaman
mantissaman deleted the fix/shapes-in-alternate-content branch July 29, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shapes inside mc:AlternateContent are never rendered

1 participant