Skip to content

Fix: render run-level text outline (<a:ln>) for PPTX glyphs - #39

Merged
Malcolmnixon merged 4 commits into
mainfrom
fix/pptx-run-text-outline
Oct 7, 2026
Merged

Malcolmnixon merged 4 commits into
mainfrom
fix/pptx-run-text-outline

Conversation

@Malcolmnixon

Copy link
Copy Markdown
Member

Description

Fixes a bug where PPTX text runs declaring a run-level <a:ln> (text outline/stroke) rendered
with that outline silently missing. A real-world deck used a near-white <a:solidFill> paired
with a purple <a:ln> stroke to produce PowerPoint's "hollow outlined numeral" effect (large "1"/
"2" badges); without outline support the numerals rendered as near-invisible pale shapes.

Root cause: PptxEffectiveRunProperties/PaintTextLayout only ever resolved and painted a run's
own <a:solidFill> fill color - there was no concept of a run's own <a:ln> (distinct from a
shape's <p:spPr>/<a:ln>, which strokes the shape's geometry, not its text).

This was investigated after a separate bug report suspected systemic font-fallback issues; direct
testing against two real-world decks confirmed theme/run font resolution (SystemFontCatalog/
ResolveTextFont) is correct, and the actual (narrower) defect was this missing outline support.

Fix

  • PptxEffectiveRunProperties gains OutlineWidthEmu/OutlineColor, resolved via a new
    GetRunOutline helper (PptxDocument.TextInheritance.cs) that reuses the existing
    ResolveLineStyle resolver already used for shape/picture/connector strokes, restricted to
    solid-color outlines (matching the pre-existing solid-color-only run fill color scope).
  • LineGlyph/PptxGlyphPlacement thread the new fields through layout, with OutlineWidthEmu
    scaled by the same autofit fontScale factor as SizeEmu.
  • PaintTextLayout builds each glyph's path in local shape space, then transforms separately for
    the fill paint vs. a ResolveStrokeOutline-based outline paint - mirroring the existing
    shape-outline rendering pattern, including its coordinate-scale-aware flatten tolerance.
  • <a:effectLst><a:glow> remains out of scope, consistent with this project's already-documented
    <a:effectRef> shape-effects deferral.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code quality improvement

Related Issues

Closes #

Pre-Submission Checklist

Build and Test

  • Code builds successfully and all tests pass: pwsh ./build.ps1 (ran dotnet build/dotnet test directly: 10,503 tests passed, 0 failed)
  • Code produces zero warnings

Code Quality

  • New code has appropriate XML documentation comments
  • Static analyzer warnings have been addressed

Quality Checks

  • All linters pass: pwsh ./lint.ps1 (passed - no errors)

Testing

  • Added unit tests for new functionality (PptxTextTests.cs: 2 new inheritance-resolution
    tests; PptxTextRenderTests.cs: 1 new outline-painting test)
  • Updated existing tests if behavior changed (none required - fill-path refactor is
    mathematically equivalent; pre-existing tests pass unmodified)
  • All tests follow the AAA (Arrange, Act, Assert) pattern
  • Test coverage is maintained or improved

Documentation

  • Updated README.md (if applicable) - not applicable, no public API/package changes
  • Updated docs/ documentation (if applicable) - added a new "Phase 2 Follow-Up: Run Text
    Outline Rendering" section to docs/design/canvas-net-pptx/pptx-document.md
  • Added code examples for new features (if applicable) - not applicable
  • Updated requirements.yaml (if applicable) - not applicable, no new/changed requirement

Additional Notes

Visually verified against the real-world fixture that reported this (slide 3, "Objective" badges):
before the fix the numerals rendered as near-invisible pale shapes; after, both render as clearly
visible purple-outlined hollow numerals, matching PowerPoint's own rendering.

Previously PaintTextLayout only ever painted a run's own <a:solidFill>
fill color, silently ignoring a run-level <a:ln> stroke. A real-world
deck used a near-white fill paired with a purple <a:ln> outline to
produce PowerPoint's hollow-outlined-numeral effect; without outline
support the numerals rendered nearly invisible.

- PptxEffectiveRunProperties gains OutlineWidthEmu/OutlineColor,
  resolved via a new GetRunOutline helper (PptxDocument.TextInheritance.cs)
  that reuses ResolveLineStyle, restricted to solid-color outlines.
- LineGlyph/PptxGlyphPlacement thread the new fields through layout.
- PaintTextLayout builds each glyph path in local shape space, then
  transforms separately for fill vs. ResolveStrokeOutline-based outline
  painting, mirroring the existing shape-outline pattern.
- Added unit tests for inheritance resolution and outline painting.
- Documented the fix in docs/design/canvas-net-pptx/pptx-document.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 11:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Three moderate issues and one documentation nit remain unresolved.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds PPTX run-level &lt;a:ln&gt; text-outline rendering for glyphs.

Changes:

  • Resolves inherited outline width and color.
  • Threads outline data through layout, autofit scaling, and painting.
  • Adds tests and design documentation.

Review notes include unresolved moderate issues for explicit stroke cancellation, autofit outline scaling coverage, and dashed outline preservation, plus a documentation nit.

File Reviewed changes
test/​DemaConsulting.CanvasNet.Pptx.Tests/​PptxTextTests.cs Tests outline property resolution.
test/​DemaConsulting.CanvasNet.Pptx.Tests/​PptxTextRenderTests.cs Tests glyph outline painting.
src/​DemaConsulting.CanvasNet.Pptx/​PptxTextLayout.cs Adds outline data to glyph placements.
src/​DemaConsulting.CanvasNet.Pptx/​PptxEffectiveTextProperties.cs Adds effective outline properties.
src/​DemaConsulting.CanvasNet.Pptx/​PptxDocument.TextRender.cs Paints glyph outlines.
src/​DemaConsulting.CanvasNet.Pptx/​PptxDocument.TextLayout.cs Propagates and scales outline data.
src/​DemaConsulting.CanvasNet.Pptx/​PptxDocument.TextInheritance.cs Resolves run-level outlines.
docs/​design/​canvas-net-pptx/​pptx-document.md Documents the implementation and scope.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/DemaConsulting.CanvasNet.Pptx/PptxDocument.TextInheritance.cs Outdated
Comment thread src/DemaConsulting.CanvasNet.Pptx/PptxDocument.TextRender.cs Outdated
…ine dash arrays

Address two unresolved Copilot code-review findings on PR #39:

1. PptxDocument.TextInheritance.cs: ResolveEffectiveRunProperties now selects
   the first tier whose <a:ln> element is present at all (run, then paragraph
   defRPr, then placeholder level, then master level) before resolving it,
   instead of chaining GetRunOutline''s resolved result with ??. This fixes a
   bug where a run''s own explicit outline cancellation (<a:ln><a:noFill/></a:ln>
   or a non-positive width) was indistinguishable from "undeclared here" and
   incorrectly fell through to inherit a shallower tier''s outline.

2. Threaded the resolved DashArray from ResolveLineStyle all the way through
   PptxEffectiveRunProperties, LineGlyph, and PptxGlyphPlacement into
   PaintTextLayout''s stroke call, replacing the previously hard-coded
   DashArray: null. A dashed run-level <a:ln> now renders dashed instead of
   silently degrading to solid. The dash array is also scaled by the autofit
   fontScale alongside OutlineWidthEmu.

Added regression tests in PptxTextTests.cs (explicit no-fill/zero-width
cancellation, dash-array resolution matching ResolveLineStyle) and
PptxTextRenderTests.cs (dashed outline paints visible gaps). Updated the
design doc''s "Phase 2 Follow-Up: Run Text Outline Rendering" section with
both corrections.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 12:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Moderate issues remain for unsupported blip-fill handling and layout/autofit test coverage; documentation nits also remain.

Review effort: Lite
Findings: 1 Medium severity · 2 Low severity

Open (3)
Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Skip unsupported run-level line paints before resolving styles

src/​DemaConsulting.CanvasNet.Pptx/​PptxDocument.TextInheritance.cs:669

GetRunOutline delegates to ResolveLineStyle without a blip resolver. For a run-level <a:ln><a:blipFill>...</a:blipFill></a:ln>, ResolveFill therefore throws PptxUnsupportedFeatureException instead of applying the documented solid-only/out-of-scope behavior of skipping the outline; a single unsupported text paint can abort rendering the whole slide. Short-circuit non-solidFill line paints before calling ResolveLineStyle (or otherwise convert unsupported paints to no outline).

Comment thread src/DemaConsulting.CanvasNet.Pptx/PptxDocument.TextLayout.cs
Comment thread docs/design/canvas-net-pptx/pptx-document.md Outdated
Comment thread src/DemaConsulting.CanvasNet.Pptx/PptxEffectiveTextProperties.cs Outdated
…line-scale test

- GetRunOutline now returns null for a run-level <a:ln><a:blipFill> instead
  of letting PptxUnsupportedFeatureException propagate and abort the whole
  slide render (gradient/pattern line paints already degraded gracefully;
  only blipFill threw).
- Documented both null cases (undeclared vs. explicitly cancelled) for
  PptxEffectiveRunProperties.OutlineWidthEmu.
- Reconciled the design doc's Scope paragraph with the actual tier-selection
  semantics and added a Correction subsection for the blip-fill short-circuit.
- Added regression tests: a run-level blipFill outline resolves to no
  outline instead of throwing, and BuildLines' autofit shrink loop scales
  OutlineWidthEmu/OutlineDashArray by the same fontScale as SizeEmu.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 13:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Two critical review issues remain unresolved: unsupported non-solid outlines can throw, and the layout regression test reparents its shared XML element.

Review effort: Lite
Findings: 2 High severity · 1 Medium severity

Open (3)
Resolved since last review (2)

Comment thread src/DemaConsulting.CanvasNet.Pptx/PptxDocument.TextInheritance.cs Outdated
Comment thread test/DemaConsulting.CanvasNet.Pptx.Tests/PptxTextLayoutTests.cs Outdated
…ment reuse

- GetRunOutline now short-circuits gradFill/pattFill/blipFill run-level
  line paints (not just blipFill) before calling ResolveLineStyle, since
  an unsupported gradient direction or pattern preset can throw
  PptxUnsupportedFeatureException just like an unresolved blipFill did -
  a single out-of-scope run stroke could otherwise abort rendering the
  whole slide.
- Fixed the autofit-scaled-outline layout regression test: the shared
  <a:ln> XElement was being reparented into only the last paragraph by
  LINQ to XML's Add/Select semantics, leaving glyph[0] without an
  outline. Each paragraph now gets its own clone via 'new XElement(ln)'.
- Added regression tests for unsupported path-gradient and unsupported
  pattern-preset run outlines resolving to no outline.
- Updated design doc with a fourth Correction subsection documenting the
  broader short-circuit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 13:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Add an end-to-end rendering test and update the required traceability artifacts.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

@Malcolmnixon
Malcolmnixon merged commit 5f978e5 into main Oct 7, 2026
7 checks passed
@Malcolmnixon
Malcolmnixon deleted the fix/pptx-run-text-outline branch October 7, 2026 14:20
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.

2 participants