aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2025-11-20 15:32:08 +0000
committerMichael Williamson <mike@zwobble.org>2025-11-20 15:32:08 +0000
commit685e1e0fb3e7dc854a124c863da45bd018b2936b (patch)
tree262ae806ffb7056bf7b7f56babc2b17c56c4007b
parent300e74989af1a9d7665c192b99a96918b3685ce9 (diff)
Handle hyperlinked wp:anchor and wp:inline elements
-rw-r--r--NEWS4
-rw-r--r--mammoth/docx/body_xml.py29
-rw-r--r--tests/docx/body_xml_tests.py47
-rw-r--r--tests/docx/document_matchers.py1
4 files changed, 69 insertions, 12 deletions
diff --git a/NEWS b/NEWS
index 45bbbb8..1f91b2d 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,7 @@
+# 1.12.0
+
+* Handle hyperlinked wp:anchor and wp:inline elements.
+
# 1.11.0
* Ignore style definitions using a style ID that has already been used.
diff --git a/mammoth/docx/body_xml.py b/mammoth/docx/body_xml.py
index e2a490a..581c9e2 100644
--- a/mammoth/docx/body_xml.py
+++ b/mammoth/docx/body_xml.py
@@ -516,29 +516,46 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f
def inline(element):
- properties = element.find_child_or_null("wp:docPr").attributes
+ properties_element = element.find_child_or_null("wp:docPr")
+
+ properties = properties_element.attributes
if properties.get("descr", "").strip():
alt_text = properties.get("descr")
else:
alt_text = properties.get("title")
+
+ hlink_click_element = properties_element.find_child_or_null("a:hlinkClick")
+ hyperlink_relationship_id = hlink_click_element.attributes.get("r:id")
+ if hyperlink_relationship_id:
+ href = relationships.find_target_by_relationship_id(hyperlink_relationship_id)
+ else:
+ href = None
+
blips = element.find_children("a:graphic") \
.find_children("a:graphicData") \
.find_children("pic:pic") \
.find_children("pic:blipFill") \
.find_children("a:blip")
- return _read_blips(blips, alt_text)
+ return _read_blips(blips, alt_text=alt_text, href=href)
- def _read_blips(blips, alt_text):
- return _ReadResult.concat(lists.map(lambda blip: _read_blip(blip, alt_text), blips))
+ def _read_blips(blips, alt_text, href):
+ return _ReadResult.concat(lists.map(lambda blip: _read_blip(blip, alt_text=alt_text, href=href), blips))
- def _read_blip(element, alt_text):
+ def _read_blip(element, alt_text, href):
blip_image = _find_blip_image(element)
if blip_image is None:
warning = results.warning("Could not find image file for a:blip element")
return _empty_result_with_message(warning)
+
+ result = _read_image(blip_image, alt_text)
+ if href is None:
+ return result
else:
- return _read_image(blip_image, alt_text)
+ return result.map(lambda image_elements: documents.hyperlink(
+ image_elements,
+ href=href,
+ ))
def _read_image(image_file, alt_text):
image_path, open_image = image_file
diff --git a/tests/docx/body_xml_tests.py b/tests/docx/body_xml_tests.py
index 5d9766b..3b62944 100644
--- a/tests/docx/body_xml_tests.py
+++ b/tests/docx/body_xml_tests.py
@@ -22,6 +22,7 @@ from .document_matchers import (
is_text,
is_table,
is_row,
+ is_image,
)
from ..testing import assert_equal
@@ -1356,10 +1357,10 @@ class ImageTests(object):
IMAGE_BYTES = b"Not an image at all!"
IMAGE_RELATIONSHIP_ID = "rId5"
- def _read_embedded_image(self, element):
+ def _read_embedded_image(self, element, relationships=None):
relationships = Relationships([
_image_relationship(self.IMAGE_RELATIONSHIP_ID, "media/hat.png"),
- ])
+ ] + (relationships or []))
mocks = funk.Mocks()
docx_file = mocks.mock()
@@ -1555,6 +1556,31 @@ class ImageTests(object):
assert_equal([], result.messages)
assert_equal(None, result.value)
+ def test_can_read_pictures_with_hyperlink_specified_in_document_properties(self):
+ drawing_element = _create_inline_image(
+ blip=_embedded_blip(self.IMAGE_RELATIONSHIP_ID),
+ doc_pr_children=[
+ xml_element("a:hlinkClick", {"r:id": "rId42"})
+ ],
+ )
+
+ result = self._read_embedded_image(
+ drawing_element,
+ relationships=[_hyperlink_relationship("rId42", "http://example.com")],
+ )
+
+ assert_that(result, is_hyperlink(
+ href="http://example.com",
+ children=is_sequence(
+ is_image(
+ content_type="image/png",
+ )
+ )
+ ))
+ image = result.children[0]
+ with image.open() as image_file:
+ assert_equal(self.IMAGE_BYTES, image_file.read())
+
def test_footnote_reference_has_id_read():
footnote_xml = xml_element("w:footnoteReference", {"w:id": "4"})
@@ -1710,9 +1736,18 @@ def _text_element(value):
return xml_element("w:t", {}, [xml_text(value)])
-def _create_inline_image(blip, description=None, title=None):
+def _create_inline_image(blip, description=None, doc_pr_children=None, title=None):
return xml_element("w:drawing", {}, [
- xml_element("wp:inline", {}, _create_image_elements(blip, description=description, title=title))
+ xml_element(
+ "wp:inline",
+ {},
+ _create_image_elements(
+ blip,
+ description=description,
+ doc_pr_children=doc_pr_children,
+ title=title,
+ )
+ ),
])
@@ -1722,7 +1757,7 @@ def _create_anchored_image(description, blip):
])
-def _create_image_elements(blip, description=None, title=None):
+def _create_image_elements(blip, description=None, doc_pr_children=None, title=None):
properties = {}
if description is not None:
properties["descr"] = description
@@ -1730,7 +1765,7 @@ def _create_image_elements(blip, description=None, title=None):
properties["title"] = title
return [
- xml_element("wp:docPr", properties),
+ xml_element("wp:docPr", properties, doc_pr_children or []),
xml_element("a:graphic", {}, [
xml_element("a:graphicData", {}, [
xml_element("pic:pic", {}, [
diff --git a/tests/docx/document_matchers.py b/tests/docx/document_matchers.py
index 6b02cf9..ef2100e 100644
--- a/tests/docx/document_matchers.py
+++ b/tests/docx/document_matchers.py
@@ -19,6 +19,7 @@ is_hyperlink = create_element_matcher(documents.Hyperlink)
is_checkbox = create_element_matcher(documents.Checkbox)
is_table = create_element_matcher(documents.Table)
is_row = create_element_matcher(documents.TableRow)
+is_image = create_element_matcher(documents.Image)
is_empty_run = is_run(children=[])