Conversation
ECMA-376 makes CT_LinearShadeProperties/@ang and CT_PathShadeProperties/@path optional, and python-pptx writes <a:lin scaled="0"/> for every fill.gradient(). oxml-drawing parsed both as required, so a deck carrying such a gradient failed to open at all, not only to render that fill. A missing angle now reads as 0 and a missing path as rect, as LibreOffice's oox import does, so the public angle and kind fields keep their types. A private flag records that the source omitted the attribute, and the writer leaves out a value still equal to that default, so a round trip does not add it. Path gradients stay unsupported in the rpptx resolver, so a missing path now reports the rectangle path gradient diagnostic instead of refusing the file. The same parser reads the fill lists of a theme's format scheme. rdocx therefore stops dropping a theme that carries such a gradient, or replacing it with the Office default when it authors a chart. GitHub issue tensorbee#170.
rpptx-render lowers a slide, layout or master background fill into PageFrame::background, and the rasteriser paints it, but the PDF writer never read the field. to_pdf() and rpptx convert --to pdf therefore wrote a white page where the PNG export draws the authored colour, and white text on a coloured slide vanished from the PDF. The writer now fills the page rectangle with the background before any element, through the resolve_paint and set_fill_paint path a shape fill uses. A gradient background registers its pattern with the page flip as its matrix, and a translucent solid registers its alpha state. Tile paint stays unpainted, as it does for shapes. On a tagged page the fill is marked as an artifact. Notes and handout PDFs now also paint the background of their notes or handout master, as their PNG export already did. A page without a background writes the same bytes as before, so rdocx output and the hash harness do not move. GitHub issue tensorbee#170.
The two fixes grow the oxml-drawing, oxml-pdf and rpptx packages, so each README archive row and its ARCHIVE_MEASUREMENTS entry in scripts/readme_doctests.py are re-measured from cargo package. Each crate is listed in ARCHIVE_REMEASUREMENT_DATES with the re-measure date, and its README row carries that date. GitHub issue tensorbee#170.
hadim
force-pushed
the
fix/rpptx-pdf-backgrounds-and-optional-gradient-attributes
branch
from
September 27, 2026 16:41
6c5c54c to
4e3ff0a
Compare
This was referenced Sep 27, 2026
This branch has not been deployed
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.
Summary
oxml-drawing:a:lin/@anganda:path/@pathbecome optional on read, asECMA-376 declares them. A missing angle reads as 0 and a missing path as
rect. A private flag onLinearGradientandPathGradientrecords thatthe source omitted the attribute, and the writer then leaves out a value
still equal to that default, so
<a:lin scaled="0"/>and<a:path><a:fillToRect .../></a:path>round trip unchanged. No public typechanges.
oxml-pdf: the PDF writer paintsPageFrame::backgroundunder the pagecontent. It fills the page rectangle through the same
resolve_paintandset_fill_paintpath a shape fill uses, so solid, linear and radial paintare covered, including a translucent solid.
oxml-drawing,oxml-pdfandrpptxre-recorded, sinceeach package grows. Each crate is added to
ARCHIVE_REMEASUREMENT_DATESand its README row carries the re-measure date.
Closes #170
Why
A deck made by python-pptx with
fill.gradient()could not be opened. python-pptxwrites
<a:lin scaled="0"/>for every gradient, andoxml-drawingparsed@ang(and@pathona:path) as required, soPresentation::from_bytesrefused the whole package with "DrawingML lin requires @ang".
Separately, a slide background given as a fill, on the slide, its layout or its
master, was drawn by the PNG export and missing from
to_pdf()andrpptx convert --to pdf.rpptx-renderlowers it intoPageFrame::background,the rasteriser paints that field, and the PDF writer never read it. White text
on a coloured title slide vanished from the PDF. Picture backgrounds were not
affected because they are lowered as page elements.
Notes
LinearGradient::anglestaysAngleandPathGradient::kindstaysPathGradientKind. The two new fields areprivate, and neither struct can be built with a struct literal outside the
crate (both already carry a private
raw_children), so the change isinvisible through
oxml-drawing, throughrpptx::Filland throughrdocx::CT_OfficeStyleSheetandDocument::theme(). A patch release ofeach family can carry it. Values built in code are unaffected:
LinearGradient::default()still writes<a:lin ang="0"/>. The writerleaves an attribute out only when the source omitted it and the value still
equals the default that every reader assumes for a missing attribute.
Equality compares the flag, so
<a:lin/>and<a:lin ang="0"/>parse tounequal values, as two fills that differ only in preserved raw children
already do.
default. I checked LibreOffice's oox import at master
(
oox/source/drawingml/misccontexts.cxxandfillproperties.cxx). It readsa missing
@angas 0 (moShadeAngle.value_or(0)), and it reads ana:pathwithout@pathasrect(
rAttribs.getToken(XML_path, XML_rect), with the comment "always set apath type, this disables linear gradient in conversion"). So a missing
@pathdoes not fall back to a linear gradient there.oxml-drawingfollows both rules, and the field docs say so.
rpptx-layoutneeds nochange. Because rpptx does not render path gradients yet, a missing path
produces the existing "rectangle path gradient" unsupported diagnostic and
the page keeps its white default, exactly as
path="rect"does. The filenow opens and every other part of the slide renders.
of a theme's
a:fmtScheme. rdocx reads its theme withCT_OfficeStyleSheet::from_xml(..).ok(), so a docx whose theme carriedsuch a gradient used to get no theme at all: layout fell back to the
default theme fonts and colours,
Document::theme()returnedNone, andensure_authored_chart_themereplaced the theme with the Office defaultwhen a chart was authored. All three now use the author's theme. rpptx's
render paths returned an error for such a theme and now render. This is the
intended direction, but rdocx output changes for these inputs. No hash
harness sample has such a theme, so the harness does not move.
@ang(ST_PositiveFixedAngle, 0 to 21600000) is still notvalidated. That is unchanged and out of scope.
GradientTarget::Backgroundat leaf 0 of its page, with the page flip as thepattern matrix, and a translucent solid background registers its alpha
state. On a page without a background nothing is registered or emitted, so
those PDFs are byte-identical. rdocx never sets the field, and
hash_harness.py --checkreports all 49 entries matching.p:bg, usually abg1fill, so their PDF pages now start with a full pagefill, as their PNG export already did. It is white in most themes.
render_export_surfaceclears the shelllayout and slide master backgrounds, but the page keeps the
p:bgof itsnotes or handout master. The bundled default notes master has
<p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef>, so every notespage PDF now starts with a full page
bg1fill, as its PNG export alreadydid. It is usually white, and a theme with a non-white
lt1now shows itscolour in the PDF as well.
Paint::Tilebackgrounds stay unpainted in PDF, as tile fills of shapesalready are, and as the rasteriser leaves them. rpptx never produces one,
since picture backgrounds are lowered as page elements. PDF/A preflight
already rejects a tile background.
LayoutResult::structurepresent) the background fill iswrapped in
/Artifact BMC ... EMC, as the HLD asks of decorative paint. Noproducer sets both today (rdocx tags but has no page background, rpptx has
backgrounds but no structure), so this only keeps the shared writer correct
for PDF/UA. Tell me if you prefer to drop it.
values in
crates/rpptx/tests/integration.rsbelong to PowerPoint oraclefiles, not to rpptx output, and
golden_png_harness.pyandpptx_ssim_harness.pycompare rasters. Rust PDFs of decks with a fillbackground (for example the F-116 cross-viewer deck, which sets a green
slide background) now carry the fill, and no test pins those bytes.
08-rendering-spec.mddoes not mention pagebackgrounds, and nothing in it becomes false, so I left it alone. A sentence
there may be worth adding in your sprint.
Tests
Added:
oxml-drawingfill::tests::gradient_geometry_without_angle_or_path_round_trips_without_adding_them:<a:lin scaled="0"/>,<a:lin/>,<a:path/>and<a:path>with afillToRectwrite back byte for byte. The parsed values areAngle(0)andPathGradientKind::Rectangle, and assigningAngle(5400000)orCircletothem writes the attribute.
ang="0"andpath="rect"in the source arekept, and
LinearGradient::default()still writesang="0".oxml-pdfwriter tests:solid_page_background_fills_the_page_before_its_content,gradient_page_background_fills_the_page_with_its_pattern_first(linear andradial, content order, page pattern resource and
/Matrix),translucent_page_background_selects_a_registered_alpha_state,tile_page_background_leaves_the_content_unchangedandtagged_page_background_is_an_artifact. The test helpercontent_forgainsa
content_for_pagevariant that takes a page and an optional structure.rpptxintegration tests:gradient_backgrounds_without_angle_or_path_open_render_and_round_tripopens a deck whose slide background is
<a:lin scaled="0"/>, checks theraster is a left to right red to blue gradient, and checks the saved slide
keeps
<a:lin scaled="0"/>. It then decodesto_pdf_deterministic()withlopdfand checks that the page content starts withq cm q cs scn re f Q, and that the named page pattern's axial shading hasa horizontal, left to right axis from
[1 0 0]to[0 0 1]. This ties thepaint that
rpptx-layoutresolves to the PDF pattern. The same deck with ana:pathwithout@pathchecks the diagnostic and the saved XML.solid_backgrounds_from_slide_layout_and_master_reach_pdf_and_pngputs a7B1E3Asolid background on the slide, the layouts or the master, and checksthat the PNG corner is that colour and that the PDF page content starts with
q cm q rg re f Qwherergis that colour andreis the MediaBox. Bothdecode the PDF with
lopdf, so they need no Poppler.Without the fixes, both rpptx tests fail (with "DrawingML lin requires @ang",
and with a page content of only
q cm Q), the oxml-drawing test fails, andfour of the five writer tests fail. The tile test pins output that does not
change. With the writer fix and without the
emit_backgroundcall, the newgradient PDF assertion fails on
q cm Q.The issue's two Python reproductions, run against
rpptx-pyrebuilt withmaturin developfrom this branch, now print:The two path variants render the white default because path gradients are
not rendered yet, with or without
@path.Run:
cargo fmt --all --check: clean.cargo clippy -p oxml-drawing -p oxml-pdf -p rpptx -p rpptx-layout --all-targets --all-features -- -D warnings: clean.cargo test --no-fail-fastforoxml-drawing,oxml-pdf,oxml-chart,rpptx-oxml,rpptx-layout,rpptx-render,rpptx-cli,rdocx-oxml,rdocx-layout,rdocx-pdf,rdocx-cli,rpptxandrdocx: everythingpasses except the environment-only failures listed below.
cargo doc --no-deps --all-featuresofoxml-drawing,oxml-pdfandrpptxwithRUSTDOCFLAGS=-D warnings: clean.cargo check --target wasm32-unknown-unknown -p rdocx-wasm -p rpptx-wasm:clean.
python3 scripts/hash_harness.py --check: 49 entries match.python3 scripts/readme_doctests.py: passes with the re-recorded rows anddates. The
rpptx-layoutrow is unchanged and still matches its package.python3 scripts/prose_check.pyandpython3 scripts/sync_agent_skills.py --check: clean.pytest crates/rpptx-py/testsagainst the rebuilt extension: 40 passed. Nobinding or stub changed.
Environment-only failures seen on this Mac:
oxml-pdfwriter::tests::rotated_linear_gradient_renders_with_its_axis_rotated(pinned pdftoppm 26.01, local 26.09).
rpptx-layoutall_corpus_preset_geometries_evaluate_or_fallback,all_corpus_slides_resolve_without_panicsandcorpus_slide_resolves_without_theme_references, andrpptx-clivalidate_rejects_corruption_and_accepts_the_pinned_corpus(gitignored/corpus/pptxmissing).rdocxliblarge_word_and_presentation_pdfs_preserve_logical_reading_orderand
word_and_powerpoint_chart_pixels_are_identical(pinned Poppler), andintegration
odt_reader_matches_pinned_libreoffice_structure,public_authored_theme_and_fonts_match_pinned_word_resolutionandsanitized_public_authoring_fixture_passes_every_conformance_stage.rdocxintegrationf269_section_page_semantics::section_page_semantics_match_pinned_libreoffice_renderand
f267_table_style_conditional_tests::every_conditional_table_region_matches_word.Both stop on their first assertion, which compares
soffice --version(local LibreOffice 26.8.0.3) with the pinned 26.2.5.2. They fail the same
way on
origin/main.