Open more real decks, and make rendered text readable - #30
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>
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.
Two independent compatibility fixes, both found by running Rostrum over a corpus
of 471 real
.pptxfiles rather than over fixtures.Rendered slide text was invisible in every browser
SVGRendereremittedfont-sizein EMU, like every other length. Browsersclamp a computed
font-sizeto a five-digit maximum before the viewBoxtransform is applied, so a 68pt title asking for
font-size="863600"wasclamped and then scaled down to roughly one pixel: text present, correctly
placed, and invisible. Every deck rendered as a coloured rectangle — including
in Lectern's own slide previews and contact sheet.
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 into EMU
space.
text-anchoris 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 every deck rendered in the viewer's default
serif whatever its brand font was. Resolved typefaces are now emitted as
font-familywith asans-seriffallback.All five text emission sites — body text, the unsupported-graphic placeholder,
chart titles, category labels, legends — go through one
textElementhelperrather 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.
A
/[trash]/part sank the whole deck[Content_Types].xmlis required to cover every part (OPC M1.2), so a packagebreaking that rule is malformed and the reader threw. PowerPoint writes them
anyway: deleting content can leave a
/[trash]/0000.datin the archive withneither 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 (2.5%), every one of which opens in PowerPoint and in python-pptx. 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.
An untyped entry is now carried rather than rejected — the same treatment as an
orphan
.relsstream, and for the same reason. It cannot become aPart(a
Partwith no content type has no legal serialization) and it cannot bedropped (lossless round-tripping is this library's standing rule), so it goes
in
untypedEntries, is re-emitted in sorted order beside the other carriedentries, and lands in
readWarningsrather than being swallowed.The guard is "no declared content type", not "lives in
[trash]": the rule isabout the declaration, and other producers leave other names behind.
Tests
685 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. Two existing SVG
assertions are updated: one checked for the EMU size string, and one checked
<text x="0"for the origin case — an attribute that no longer exists, so itwould have kept passing while testing nothing.
🤖 Generated with Claude Code