fix: pptx line break and space handling (#1664)
Signed-off-by: Martin Wind <martin.wind@im-c.at>
This commit is contained in:
parent
b886e4df31
commit
f28d23cf03
@ -20,6 +20,7 @@ from docling_core.types.doc.document import ContentLayer
|
|||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
from pptx import Presentation
|
from pptx import Presentation
|
||||||
from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
|
from pptx.enum.shapes import MSO_SHAPE_TYPE, PP_PLACEHOLDER
|
||||||
|
from pptx.oxml.text import CT_TextLineBreak
|
||||||
|
|
||||||
from docling.backend.abstract_backend import (
|
from docling.backend.abstract_backend import (
|
||||||
DeclarativeDocumentBackend,
|
DeclarativeDocumentBackend,
|
||||||
@ -120,136 +121,91 @@ class MsPowerpointDocumentBackend(DeclarativeDocumentBackend, PaginatedDocumentB
|
|||||||
|
|
||||||
return prov
|
return prov
|
||||||
|
|
||||||
def handle_text_elements(self, shape, parent_slide, slide_ind, doc, slide_size): # noqa: C901
|
def handle_text_elements(self, shape, parent_slide, slide_ind, doc, slide_size):
|
||||||
is_a_list = False
|
|
||||||
is_list_group_created = False
|
is_list_group_created = False
|
||||||
enum_list_item_value = 0
|
enum_list_item_value = 0
|
||||||
new_list = None
|
new_list = None
|
||||||
bullet_type = "None"
|
|
||||||
list_label = GroupLabel.LIST
|
|
||||||
doc_label = DocItemLabel.LIST_ITEM
|
doc_label = DocItemLabel.LIST_ITEM
|
||||||
prov = self.generate_prov(shape, slide_ind, shape.text.strip(), slide_size)
|
prov = self.generate_prov(shape, slide_ind, shape.text.strip(), slide_size)
|
||||||
|
|
||||||
# Identify if shape contains lists
|
def is_list_item(paragraph):
|
||||||
for paragraph in shape.text_frame.paragraphs:
|
"""Check if the paragraph is a list item."""
|
||||||
# Check if paragraph is a bullet point using the `element` XML
|
|
||||||
p = paragraph._element
|
p = paragraph._element
|
||||||
if (
|
if (
|
||||||
p.find(".//a:buChar", namespaces={"a": self.namespaces["a"]})
|
p.find(".//a:buChar", namespaces={"a": self.namespaces["a"]})
|
||||||
is not None
|
is not None
|
||||||
):
|
):
|
||||||
bullet_type = "Bullet"
|
return (True, "Bullet")
|
||||||
is_a_list = True
|
|
||||||
elif (
|
elif (
|
||||||
p.find(".//a:buAutoNum", namespaces={"a": self.namespaces["a"]})
|
p.find(".//a:buAutoNum", namespaces={"a": self.namespaces["a"]})
|
||||||
is not None
|
is not None
|
||||||
):
|
):
|
||||||
bullet_type = "Numbered"
|
return (True, "Numbered")
|
||||||
is_a_list = True
|
elif paragraph.level > 0:
|
||||||
else:
|
|
||||||
is_a_list = False
|
|
||||||
|
|
||||||
if paragraph.level > 0:
|
|
||||||
# Most likely a sub-list
|
# Most likely a sub-list
|
||||||
is_a_list = True
|
return (True, "None")
|
||||||
|
|
||||||
if is_a_list:
|
|
||||||
# Determine if this is an unordered list or an ordered list.
|
|
||||||
# Set GroupLabel.ORDERED_LIST when it fits.
|
|
||||||
if bullet_type == "Numbered":
|
|
||||||
list_label = GroupLabel.ORDERED_LIST
|
|
||||||
|
|
||||||
if is_a_list:
|
|
||||||
_log.debug("LIST DETECTED!")
|
|
||||||
else:
|
else:
|
||||||
_log.debug("No List")
|
return (False, "None")
|
||||||
|
|
||||||
# If there is a list inside of the shape, create a new docling list to assign list items to
|
|
||||||
# if is_a_list:
|
|
||||||
# new_list = doc.add_group(
|
|
||||||
# label=list_label, name=f"list", parent=parent_slide
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Iterate through paragraphs to build up text
|
# Iterate through paragraphs to build up text
|
||||||
for paragraph in shape.text_frame.paragraphs:
|
for paragraph in shape.text_frame.paragraphs:
|
||||||
# p_text = paragraph.text.strip()
|
is_a_list, bullet_type = is_list_item(paragraph)
|
||||||
p = paragraph._element
|
p = paragraph._element
|
||||||
enum_list_item_value += 1
|
|
||||||
inline_paragraph_text = ""
|
|
||||||
inline_list_item_text = ""
|
|
||||||
|
|
||||||
for e in p.iterfind(".//a:r", namespaces={"a": self.namespaces["a"]}):
|
# Convert line breaks to spaces and accumulate text
|
||||||
if len(e.text.strip()) > 0:
|
p_text = ""
|
||||||
e_is_a_list_item = False
|
for e in p.content_children:
|
||||||
is_numbered = False
|
if isinstance(e, CT_TextLineBreak):
|
||||||
if (
|
p_text += " "
|
||||||
p.find(".//a:buChar", namespaces={"a": self.namespaces["a"]})
|
else:
|
||||||
is not None
|
p_text += e.text
|
||||||
):
|
|
||||||
bullet_type = "Bullet"
|
|
||||||
e_is_a_list_item = True
|
|
||||||
elif (
|
|
||||||
p.find(".//a:buAutoNum", namespaces={"a": self.namespaces["a"]})
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
bullet_type = "Numbered"
|
|
||||||
is_numbered = True
|
|
||||||
e_is_a_list_item = True
|
|
||||||
else:
|
|
||||||
e_is_a_list_item = False
|
|
||||||
|
|
||||||
if e_is_a_list_item:
|
if is_a_list:
|
||||||
if len(inline_paragraph_text) > 0:
|
enum_marker = ""
|
||||||
# output accumulated inline text:
|
enumerated = bullet_type == "Numbered"
|
||||||
doc.add_text(
|
|
||||||
label=doc_label,
|
if not is_list_group_created:
|
||||||
parent=parent_slide,
|
new_list = doc.add_group(
|
||||||
text=inline_paragraph_text,
|
label=GroupLabel.ORDERED_LIST
|
||||||
prov=prov,
|
if enumerated
|
||||||
)
|
else GroupLabel.LIST,
|
||||||
# Set marker and enumerated arguments if this is an enumeration element.
|
name="list",
|
||||||
inline_list_item_text += e.text
|
parent=parent_slide,
|
||||||
# print(e.text)
|
)
|
||||||
else:
|
is_list_group_created = True
|
||||||
# Assign proper label to the text, depending if it's a Title or Section Header
|
enum_list_item_value = 0
|
||||||
# For other types of text, assign - PARAGRAPH
|
|
||||||
doc_label = DocItemLabel.PARAGRAPH
|
if enumerated:
|
||||||
if shape.is_placeholder:
|
enum_list_item_value += 1
|
||||||
placeholder_type = shape.placeholder_format.type
|
enum_marker = str(enum_list_item_value) + "."
|
||||||
if placeholder_type in [
|
|
||||||
PP_PLACEHOLDER.CENTER_TITLE,
|
doc.add_list_item(
|
||||||
PP_PLACEHOLDER.TITLE,
|
marker=enum_marker,
|
||||||
]:
|
enumerated=enumerated,
|
||||||
# It's a title
|
parent=new_list,
|
||||||
doc_label = DocItemLabel.TITLE
|
text=p_text,
|
||||||
elif placeholder_type == PP_PLACEHOLDER.SUBTITLE:
|
prov=prov,
|
||||||
DocItemLabel.SECTION_HEADER
|
)
|
||||||
enum_list_item_value = 0
|
else: # is paragraph not a list item
|
||||||
inline_paragraph_text += e.text
|
# Assign proper label to the text, depending if it's a Title or Section Header
|
||||||
|
# For other types of text, assign - PARAGRAPH
|
||||||
|
doc_label = DocItemLabel.PARAGRAPH
|
||||||
|
if shape.is_placeholder:
|
||||||
|
placeholder_type = shape.placeholder_format.type
|
||||||
|
if placeholder_type in [
|
||||||
|
PP_PLACEHOLDER.CENTER_TITLE,
|
||||||
|
PP_PLACEHOLDER.TITLE,
|
||||||
|
]:
|
||||||
|
# It's a title
|
||||||
|
doc_label = DocItemLabel.TITLE
|
||||||
|
elif placeholder_type == PP_PLACEHOLDER.SUBTITLE:
|
||||||
|
DocItemLabel.SECTION_HEADER
|
||||||
|
|
||||||
if len(inline_paragraph_text) > 0:
|
|
||||||
# output accumulated inline text:
|
# output accumulated inline text:
|
||||||
doc.add_text(
|
doc.add_text(
|
||||||
label=doc_label,
|
label=doc_label,
|
||||||
parent=parent_slide,
|
parent=parent_slide,
|
||||||
text=inline_paragraph_text,
|
text=p_text,
|
||||||
prov=prov,
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(inline_list_item_text) > 0:
|
|
||||||
enum_marker = ""
|
|
||||||
if is_numbered:
|
|
||||||
enum_marker = str(enum_list_item_value) + "."
|
|
||||||
if not is_list_group_created:
|
|
||||||
new_list = doc.add_group(
|
|
||||||
label=list_label, name="list", parent=parent_slide
|
|
||||||
)
|
|
||||||
is_list_group_created = True
|
|
||||||
doc.add_list_item(
|
|
||||||
marker=enum_marker,
|
|
||||||
enumerated=is_numbered,
|
|
||||||
parent=new_list,
|
|
||||||
text=inline_list_item_text,
|
|
||||||
prov=prov,
|
prov=prov,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
3
tests/data/groundtruth/docling_v2/powerpoint_bad_text.pptx.itxt
vendored
Normal file
3
tests/data/groundtruth/docling_v2/powerpoint_bad_text.pptx.itxt
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
item-0 at level 0: unspecified: group _root_
|
||||||
|
item-1 at level 1: chapter: group slide-0
|
||||||
|
item-2 at level 2: title: X-Library The fully customisable ... llection exclusively for our customers
|
86
tests/data/groundtruth/docling_v2/powerpoint_bad_text.pptx.json
vendored
Normal file
86
tests/data/groundtruth/docling_v2/powerpoint_bad_text.pptx.json
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"schema_name": "DoclingDocument",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"name": "powerpoint_bad_text",
|
||||||
|
"origin": {
|
||||||
|
"mimetype": "application/vnd.ms-powerpoint",
|
||||||
|
"binary_hash": 1443005848482130016,
|
||||||
|
"filename": "powerpoint_bad_text.pptx"
|
||||||
|
},
|
||||||
|
"furniture": {
|
||||||
|
"self_ref": "#/furniture",
|
||||||
|
"children": [],
|
||||||
|
"content_layer": "furniture",
|
||||||
|
"name": "_root_",
|
||||||
|
"label": "unspecified"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"self_ref": "#/body",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"$ref": "#/groups/0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content_layer": "body",
|
||||||
|
"name": "_root_",
|
||||||
|
"label": "unspecified"
|
||||||
|
},
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"self_ref": "#/groups/0",
|
||||||
|
"parent": {
|
||||||
|
"$ref": "#/body"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"$ref": "#/texts/0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content_layer": "body",
|
||||||
|
"name": "slide-0",
|
||||||
|
"label": "chapter"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"texts": [
|
||||||
|
{
|
||||||
|
"self_ref": "#/texts/0",
|
||||||
|
"parent": {
|
||||||
|
"$ref": "#/groups/0"
|
||||||
|
},
|
||||||
|
"children": [],
|
||||||
|
"content_layer": "body",
|
||||||
|
"label": "title",
|
||||||
|
"prov": [
|
||||||
|
{
|
||||||
|
"page_no": 1,
|
||||||
|
"bbox": {
|
||||||
|
"l": 1041400.0,
|
||||||
|
"t": 4582390.0,
|
||||||
|
"r": 8083550.0,
|
||||||
|
"b": 1689099.0,
|
||||||
|
"coord_origin": "BOTTOMLEFT"
|
||||||
|
},
|
||||||
|
"charspan": [
|
||||||
|
0,
|
||||||
|
118
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"orig": "X-Library The fully customisable and copyright-free standard content template collection exclusively for our customers",
|
||||||
|
"text": "X-Library The fully customisable and copyright-free standard content template collection exclusively for our customers"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pictures": [],
|
||||||
|
"tables": [],
|
||||||
|
"key_value_items": [],
|
||||||
|
"form_items": [],
|
||||||
|
"pages": {
|
||||||
|
"1": {
|
||||||
|
"size": {
|
||||||
|
"width": 12190413.0,
|
||||||
|
"height": 6858000.0
|
||||||
|
},
|
||||||
|
"page_no": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
tests/data/groundtruth/docling_v2/powerpoint_bad_text.pptx.md
vendored
Normal file
1
tests/data/groundtruth/docling_v2/powerpoint_bad_text.pptx.md
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
# X-Library The fully customisable and copyright-free standard content template collection exclusively for our customers
|
BIN
tests/data/pptx/powerpoint_bad_text.pptx
vendored
Normal file
BIN
tests/data/pptx/powerpoint_bad_text.pptx
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user