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
16 changes: 9 additions & 7 deletions docling/backend/msword_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2490,13 +2490,15 @@ def _get_paragraph_position(self, paragraph_element):
for attr_name in ["y", "top", "positionY", "y-position", "position"]:
value = elem.get(attr_name)
if value:
try:
# Remove any non-numeric characters (like 'pt', 'px', etc.)
clean_value = re.sub(r"[^0-9.]", "", value)
if clean_value:
return float(clean_value)
except (ValueError, TypeError):
pass
# Read the leading number and ignore a unit suffix like
# 'pt' or 'px'. The sign is part of it: a drawing anchored
# above or left of its reference point carries a legal
# negative offset, and dropping the minus turns it into a
# position further down. A value with no number at all
# falls through to the next candidate.
match = re.search(r"-?\d*\.?\d+", value)
if match:
return float(match.group())

# Check for position in transform attribute
transform = elem.get("transform")
Expand Down
70 changes: 70 additions & 0 deletions tests/test_backend_msword.py
Original file line number Diff line number Diff line change
Expand Up @@ -2472,6 +2472,76 @@ def test_footnotes_and_endnotes_are_not_dropped():
)


def _position_of(value: str, attr: str = "y") -> float | int | None:
"""Read one position attribute back through _get_paragraph_position."""
from lxml import etree

from docling.backend.msword_backend import MsWordDocumentBackend

backend = MsWordDocumentBackend.__new__(MsWordDocumentBackend)
elem = etree.fromstring(
f'<p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
f' {attr}="{value}"/>'
)

return MsWordDocumentBackend._get_paragraph_position(backend, elem)


@pytest.mark.parametrize("attr", ["y", "top", "positionY", "y-position"])
def test_get_paragraph_position_keeps_a_negative_sign(attr: str):
"""A drawing anchored above its reference point has a legal negative offset.

The position feeds the top-to-bottom sort of floating textbox paragraphs, so
dropping the sign moves that drawing to the wrong end of the document.
"""
assert _position_of("-45.5", attr=attr) == -45.5


def test_get_paragraph_position_still_strips_units():
assert _position_of("45.5pt") == 45.5


@pytest.mark.parametrize(
("value", "expected"), [("2e-5", 2.0), ("-2e-5", -2.0), ("1.5e-3in", 1.5)]
)
def test_get_paragraph_position_reads_exponent_notation_as_its_mantissa(
value: str, expected: float
):
"""A value in exponent notation is read, not dropped and not inflated.

Deleting every character outside the digits, the period and the minus leaves
"2-5" for "2e-5", which float() rejects, so the offset was swallowed by the
except and the drawing fell back to its source line. Deleting the minus too,
as the code did before this branch, leaves "25": an offset a million times
too large. Matching the number reads what is written, sign included. Word
writes these lengths with a unit suffix rather than an exponent, so this is
about the parser staying honest on input it did not expect.
"""
assert _position_of(value) == expected


@pytest.mark.parametrize(("value", "expected"), [(".5", 0.5), ("-.5", -0.5)])
def test_get_paragraph_position_keeps_a_decimal_without_its_leading_zero(
value: str, expected: float
):
r"""A decimal written as ".5" keeps its magnitude and its sign.

This one guards the pattern rather than the fix: `-?\d+(?:\.\d+)?` requires a
digit before the period, so it would match the "5" in ".5" and return 5.0,
ten times too large and positive for "-.5".
"""
assert _position_of(value) == expected


def test_get_paragraph_position_falls_through_a_value_with_no_number():
"""An attribute like y="auto" holds no offset, so the next hint decides.

re.search returns None there, and the source line is the documented last
resort of this method. A single parsed element is on line 1.
"""
assert _position_of("auto") == 1


@pytest.mark.parametrize(
("note_body", "expected"),
[
Expand Down
Loading