diff --git a/docling/backend/opendocument_backend.py b/docling/backend/opendocument_backend.py index 1beaac53f4..36896e83f6 100644 --- a/docling/backend/opendocument_backend.py +++ b/docling/backend/opendocument_backend.py @@ -303,32 +303,31 @@ def _odf_text_runs( odf_obj: OdfDocument | None, inherited_formatting: Formatting | None = None, ) -> list[_OdfTextRun]: - style_name = getattr(element, "attributes", {}).get("text:style-name") + style_name = element.attributes.get("text:style-name") formatting = _formatting_from_odf_text_style( odf_obj, style_name, inherited_formatting ) - tag = getattr(element, "tag", None) + tag = element.tag if tag == "text:line-break": - text = getattr(element, "text", "\n") or "\n" - text_recursive = getattr(element, "text_recursive", "") - if text_recursive.startswith(text): - text = text_recursive - return [_OdfTextRun(text=text, formatting=formatting)] + return [_OdfTextRun(text=element.text or "\n", formatting=formatting)] if tag == "text:tab": return [_OdfTextRun(text="\t", formatting=formatting)] runs: list[_OdfTextRun] = [] - text = getattr(element, "text", "") + children = element.children + text = element.text if text: runs.append(_OdfTextRun(text=text, formatting=formatting)) - for child in getattr(element, "children", []): + for child in children: runs.extend(_odf_text_runs(child, odf_obj, formatting)) + if child.tail: + runs.append(_OdfTextRun(text=child.tail, formatting=formatting)) - if not runs and not getattr(element, "children", []): - text_recursive = getattr(element, "text_recursive", "") - if text_recursive: - runs.append(_OdfTextRun(text=text_recursive, formatting=formatting)) + if not runs and not children: + inner_text = element.inner_text + if inner_text: + runs.append(_OdfTextRun(text=inner_text, formatting=formatting)) return runs diff --git a/tests/data/odf/groundtruth/text_document_03.odt.html b/tests/data/odf/groundtruth/text_document_03.odt.html index d1dc80dd85..e7973a727e 100644 --- a/tests/data/odf/groundtruth/text_document_03.odt.html +++ b/tests/data/odf/groundtruth/text_document_03.odt.html @@ -202,7 +202,7 @@
Before table
| Column A | Column B | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Simple cell upper left | Simple cell with bold and italic text | ||||||||||||
| Rich cell
|
| Column A | Column B |
|---|---|
| Only text | Owner avatar |
Text and picture diff --git a/tests/data/odf/groundtruth/text_document_03.odt.itxt b/tests/data/odf/groundtruth/text_document_03.odt.itxt index c57b0febc6..313af5e5a6 100644 --- a/tests/data/odf/groundtruth/text_document_03.odt.itxt +++ b/tests/data/odf/groundtruth/text_document_03.odt.itxt @@ -46,59 +46,63 @@ A nested table item-42 at level 1: inline: group group item-43 at level 2: text: After table with item-44 at level 2: text: bold - item-45 at level 2: text: underline - item-46 at level 2: text: strikethrough - item-47 at level 2: text: italic - item-48 at level 1: section_header: Table with pictures - item-49 at level 1: table with [3x2] - item-50 at level 2: unspecified: group rich_cell_group_4_1_1 - item-51 at level 3: picture - item-52 at level 3: text: Owner avatar - item-53 at level 2: unspecified: group rich_cell_group_4_0_2 - item-54 at level 3: text: Text and picture + item-45 at level 2: text: , + item-46 at level 2: text: underline + item-47 at level 2: text: , + item-48 at level 2: text: strikethrough + item-49 at level 2: text: , and + item-50 at level 2: text: italic + item-51 at level 2: text: formatting + item-52 at level 1: section_header: Table with pictures + item-53 at level 1: table with [3x2] + item-54 at level 2: unspecified: group rich_cell_group_4_1_1 item-55 at level 3: picture item-56 at level 3: text: Owner avatar - item-57 at level 1: section_header: Lists with same numId in different cells - item-58 at level 1: table with [2x1] - item-59 at level 2: unspecified: group rich_cell_group_5_0_0 - item-60 at level 3: list: group list - item-61 at level 4: list_item: Cell 1 item 1 - item-62 at level 4: list_item: Cell 1 item 2 - item-63 at level 2: unspecified: group rich_cell_group_5_0_1 + item-57 at level 2: unspecified: group rich_cell_group_4_0_2 + item-58 at level 3: text: Text and picture + item-59 at level 3: picture + item-60 at level 3: text: Owner avatar + item-61 at level 1: section_header: Lists with same numId in different cells + item-62 at level 1: table with [2x1] + item-63 at level 2: unspecified: group rich_cell_group_5_0_0 item-64 at level 3: list: group list - item-65 at level 4: list_item: Cell 2 item 1 - item-66 at level 4: list_item: Cell 2 item 2 - item-67 at level 1: section_header: Lists with different numIds in different cells - item-68 at level 1: table with [2x1] - item-69 at level 2: unspecified: group rich_cell_group_6_0_0 - item-70 at level 3: list: group list - item-71 at level 4: list_item: Cell 1 item 1 - item-72 at level 4: list_item: Cell 1 item 2 - item-73 at level 2: unspecified: group rich_cell_group_6_0_1 + item-65 at level 4: list_item: Cell 1 item 1 + item-66 at level 4: list_item: Cell 1 item 2 + item-67 at level 2: unspecified: group rich_cell_group_5_0_1 + item-68 at level 3: list: group list + item-69 at level 4: list_item: Cell 2 item 1 + item-70 at level 4: list_item: Cell 2 item 2 + item-71 at level 1: section_header: Lists with different numIds in different cells + item-72 at level 1: table with [2x1] + item-73 at level 2: unspecified: group rich_cell_group_6_0_0 item-74 at level 3: list: group list - item-75 at level 4: list_item: Cell 2 item 1 - item-76 at level 4: list_item: Cell 2 item 2 - item-77 at level 1: section_header: Multiple columns with lists - item-78 at level 1: table with [2x2] - item-79 at level 2: unspecified: group rich_cell_group_7_0_0 - item-80 at level 3: list: group list - item-81 at level 4: list_item: R1C1 item 1 - item-82 at level 4: list_item: R1C1 item 2 - item-83 at level 2: unspecified: group rich_cell_group_7_1_0 + item-75 at level 4: list_item: Cell 1 item 1 + item-76 at level 4: list_item: Cell 1 item 2 + item-77 at level 2: unspecified: group rich_cell_group_6_0_1 + item-78 at level 3: list: group list + item-79 at level 4: list_item: Cell 2 item 1 + item-80 at level 4: list_item: Cell 2 item 2 + item-81 at level 1: section_header: Multiple columns with lists + item-82 at level 1: table with [2x2] + item-83 at level 2: unspecified: group rich_cell_group_7_0_0 item-84 at level 3: list: group list - item-85 at level 4: list_item: R1C2 item 1 - item-86 at level 4: list_item: R1C2 item 2 - item-87 at level 2: unspecified: group rich_cell_group_7_0_1 + item-85 at level 4: list_item: R1C1 item 1 + item-86 at level 4: list_item: R1C1 item 2 + item-87 at level 2: unspecified: group rich_cell_group_7_1_0 item-88 at level 3: list: group list - item-89 at level 4: list_item: R2C1 item 1 - item-90 at level 4: list_item: R2C1 item 2 - item-91 at level 2: unspecified: group rich_cell_group_7_1_1 + item-89 at level 4: list_item: R1C2 item 1 + item-90 at level 4: list_item: R1C2 item 2 + item-91 at level 2: unspecified: group rich_cell_group_7_0_1 item-92 at level 3: list: group list - item-93 at level 4: list_item: R2C2 item 1 - item-94 at level 4: list_item: R2C2 item 2 - item-95 at level 1: section_header: Mixed content - list and regular text in different cells - item-96 at level 1: table with [2x1] - item-97 at level 2: unspecified: group rich_cell_group_8_0_0 - item-98 at level 3: list: group list - item-99 at level 4: list_item: List item 1 - item-100 at level 4: list_item: List item 2 \ No newline at end of file + item-93 at level 4: list_item: R2C1 item 1 + item-94 at level 4: list_item: R2C1 item 2 + item-95 at level 2: unspecified: group rich_cell_group_7_1_1 + item-96 at level 3: list: group list + item-97 at level 4: list_item: R2C2 item 1 + item-98 at level 4: list_item: R2C2 item 2 + item-99 at level 1: section_header: Mixed content - list and regular text in different cells + item-100 at level 1: table with [2x1] + item-101 at level 2: unspecified: group rich_cell_group_8_0_0 + item-102 at level 3: list: group list + item-103 at level 4: list_item: List item 1 + item-104 at level 4: list_item: List item 2 \ No newline at end of file diff --git a/tests/data/odf/groundtruth/text_document_03.odt.json b/tests/data/odf/groundtruth/text_document_03.odt.json index f6037b03b4..06adc08f82 100644 --- a/tests/data/odf/groundtruth/text_document_03.odt.json +++ b/tests/data/odf/groundtruth/text_document_03.odt.json @@ -36,31 +36,31 @@ "$ref": "#/groups/12" }, { - "$ref": "#/texts/30" + "$ref": "#/texts/34" }, { "$ref": "#/tables/4" }, { - "$ref": "#/texts/34" + "$ref": "#/texts/38" }, { "$ref": "#/tables/5" }, { - "$ref": "#/texts/39" + "$ref": "#/texts/43" }, { "$ref": "#/tables/6" }, { - "$ref": "#/texts/44" + "$ref": "#/texts/48" }, { "$ref": "#/tables/7" }, { - "$ref": "#/texts/53" + "$ref": "#/texts/57" }, { "$ref": "#/tables/8" @@ -304,6 +304,18 @@ }, { "$ref": "#/texts/29" + }, + { + "$ref": "#/texts/30" + }, + { + "$ref": "#/texts/31" + }, + { + "$ref": "#/texts/32" + }, + { + "$ref": "#/texts/33" } ], "content_layer": "body", @@ -320,7 +332,7 @@ "$ref": "#/pictures/0" }, { - "$ref": "#/texts/31" + "$ref": "#/texts/35" } ], "content_layer": "body", @@ -334,13 +346,13 @@ }, "children": [ { - "$ref": "#/texts/32" + "$ref": "#/texts/36" }, { "$ref": "#/pictures/1" }, { - "$ref": "#/texts/33" + "$ref": "#/texts/37" } ], "content_layer": "body", @@ -368,10 +380,10 @@ }, "children": [ { - "$ref": "#/texts/35" + "$ref": "#/texts/39" }, { - "$ref": "#/texts/36" + "$ref": "#/texts/40" } ], "content_layer": "body", @@ -399,10 +411,10 @@ }, "children": [ { - "$ref": "#/texts/37" + "$ref": "#/texts/41" }, { - "$ref": "#/texts/38" + "$ref": "#/texts/42" } ], "content_layer": "body", @@ -430,10 +442,10 @@ }, "children": [ { - "$ref": "#/texts/40" + "$ref": "#/texts/44" }, { - "$ref": "#/texts/41" + "$ref": "#/texts/45" } ], "content_layer": "body", @@ -461,10 +473,10 @@ }, "children": [ { - "$ref": "#/texts/42" + "$ref": "#/texts/46" }, { - "$ref": "#/texts/43" + "$ref": "#/texts/47" } ], "content_layer": "body", @@ -492,10 +504,10 @@ }, "children": [ { - "$ref": "#/texts/45" + "$ref": "#/texts/49" }, { - "$ref": "#/texts/46" + "$ref": "#/texts/50" } ], "content_layer": "body", @@ -523,10 +535,10 @@ }, "children": [ { - "$ref": "#/texts/47" + "$ref": "#/texts/51" }, { - "$ref": "#/texts/48" + "$ref": "#/texts/52" } ], "content_layer": "body", @@ -554,10 +566,10 @@ }, "children": [ { - "$ref": "#/texts/49" + "$ref": "#/texts/53" }, { - "$ref": "#/texts/50" + "$ref": "#/texts/54" } ], "content_layer": "body", @@ -585,10 +597,10 @@ }, "children": [ { - "$ref": "#/texts/51" + "$ref": "#/texts/55" }, { - "$ref": "#/texts/52" + "$ref": "#/texts/56" } ], "content_layer": "body", @@ -616,10 +628,10 @@ }, "children": [ { - "$ref": "#/texts/54" + "$ref": "#/texts/58" }, { - "$ref": "#/texts/55" + "$ref": "#/texts/59" } ], "content_layer": "body", @@ -1021,6 +1033,18 @@ "content_layer": "body", "label": "text", "prov": [], + "orig": ", ", + "text": ", " + }, + { + "self_ref": "#/texts/28", + "parent": { + "$ref": "#/groups/12" + }, + "children": [], + "content_layer": "body", + "label": "text", + "prov": [], "orig": "underline", "text": "underline", "formatting": { @@ -1032,7 +1056,19 @@ } }, { - "self_ref": "#/texts/28", + "self_ref": "#/texts/29", + "parent": { + "$ref": "#/groups/12" + }, + "children": [], + "content_layer": "body", + "label": "text", + "prov": [], + "orig": ", ", + "text": ", " + }, + { + "self_ref": "#/texts/30", "parent": { "$ref": "#/groups/12" }, @@ -1051,7 +1087,19 @@ } }, { - "self_ref": "#/texts/29", + "self_ref": "#/texts/31", + "parent": { + "$ref": "#/groups/12" + }, + "children": [], + "content_layer": "body", + "label": "text", + "prov": [], + "orig": ", and ", + "text": ", and " + }, + { + "self_ref": "#/texts/32", "parent": { "$ref": "#/groups/12" }, @@ -1070,7 +1118,19 @@ } }, { - "self_ref": "#/texts/30", + "self_ref": "#/texts/33", + "parent": { + "$ref": "#/groups/12" + }, + "children": [], + "content_layer": "body", + "label": "text", + "prov": [], + "orig": " formatting", + "text": " formatting" + }, + { + "self_ref": "#/texts/34", "parent": { "$ref": "#/body" }, @@ -1083,7 +1143,7 @@ "level": 2 }, { - "self_ref": "#/texts/31", + "self_ref": "#/texts/35", "parent": { "$ref": "#/groups/13" }, @@ -1095,7 +1155,7 @@ "text": "Owner avatar" }, { - "self_ref": "#/texts/32", + "self_ref": "#/texts/36", "parent": { "$ref": "#/groups/14" }, @@ -1107,7 +1167,7 @@ "text": "Text and picture" }, { - "self_ref": "#/texts/33", + "self_ref": "#/texts/37", "parent": { "$ref": "#/groups/14" }, @@ -1119,7 +1179,7 @@ "text": "Owner avatar" }, { - "self_ref": "#/texts/34", + "self_ref": "#/texts/38", "parent": { "$ref": "#/body" }, @@ -1132,7 +1192,7 @@ "level": 2 }, { - "self_ref": "#/texts/35", + "self_ref": "#/texts/39", "parent": { "$ref": "#/groups/16" }, @@ -1146,7 +1206,7 @@ "marker": "" }, { - "self_ref": "#/texts/36", + "self_ref": "#/texts/40", "parent": { "$ref": "#/groups/16" }, @@ -1160,7 +1220,7 @@ "marker": "" }, { - "self_ref": "#/texts/37", + "self_ref": "#/texts/41", "parent": { "$ref": "#/groups/18" }, @@ -1174,7 +1234,7 @@ "marker": "" }, { - "self_ref": "#/texts/38", + "self_ref": "#/texts/42", "parent": { "$ref": "#/groups/18" }, @@ -1188,7 +1248,7 @@ "marker": "" }, { - "self_ref": "#/texts/39", + "self_ref": "#/texts/43", "parent": { "$ref": "#/body" }, @@ -1201,7 +1261,7 @@ "level": 2 }, { - "self_ref": "#/texts/40", + "self_ref": "#/texts/44", "parent": { "$ref": "#/groups/20" }, @@ -1215,7 +1275,7 @@ "marker": "" }, { - "self_ref": "#/texts/41", + "self_ref": "#/texts/45", "parent": { "$ref": "#/groups/20" }, @@ -1229,7 +1289,7 @@ "marker": "" }, { - "self_ref": "#/texts/42", + "self_ref": "#/texts/46", "parent": { "$ref": "#/groups/22" }, @@ -1243,7 +1303,7 @@ "marker": "" }, { - "self_ref": "#/texts/43", + "self_ref": "#/texts/47", "parent": { "$ref": "#/groups/22" }, @@ -1257,7 +1317,7 @@ "marker": "" }, { - "self_ref": "#/texts/44", + "self_ref": "#/texts/48", "parent": { "$ref": "#/body" }, @@ -1270,7 +1330,7 @@ "level": 2 }, { - "self_ref": "#/texts/45", + "self_ref": "#/texts/49", "parent": { "$ref": "#/groups/24" }, @@ -1284,7 +1344,7 @@ "marker": "" }, { - "self_ref": "#/texts/46", + "self_ref": "#/texts/50", "parent": { "$ref": "#/groups/24" }, @@ -1298,7 +1358,7 @@ "marker": "" }, { - "self_ref": "#/texts/47", + "self_ref": "#/texts/51", "parent": { "$ref": "#/groups/26" }, @@ -1312,7 +1372,7 @@ "marker": "" }, { - "self_ref": "#/texts/48", + "self_ref": "#/texts/52", "parent": { "$ref": "#/groups/26" }, @@ -1326,7 +1386,7 @@ "marker": "" }, { - "self_ref": "#/texts/49", + "self_ref": "#/texts/53", "parent": { "$ref": "#/groups/28" }, @@ -1340,7 +1400,7 @@ "marker": "" }, { - "self_ref": "#/texts/50", + "self_ref": "#/texts/54", "parent": { "$ref": "#/groups/28" }, @@ -1354,7 +1414,7 @@ "marker": "" }, { - "self_ref": "#/texts/51", + "self_ref": "#/texts/55", "parent": { "$ref": "#/groups/30" }, @@ -1368,7 +1428,7 @@ "marker": "" }, { - "self_ref": "#/texts/52", + "self_ref": "#/texts/56", "parent": { "$ref": "#/groups/30" }, @@ -1382,7 +1442,7 @@ "marker": "" }, { - "self_ref": "#/texts/53", + "self_ref": "#/texts/57", "parent": { "$ref": "#/body" }, @@ -1395,7 +1455,7 @@ "level": 2 }, { - "self_ref": "#/texts/54", + "self_ref": "#/texts/58", "parent": { "$ref": "#/groups/32" }, @@ -1409,7 +1469,7 @@ "marker": "" }, { - "self_ref": "#/texts/55", + "self_ref": "#/texts/59", "parent": { "$ref": "#/groups/32" }, diff --git a/tests/data/odf/groundtruth/text_document_03.odt.md b/tests/data/odf/groundtruth/text_document_03.odt.md index 126f76abaf..fc2e9769d8 100644 --- a/tests/data/odf/groundtruth/text_document_03.odt.md +++ b/tests/data/odf/groundtruth/text_document_03.odt.md @@ -15,7 +15,7 @@ Before table | Simple cell upper left | Simple cell with bold and italic text | | A B C Cell 1 Cell 2 Cell 3 | Rich cell A nested table A B C Cell 1 Cell 2 Cell 3 | -After table with **bold** underline ~~strikethrough~~ *italic* +After table with **bold** , underline , ~~strikethrough~~ , and *italic* formatting ### Table with pictures diff --git a/tests/test_backend_opendocument.py b/tests/test_backend_opendocument.py index 6d80e6cc4f..558ce6ff21 100644 --- a/tests/test_backend_opendocument.py +++ b/tests/test_backend_opendocument.py @@ -39,14 +39,17 @@ pytest.importorskip("odfdo") from odfdo import ( + Bookmark, Document as OdfDocument, DrawPage, Frame, Header, + LineBreak, List as OdfList, ListItem, Paragraph, Section, + Spacer, Span, Style, Table, @@ -275,6 +278,55 @@ def test_odp_conversion(odp_path: Path): } <= body_texts +@pytest.mark.parametrize( + ("input_format", "document_type", "suffix"), + [ + (InputFormat.ODT, "text", ".odt"), + (InputFormat.ODP, "presentation", ".odp"), + ], +) +def test_odf_preserves_text_after_inline_children( + tmp_path: Path, + input_format: InputFormat, + document_type: str, + suffix: str, +): + path = tmp_path / f"inline_tail{suffix}" + doc = OdfDocument(document_type) + body = doc.body + body.clear() + + paragraph = Paragraph("Lead") + span = Span() + span.append(Spacer()) + span.append("connective prose here") + paragraph.append(span) + paragraph.append(Bookmark("inline-marker")) + paragraph.append(" bookmarked tail") + paragraph.append(LineBreak()) + paragraph.append("next line") + + if input_format == InputFormat.ODT: + body.append(paragraph) + else: + page = DrawPage("page1", name="Slide One") + page.append( + Frame.text_frame( + paragraph, + size=("10cm", "5cm"), + position=("1cm", "1cm"), + ) + ) + body.append(page) + + doc.save(str(path)) + + result = DocumentConverter(allowed_formats=[input_format]).convert(path) + text_items = [item.text for item in result.document.texts if item.label == "text"] + + assert text_items == ["Lead connective prose here bookmarked tail\nnext line"] + + def test_ods_merged_cells(tmp_path: Path): path = tmp_path / "merged.ods" doc = OdfDocument("spreadsheet") |