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
Binary file added assets/bugfixes/issue-1252/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-1252/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-1252/gt.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
157 changes: 157 additions & 0 deletions assets/bugfixes/issue-1252/layout-audit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
{
"pages": [
{
"lines": {
"gt": 10,
"out": 10,
"matched": 10,
"missing": 0,
"extra": 0,
"deviant": 0,
"missing_text": [],
"extra_text": []
},
"baseline": {
"mean_abs_dy": 0.055994999999990115,
"worst_dy": 0.07998999999998091,
"worst_dy_signed": -0.07998999999998091,
"worst_line": "Hxg20-1"
},
"dx0": {
"mean_abs": 0.0,
"worst": 0.0
},
"width": {
"mean_abs_pct": 0.0724474522534723,
"worst_pct": 0.16320951942667283
},
"instances": {
"compared": 10,
"large_shift_threshold": 5.0,
"large_shift_count": 0,
"large_shifts": [],
"fine_shift_threshold": 0.12,
"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": 9,
"worst_delta": 0.11998999999997295
},
"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": 2,
"out_count": 1,
"canonical_gt_count": 2,
"canonical_out_count": 1,
"matched": 1,
"unmatched_gt": 1,
"unmatched_out": 0,
"mean_center_delta": 3.25962899694332e-06,
"geometry_threshold": 0.12,
"geometry_mismatch_count": 0,
"geometry_mismatch_samples": [],
"samples": [
{
"kind": "fill",
"geometry_kind": "rectangle",
"gt_indices": [
1
],
"out_indices": [
0
],
"gt_bbox": [
0.0,
0.0,
200.0,
249.999993480742
],
"out_bbox": [
0.0,
0.0,
200.0,
250.0
],
"dx": 0.0,
"dy": 0.0,
"dwidth": 0.0,
"dheight": 6.51925799388664e-06,
"edges": {
"left": 0.0,
"top": 0.0,
"right": 0.0,
"bottom": 6.51925799388664e-06
},
"center_dx": 0.0,
"center_dy": 3.25962899694332e-06,
"max_abs_delta": 6.51925799388664e-06
}
],
"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": 6.51925799388664e-06,
"worst": 6.51925799388664e-06
},
"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": 6.51925799388664e-06,
"worst": 6.51925799388664e-06
}
}
},
"noise_floor": 0.12
}
],
"gt_pages": 1,
"out_pages": 1
}
22 changes: 22 additions & 0 deletions assets/bugfixes/issue-1252/render-clusters-page-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"clusters": [],
"dpi": 300,
"duplicate_disposition_cluster_ids": [],
"errors": [],
"fuzz_percent": 5,
"minimum_area_pt2": 20.0,
"page": 1,
"passed": true,
"renderer_observations": [],
"schema_version": 1,
"strict": true,
"summary": {
"dispositioned": 0,
"duplicate": 0,
"total": 0,
"undispositioned": 0,
"unknown": 0
},
"undispositioned_cluster_ids": [],
"unknown_disposition_cluster_ids": []
}
60 changes: 55 additions & 5 deletions crates/office2pdf/src/render/typst_gen_fixed_page_textbox_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3116,9 +3116,10 @@ fn powerpoint_empty_boundary_breaks_do_not_move_a_centred_title() {
/// the whole box — 12.00pt for 12.5pt over 10pt. A nine-line probe of the same
/// pair paces it at 12.96pt and its 10pt-over-12.5pt partner at 14.04pt, where
/// the whole-box reading gives 12.00 and 15.00; the two sum to `1.2 x 22.5`
/// either way, so only the boundary between them was ever in question. The
/// half-point tolerances below are what separates the two models, and the
/// 0.36pt inside them is the paragraph-wide seat share tracked in #1252.
/// either way, so only the boundary between them was ever in question. Each
/// line now rounds its own seat (#1252); the half-point tolerances below allow
/// the native export's per-baseline coordinate dither while still separating
/// the two models.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn powerpoint_hard_break_advance_clears_the_line_above_it() {
Expand Down Expand Up @@ -3184,8 +3185,8 @@ fn powerpoint_hard_break_advance_clears_the_line_above_it() {
assert_eq!(baselines.len(), 3, "expected three lines: {baselines:?}");
let (top_em, _) = crate::render::pdf::powerpoint_line_box_em(family)
.expect("the Arial-compatible line metrics must resolve");
// The paragraph's largest size decides its line box, and PowerPoint seats
// the baseline a whole number of points below its top (issue #1074).
// PowerPoint seats the baseline a whole number of points below the first
// line's top (issue #1074).
let expected_first_baseline = 72.0 + (top_em * 12.5).round();
assert!(
(baselines[0] - expected_first_baseline).abs() < 0.01,
Expand Down Expand Up @@ -4220,3 +4221,52 @@ fn a_hard_broken_slide_line_advances_by_its_own_font_size() {
}
}
}

/// A mixed-size hard-broken paragraph rounds the seat of every line at that
/// line's own size, rather than rounding once at the paragraph maximum and
/// rescaling the resulting ratio (#1252).
///
/// The native PowerPoint probe behind the issue measures the most visible
/// Arial 20/10 transition at 14.88pt down and 21.12pt back up, which is the
/// export's 0.24pt coordinate grid around this model's 15pt and 21pt. The
/// paragraph-wide split emits 14.5pt and 21.5pt instead. The other families
/// keep the regression tied to the shared line-box model rather than to one
/// lucky Arial rounding boundary.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn mixed_size_hard_breaks_round_each_lines_own_seat() {
for (family, sizes_pt) in [
("Arial", [20.0_f64, 10.0, 20.0]),
("Georgia", [20.0_f64, 10.0, 20.0]),
("Verdana", [20.0_f64, 10.0, 20.0]),
("Arial", [30.0_f64, 10.0, 30.0]),
] {
let baselines = hard_broken_slide_baselines(family, &sizes_pt, true);
let (plain_ascent_em, _) = crate::render::pdf::powerpoint_line_box_em(family)
.unwrap_or_else(|| panic!("the {family} line metrics must resolve"));
let line_box_pt = |size_pt: f64| -> (f64, f64) {
let (top_em, bottom_em) =
powerpoint_percentage_line_box_em(plain_ascent_em, size_pt, 1.0);
(top_em * size_pt, bottom_em * size_pt)
};

for ((sizes, baselines), direction) in sizes_pt
.windows(2)
.zip(baselines.windows(2))
.zip(["down", "up"])
{
let (_, preceding_bottom_pt) = line_box_pt(sizes[0]);
let (following_top_pt, _) = line_box_pt(sizes[1]);
let expected_pt = preceding_bottom_pt + following_top_pt;
let actual_pt = baselines[1] - baselines[0];
assert!(
(actual_pt - expected_pt).abs() < 0.01,
"the {family} {direction} break from {}pt to {}pt must use each \
line's independently rounded seat: expected {expected_pt}pt, \
got {actual_pt}pt from {baselines:?}",
sizes[0],
sizes[1],
);
}
}
}
22 changes: 16 additions & 6 deletions crates/office2pdf/src/render/typst_gen_table_codegen_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1618,9 +1618,8 @@ fn test_slide_table_cell_uses_the_powerpoint_line_box() {
/// every hard-broken line under 10pt at a flat 12.00pt (issue #1172). What
/// actually carries the gap is the 13pt line's descent.
///
/// The 0.37pt that remains is the paragraph-wide seat share, tracked in #1252;
/// the tolerance here is the half point that separates a preceding line's
/// descent from no descent at all.
/// Each line now rounds its own seat (#1252), which predicts 11.60pt for this
/// boundary and remains within the native export's half-point dither band.
///
/// This compiles the real table-cell path so its vertical centring and line
/// box cannot make a source-only assertion pass by accident (issue #683).
Expand Down Expand Up @@ -1702,10 +1701,21 @@ fn slide_table_hard_break_clears_the_taller_line_above_it() {
.baseline_pt;

let advance_pt: f64 = word_baseline - docx_baseline;
let (plain_ascent_em, _) = crate::render::pdf::powerpoint_line_box_em(family)
.expect("the Arial-compatible line metrics must resolve");
let line_box_pt = |size_pt: f64| -> (f64, f64) {
let (top_em, bottom_em) = powerpoint_percentage_line_box_em(plain_ascent_em, size_pt, 1.0);
(top_em * size_pt, bottom_em * size_pt)
};
let (_, docx_bottom_pt) = line_box_pt(13.0);
let (word_top_pt, _) = line_box_pt(9.5);
let expected_pt = docx_bottom_pt + word_top_pt;
assert!(
advance_pt > 1.2 * 9.5 + 0.5,
"the 13pt line's descent must push the 9.5pt line past its own 11.4pt \
box: {docx_baseline}, {word_baseline}\n{}",
(advance_pt - expected_pt).abs() < 0.01,
"the break must combine the 13pt line's independently rounded descent \
with the 9.5pt line's independently rounded seat: expected \
{expected_pt}pt, got {advance_pt}pt from {docx_baseline}, \
{word_baseline}\n{}",
output.source
);
assert!(
Expand Down
Loading
Loading