Skip to content

fix(decompile): honour <a:alpha> on fills via pre-blend on white#22

Closed
marsmike wants to merge 1 commit into
mainfrom
fix/alpha-fills
Closed

fix(decompile): honour <a:alpha> on fills via pre-blend on white#22
marsmike wants to merge 1 commit into
mainfrom
fix/alpha-fills

Conversation

@marsmike
Copy link
Copy Markdown
Owner

Summary

PPTX colour elements carry <a:alpha val=\"N\"/> (N is 0..100000 = 0..100% opacity) for Venn diagrams, overlay panels, glass-effect cards. The previous _resolve_solid and _resolve_fill paths ignored alpha entirely — semi-transparent fills decompiled at full opacity.

Threading true alpha through the build pipeline (expander → emitter → python-pptx fill APIs) is a larger change. As a contained first pass this PR pre-multiplies the source alpha against a white slide background:

blended_c = c * alpha + 255 * (1 - alpha)

For typical white-canvas slides this reproduces the perceived colour of non-overlapping semi-transparent fills exactly. Overlapping regions (Venn intersections) are still off — real alpha compositing would darken the intersection — but the standalone fills now match source pixels, which is the dominant contribution to diff.

Result

Measured on the 99-slide showcase deck:

  • slide-61 (Venn diagram, 3 overlapping semi-transparent blue circles): 18.05% → 5.8% struct_diff (one fix, no other changes)

Test plan

  • All 996 existing tests still pass
  • Manually verified slide-61 (Venn) — non-overlap regions now match source hue; overlap still darker in source than render (real alpha would fix that)

🤖 Generated with Claude Code

PPTX colour elements can carry `<a:alpha val="N"/>` where N is 0..100000
encoding 0..100% opacity. Source decks use this for Venn diagrams,
overlay panels, glass-effect cards, and any composition where shapes
sit on top of each other and the overlap region should mix.

The previous `_resolve_solid` and `_resolve_fill` paths ignored the
alpha element entirely — semi-transparent fills decompiled at full
opacity, so Venn circles render as solid blobs instead of the
expected lighter-on-white standalone + darker overlap regions.

Threading true alpha through the build pipeline would require the
expander, emitter, and python-pptx fill APIs to learn about
transparency. As a contained first pass, we pre-multiply the source
alpha against a white slide background:

  blended_c = c * alpha + 255 * (1 - alpha)

For typical white-canvas slides this reproduces the perceived colour
of standalone (non-overlapping) semi-transparent fills exactly. For
overlapping regions (Venn intersections) the result is still wrong —
real alpha compositing would darken the intersection — but the
standalone fills now match source pixels, which is the dominant
contribution to the diff.

On the showcase deck slide-61 (the Venn diagram example) went from
18.05% struct_diff to 5.8% in a single change.

Two new helpers:

- `_alpha_for_color(color_el)` — read `<a:alpha>` child, returns 0..1
- `_blend_on_white(rgb, alpha)` — apply the pre-multiply formula

Both `_resolve_solid` (used by line strokes, table cells, chart
series) and `_resolve_fill` (shape fills, including the grpFill walk)
now apply the blend on srgb AND scheme colour paths. Theme-scheme
colours also pick up `<a:alpha>` when the source layers it onto an
inherited brand accent.

Signed-off-by: Mike Mueller <mike@objektarium.de>
@marsmike
Copy link
Copy Markdown
Owner Author

Consolidated into PR #23 along with #21. All three XML deep-read fixes (chart-axis visibility, alpha-fills, layout font inheritance) now ship together.

@marsmike marsmike closed this May 20, 2026
marsmike added a commit that referenced this pull request May 21, 2026
`_text_runs` defaulted any paragraph whose run/`<a:pPr>`/`<a:lstStyle>`
all omitted explicit `sz` to a hardcoded 18pt. PowerPoint's actual
cascade reaches into the slide layout and slide master:

  1. slide-level run `<a:rPr sz="...">`            (already honoured)
  2. paragraph `<a:pPr><a:defRPr sz="...">`        (already honoured)
  3. slide's `<a:txBody><a:lstStyle>...defRPr sz`  (already honoured)
  4. **layout's placeholder `<p:txBody><a:lstStyle>...defRPr sz`** ← new
  5. **master's `<p:txStyles><p:titleStyle|bodyStyle>...defRPr sz`** ← new
  6. hardcoded 18pt fallback

Steps 4 and 5 are the layout-inheritance cascade. On a typical chapter-
divider layout the slide-level placeholder writes only `<a:t>Chapter
title</a:t>` and inherits the 60pt+ headline size from the layout's
matching placeholder. Without the lookup we render at 18pt — visibly
too small.

New helper `_layout_placeholder_default_sz(slide, ph_type, ph_idx)`
walks the layout (then the master's `<p:txStyles>`) for the matching
placeholder. `_text_runs` gains an `inherited_default_sz` kwarg fed by
`_emit_sp` whenever the shape carries `ph_type`/`ph_idx`. The cascade
priority remains correct: slide-level lstStyle still wins over
inherited; inherited only fires when no slide-level default exists.

End-to-end on the 99-slide showcase, plus stacks cleanly with the
chart-axis (PR #21) and alpha-fills (PR #22) improvements:

  mean struct_diff: 8.21% → 6.71% (91.79% → 93.29% quality)
  slides above 15% threshold: 13 → 5

Signed-off-by: Mike Mueller <mike@objektarium.de>
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.

1 participant