diff --git a/assets/bugfixes/issue-418/after.jpg b/assets/bugfixes/issue-418/after.jpg new file mode 100644 index 00000000..2bdd84ed Binary files /dev/null and b/assets/bugfixes/issue-418/after.jpg differ diff --git a/assets/bugfixes/issue-418/before.jpg b/assets/bugfixes/issue-418/before.jpg new file mode 100644 index 00000000..0bd62659 Binary files /dev/null and b/assets/bugfixes/issue-418/before.jpg differ diff --git a/assets/bugfixes/issue-418/gt.jpg b/assets/bugfixes/issue-418/gt.jpg new file mode 100644 index 00000000..17ea6df8 Binary files /dev/null and b/assets/bugfixes/issue-418/gt.jpg differ diff --git a/crates/office2pdf/src/render/typst_gen_advanced_tests.rs b/crates/office2pdf/src/render/typst_gen_advanced_tests.rs index 8efeb038..d5e4520d 100644 --- a/crates/office2pdf/src/render/typst_gen_advanced_tests.rs +++ b/crates/office2pdf/src/render/typst_gen_advanced_tests.rs @@ -114,11 +114,11 @@ fn test_data_bar_fixed_row_height_codegen() { }); let doc = make_doc(vec![page]); let output = generate_typst(&doc).unwrap(); - // With a 24pt row and 5pt top/bottom insets, the bar fills the cell's - // 14pt content height instead of resolving against the page. + // A 24pt row less Excel's 2pt clearance per side gives a concrete 20pt + // bar instead of a height that resolves against the page. assert!( - output.source.contains("height: 14pt"), - "DataBar in a fixed-height row should fill the cell content height. Got: {}", + output.source.contains("height: 20pt"), + "DataBar in a fixed-height row should be inset from the row edges. Got: {}", output.source, ); assert!( @@ -1144,3 +1144,103 @@ fn test_header_without_non_repeating_rows_stays_a_single_block() { assert!(!output.source.contains("repeat: false")); assert!(!output.source.contains("level: 2")); } + +/// Excel insets data bars from the row's top and bottom edges instead of +/// filling the cell: native Excel PDFs print a 10 pt bar in a 14 pt row. +#[test] +fn test_data_bar_is_inset_from_the_row_edges() { + use crate::ir::DataBarInfo; + + let cell = TableCell { + content: vec![Block::Paragraph(Paragraph { + style: ParagraphStyle::default(), + runs: vec![Run { + text: "120".to_string(), + style: TextStyle::default(), + href: None, + footnote: None, + }], + })], + data_bar: Some(DataBarInfo { + color: Color::new(0x63, 0x8E, 0xC6), + fill_pct: 24.0, + }), + ..TableCell::default() + }; + let table = Table { + rows: vec![TableRow { + cells: vec![cell], + height: Some(14.0), + }], + column_widths: vec![100.0], + ..Table::default() + }; + let page = Page::Sheet(SheetPage { + name: "Sheet1".to_string(), + size: PageSize::default(), + margins: Margins::default(), + table, + header: None, + footer: None, + charts: vec![], + images: Vec::new(), + text_boxes: Vec::new(), + }); + let doc = make_doc(vec![page]); + let output = generate_typst(&doc).unwrap(); + assert!( + output.source.contains("height: 10pt"), + "a 14pt row must print a 10pt bar. Got: {}", + output.source, + ); +} + +/// The inset never collapses the bar in very short rows. +#[test] +fn test_data_bar_in_a_short_row_keeps_a_visible_height() { + use crate::ir::DataBarInfo; + + let cell = TableCell { + content: vec![Block::Paragraph(Paragraph { + style: ParagraphStyle::default(), + runs: vec![Run { + text: "3".to_string(), + style: TextStyle::default(), + href: None, + footnote: None, + }], + })], + data_bar: Some(DataBarInfo { + color: Color::new(0x63, 0x8E, 0xC6), + fill_pct: 50.0, + }), + ..TableCell::default() + }; + let table = Table { + rows: vec![TableRow { + cells: vec![cell], + height: Some(3.0), + }], + column_widths: vec![100.0], + ..Table::default() + }; + let page = Page::Sheet(SheetPage { + name: "Sheet1".to_string(), + size: PageSize::default(), + margins: Margins::default(), + table, + header: None, + footer: None, + charts: vec![], + images: Vec::new(), + text_boxes: Vec::new(), + }); + let doc = make_doc(vec![page]); + let output = generate_typst(&doc).unwrap(); + assert!( + !output.source.contains("height: 0pt"), + "the bar must stay visible. Got: {}", + output.source, + ); + assert!(!output.source.contains("height: -")); +} diff --git a/crates/office2pdf/src/render/typst_gen_tables.rs b/crates/office2pdf/src/render/typst_gen_tables.rs index 4155c0f1..6eb7d31c 100644 --- a/crates/office2pdf/src/render/typst_gen_tables.rs +++ b/crates/office2pdf/src/render/typst_gen_tables.rs @@ -217,6 +217,14 @@ fn generate_table_rows( Ok(()) } +/// Excel does not fill the cell with a data bar: it insets the bar from the +/// row's top and bottom edges. Native Excel PDF exports of the business corpus +/// print a 10 pt bar in every 14 pt row, which is 2 pt of clearance per side. +const DATA_BAR_VERTICAL_INSET_PT: f64 = 2.0; + +/// Floor for rows shorter than the inset, so a bar never vanishes or inverts. +const DATA_BAR_MIN_HEIGHT_PT: f64 = 1.0; + fn generate_table_cell( out: &mut String, cell: &TableCell, @@ -255,15 +263,15 @@ fn generate_table_cell( // no cell frame to resolve against and blows up to the page height, // smearing over neighboring rows (issue #362). let pct = db.fill_pct.clamp(0.0, 100.0); - let padding = cell.padding.unwrap_or(default_cell_padding); let bar_height: String = match row_height { Some(height) => { - let content_height = (height - padding.top - padding.bottom).max(1.0); - format!("{}pt", format_f64(content_height)) + let inset_height = + (height - 2.0 * DATA_BAR_VERTICAL_INSET_PT).max(DATA_BAR_MIN_HEIGHT_PT); + format!("{}pt", format_f64(inset_height)) } // Excel sizes default rows to the font's line box; 1.2em tracks - // that for single-line numeric cells. - None => "1.2em".to_string(), + // that for single-line numeric cells, less the same inset. + None => format!("1.2em - {}pt", format_f64(2.0 * DATA_BAR_VERTICAL_INSET_PT)), }; let _ = write!( out,