diff --git a/assets/bugfixes/issue-187/after.jpg b/assets/bugfixes/issue-187/after.jpg new file mode 100644 index 00000000..00b6eda7 Binary files /dev/null and b/assets/bugfixes/issue-187/after.jpg differ diff --git a/assets/bugfixes/issue-187/before.jpg b/assets/bugfixes/issue-187/before.jpg new file mode 100644 index 00000000..74891f7d Binary files /dev/null and b/assets/bugfixes/issue-187/before.jpg differ diff --git a/assets/bugfixes/issue-187/gt.jpg b/assets/bugfixes/issue-187/gt.jpg new file mode 100644 index 00000000..f0554b9e Binary files /dev/null and b/assets/bugfixes/issue-187/gt.jpg differ diff --git a/assets/bugfixes/issue-333/compare.jpg b/assets/bugfixes/issue-333/compare.jpg new file mode 100644 index 00000000..26953beb Binary files /dev/null and b/assets/bugfixes/issue-333/compare.jpg differ diff --git a/assets/bugfixes/issue-334/compare.jpg b/assets/bugfixes/issue-334/compare.jpg new file mode 100644 index 00000000..26953beb Binary files /dev/null and b/assets/bugfixes/issue-334/compare.jpg differ diff --git a/assets/bugfixes/issue-335/compare.jpg b/assets/bugfixes/issue-335/compare.jpg new file mode 100644 index 00000000..26953beb Binary files /dev/null and b/assets/bugfixes/issue-335/compare.jpg differ diff --git a/assets/bugfixes/issue-336/compare.jpg b/assets/bugfixes/issue-336/compare.jpg new file mode 100644 index 00000000..26953beb Binary files /dev/null and b/assets/bugfixes/issue-336/compare.jpg differ diff --git a/crates/office2pdf/src/parser/docx.rs b/crates/office2pdf/src/parser/docx.rs index 104660a5..ef8d7074 100644 --- a/crates/office2pdf/src/parser/docx.rs +++ b/crates/office2pdf/src/parser/docx.rs @@ -302,6 +302,7 @@ impl Parser for DocxParser { &hyperlinks, &style_map, &ctx, + &docx.styles, )]; // Inject math equations for this body child let eqs = math.take(idx); @@ -326,7 +327,7 @@ impl Parser for DocxParser { ))])] } docx_rs::DocumentChild::StructuredDataTag(sdt) => { - convert_sdt_children(sdt, &images, &hyperlinks, &style_map, &ctx) + convert_sdt_children(sdt, &images, &hyperlinks, &style_map, &ctx, &docx.styles) } _ => vec![TaggedElement::Plain(vec![])], })); @@ -402,13 +403,14 @@ fn convert_sdt_children( hyperlinks: &HyperlinkMap, style_map: &StyleMap, ctx: &DocxConversionContext, + styles: &docx_rs::Styles, ) -> Vec { let mut result = Vec::new(); for child in &sdt.children { match child { docx_rs::StructuredDataTagChild::Paragraph(para) => { result.push(convert_paragraph_element( - para, images, hyperlinks, style_map, ctx, + para, images, hyperlinks, style_map, ctx, styles, )); } docx_rs::StructuredDataTagChild::Table(table) => { @@ -418,7 +420,7 @@ fn convert_sdt_children( } docx_rs::StructuredDataTagChild::StructuredDataTag(nested) => { result.extend(convert_sdt_children( - nested, images, hyperlinks, style_map, ctx, + nested, images, hyperlinks, style_map, ctx, styles, )); } _ => {} @@ -435,8 +437,9 @@ fn convert_paragraph_element( hyperlinks: &HyperlinkMap, style_map: &StyleMap, ctx: &DocxConversionContext, + styles: &docx_rs::Styles, ) -> TaggedElement { - let num_info = extract_num_info(para); + let num_info = extract_num_info(para, styles); // Build the paragraph IR let mut blocks = Vec::new(); diff --git a/crates/office2pdf/src/parser/docx_lists.rs b/crates/office2pdf/src/parser/docx_lists.rs index f9c441b8..58007213 100644 --- a/crates/office2pdf/src/parser/docx_lists.rs +++ b/crates/office2pdf/src/parser/docx_lists.rs @@ -217,11 +217,37 @@ pub(super) fn build_numbering_map(numberings: &docx_rs::Numberings) -> Numbering } /// Extract numbering info from a paragraph, if it has numPr. -pub(super) fn extract_num_info(para: &docx_rs::Paragraph) -> Option { - if !para.has_numbering { - return None; +/// +/// Falls back to the style definition when the paragraph carries no inline +/// `` — this handles Word built-in list styles such as "List Bullet" +/// and "List Number" whose numbering is defined on the style, not the paragraph. +pub(super) fn extract_num_info( + para: &docx_rs::Paragraph, + styles: &docx_rs::Styles, +) -> Option { + if para.has_numbering { + let numbering_property = para.property.numbering_property.as_ref()?; + let num_id = numbering_property.id.as_ref()?.id; + let level = numbering_property + .level + .as_ref() + .map_or(0, |level| level.val as u32); + if num_id == 0 { + return None; + } + return Some(NumInfo { num_id, level }); } - let numbering_property = para.property.numbering_property.as_ref()?; + + let style_id = para + .property + .style + .as_ref() + .map(|style| style.val.as_str())?; + let style = styles + .styles + .iter() + .find(|style| style.style_id == style_id)?; + let numbering_property = style.paragraph_property.numbering_property.as_ref()?; let num_id = numbering_property.id.as_ref()?.id; let level = numbering_property .level @@ -291,10 +317,11 @@ fn finalize_list(numbered_items: Vec, numberings: &NumberingMap) - } } -/// Group consecutive list paragraphs into List blocks. Adjacent list paragraphs -/// are merged into a single list even when their `numId` differs, so ordered -/// numbering continues and `ilvl` nesting is preserved (issue #176). Any -/// non-list element ends the current list. +/// Group consecutive list paragraphs into List blocks. Compatible adjacent list +/// paragraphs are merged even when their `numId` differs, so ordered numbering +/// continues and `ilvl` nesting is preserved (issue #176). A top-level change +/// between ordered and unordered numbering starts a new list, as does any +/// non-list element. pub(super) fn group_into_lists( elements: Vec, numberings: &NumberingMap, @@ -318,10 +345,30 @@ pub(super) fn group_into_lists( .is_some_and(|previous| *previous != info.num_id); let has_explicit_restart = resolved_level .is_some_and(|level| level.has_start_override && is_num_id_change); - let is_first_in_block = current_list.is_empty(); let changes_series = current_list .last() .is_some_and(|numbered| numbered.series != series); + let current_root = current_list + .iter() + .filter_map(|numbered| { + numberings + .get(&numbered.num_id) + .and_then(|numbering| numbering.levels.get(&numbered.item.level)) + .map(|level| (numbered.item.level, level.style.kind)) + }) + .min_by_key(|(level, _)| *level); + let starts_new_list = changes_series + && current_root.is_some_and(|(root_level, root_kind)| { + info.level <= root_level + && resolved_level.is_some_and(|level| level.style.kind != root_kind) + }); + if starts_new_list { + result.push(Block::List(finalize_list( + std::mem::take(&mut current_list), + numberings, + ))); + } + let is_first_in_block = current_list.is_empty(); let mut item = ListItem { content: vec![paragraph], level: info.level, diff --git a/crates/office2pdf/src/parser/docx_tables.rs b/crates/office2pdf/src/parser/docx_tables.rs index 36568070..603db265 100644 --- a/crates/office2pdf/src/parser/docx_tables.rs +++ b/crates/office2pdf/src/parser/docx_tables.rs @@ -84,7 +84,6 @@ fn extract_cell_padding( inherited_padding: Option, ) -> Option { let margins_json = prop_json.and_then(|j| j.get("margins"))?; - extract_insets_from_margins_json(margins_json)?; let mut merged_padding = inherited_padding.unwrap_or_default(); if let Some(top) = margins_json.get("top").and_then(extract_margin_side_points) { @@ -583,7 +582,7 @@ fn extract_cell_shading(shading_json: &serde_json::Value) -> Option { return None; } let fill = shading_json.get("fill").and_then(|v| v.as_str())?; - if fill == "auto" || fill == "FFFFFF" || fill.is_empty() { + if fill == "auto" || fill.is_empty() { return None; } parse_hex_color(fill) diff --git a/crates/office2pdf/tests/docx_fixtures.rs b/crates/office2pdf/tests/docx_fixtures.rs index 8f294e28..af82bc20 100644 --- a/crates/office2pdf/tests/docx_fixtures.rs +++ b/crates/office2pdf/tests/docx_fixtures.rs @@ -209,7 +209,6 @@ fn structure_pr_187_contributor_acceptance_supported_behavior() { } #[test] -#[ignore = "pending PR #187 adaptation: retain explicit white cell shading"] fn acceptance_pr_187_contributor_acceptance_explicit_white_shading() { let pages = flow_pages(PR_187_FIXTURE); let table = pr_187_table(&pages); @@ -221,7 +220,6 @@ fn acceptance_pr_187_contributor_acceptance_explicit_white_shading() { } #[test] -#[ignore = "pending PR #187 adaptation: resolve paragraph numbering from its style"] fn acceptance_pr_187_contributor_acceptance_style_inherited_numbering() { let pages = flow_pages(PR_187_FIXTURE); let lists = pages