From a8f96c8dd030f02c23abc44af58d6aa53f6b7e07 Mon Sep 17 00:00:00 2001 From: Yonghye Kwon Date: Sun, 26 Jul 2026 06:29:17 +0900 Subject: [PATCH] fix(xlsx): overlay same-row worksheet drawings Excel lays shapes anchored to the same row out side by side over the grid. office2pdf reserved a full-width flow box per shape, so each one advanced the flow before the next: three shapes on row 3 formed a diagonal stack at 150 DPI tops of 246, 486, and 726 px against Excel's 188, 188, 188, and the tripled reservation spilled the sheet onto a blank second page. Emit one reserved box per anchor row, as tall as the tallest shape on it, and place each shape at its own horizontal offset inside it. Charts stay ordinary flow content. On the regression fixture the three shapes now share a top of 246 px and the workbook exports to one page. The 5% fuzz pixel difference against the native Excel export falls from 290,500 to 121,943. The remaining offset from Excel's 188 px, and the undersized shape bounds, are the anchor metrics tracked in #460. `generate_sheet_anchor` is gone: its text-box and image arms were dead once every placed drawing routed through the row emitter, and charts call `generate_chart` directly. Fixes #459 Signed-off-by: Yonghye Kwon --- assets/bugfixes/issue-459/after.jpg | Bin 0 -> 27102 bytes assets/bugfixes/issue-459/before.jpg | Bin 0 -> 27211 bytes assets/bugfixes/issue-459/gt.jpg | Bin 0 -> 27447 bytes crates/office2pdf/src/render/typst_gen.rs | 100 +++++++++++++---- .../src/render/typst_gen_advanced_tests.rs | 104 ++++++++++++++++++ 5 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 assets/bugfixes/issue-459/after.jpg create mode 100644 assets/bugfixes/issue-459/before.jpg create mode 100644 assets/bugfixes/issue-459/gt.jpg diff --git a/assets/bugfixes/issue-459/after.jpg b/assets/bugfixes/issue-459/after.jpg new file mode 100644 index 0000000000000000000000000000000000000000..55ace5f6c776057845b30a909fc224069627d09c GIT binary patch literal 27102 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 literal 0 HcmV?d00001 diff --git a/assets/bugfixes/issue-459/before.jpg b/assets/bugfixes/issue-459/before.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44f7f775972b75c050ef91340d5bfe8f37624b94 GIT binary patch literal 27211 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 literal 0 HcmV?d00001 diff --git a/crates/office2pdf/src/render/typst_gen.rs b/crates/office2pdf/src/render/typst_gen.rs index cf0e07ae..43cb23ac 100644 --- a/crates/office2pdf/src/render/typst_gen.rs +++ b/crates/office2pdf/src/render/typst_gen.rs @@ -569,10 +569,19 @@ fn generate_table_with_anchors( out.push('\n'); row_start = row_end + 1; } - // Emit the anchored element - generate_sheet_anchor(out, &sorted_charts[chart_idx].1, ctx); - out.push('\n'); - chart_idx += 1; + // Emit every drawing anchored to this row together, so same-row + // shapes overlay rather than stack (issue #459). + let row_end_idx: usize = sorted_charts[chart_idx..] + .iter() + .take_while(|(row, _)| *row == sorted_charts[chart_idx].0) + .count() + + chart_idx; + let row_anchors: Vec<&SheetAnchor> = sorted_charts[chart_idx..row_end_idx] + .iter() + .map(|(_, anchor)| anchor) + .collect(); + generate_sheet_anchor_row(out, &row_anchors, ctx); + chart_idx = row_end_idx; } } @@ -596,24 +605,81 @@ fn generate_table_with_anchors( out.push('\n'); } - // Emit any remaining anchors (anchored beyond last row, e.g., u32::MAX) + // Emit any remaining anchors (anchored beyond last row, e.g., u32::MAX), + // still grouped by the row they share. while chart_idx < sorted_charts.len() { - generate_sheet_anchor(out, &sorted_charts[chart_idx].1, ctx); - out.push('\n'); - chart_idx += 1; + let row_end_idx: usize = sorted_charts[chart_idx..] + .iter() + .take_while(|(row, _)| *row == sorted_charts[chart_idx].0) + .count() + + chart_idx; + let row_anchors: Vec<&SheetAnchor> = sorted_charts[chart_idx..row_end_idx] + .iter() + .map(|(_, anchor)| anchor) + .collect(); + generate_sheet_anchor_row(out, &row_anchors, ctx); + chart_idx = row_end_idx; } Ok(()) } -fn generate_sheet_anchor(out: &mut String, anchor: &SheetAnchor, ctx: &mut GenCtx) { +/// Emit every drawing anchored to one worksheet row. +/// +/// Excel lays same-row shapes out side by side over the grid. Reserving a +/// flow box per shape advanced the flow between them, so three shapes on row +/// 3 formed a diagonal stack and reserved three times the height, spilling +/// onto a blank second page (issue #459). One reserved box, as tall as the +/// tallest shape, holds them all; each keeps its own horizontal offset. +/// +/// Charts are ordinary flow content and keep their own placement. +fn generate_sheet_anchor_row(out: &mut String, anchors: &[&SheetAnchor], ctx: &mut GenCtx) { + for anchor in anchors.iter() { + if let SheetAnchor::Chart(chart) = anchor { + generate_chart(out, chart); + out.push('\n'); + } + } + + let placed: Vec<&&SheetAnchor> = anchors + .iter() + .filter(|anchor| !matches!(anchor, SheetAnchor::Chart(_))) + .collect(); + if placed.is_empty() { + return; + } + let reserved_height_pt: f64 = placed + .iter() + .map(|anchor| sheet_anchor_height_pt(anchor)) + .fold(0.0, f64::max); + let _ = write!( + out, + "#box(width: 100%, height: {}pt)[", + format_f64(reserved_height_pt) + ); + for anchor in placed { + write_placed_sheet_anchor(out, anchor, ctx); + } + out.push_str("]\n"); +} + +/// The flow height a placed drawing occupies. +fn sheet_anchor_height_pt(anchor: &SheetAnchor) -> f64 { match anchor { - SheetAnchor::Chart(chart) => generate_chart(out, chart), + SheetAnchor::TextBox(text_box) => text_box.height, + SheetAnchor::Image(sheet_image) => sheet_image.image.height.unwrap_or(100.0), + SheetAnchor::Chart(_) => 0.0, + } +} + +/// Place one drawing at its horizontal offset inside the row's reserved box. +fn write_placed_sheet_anchor(out: &mut String, anchor: &SheetAnchor, ctx: &mut GenCtx) { + match anchor { + SheetAnchor::Chart(_) => {} SheetAnchor::TextBox(text_box) => { let _ = write!( out, - "#box(width: 100%, height: {}pt)[#place(top + left, dx: {}pt)[#box(width: {}pt, height: {}pt", - format_f64(text_box.height), + "#place(top + left, dx: {}pt)[#box(width: {}pt, height: {}pt", format_f64(text_box.x_offset_pt), format_f64(text_box.width), format_f64(text_box.height), @@ -639,20 +705,16 @@ fn generate_sheet_anchor(out: &mut String, anchor: &SheetAnchor, ctx: &mut GenCt if text_box.vertical_center { out.push(']'); } - out.push_str("]]]\n"); + out.push_str("]]"); } SheetAnchor::Image(sheet_image) => { - // Keep the anchor's horizontal position: reserve the image height - // in the flow and place the image at its column offset. - let height: f64 = sheet_image.image.height.unwrap_or(100.0); let _ = write!( out, - "#box(width: 100%, height: {}pt)[#place(top + left, dx: {}pt)[", - format_f64(height), + "#place(top + left, dx: {}pt)[", format_f64(sheet_image.x_offset_pt), ); generate_image(out, &sheet_image.image, ctx); - out.push_str("]]\n"); + out.push(']'); } } } diff --git a/crates/office2pdf/src/render/typst_gen_advanced_tests.rs b/crates/office2pdf/src/render/typst_gen_advanced_tests.rs index f28d31f9..34fe7273 100644 --- a/crates/office2pdf/src/render/typst_gen_advanced_tests.rs +++ b/crates/office2pdf/src/render/typst_gen_advanced_tests.rs @@ -1339,3 +1339,107 @@ fn test_non_arrow_icon_set_still_renders_as_text() { assert!(output.source.contains("text(fill: rgb(214, 85, 50)")); assert!(!output.source.contains("polygon(")); } + +/// A worksheet text box anchored after `anchor_row` at `x_offset_pt`. +fn make_sheet_text_box(anchor_row: u32, x_offset_pt: f64, height: f64) -> crate::ir::SheetTextBox { + crate::ir::SheetTextBox { + anchor_row, + x_offset_pt, + width: 100.0, + height, + paragraphs: vec![Paragraph { + style: ParagraphStyle::default(), + runs: vec![Run { + text: format!("shape at {x_offset_pt}"), + style: TextStyle::default(), + href: None, + footnote: None, + }], + }], + fill: None, + border: None, + vertical_center: false, + } +} + +fn sheet_page_with_text_boxes(text_boxes: Vec) -> Page { + Page::Sheet(SheetPage { + name: "Sheet1".to_string(), + size: PageSize::default(), + margins: Margins::default(), + table: Table { + rows: vec![TableRow { + cells: vec![TableCell::default()], + height: None, + }], + column_widths: vec![100.0], + ..Table::default() + }, + header: None, + footer: None, + charts: vec![], + images: Vec::new(), + text_boxes, + }) +} + +#[test] +fn test_same_row_sheet_drawings_share_one_reserved_box() { + // Excel draws shapes anchored to the same row side by side. Emitting one + // reserved flow box each stacked them diagonally and reserved three + // times the height, which spilled onto a blank second page (issue #459). + let doc = make_doc(vec![sheet_page_with_text_boxes(vec![ + make_sheet_text_box(1, 0.0, 60.0), + make_sheet_text_box(1, 200.0, 60.0), + make_sheet_text_box(1, 400.0, 60.0), + ])]); + let source = generate_typst(&doc).unwrap().source; + + assert_eq!( + source.matches("#box(width: 100%, height: 60pt)").count(), + 1, + "same-row drawings share one reserved box: {source}" + ); + for dx in ["dx: 0pt", "dx: 200pt", "dx: 400pt"] { + assert!( + source.contains(dx), + "each drawing keeps its own horizontal offset ({dx}): {source}" + ); + } +} + +#[test] +fn test_same_row_sheet_drawings_reserve_the_tallest_height() { + // The row's reserved height is the tallest shape on it, not the sum. + let doc = make_doc(vec![sheet_page_with_text_boxes(vec![ + make_sheet_text_box(1, 0.0, 40.0), + make_sheet_text_box(1, 200.0, 90.0), + ])]); + let source = generate_typst(&doc).unwrap().source; + + assert_eq!( + source.matches("#box(width: 100%, height: ").count(), + 1, + "the row reserves a single box: {source}" + ); + assert!( + source.contains("#box(width: 100%, height: 90pt)"), + "the reserved height is the tallest shape, not the sum: {source}" + ); +} + +#[test] +fn test_drawings_on_different_rows_keep_separate_boxes() { + // Triangulation: shapes on different rows still advance the flow. + let doc = make_doc(vec![sheet_page_with_text_boxes(vec![ + make_sheet_text_box(1, 0.0, 60.0), + make_sheet_text_box(5, 0.0, 60.0), + ])]); + let source = generate_typst(&doc).unwrap().source; + + assert_eq!( + source.matches("#box(width: 100%, height: 60pt)").count(), + 2, + "different rows reserve their own boxes: {source}" + ); +}