Skip to content

fix(pptx): carry c:txPr run properties into chart text - #359

Open
dsaad68 wants to merge 2 commits into
openooxml:mainfrom
dsaad68:fix/pptx-chart-text-props
Open

fix(pptx): carry c:txPr run properties into chart text#359
dsaad68 wants to merge 2 commits into
openooxml:mainfrom
dsaad68:fix/pptx-chart-text-props

Conversation

@dsaad68

@dsaad68 dsaad68 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

TL;DR:

Before/After:
The repository's new chart fixture, crates/pptx-render/tests/fixtures/chart-text-properties.pptx,
rendered with Georgia mapped to Liberation Serif and Verdana to Liberation Sans. Its chart declares
+mj-lt at chart scope, i="1" spc="300" on the title with spc="600" on the title's own run, and
b="1" plus Verdana on the category axis:

fixture

minimal-chart slide 1, a PowerPoint design template, is the corpus repro. Its chart names
Open Sans, which neither engine has, so what changes there is which substitute is used —
LibreOffice, before, after:

minimal-chart slide 1

Repro file:
minimal-chart, slide 1. Every axis label of its chart declares
<a:defRPr sz="1197" b="1" ...><a:latin typeface="Open Sans"/></a:defRPr> and was drawn in the
theme's Calibri. The same shapes are reproduced as a committed fixture,
crates/pptx-render/tests/fixtures/chart-text-properties.pptx, described in
chart-text-properties.md beside it.

Summary:

  • Chart text is drawn in the family and slant the plot geometry resolved from c:txPr, instead of
    the theme minor font upright. chart_text_primitive was resolving +mn-lt itself and passing
    false for italic, so only weight, size and colour of a c:txPr ever reached the screen.
  • A chart that names no typeface still inherits the theme minor font: the renderer injects it into
    the chart-scope PlotTextStyle, which every other scope already inherits from, rather than
    overriding the resolved font. A +mj-lt-style token resolves through the theme the same way a
    shape run's family does. Hosts that pass no default keep the shared crate's own fallback.
  • a:defRPr/@spc is parsed into ChartTextProperties, resolved into PlotFont as a pixel
    tracking, and applied after every cluster of a chart text with no trailing gap, disabling
    optional ligatures when it is non-zero — the same rules the shape text path uses. This is what
    carries fix(pptx): apply run-level spc as character tracking #325's tracking across the chart boundary.
  • A chart title's first c:rich run now overrides the c:txPr paragraph default it sits under
    rather than being ignored whenever a c:txPr exists. PowerPoint writes a title's own tracking on
    that run, so this is the difference between reading spc="300" and reading the spc="0" beside
    it.
  • Attaching a source refreshes the chart, title, legend and axis text properties of a stored chart
    part, next to the fills and lines that were already merged. No schema bump: a document that never
    reattaches its source keeps its stored chart text.
  • The new fields default to absent on ChartTextProperties, PlotTextStyle and PlotFont, so
    xlsx-render and docx-layout are unchanged. crates/docx-layout/tests/chart_snapshots.rs and
    crates/xlsx-render/tests/snapshots/chart_display_list.json are byte-identical and were not
    touched. docx-layout carries spacingPt through its own deserializer but describes a font with
    a CSS shorthand, which has no letter-spacing, so docx charts stay untracked.

Measured against a baseline built from this worktree with the change reverted and the binding
rebuilt, over all 17 chart-bearing slides in the corpus:

  • minimal-chart/01 3.46 -> 3.51 (+0.05), four slides of one third-party deck 12.41 -> 12.45
    (+0.04), one slide +0.01, the other eleven +0.00.
  • The metric disagrees with the fix, and it is font substitution. Both decks that move name
    Open Sans, which the harness does not register, so their chart text moves from Carlito (the
    theme's Calibri) to the fallback face. Registering any face under Open Sans and re-rendering the
    same two slides with the same engine gives 3.43 for minimal-chart/01, below the 3.46
    baseline, and 12.41 for the other, equal to it. Nothing in the corpus regresses once the font
    a chart asks for exists.
  • The tracked title is the direct check: LibreOffice draws it 300px wide centred on x=639, this
    branch draws it 286px wide on the same centre, and main drew it 184px wide. It is a net zero on
    the metric because that title also sits ~18px above where LibreOffice puts it, so more correct ink
    lands in the wrong band. That vertical offset is a separate defect.

Test plan:

  • cargo test -p betteroffice-drawingml --features chart — 132 tests. The chart module is
    behind a non-default chart feature; a plain cargo test runs none of them.
  • cargo test -p betteroffice-pptx-render -p betteroffice-pptx-parse -p betteroffice-pptx-edit -p betteroffice-docx-layout -p betteroffice-xlsx-render — green, including the
    schema-migration tests and both shared chart snapshots, neither of which needed rebaselining.
  • cargo clippy --workspace --all-targets --features chart and cargo fmt --check — clean.
  • cargo check inside bindings/python-pptx, which is a separate cargo workspace the workspace
    build does not cover.
  • The two new render tests fail on 0f474aa and pass here:
    chart_text_takes_its_family_slant_size_and_tracking_from_c_txpr and
    tracking_spaces_the_glyphs_of_a_chart_title_without_a_trailing_gap.
  • Re-render minimal-chart slide 1 with a face registered under Open Sans: the fine diff
    against the LibreOffice reference falls to 3.43 from 3.46.

Closes #358

- `chart_text_primitive` draws with the family, slant and character spacing the
  plot geometry resolved, instead of the theme minor font upright and untracked.
  A chart that names no typeface inherits the theme minor font, injected at
  chart scope so every scope keeps inheriting from it, and a `+mj-lt`-style
  token resolves through the theme the same way a shape run's does.
- `a:defRPr/@spc` parses into `ChartTextProperties`, resolves into `PlotFont` as
  a pixel tracking and spaces the glyphs of one chart text without a trailing
  gap.
- A chart title's first `c:rich` run overrides the `c:txPr` paragraph default it
  sits under, which is where PowerPoint writes a title's own tracking.
- Attaching a source refreshes the text properties of a stored chart part.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@openooxml-bot

openooxml-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA — thank you! ✍️ ✅

Posted by the CLA bot.

@greptile-apps

greptile-apps Bot commented Sep 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR parses chart character spacing, carries chart font family and slant into PPTX rendering, refreshes stored chart text properties when a source is attached, and adds a focused chart fixture. Three correctness gaps remain:

  • Rich title paragraph defaults are skipped when title-level txPr exists.
  • Composed-slide charts still cannot carry character spacing to their host-laid-out text.
  • Fallback legend measurement can become negative for valid negative tracking.

Confidence Score: 2/5

The PR is not yet safe to merge because common title inheritance and composed/fallback chart-text paths can still render incorrect formatting or legend geometry.

Three blocking correctness issues remain: title paragraph defaults can be lost, composed chart output drops tracking, and negative tracking can yield invalid fallback legend widths.

Files Needing Attention: crates/ooxml-drawingml/src/chart/parse.rs, crates/ooxml-drawingml/src/chart/geometry.rs, crates/pptx-render/src/lib.rs

Important Files Changed

Filename Overview
crates/ooxml-drawingml/src/chart/parse.rs Parses spacing and title run overrides, but omits rich paragraph defaults from a three-level title style cascade.
crates/ooxml-drawingml/src/chart/geometry.rs Propagates tracking through chart styles, but its unmeasured legend-width fallback can return negative and trailing-gap-inflated widths.
crates/pptx-render/src/layout.rs Correctly resolves chart font faces and applies cluster-based tracking without a trailing gap on the normal slide-layout path.
crates/pptx-render/src/lib.rs Carries chart italics into composed output but still lacks a carrier for chart character spacing.
crates/pptx-edit/src/deck.rs Refreshes chart-, title-, legend-, and axis-level text properties during source attachment.
crates/pptx-render/tests/chart_text_properties.rs Adds strong integration coverage for the normal renderer, though not the composed chart path or richer title cascades.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  XML["Chart XML: txPr / rich text"] --> Parse["ChartTextProperties"]
  Parse --> Plot["PlotTextStyle / PlotFont"]
  Plot --> Geometry["Chart geometry and measurement"]
  Geometry --> Slide["SlideRenderer positioned glyphs"]
  Geometry --> Composed["Composed chart logical TextRun"]
  Slide --> Display["PPTX display list"]
  Composed --> Host["Host-side text layout"]
Loading

Comments Outside Diff (1)

  1. crates/pptx-render/src/lib.rs, line 310-320 (link)

    P1 Composed charts drop tracking

    The composed-slide chart path emits only an unpositioned TextRun, which has no letter-spacing field, and leaves lines empty. As a result, chart text with c:txPr/@spc renders without tracking on this path, even though chart geometry may already have measured and positioned the legend using that spacing.

    Knowledge Base Used: PPTX rendering and WebAssembly

Reviews (1): Last reviewed commit: "fix(pptx): carry c:txPr run properties i..." | Re-trigger Greptile

Comment thread crates/ooxml-drawingml/src/chart/parse.rs
Comment thread crates/ooxml-drawingml/src/chart/geometry.rs Outdated
The fallback multiplied the character count by size/2 plus tracking, so a
negative spc inverted it, and it counted a gap after the last character. A sink
without text measurement — docx, xlsx, composed pptx — then wrapped and placed
legend entries off a negative width.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dsaad68

dsaad68 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Two threads, two different answers.

Negative tracking breaks widths — real, fixed (08d8aa6c).

Confirmed by reading it: the fallback was chars * (size_px * 0.5 + letter_spacing_px), so a
negative spc large enough inverts it, and it charged a gap after the last character where a
measured line has n - 1. Every sink without text measurement — docx, xlsx, composed pptx — took
that width straight into wrapping and placement.

Extracted it as fallback_label_width so it can be tested without a sink, clamped at zero, and
moved to n - 1 gaps. The test asserts both: a label at spc = -40 stays non-negative, and six
characters carry five gaps rather than six.

This is the same defect class as the one found on #325 in ShapedCluster, where a clamped width
kept an unclamped negative tracking. Worth knowing there is a third place with the same shape if
anyone is looking.

Title defaults are skipped — I do not think this one holds.

parse_title_text picks c:txPr as the base and then overlays the first c:rich run's a:rPr,
so the precedence is run over txPr. Your reading is that a rich a:defRPr should sit between
them: run > rich a:defRPr > txPr.

I agree that is the correct precedence in principle, and I have not implemented it — but I could
not find a case where it changes a pixel, and I would rather not add an inheritance rung on
reasoning alone. No corpus chart writes both a c:title/c:txPr and a rich a:defRPr with
conflicting values; where both exist the rich default is empty. If you have a deck where the two
disagree I will take it and fix this properly, with that deck as the fixture. Recorded in the
cluster's NOTES.md either way so it is not lost.

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.

pptx: chart text drops its c:txPr family, slant and tracking

1 participant