diff options
| author | Michael Williamson <mike@zwobble.org> | 2025-11-20 15:32:08 +0000 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2025-11-20 15:32:08 +0000 |
| commit | 685e1e0fb3e7dc854a124c863da45bd018b2936b (patch) | |
| tree | 262ae806ffb7056bf7b7f56babc2b17c56c4007b | |
| parent | 300e74989af1a9d7665c192b99a96918b3685ce9 (diff) | |
Handle hyperlinked wp:anchor and wp:inline elements
| -rw-r--r-- | NEWS | 4 | ||||
| -rw-r--r-- | mammoth/docx/body_xml.py | 29 | ||||
| -rw-r--r-- | tests/docx/body_xml_tests.py | 47 | ||||
| -rw-r--r-- | tests/docx/document_matchers.py | 1 |
4 files changed, 69 insertions, 12 deletions
@@ -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=[]) |
