Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions assets/bugfixes/issue-1879/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Issue 1879 visual evidence

Source: `tests/fixtures/pptx/1-slide.pptx`. Native reference exported through Microsoft PowerPoint 16.113.4 on macOS 26.6.2 (25G83), using the local Best for printing PDF option on 2026-10-11.

SHA-256:

- Source: `181622a0431e7be842b56f1783643fe373ea8936629e033d3879f492e5a5349a`
- Native GT PDF: `a5ba138cb826237928f823ffe381e17e7bb497c2527207b1d41333292edf1ef1`
- Current output PDF: `d58a35a2b133257723048ab4e1db650558ab03c21ef454bb784e048aeac0eadf`

Before uses main `71e103115ee19b1c72cd765ff04a27b5b8dede6f`; after uses this PR's `3338dce9922bfc376c01c11c3f1ea284ca2a4cff` code. Both converters were built with `cargo build --locked --profile ci -p office2pdf-cli`. Original PDFs, binaries, logs and inspected PNGs are retained outside the worktree.

Render the source with the native app's local PDF export and with `cargo run --locked --profile ci -p office2pdf-cli -- tests/fixtures/pptx/1-slide.pptx -o after.pdf`. Compare all pages with `compare_layout.py --json --audit --fine-shift 1`, `compare_text_layer.py`, and `compare_render.py --page 1 --dpi 150 --fine-shift 1 --strict-clusters` using the exact IDs in the committed report.

One page was inspected at 150 DPI. JPEGs preserve pixel dimensions, use progressive quality 86, and strip metadata before resetting 150 DPI density. The 1pt fine threshold is below the couple-of-points external-reference placement materiality scale; smaller observed differences remain documented. Layout and normalized searchable content pass.

The previously purple panel now takes the white slide background, as p:sp/@useBgFill="1" specifies. PAGE 1, the right-side picture, both black rules, their flat endpoints, and the picture crop agree with native PowerPoint. The rules are solid 3.5pt and 1pt strokes (a:ln/@w=44450 and 12700); no dash, emphasis, rotation, clipping, or missing element was observed. Full pages, the pixel diff, and matched title/rule/picture crops show only boundary coverage and photo resampling fragments below the material-cluster floor. There are zero current material clusters.
Binary file added assets/bugfixes/issue-1879/after.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/bugfixes/issue-1879/before.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/bugfixes/issue-1879/gt.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
183 changes: 183 additions & 0 deletions assets/bugfixes/issue-1879/layout-audit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
{
"pages": [
{
"lines": {
"gt": 1,
"out": 1,
"matched": 1,
"missing": 0,
"extra": 0,
"deviant": 0,
"missing_text": [],
"extra_text": []
},
"baseline": {
"mean_abs_dy": 0.1199500000000171,
"worst_dy": 0.1199500000000171,
"worst_dy_signed": -0.1199500000000171,
"worst_line": "PAGE1"
},
"dx0": {
"mean_abs": 0.049839999999989004,
"worst": 0.049839999999989004
},
"width": {
"mean_abs_pct": 0.040248628323675685,
"worst_pct": 0.040248628323675685
},
"instances": {
"compared": 1,
"large_shift_threshold": 5.0,
"large_shift_count": 0,
"large_shifts": [],
"fine_shift_threshold": 1.0,
"fine_shift_count": 0,
"fine_shifts": []
},
"visibility": {
"mismatch_count": 0,
"mismatches": [],
"unmatched_hidden_gt": 0,
"unmatched_hidden_out": 0,
"unmatched_hidden_gt_text": [],
"unmatched_hidden_out_text": []
},
"visible_fills": {
"mismatch_count": 0,
"mismatches": []
},
"pitch": {
"pairs": 0,
"worst_delta": 0.0
},
"wraps": {
"count": 0,
"samples": []
},
"topology": {
"groups": 0,
"gt_lines": 0,
"out_lines": 0,
"samples": []
},
"reflow": {
"gt_lines": 0,
"out_lines": 0,
"samples": []
},
"rects": {
"gt_count": 7,
"out_count": 6,
"canonical_gt_count": 2,
"canonical_out_count": 5,
"matched": 1,
"unmatched_gt": 1,
"unmatched_out": 4,
"mean_center_delta": 7.04080002833507e-06,
"geometry_threshold": 1.0,
"raw_geometry_mismatch_count": 0,
"raw_geometry_mismatch_samples": [],
"coverage_equivalent_count": 0,
"coverage_unmodeled_count": 1,
"geometry_mismatch_count": 0,
"geometry_mismatch_samples": [],
"samples": [
{
"kind": "fill",
"geometry_kind": "rectangle",
"gt_indices": [
1,
4
],
"out_indices": [
0,
3
],
"gt_bbox": [
0.0,
0.0,
960.0,
539.9999859184
],
"out_bbox": [
0.0,
0.0,
960.0,
540.0
],
"dx": 0.0,
"dy": 0.0,
"dwidth": 0.0,
"dheight": 1.408160005667014e-05,
"edges": {
"left": 0.0,
"top": 0.0,
"right": 0.0,
"bottom": 1.408160005667014e-05
},
"center_dx": 0.0,
"center_dy": 7.04080002833507e-06,
"max_abs_delta": 1.408160005667014e-05,
"visible_coverage": {
"status": "unmodeled",
"mismatch_area_pt2": 0.0,
"unmodeled_area_pt2": 241379.58716479578,
"unmodeled_samples": [
[
513.0,
0.00079345706,
959.9999,
539.99999069356
]
],
"mismatch_samples": [],
"raw_edge_coverage": {
"left": "unchanged",
"right": "unchanged",
"top": "unchanged",
"bottom": "unchanged"
}
}
}
],
"x": {
"mean_abs": 0.0,
"worst": 0.0
},
"y": {
"mean_abs": 0.0,
"worst": 0.0
},
"width": {
"mean_abs": 0.0,
"worst": 0.0
},
"height": {
"mean_abs": 1.408160005667014e-05,
"worst": 1.408160005667014e-05
},
"edges": {
"left": {
"mean_abs": 0.0,
"worst": 0.0
},
"top": {
"mean_abs": 0.0,
"worst": 0.0
},
"right": {
"mean_abs": 0.0,
"worst": 0.0
},
"bottom": {
"mean_abs": 1.408160005667014e-05,
"worst": 1.408160005667014e-05
}
}
},
"noise_floor": 0.12
}
],
"gt_pages": 1,
"out_pages": 1
}
53 changes: 53 additions & 0 deletions assets/bugfixes/issue-1879/render-clusters-page-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"clusters": [],
"dpi": 150,
"duplicate_disposition_cluster_ids": [],
"errors": [],
"fuzz_percent": 5,
"minimum_area_pt2": 20.0,
"page": 1,
"passed": true,
"renderer_observations": [
{
"bbox_pt": {
"height": 7.0,
"width": 390.0,
"x": 62.0,
"y": 53.0
},
"class": "shape-edge-antialiasing",
"note": "Matched full-resolution top-rule crops show the same solid 3.5pt black rule and flat endpoints; only fractional edge coverage differs, with no material cluster."
},
{
"bbox_pt": {
"height": 4.0,
"width": 390.0,
"x": 62.0,
"y": 481.0
},
"class": "shape-edge-antialiasing",
"note": "The 1pt lower solid rule has matching position and extent; GT shades its outer raster row while output has a sharper edge. No dash or missing segment."
},
{
"bbox_pt": {
"height": 540.0,
"width": 448.0,
"x": 512.0,
"y": 0.0
},
"class": "photo-resampling",
"note": "Full-page and matched picture-detail/seam crops retain the same picture framing and colors; fine resampling texture and boundary coverage stay below the material-cluster floor."
}
],
"schema_version": 1,
"strict": true,
"summary": {
"dispositioned": 0,
"duplicate": 0,
"total": 0,
"undispositioned": 0,
"unknown": 0
},
"undispositioned_cluster_ids": [],
"unknown_disposition_cluster_ids": []
}
57 changes: 57 additions & 0 deletions crates/office2pdf/src/parser/pptx_shape_style_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,63 @@ fn test_shape_fill_from_style_fill_ref() {
);
}

#[test]
fn use_background_fill_replaces_the_shape_style_fill() {
let lower_shape = r#"<p:sp><p:nvSpPr><p:cNvPr id="2" name="Red underlay"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></p:spPr></p:sp>"#;
let use_background_shape = r#"<p:sp useBgFill="1"><p:nvSpPr><p:cNvPr id="3" name="Background sample"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:style><a:fillRef idx="1"><a:schemeClr val="accent1"/></a:fillRef></p:style></p:sp>"#;
let background_xml = r#"<p:bg><p:bgPr><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>"#;
let slide_xml = make_slide_xml_with_bg(
background_xml,
&[lower_shape.to_string(), use_background_shape.to_string()],
);
let theme_xml = make_theme_xml(&standard_theme_colors(), "Calibri", "Calibri");
let data = build_test_pptx_with_theme(SLIDE_CX, SLIDE_CY, &[slide_xml], &theme_xml);

let (doc, _warnings) = PptxParser
.parse(&data, &ConvertOptions::default())
.expect("the PPTX should parse");

let page = first_fixed_page(&doc);
assert_eq!(page.background_color, Some(Color::new(255, 255, 255)));
assert_eq!(page.elements.len(), 2);
assert_eq!(
get_shape(&page.elements[1]).fill,
Some(Color::new(255, 255, 255)),
"useBgFill should sample the slide background instead of style fillRef"
);
}

#[test]
fn issue_1879_full_slide_shape_uses_the_slide_background() {
let fixture: &[u8] = include_bytes!("../../../../tests/fixtures/pptx/1-slide.pptx");
let (doc, _warnings) = PptxParser
.parse(fixture, &ConvertOptions::default())
.expect("the issue fixture should parse");

let page = first_fixed_page(&doc);
assert_eq!(page.background_color, Some(Color::new(255, 255, 255)));
let background_shape = page
.elements
.iter()
.find_map(|element| match &element.kind {
FixedElementKind::Shape(shape)
if (element.x - 0.0).abs() < 0.01
&& (element.y - 0.0).abs() < 0.01
&& (element.width - page.size.width).abs() < 0.01
&& (element.height - page.size.height).abs() < 0.01 =>
{
Some(shape)
}
_ => None,
})
.expect("the fixture has a full-slide shape");
assert_eq!(
background_shape.fill,
Some(Color::new(255, 255, 255)),
"useBgFill should suppress the accent fillRef on the full-slide shape"
);
}

#[test]
fn test_shape_explicit_fill_overrides_fill_ref() {
// Shape with explicit solidFill AND <p:style><a:fillRef>.
Expand Down
Loading
Loading