Answer what colour a slide actually is - #31
Merged
Conversation
Two faults made every deck render as a coloured rectangle with no readable text, which is what any WebKit-backed viewer shows — Lectern's own slide previews and contact sheet included. Font sizes went out in EMU, like every other length here. Browsers clamp a computed font-size to a five-digit maximum *before* the viewBox transform is applied, so a 68pt title asking for font-size="863600" was clamped and then scaled down to roughly one pixel: text present, correctly placed, invisible. Sizes now go out in points under a per-text translate(x,y) scale(12700), which keeps the number far below the clamp and puts the glyphs back in EMU space. text-anchor is unaffected — it anchors at x=0 of the scaled space, which the translate has already moved to the anchor point. The renderer also resolved each run's typeface to pick wrapping metrics but never named it in the markup, so a deck rendered in the viewer's default serif whatever its brand font was. Resolved typefaces are now emitted as font-family with a sans-serif fallback. All five text emission sites (body text, the unsupported-graphic placeholder, chart titles, category labels, legends) go through one textElement helper rather than repeating the attribute soup four more ways. Verified on a 49-slide Keynote-authored deck: previously 49 black rectangles, now fully legible in its own Helvetica Neue. 678 Rostrum tests and 162 Lectern tests pass. One existing assertion checked for the EMU size string and is updated to the points encoding; a second checked `<text x="0"` for the origin case, which no longer exists as an attribute and would have passed vacuously — it now checks translate(0, instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`[Content_Types].xml` is required to cover every part (OPC M1.2), so a package that breaks that rule is malformed and the reader threw. PowerPoint writes them anyway: deleting content can leave a `/[trash]/0000.dat` in the archive with neither an Override nor a matching Default, and PowerPoint reopens its own file without complaint. Refusing the whole deck over an entry nothing references cost 12 of 471 real decks in one library — 2.5%, every one of which opens in PowerPoint and in python-pptx. Measured, not guessed: the same corpus's other 13 failures are genuinely truncated archives that Python also refuses, and those still throw. No deck in 471 hit the zip64 path. So an untyped entry is now carried rather than rejected — the same treatment as an orphan `.rels` stream, and for the same reason. It cannot become a `Part`, since a `Part` with no content type has no legal serialization, and it cannot be dropped, since lossless round-tripping is this library's standing rule. It goes in `untypedEntries`, is re-emitted in sorted order beside the other carried entries, and is recorded in `readWarnings` rather than swallowed. The guard is "no declared content type", not "lives in [trash]": the rule is about the declaration, and other producers leave other names behind. `ContentTypesMap` gains a non-throwing `declaredContentType(for:)` so `read` can branch rather than catch. The throwing `contentType(for:)` is unchanged. 678 existing tests still pass, including the byte-identical round-trip corpus. Seven new tests cover carrying, the read warning, survival across a resave, resave being a fixed point, and that a declared extension is still a part. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`setBackground` had no counterpart, so a caller could write a slide's background but never ask what one was — which makes "build a new slide that looks like this deck" impossible to do faithfully. Consulting the theme is not a substitute, and the gap is not academic. Plenty of real decks carry their look on the slide rather than in the theme: a Keynote export puts `<p:bg><a:solidFill><a:srgbClr val="000000"/>` on every slide while the theme's dk1/lt1 stay at the Office defaults, so a reader consulting only the theme concludes the deck is light and renders white slides for a deck that is emphatically black. `Slide.solidBackground` answers that one narrow question and stays quiet when the answer is not simple — nil for an inherited background, a gradient or a picture fill, rather than guessing at a representative colour. `relativeLuminance`, `contrastRatio(with:)`, `onColor(dark:light:)` and `bestTextColor(on:options:)` become public. Anything building slides on top of Rostrum has to make the same light-text-or-dark-text decision `DeckStyle` already makes internally, and the alternative is every caller hand-rolling its own luminance — which is how two parts of one deck end up disagreeing about whether a background is dark. 685 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Slide.solidBackground` answers a narrow question — does this slide set a solid background itself — and says so. What was missing is the question callers actually have: what will the audience see. Almost nothing about a real deck's appearance is where a naive reader looks for it. The theme's lt1 is usually the untouched Office FFFFFF. The slide usually carries no p:bg at all. The deck's near-black ground is sitting on a *layout*, written as `<a:schemeClr val="tx1">`, which only means near-black once the master's clrMap has been applied to it. So every one of the three obvious answers — read the theme, read the slide, read the raw value — returns white for decks that are emphatically not white. `Slide.effectiveBackground` walks slide → layout → master in PowerPoint's own order and resolves what it finds: srgbClr, schemeClr through the theme, sysClr, a gradient's first stop, and p:bgRef's colour child. A picture fill reports as `.picture` rather than inventing a colour, an explicit a:noFill stops the chain rather than letting it climb past to report a background the audience would never see, and a declaration whose colour cannot be resolved changes nothing at all — the chain carries on as though that part had said nothing, which is the only answer that cannot make up a colour the deck does not have. `Presentation.prevailingBackground` answers the related question for a slide being *added*, which has nothing to inherit from: whatever its neighbours do. The mode rather than the first slide's, because a title slide is very often the one slide that breaks the pattern and taking it would dress every added slide as a title. Nil when nothing reaches a majority, which is the deck saying it has no single ground. The walk lives in one place so the SVG renderer and this cannot drift apart. 13 new tests, 698 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Slide.solidBackgroundanswers a narrow question — does this slide set a solid background itself — and its doc comment says so. What was missing is the question callers actually have: what will the audience see.Almost nothing about a real deck's appearance is where a naive reader looks for it:
lt1is usually the untouched OfficeFFFFFFp:bgat all<a:schemeClr val="tx1">, which only means near-black after the master'sclrMapis appliedSo all three obvious answers return white for decks that are emphatically not white. Measured on one real deck: 0 of 14 slides carry a
p:bg, 13 of 46 layouts do.What's added
Slide.effectiveBackgroundwalks slide → layout → master in PowerPoint's own order and resolvessrgbClr,schemeClr(through the theme, soclrMapis honoured),sysClr, a gradient's first stop, andp:bgRef's colour child.Three cases it refuses to guess at:
.picturerather than inventing a coloura:noFillstops the chain rather than climbing past to report a background the audience would never seePresentation.prevailingBackgroundanswers the related question for a slide being added, which has nothing to inherit from: whatever its neighbours do. The mode rather than the first slide's, since a title slide is very often the one that breaks the pattern.The walk lives in
BackgroundResolverso this andSVGRenderercannot drift apart.Tests
13 new, 698 pass.
🤖 Generated with Claude Code