Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/max-lines-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ docling/backend/html_backend.py
docling/backend/msexcel_backend.py
docling/backend/mspowerpoint_backend.py
docling/backend/msword_backend.py
docling/backend/xml/jats_backend.py
docling/backend/xml/uspto_backend.py
docling/backend/opendocument_backend.py
docling/datamodel/pipeline_options.py
Expand All @@ -29,4 +30,5 @@ tests/test_service_client_sdk_unit.py
tests/test_backend_opendocument.py
tests/test_backend_html.py
tests/test_backend_msword.py
tests/test_backend_jats.py
tests/test_cli.py
55 changes: 47 additions & 8 deletions docling/backend/msword_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
DocItemLabel,
DoclingDocument,
DocumentOrigin,
GroupItem,
GroupLabel,
ImageRef,
ListGroup,
Expand Down Expand Up @@ -2669,9 +2670,14 @@ def _create_or_reuse_parent(
prev_parent: NodeItem | None,
paragraph_elements: list,
) -> NodeItem | None:
has_script = any(
fmt is not None
and getattr(fmt, "script", Script.BASELINE) != Script.BASELINE
for _, fmt, _ in paragraph_elements
)
return (
doc.add_inline_group(parent=prev_parent, content_layer=self.content_layer)
if len(paragraph_elements) > 1
if (len(paragraph_elements) > 1 or has_script)
else prev_parent
)

Expand Down Expand Up @@ -3452,13 +3458,45 @@ def _group_cell_elements(
parent=docling_table,
content_layer=content_layer,
)
seen_parents: set[str] = set()
for prov in provs_in_cell:
group_element.children.append(prov)
pr_item = prov.resolve(doc)
item_parent = pr_item.parent.resolve(doc)
if pr_item.get_ref() in item_parent.children:
item_parent.children.remove(pr_item.get_ref())
pr_item.parent = group_element.get_ref()
item_parent = pr_item.parent.resolve(doc) if pr_item.parent else None
if (
item_parent is not None
and isinstance(item_parent, GroupItem)
and item_parent.label == GroupLabel.INLINE
):
if item_parent.self_ref not in seen_parents:
seen_parents.add(item_parent.self_ref)
old_grandparent = (
item_parent.parent.resolve(doc) if item_parent.parent else None
)
if (
old_grandparent
and item_parent.get_ref() in old_grandparent.children
):
old_grandparent.children.remove(item_parent.get_ref())
item_parent.parent = group_element.get_ref()
group_element.children.append(item_parent.get_ref())
else:
if (
isinstance(pr_item, TextItem)
and pr_item.formatting
and pr_item.formatting.script in (Script.SUB, Script.SUPER)
):
if item_parent and pr_item.get_ref() in item_parent.children:
item_parent.children.remove(pr_item.get_ref())
inline_group = doc.add_inline_group(
parent=group_element, content_layer=content_layer
)
pr_item.parent = inline_group.get_ref()
inline_group.children.append(pr_item.get_ref())
else:
if item_parent and pr_item.get_ref() in item_parent.children:
item_parent.children.remove(pr_item.get_ref())
pr_item.parent = group_element.get_ref()
group_element.children.append(prov)
ref_for_rich_cell = group_element.get_ref()
return ref_for_rich_cell

Expand Down Expand Up @@ -3689,10 +3727,11 @@ def _is_rich_table_cell(self, cell: _Cell) -> bool:
# paragraph must contain runs with no run-properties
for para in paragraphs:
runs = list(para.iterchildren(f"{_W_NS_CLARK}r"))
para_obj = Paragraph(para, self.docx_obj)
for rn in runs:
item: Run = Run(rn, self.docx_obj)
item: Run = Run(rn, para_obj)
if item is not None:
fm = MsWordDocumentBackend._get_format_from_run(item)
fm = MsWordDocumentBackend._get_format_from_run(item, para_obj)
if fm != Formatting():
return True

Expand Down
2 changes: 1 addition & 1 deletion docling/backend/opendocument_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,7 @@ def _odf_cell_has_rich_content(cell: Any) -> bool:
elif isinstance(child, Paragraph):
if _clean_odf_text_lines(child.text_recursive):
non_empty_paragraphs += 1
if child.get_images():
if child.get_images() or child.get_elements("text:span | text:a"):
return True
elif isinstance(child, OdfTable):
if _odf_table_has_content(child):
Expand Down
130 changes: 108 additions & 22 deletions docling/backend/xml/jats_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
GroupLabel,
ImageRef,
NodeItem,
RichTableCell,
TableCell,
TableData,
TableItem,
TextItem,
)
from docling_core.types.doc.document import Formatting, Script
Expand Down Expand Up @@ -98,8 +100,12 @@
_JATS_FORMAT_TAG_MAP: Final[dict[str, dict[str, bool | Script]]] = {
"bold": {"bold": True},
"italic": {"italic": True},
"b": {"bold": True},
"i": {"italic": True},
"underline": {"underline": True},
"u": {"underline": True},
"strike": {"strikethrough": True},
"s": {"strikethrough": True},
"sub": {"script": Script.SUB},
"sup": {"script": Script.SUPER},
}
Expand Down Expand Up @@ -239,12 +245,13 @@ def __init__(
):
self.valid = True
return
for ent in doc_info.internalDTD.iterentities():
if ent.system_url and any(
kwd in ent.system_url for kwd in JATS_DTD_URL
):
self.valid = True
return
if doc_info.internalDTD is not None:
for ent in doc_info.internalDTD.iterentities():
if ent.system_url and any(
kwd in ent.system_url for kwd in JATS_DTD_URL
):
self.valid = True
return
except Exception as exc:
raise DocumentLoadError(
f"Could not initialize JATS backend for file with hash {self.document_hash}."
Expand Down Expand Up @@ -900,7 +907,16 @@ def _emit_inline(
segments = JatsDocumentBackend._strip_segments(segments)
if not segments:
return
container = doc.add_inline_group(parent=parent) if len(segments) > 1 else parent
has_script = any(
s.formatting is not None
and getattr(s.formatting, "script", Script.BASELINE) != Script.BASELINE
for s in segments
)
container = (
doc.add_inline_group(parent=parent)
if (len(segments) > 1 or has_script)
else parent
)
for segment in segments:
doc.add_text(
label=segment.label,
Expand Down Expand Up @@ -1031,8 +1047,43 @@ def _add_metadata(
return

@staticmethod
def parse_table_data(element: Tag) -> TableData | None:
# TODO, see how to implement proper support for rich tables from HTML backend
def _walk_soup_cell(
cell: Tag,
formatting: Formatting | None = None,
hyperlink: AnyUrl | Path | None = None,
) -> list[InlineSegment]:
from bs4 import NavigableString

segments: list[InlineSegment] = []
for child in cell.children:
if isinstance(child, NavigableString):
text = str(child)
if text:
JatsDocumentBackend._append_run(
segments, text, formatting, hyperlink
)
elif isinstance(child, Tag):
current_fmt = JatsDocumentBackend._merge_formatting(
formatting, child.name
)
current_href = hyperlink
if child.name in ("ext-link", "a"):
href = child.get("xlink:href") or child.get("href")
if href:
current_href = JatsDocumentBackend._parse_ext_link_href(href)
segments.extend(
JatsDocumentBackend._walk_soup_cell(
child, current_fmt, current_href
)
)
return segments

@staticmethod
def parse_table_data(
element: Tag,
doc: DoclingDocument | None = None,
table_item: TableItem | None = None,
) -> TableData | None:
nested_tables = element.find("table")
if nested_tables is not None:
_log.debug("Skipping nested table.")
Expand Down Expand Up @@ -1124,17 +1175,44 @@ def parse_table_data(element: Tag) -> TableData | None:
if row_idx + r < num_rows and col_idx + c < num_cols:
grid[row_idx + r][col_idx + c] = text

table_cell = TableCell(
text=text,
row_span=row_span,
col_span=col_span,
start_row_offset_idx=start_row_span + row_idx,
end_row_offset_idx=start_row_span + row_idx + row_span,
start_col_offset_idx=col_idx,
end_col_offset_idx=col_idx + col_span,
column_header=col_header,
row_header=((not col_header) and html_cell.name == "th"),
segments = JatsDocumentBackend._walk_soup_cell(html_cell)
has_rich = any(
s.formatting is not None
and getattr(s.formatting, "script", Script.BASELINE)
!= Script.BASELINE
for s in segments
)
if has_rich and doc is not None and table_item is not None:
group = doc.add_group(
label=GroupLabel.UNSPECIFIED,
name=f"rich_cell_group_{len(doc.tables)}_{col_idx}_{start_row_span + row_idx}",
parent=table_item,
)
JatsDocumentBackend._emit_inline(doc, group, segments)
table_cell = RichTableCell(
text=text,
row_span=row_span,
col_span=col_span,
start_row_offset_idx=start_row_span + row_idx,
end_row_offset_idx=start_row_span + row_idx + row_span,
start_col_offset_idx=col_idx,
end_col_offset_idx=col_idx + col_span,
column_header=col_header,
row_header=((not col_header) and html_cell.name == "th"),
ref=group.get_ref(),
)
else:
table_cell = TableCell(
text=text,
row_span=row_span,
col_span=col_span,
start_row_offset_idx=start_row_span + row_idx,
end_row_offset_idx=start_row_span + row_idx + row_span,
start_col_offset_idx=col_idx,
end_col_offset_idx=col_idx + col_span,
column_header=col_header,
row_header=((not col_header) and html_cell.name == "th"),
)
data.table_cells.append(table_cell)

return data
Expand All @@ -1147,8 +1225,6 @@ def _add_table(
if not isinstance(table_tag, Tag):
return

data = JatsDocumentBackend.parse_table_data(table_tag)
# TODO: format label vs caption once styling is supported
label = table_xml_component["label"]
caption = table_xml_component["caption"]
table_text: str = f"{label}{' ' if label and caption else ''}{caption}"
Expand All @@ -1157,8 +1233,18 @@ def _add_table(
if table_text
else None
)
table_item = doc.add_table(
data=TableData(num_rows=0, num_cols=0, table_cells=[]),
parent=parent,
caption=table_caption,
)
data = JatsDocumentBackend.parse_table_data(
table_tag, doc=doc, table_item=table_item
)
if data is not None:
doc.add_table(data=data, parent=parent, caption=table_caption)
table_item.data = data
else:
doc.delete_items(node_items=[table_item])

return

Expand Down
5 changes: 4 additions & 1 deletion docling/utils/chandra_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,10 @@ def _emit_runs(
else:
# An equation block mixing prose and inline math is text, not one formula.
label = DocItemLabel.TEXT
if len(merged) > 1:
if len(merged) > 1 or any(
run.formatting and run.formatting.script in (Script.SUB, Script.SUPER)
for run in merged
):
if label != DocItemLabel.TEXT:
parent = self.doc.add_text(
label=label,
Expand Down
18 changes: 2 additions & 16 deletions tests/data/docx/groundtruth/docx_rich_cells.docx.html
Original file line number Diff line number Diff line change
Expand Up @@ -196,26 +196,12 @@ <h3>Table with rich cells</h3>
<li style="list-style-type: '1. ';">Number one</li>
<li style="list-style-type: '2. ';">Number two</li>
<li style="list-style-type: '3. ';">Number three</li>
</ol></td><td><p>This is simple text with</p>
<strong><p>bold</p></strong>
<p>,</p>
<del><p>strikethrough</p></del>
<p>and</p>
<em><p>italic</p></em>
<p>formatting with x</p>
<sup><p>2</p></sup>
<p>and H</p>
<sub><p>2</p></sub>
<p>O</p></td></tr><tr><td><p>This is a paragraph</p>
</ol></td><td><span class='inline-group'>This is simple text with <strong>bold</strong> , <del>strikethrough</del> and <em>italic</em> formatting with x <sup>2</sup> and H <sub>2</sub> O</span></td></tr><tr><td><p>This is a paragraph</p>
<p>This is another paragraph</p></td><td></td></tr></tbody></table>
<p></p>
<h3>Table with nested table</h3>
<p>Before table</p>
<table><tbody><tr><th>Column A</th><th>Column B</th></tr><tr><td>Simple cell upper left</td><td><p>Simple cell with</p>
<strong><p>bold</p></strong>
<p>and</p>
<em><p>italic</p></em>
<p>text</p></td></tr><tr><td><table><tbody><tr><th>A</th><th>B</th><th>C</th></tr><tr><td><em><p>Cell 1</p></em></td><td><u><p>Cell 2</p></u></td><td><strong><p>Cell 3</p></strong></td></tr></tbody></table>
<table><tbody><tr><th>Column A</th><th>Column B</th></tr><tr><td>Simple cell upper left</td><td><span class='inline-group'>Simple cell with <strong>bold</strong> and <em>italic</em> text</span></td></tr><tr><td><table><tbody><tr><th>A</th><th>B</th><th>C</th></tr><tr><td><em><p>Cell 1</p></em></td><td><u><p>Cell 2</p></u></td><td><strong><p>Cell 3</p></strong></td></tr></tbody></table>
<p></p></td><td><p>Rich cell<br>A nested table</p>
<table><tbody><tr><th>A</th><th>B</th><th>C</th></tr><tr><td><del><p>Cell 1</p></del></td><td><em><strong><p>Cell 2</p></strong></em></td><td><u><p>Cell 3</p></u></td></tr></tbody></table>
<p></p></td></tr></tbody></table>
Expand Down
Loading
Loading