Skip to content
Binary file added assets/bugfixes/issue-187/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-187/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-187/gt.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-333/compare.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-334/compare.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-335/compare.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-336/compare.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 7 additions & 4 deletions crates/office2pdf/src/parser/docx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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![])],
}));
Expand Down Expand Up @@ -402,13 +403,14 @@ fn convert_sdt_children(
hyperlinks: &HyperlinkMap,
style_map: &StyleMap,
ctx: &DocxConversionContext,
styles: &docx_rs::Styles,
) -> Vec<TaggedElement> {
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) => {
Expand All @@ -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,
));
}
_ => {}
Expand All @@ -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();
Expand Down
65 changes: 56 additions & 9 deletions crates/office2pdf/src/parser/docx_lists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NumInfo> {
if !para.has_numbering {
return None;
///
/// Falls back to the style definition when the paragraph carries no inline
/// `<w:numPr>` — 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<NumInfo> {
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
Expand Down Expand Up @@ -291,10 +317,11 @@ fn finalize_list(numbered_items: Vec<NumberedItem>, 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<TaggedElement>,
numberings: &NumberingMap,
Expand All @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions crates/office2pdf/src/parser/docx_tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ fn extract_cell_padding(
inherited_padding: Option<Insets>,
) -> Option<Insets> {
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) {
Expand Down Expand Up @@ -583,7 +582,7 @@ fn extract_cell_shading(shading_json: &serde_json::Value) -> Option<Color> {
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)
Expand Down
2 changes: 0 additions & 2 deletions crates/office2pdf/tests/docx_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
Loading