From bfcab3d6778e6f622bb4a6b241bdb4bab22ba378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20J=C3=A9gou?= Date: Thu, 3 Apr 2025 15:11:50 +0200 Subject: [PATCH] feat(docx): add text formatting and hyperlink support (#630) * feat: Enable markdown text formatting for docx Signed-off-by: SimJeg * Fix imports Signed-off-by: SimJeg * Use Formatting Signed-off-by: SimJeg * Handle hyperlink Signed-off-by: SimJeg * Handle formatting properly for DocItemLabel.PARAGRAPH Signed-off-by: SimJeg * Use inline group Signed-off-by: SimJeg * Handle bullet lists Signed-off-by: SimJeg * Strip elements Signed-off-by: SimJeg * Strip elements Signed-off-by: SimJeg * Run black and mypy Signed-off-by: SimJeg * Handle header and footer Signed-off-by: SimJeg * Use inline_fmt everywhere Signed-off-by: SimJeg * Run precommit Signed-off-by: SimJeg * Address feedback Signed-off-by: SimJeg * Fix add_list_item Signed-off-by: SimJeg * fix minor bugs, mark helper methods internal Signed-off-by: Panos Vagenas --------- Signed-off-by: SimJeg Signed-off-by: Panos Vagenas Co-authored-by: Panos Vagenas --- docling/backend/msword_backend.py | 306 +++++++--- tests/data/docx/unit_test_formatting.docx | Bin 0 -> 19731 bytes .../docling_v2/unit_test_formatting.docx.itxt | 30 + .../docling_v2/unit_test_formatting.docx.json | 577 ++++++++++++++++++ .../docling_v2/unit_test_formatting.docx.md | 17 + tests/test_backend_msword.py | 8 +- 6 files changed, 852 insertions(+), 86 deletions(-) create mode 100644 tests/data/docx/unit_test_formatting.docx create mode 100644 tests/data/groundtruth/docling_v2/unit_test_formatting.docx.itxt create mode 100644 tests/data/groundtruth/docling_v2/unit_test_formatting.docx.json create mode 100644 tests/data/groundtruth/docling_v2/unit_test_formatting.docx.md diff --git a/docling/backend/msword_backend.py b/docling/backend/msword_backend.py index 5094c8f..d6b73f7 100644 --- a/docling/backend/msword_backend.py +++ b/docling/backend/msword_backend.py @@ -14,15 +14,19 @@ from docling_core.types.doc import ( TableCell, TableData, ) +from docling_core.types.doc.document import Formatting from docx import Document from docx.document import Document as DocxDocument from docx.oxml.table import CT_Tc from docx.oxml.xmlchemy import BaseOxmlElement from docx.table import Table, _Cell +from docx.text.hyperlink import Hyperlink from docx.text.paragraph import Paragraph +from docx.text.run import Run from lxml import etree from lxml.etree import XPath from PIL import Image, UnidentifiedImageError +from pydantic import AnyUrl from typing_extensions import override from docling.backend.abstract_backend import DeclarativeDocumentBackend @@ -118,14 +122,14 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): doc = DoclingDocument(name=self.file.stem or "file", origin=origin) if self.is_valid(): assert self.docx_obj is not None - doc = self.walk_linear(self.docx_obj.element.body, self.docx_obj, doc) + doc = self._walk_linear(self.docx_obj.element.body, self.docx_obj, doc) return doc else: raise RuntimeError( f"Cannot convert doc with {self.document_hash} because the backend failed to init." ) - def update_history( + def _update_history( self, name: str, level: Optional[int], @@ -138,26 +142,26 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): self.history["numids"].append(numid) self.history["indents"].append(ilevel) - def prev_name(self) -> Optional[str]: + def _prev_name(self) -> Optional[str]: return self.history["names"][-1] - def prev_level(self) -> Optional[int]: + def _prev_level(self) -> Optional[int]: return self.history["levels"][-1] - def prev_numid(self) -> Optional[int]: + def _prev_numid(self) -> Optional[int]: return self.history["numids"][-1] - def prev_indent(self) -> Optional[int]: + def _prev_indent(self) -> Optional[int]: return self.history["indents"][-1] - def get_level(self) -> int: + def _get_level(self) -> int: """Return the first None index.""" for k, v in self.parents.items(): if k >= 0 and v == None: return k return 0 - def walk_linear( + def _walk_linear( self, body: BaseOxmlElement, docx_obj: DocxDocument, @@ -177,12 +181,12 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): # Check for Tables if element.tag.endswith("tbl"): try: - self.handle_tables(element, docx_obj, doc) + self._handle_tables(element, docx_obj, doc) except Exception: _log.debug("could not parse a table, broken docx table") elif drawing_blip: - self.handle_pictures(docx_obj, drawing_blip, doc) + self._handle_pictures(docx_obj, drawing_blip, doc) # Check for the sdt containers, like table of contents elif tag_name in ["sdt"]: sdt_content = element.find(".//w:sdtContent", namespaces=namespaces) @@ -190,16 +194,18 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): # Iterate paragraphs, runs, or text inside . paragraphs = sdt_content.findall(".//w:p", namespaces=namespaces) for p in paragraphs: - self.handle_text_elements(p, docx_obj, doc) + self._handle_text_elements(p, docx_obj, doc) # Check for Text elif tag_name in ["p"]: # "tcPr", "sectPr" - self.handle_text_elements(element, docx_obj, doc) + self._handle_text_elements(element, docx_obj, doc) else: _log.debug(f"Ignoring element in DOCX with tag: {tag_name}") return doc - def str_to_int(self, s: Optional[str], default: Optional[int] = 0) -> Optional[int]: + def _str_to_int( + self, s: Optional[str], default: Optional[int] = 0 + ) -> Optional[int]: if s is None: return None try: @@ -207,7 +213,7 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): except ValueError: return default - def split_text_and_number(self, input_string: str) -> list[str]: + def _split_text_and_number(self, input_string: str) -> list[str]: match = re.match(r"(\D+)(\d+)$|^(\d+)(\D+)", input_string) if match: parts = list(filter(None, match.groups())) @@ -215,7 +221,7 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): else: return [input_string] - def get_numId_and_ilvl( + def _get_numId_and_ilvl( self, paragraph: Paragraph ) -> tuple[Optional[int], Optional[int]]: # Access the XML element of the paragraph @@ -230,12 +236,12 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): numId = numId_elem.get(self.XML_KEY) if numId_elem is not None else None ilvl = ilvl_elem.get(self.XML_KEY) if ilvl_elem is not None else None - return self.str_to_int(numId, None), self.str_to_int(ilvl, None) + return self._str_to_int(numId, None), self._str_to_int(ilvl, None) return None, None # If the paragraph is not part of a list - def get_heading_and_level(self, style_label: str) -> tuple[str, Optional[int]]: - parts = self.split_text_and_number(style_label) + def _get_heading_and_level(self, style_label: str) -> tuple[str, Optional[int]]: + parts = self._split_text_and_number(style_label) if len(parts) == 2: parts.sort() @@ -243,15 +249,15 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): label_level: Optional[int] = 0 if parts[0].strip().lower() == "heading": label_str = "Heading" - label_level = self.str_to_int(parts[1], None) + label_level = self._str_to_int(parts[1], None) if parts[1].strip().lower() == "heading": label_str = "Heading" - label_level = self.str_to_int(parts[0], None) + label_level = self._str_to_int(parts[0], None) return label_str, label_level return style_label, None - def get_label_and_level(self, paragraph: Paragraph) -> tuple[str, Optional[int]]: + def _get_label_and_level(self, paragraph: Paragraph) -> tuple[str, Optional[int]]: if paragraph.style is None: return "Normal", None @@ -264,16 +270,82 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): if ":" in label: parts = label.split(":") if len(parts) == 2: - return parts[0], self.str_to_int(parts[1], None) + return parts[0], self._str_to_int(parts[1], None) if "heading" in label.lower(): - return self.get_heading_and_level(label) + return self._get_heading_and_level(label) if "heading" in name.lower(): - return self.get_heading_and_level(name) + return self._get_heading_and_level(name) return label, None - def handle_equations_in_text(self, element, text): + @classmethod + def _get_format_from_run(cls, run: Run) -> Optional[Formatting]: + has_any_formatting = run.bold or run.italic or run.underline + return ( + Formatting( + bold=run.bold or False, + italic=run.italic or False, + underline=run.underline or False, + ) + if has_any_formatting + else None + ) + + def _get_paragraph_elements(self, paragraph: Paragraph): + """ + Extract paragraph elements along with their formatting and hyperlink + """ + + # for now retain empty paragraphs for backwards compatibility: + if paragraph.text.strip() == "": + return [("", None, None)] + + paragraph_elements: list[ + tuple[str, Optional[Formatting], Optional[Union[AnyUrl, Path]]] + ] = [] + group_text = "" + previous_format = None + + # Iterate over the runs of the paragraph and group them by format + for c in paragraph.iter_inner_content(): + if isinstance(c, Hyperlink): + text = c.text + hyperlink = Path(c.address) + format = self._get_format_from_run(c.runs[0]) + elif isinstance(c, Run): + text = c.text + hyperlink = None + format = self._get_format_from_run(c) + else: + continue + + if (len(text.strip()) and format != previous_format) or ( + hyperlink is not None + ): + # If the style changes for a non empty text, add the previous group + if len(group_text.strip()) > 0: + paragraph_elements.append( + (group_text.strip(), previous_format, None) + ) + group_text = "" + + # If there is a hyperlink, add it immediately + if hyperlink is not None: + paragraph_elements.append((text.strip(), format, hyperlink)) + text = "" + else: + previous_format = format + + group_text += text + + # Format the last group + if len(group_text.strip()) > 0: + paragraph_elements.append((group_text.strip(), format, None)) + + return paragraph_elements + + def _handle_equations_in_text(self, element, text): only_texts = [] only_equations = [] texts_and_equations = [] @@ -319,7 +391,20 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): return output_text, only_equations - def handle_text_elements( + def _create_or_reuse_parent( + self, + *, + doc: DoclingDocument, + prev_parent: Optional[NodeItem], + paragraph_elements: list, + ) -> Optional[NodeItem]: + return ( + doc.add_group(label=GroupLabel.INLINE, parent=prev_parent) + if len(paragraph_elements) > 1 + else prev_parent + ) + + def _handle_text_elements( self, element: BaseOxmlElement, docx_obj: DocxDocument, @@ -328,10 +413,11 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): paragraph = Paragraph(element, docx_obj) raw_text = paragraph.text - text, equations = self.handle_equations_in_text(element=element, text=raw_text) + text, equations = self._handle_equations_in_text(element=element, text=raw_text) if text is None: return + paragraph_elements = self._get_paragraph_elements(paragraph) text = text.strip() # Common styles for bullet and numbered lists. @@ -339,8 +425,8 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): # Identify wether list is a numbered list or not # is_numbered = "List Bullet" not in paragraph.style.name is_numbered = False - p_style_id, p_level = self.get_label_and_level(paragraph) - numid, ilevel = self.get_numId_and_ilvl(paragraph) + p_style_id, p_level = self._get_label_and_level(paragraph) + numid, ilevel = self._get_numId_and_ilvl(paragraph) if numid == 0: numid = None @@ -351,18 +437,18 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): and ilevel is not None and p_style_id not in ["Title", "Heading"] ): - self.add_listitem( - doc, - numid, - ilevel, - text, - is_numbered, + self._add_list_item( + doc=doc, + numid=numid, + ilevel=ilevel, + elements=paragraph_elements, + is_numbered=is_numbered, ) - self.update_history(p_style_id, p_level, numid, ilevel) + self._update_history(p_style_id, p_level, numid, ilevel) return elif ( numid is None - and self.prev_numid() is not None + and self._prev_numid() is not None and p_style_id not in ["Title", "Heading"] ): # Close list if self.level_at_new_list: @@ -390,12 +476,12 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): ) else: is_numbered_style = False - self.add_header(doc, p_level, text, is_numbered_style) + self._add_header(doc, p_level, text, is_numbered_style) elif len(equations) > 0: if (raw_text is None or len(raw_text) == 0) and len(text) > 0: # Standalone equation - level = self.get_level() + level = self._get_level() doc.add_text( label=DocItemLabel.FORMULA, parent=self.parents[level - 1], @@ -403,7 +489,7 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): ) else: # Inline equation - level = self.get_level() + level = self._get_level() inline_equation = doc.add_group( label=GroupLabel.INLINE, parent=self.parents[level - 1] ) @@ -442,30 +528,50 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): "ListBullet", "Quote", ]: - level = self.get_level() - doc.add_text( - label=DocItemLabel.PARAGRAPH, parent=self.parents[level - 1], text=text + level = self._get_level() + parent = self._create_or_reuse_parent( + doc=doc, + prev_parent=self.parents.get(level - 1), + paragraph_elements=paragraph_elements, ) + for text, format, hyperlink in paragraph_elements: + doc.add_text( + label=DocItemLabel.PARAGRAPH, + parent=parent, + text=text, + formatting=format, + hyperlink=hyperlink, + ) else: # Text style names can, and will have, not only default values but user values too # hence we treat all other labels as pure text - level = self.get_level() - doc.add_text( - label=DocItemLabel.PARAGRAPH, parent=self.parents[level - 1], text=text + level = self._get_level() + parent = self._create_or_reuse_parent( + doc=doc, + prev_parent=self.parents.get(level - 1), + paragraph_elements=paragraph_elements, ) + for text, format, hyperlink in paragraph_elements: + doc.add_text( + label=DocItemLabel.PARAGRAPH, + parent=parent, + text=text, + formatting=format, + hyperlink=hyperlink, + ) - self.update_history(p_style_id, p_level, numid, ilevel) + self._update_history(p_style_id, p_level, numid, ilevel) return - def add_header( + def _add_header( self, doc: DoclingDocument, curr_level: Optional[int], text: str, is_numbered_style: bool = False, ) -> None: - level = self.get_level() + level = self._get_level() if isinstance(curr_level, int): if curr_level > level: # add invisible group @@ -521,19 +627,20 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): ) return - def add_listitem( + def _add_list_item( self, + *, doc: DoclingDocument, numid: int, ilevel: int, - text: str, + elements: list, is_numbered: bool = False, ) -> None: enum_marker = "" - level = self.get_level() - prev_indent = self.prev_indent() - if self.prev_numid() is None: # Open new list + level = self._get_level() + prev_indent = self._prev_indent() + if self._prev_numid() is None: # Open new list self.level_at_new_list = level self.parents[level] = doc.add_group( @@ -545,15 +652,23 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): if is_numbered: enum_marker = str(self.listIter) + "." is_numbered = True - doc.add_list_item( - marker=enum_marker, - enumerated=is_numbered, - parent=self.parents[level], - text=text, + new_parent = self._create_or_reuse_parent( + doc=doc, + prev_parent=self.parents[level], + paragraph_elements=elements, ) + for text, format, hyperlink in elements: + doc.add_list_item( + marker=enum_marker, + enumerated=is_numbered, + parent=new_parent, + text=text, + formatting=format, + hyperlink=hyperlink, + ) elif ( - self.prev_numid() == numid + self._prev_numid() == numid and self.level_at_new_list is not None and prev_indent is not None and prev_indent < ilevel @@ -581,15 +696,23 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): if is_numbered: enum_marker = str(self.listIter) + "." is_numbered = True - doc.add_list_item( - marker=enum_marker, - enumerated=is_numbered, - parent=self.parents[self.level_at_new_list + ilevel], - text=text, - ) + new_parent = self._create_or_reuse_parent( + doc=doc, + prev_parent=self.parents[self.level_at_new_list + ilevel], + paragraph_elements=elements, + ) + for text, format, hyperlink in elements: + doc.add_list_item( + marker=enum_marker, + enumerated=is_numbered, + parent=new_parent, + text=text, + formatting=format, + hyperlink=hyperlink, + ) elif ( - self.prev_numid() == numid + self._prev_numid() == numid and self.level_at_new_list is not None and prev_indent is not None and ilevel < prev_indent @@ -603,29 +726,46 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): if is_numbered: enum_marker = str(self.listIter) + "." is_numbered = True - doc.add_list_item( - marker=enum_marker, - enumerated=is_numbered, - parent=self.parents[self.level_at_new_list + ilevel], - text=text, + new_parent = self._create_or_reuse_parent( + doc=doc, + prev_parent=self.parents[self.level_at_new_list + ilevel], + paragraph_elements=elements, ) + for text, format, hyperlink in elements: + doc.add_list_item( + marker=enum_marker, + enumerated=is_numbered, + parent=new_parent, + text=text, + formatting=format, + hyperlink=hyperlink, + ) self.listIter = 0 - elif self.prev_numid() == numid or prev_indent == ilevel: + elif self._prev_numid() == numid or prev_indent == ilevel: # TODO: Set marker and enumerated arguments if this is an enumeration element. self.listIter += 1 if is_numbered: enum_marker = str(self.listIter) + "." is_numbered = True - doc.add_list_item( - marker=enum_marker, - enumerated=is_numbered, - parent=self.parents[level - 1], - text=text, + new_parent = self._create_or_reuse_parent( + doc=doc, + prev_parent=self.parents[level - 1], + paragraph_elements=elements, ) + for text, format, hyperlink in elements: + # Add the list item to the parent group + doc.add_list_item( + marker=enum_marker, + enumerated=is_numbered, + parent=new_parent, + text=text, + formatting=format, + hyperlink=hyperlink, + ) return - def handle_tables( + def _handle_tables( self, element: BaseOxmlElement, docx_obj: DocxDocument, @@ -640,7 +780,7 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): cell_element = table.rows[0].cells[0] # In case we have a table of only 1 cell, we consider it furniture # And proceed processing the content of the cell as though it's in the document body - self.walk_linear(cell_element._element, docx_obj, doc) + self._walk_linear(cell_element._element, docx_obj, doc) return data = TableData(num_rows=num_rows, num_cols=num_cols) @@ -685,11 +825,11 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): data.table_cells.append(table_cell) col_idx += cell.grid_span - level = self.get_level() + level = self._get_level() doc.add_table(data=data, parent=self.parents[level - 1]) return - def handle_pictures( + def _handle_pictures( self, docx_obj: DocxDocument, drawing_blip: Any, doc: DoclingDocument ) -> None: def get_docx_image(drawing_blip): @@ -702,7 +842,7 @@ class MsWordDocumentBackend(DeclarativeDocumentBackend): image_data = image_part.blob # Get the binary image data return image_data - level = self.get_level() + level = self._get_level() # Open the BytesIO object with PIL to create an Image try: image_data = get_docx_image(drawing_blip) diff --git a/tests/data/docx/unit_test_formatting.docx b/tests/data/docx/unit_test_formatting.docx new file mode 100644 index 0000000000000000000000000000000000000000..5d08668e4faf4fe93982a4f0b62ddd177597f5ff GIT binary patch literal 19731 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 z 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