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-351/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-351/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-351/gt.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions crates/office2pdf/src/ir/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ pub struct ParagraphStyle {
pub direction: Option<TextDirection>,
/// Custom tab stop positions for this paragraph.
pub tab_stops: Option<Vec<TabStop>>,
/// Paragraph-wide shading fill (`w:pPr/w:shd`), painted behind the full
/// paragraph width like Word's code-block backgrounds.
pub background: Option<Color>,
}

/// A custom tab stop definition.
Expand Down Expand Up @@ -216,6 +219,9 @@ impl ParagraphStyle {
if other.tab_stops.is_some() {
self.tab_stops = other.tab_stops.clone();
}
if other.background.is_some() {
self.background = other.background;
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/office2pdf/src/ir/style_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ fn paragraph_style_merge_from_all_none_source_preserves_target() {
alignment: TabAlignment::Left,
leader: TabLeader::None,
}]),
background: Some(Color::new(0xEE, 0xEE, 0xEE)),
};
let original: ParagraphStyle = target.clone();
let source = ParagraphStyle::default();
Expand Down Expand Up @@ -228,6 +229,7 @@ fn paragraph_style_merge_from_all_some_source_overwrites_target() {
space_after: Some(16.0),
heading_level: Some(1),
direction: Some(TextDirection::Rtl),
background: Some(Color::new(0xF4, 0xF4, 0xF4)),
tab_stops: Some(vec![TabStop {
position: 144.0,
alignment: TabAlignment::Right,
Expand Down
18 changes: 18 additions & 0 deletions crates/office2pdf/src/parser/docx_style_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,21 @@ fn test_doc_default_theme_font_resolves_via_theme() {
let no_theme = serde_json::json!({ "fonts": { "ascii": "Arial" } });
assert_eq!(resolve_theme_font_family(&no_theme, &theme), None);
}

#[test]
fn test_paragraph_shading_extracted_as_background() {
// Word paints w:pPr/w:shd behind the whole paragraph (code blocks in
// the CLI-manual fixture); the fill must reach the IR (issue #351).
let mut shaded = docx_rs::Paragraph::new()
.add_run(docx_rs::Run::new().add_text("$ cargo install office2pdf-cli"));
shaded.property = shaded
.property
.shading(docx_rs::Shading::new().fill("F4F4F4"));
let data = build_docx_bytes(vec![shaded]);

let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let para = first_paragraph(&doc);

assert_eq!(para.style.background, Some(Color::new(0xF4, 0xF4, 0xF4)));
}
3 changes: 3 additions & 0 deletions crates/office2pdf/src/parser/docx_styles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ pub(super) fn merge_paragraph_style(
explicit_tab_overrides,
inherited_tab_stops.as_deref(),
),
background: explicit
.background
.or(style_paragraph.and_then(|style| style.background)),
}
}

Expand Down
9 changes: 9 additions & 0 deletions crates/office2pdf/src/parser/docx_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub(super) fn extract_paragraph_style(prop: &docx_rs::ParagraphProperty) -> Para
let (indent_left, indent_right, indent_first_line) = extract_indent(&prop.indent);
let (line_spacing, space_before, space_after) = extract_line_spacing(&prop.line_spacing);
let tab_stops = extract_tab_stops(&prop.tabs);
let background = extract_paragraph_shading(&prop.shading);

ParagraphStyle {
alignment,
Expand All @@ -34,9 +35,17 @@ pub(super) fn extract_paragraph_style(prop: &docx_rs::ParagraphProperty) -> Para
heading_level: None,
direction: None,
tab_stops,
background,
}
}

/// Word paints `w:pPr/w:shd` behind the whole paragraph. Only the fill color
/// participates in print output; "auto" means no shading.
fn extract_paragraph_shading(shading: &Option<docx_rs::Shading>) -> Option<Color> {
let shading = shading.as_ref()?;
xml_util::parse_hex_color(&shading.fill)
}

fn extract_indent(indent: &Option<docx_rs::Indent>) -> (Option<f64>, Option<f64>, Option<f64>) {
let Some(indent) = indent else {
return (None, None, None);
Expand Down
27 changes: 27 additions & 0 deletions crates/office2pdf/src/render/typst_gen_paragraph_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,3 +703,30 @@ fn test_no_document_grid_keeps_default_line_height() {
let result = generate_typst(&doc).unwrap().source;
assert!(!result.contains("top-edge"), "no grid: {result}");
}

#[test]
fn test_generate_paragraph_with_background_shading() {
// w:pPr/w:shd paints the whole paragraph; the block wrapper must carry
// the fill so the shading spans the full line width (issue #351).
let doc = make_doc(vec![make_flow_page(vec![Block::Paragraph(Paragraph {
style: ParagraphStyle {
background: Some(Color::new(0xF4, 0xF4, 0xF4)),
..ParagraphStyle::default()
},
runs: vec![Run {
text: "$ cargo install office2pdf-cli".to_string(),
style: TextStyle::default(),
href: None,
footnote: None,
}],
})])]);
let result = generate_typst(&doc).unwrap().source;
assert!(
result.contains("fill: rgb(244, 244, 244)"),
"paragraph shading must fill the block wrapper: {result}"
);
assert!(
result.contains("#block(width: 100%"),
"shaded paragraphs need the full-width block wrapper: {result}"
);
}
9 changes: 9 additions & 0 deletions crates/office2pdf/src/render/typst_gen_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub(super) fn generate_paragraph(
pub(super) fn needs_block_wrapper(style: &ParagraphStyle) -> bool {
style.space_before.is_some()
|| style.space_after.is_some()
|| style.background.is_some()
|| style.line_spacing.is_some()
|| style.line_box.is_some()
|| matches!(style.alignment, Some(Alignment::Justify))
Expand Down Expand Up @@ -145,6 +146,14 @@ fn write_block_params_continuation(out: &mut String, style: &ParagraphStyle) {
if let Some(below) = style.space_after {
let _ = write!(out, ", below: {}pt", format_f64(below));
}
if let Some(background) = style.background {
// Word paints w:pPr/w:shd across the full paragraph width.
let _ = write!(
out,
", fill: rgb({}, {}, {})",
background.r, background.g, background.b
);
}
}

pub(super) fn write_par_settings(out: &mut String, style: &ParagraphStyle) {
Expand Down
Loading