aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2025-11-20 15:56:45 +0000
committerMichael Williamson <mike@zwobble.org>2025-11-20 15:56:45 +0000
commit076de3f0567b44e349f7f7af99e5c4ad215646a2 (patch)
tree7e83c40fd37aa8d7c8586af9c464135968e1311f
parent49105a4ae164dda057b4b883f19533048278138d (diff)
Handle hyperlinked wp:anchor and wp:inline elements
-rw-r--r--NEWS4
-rw-r--r--README.md1
-rw-r--r--src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java74
-rw-r--r--src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java52
-rw-r--r--src/test/java/org/zwobble/mammoth/tests/docx/DocumentMatchers.java5
5 files changed, 119 insertions, 17 deletions
diff --git a/NEWS b/NEWS
index 372e64b..6896e19 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/README.md b/README.md
index f9d3df1..30da8c2 100644
--- a/README.md
+++ b/README.md
@@ -330,6 +330,7 @@ For instance:
automatically convert the document into HTML on the server,
and embed the HTML into your website,
this may allow arbitrary files on the server to be read and exfiltrated.
+
To avoid this issue, access to any such external files is disabled by default.
To enable access when converting trusted source documents,
call `enableExternalFileAccess()`.
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java
index 1f5aff4..9983d09 100644
--- a/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java
+++ b/src/main/java/org/zwobble/mammoth/internal/docx/StatefulBodyXmlReader.java
@@ -694,34 +694,52 @@ class StatefulBodyXmlReader {
private ReadResult readInline(XmlElement element) {
XmlElementLike properties = element.findChildOrEmpty("wp:docPr");
+
Optional<String> altText = Optionals.first(
properties.getAttributeOrNone("descr").filter(description -> !description.trim().isEmpty()),
properties.getAttributeOrNone("title")
);
+
+ XmlElementLike hlinkClickElement = properties.findChildOrEmpty("a:hlinkClick");
+ Optional<String> href = hlinkClickElement.getAttributeOrNone("r:id")
+ .map(relationships::findTargetByRelationshipId);
+
XmlElementList blips = element.findChildren("a:graphic")
.findChildren("a:graphicData")
.findChildren("pic:pic")
.findChildren("pic:blipFill")
.findChildren("a:blip");
- return readBlips(blips, altText);
+ return readBlips(blips, altText, href);
}
- private ReadResult readBlips(XmlElementList blips, Optional<String> altText) {
- return ReadResult.flatMap(blips, blip -> readBlip(blip, altText));
+ private ReadResult readBlips(
+ XmlElementList blips,
+ Optional<String> altText,
+ Optional<String> href
+ ) {
+ return ReadResult.flatMap(blips, blip -> readBlip(blip, altText, href));
}
- private ReadResult readBlip(XmlElement blip, Optional<String> altText) {
- Optional<String> embedRelationshipId = blip.getAttributeOrNone("r:embed");
- Optional<String> linkRelationshipId = blip.getAttributeOrNone("r:link");
- if (embedRelationshipId.isPresent()) {
- String imagePath = relationshipIdToDocxPath(embedRelationshipId.get());
- return readImage(imagePath, altText, () -> Archives.getInputStream(file, imagePath));
- } else if (linkRelationshipId.isPresent()) {
- String imagePath = relationships.findTargetByRelationshipId(linkRelationshipId.get());
- return readImage(imagePath, altText, () -> fileReader.getInputStream(imagePath));
- } else {
+ private ReadResult readBlip(
+ XmlElement blip,
+ Optional<String> altText,
+ Optional<String> href
+ ) {
+ Optional<BlipImage> blipImage = findBlipImage(blip);
+
+ if (!blipImage.isPresent()) {
return ReadResult.emptyWithWarning("Could not find image file for a:blip element");
}
+
+ ReadResult result = readImage(blipImage.get().path, altText, blipImage.get().open);
+
+ if (!href.isPresent()) {
+ return result;
+ }
+
+ return result.map(imageElements -> list(
+ new Hyperlink(href, Optional.empty(), Optional.empty(), imageElements)
+ ));
}
private ReadResult readImage(String imagePath, Optional<String> altText, InputStreamSupplier open) {
@@ -736,6 +754,36 @@ class StatefulBodyXmlReader {
}
}
+ private static class BlipImage {
+ private final String path;
+ private final InputStreamSupplier open;
+
+ public BlipImage(String path, InputStreamSupplier open) {
+ this.path = path;
+ this.open = open;
+ }
+ }
+
+ private Optional<BlipImage> findBlipImage(XmlElement blip) {
+ Optional<String> embedRelationshipId = blip.getAttributeOrNone("r:embed");
+ Optional<String> linkRelationshipId = blip.getAttributeOrNone("r:link");
+ if (embedRelationshipId.isPresent()) {
+ String imagePath = relationshipIdToDocxPath(embedRelationshipId.get());
+ return Optional.of(new BlipImage(
+ imagePath,
+ () -> Archives.getInputStream(file, imagePath)
+ ));
+ } else if (linkRelationshipId.isPresent()) {
+ String imagePath = relationships.findTargetByRelationshipId(linkRelationshipId.get());
+ return Optional.of(new BlipImage(
+ imagePath,
+ () -> fileReader.getInputStream(imagePath)
+ ));
+ } else {
+ return Optional.empty();
+ }
+ }
+
private ReadResult readSdt(XmlElement element) {
ReadResult contentResult = readElements(
element.findChildOrEmpty("w:sdtContent").getChildren()
diff --git a/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java b/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java
index ea3e3f3..71d8627 100644
--- a/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java
+++ b/src/test/java/org/zwobble/mammoth/tests/docx/BodyXmlTests.java
@@ -1944,6 +1944,42 @@ public class BodyXmlTests {
assertThat(result, isInternalSuccess(empty()));
}
+ @Test
+ public void canReadPicturesWithHyperlinkSpecifiedInDocumentProperties() throws IOException {
+ XmlElement element = element("w:drawing", list(
+ element("wp:inline", list(
+ element("wp:docPr", map(), list(
+ element("a:hlinkClick", map("r:id", "rId42"))
+ )),
+ graphicXml(embeddedBlipXml(IMAGE_RELATIONSHIP_ID))
+ ))
+ ));
+ Relationships relationships = new Relationships(list(
+ imageRelationship(IMAGE_RELATIONSHIP_ID, "media/hat.png"),
+ hyperlinkRelationship("rId42", "http://example.com")
+ ));
+ Archive file = InMemoryArchive.fromStrings(map("word/media/hat.png", IMAGE_BYTES));
+
+ DocumentElement result = readSuccess(
+ bodyReader(relationships, file),
+ element
+ );
+
+ assertThat(result, isHyperlink(
+ hasHref("http://example.com"),
+ hasChildren(
+ isImage(
+ hasProperty("contentType", deepEquals(Optional.of("image/png")))
+ )
+ )
+ ));
+ Image image = (Image) ((Hyperlink) result).getChildren().get(0);
+ assertThat(
+ toString(image.open()),
+ equalTo(IMAGE_BYTES)
+ );
+ }
+
private XmlElement inlineImageXml(XmlElement blip, String description) {
return inlineImageXml(blip, Optional.of(description), Optional.empty());
}
@@ -1965,10 +2001,18 @@ public class BodyXmlTests {
return list(
element("wp:docPr", properties),
- element("a:graphic", list(
- element("a:graphicData", list(
- element("pic:pic", list(
- element("pic:blipFill", list(blip)))))))));
+ graphicXml(blip)
+ );
+ }
+
+ private XmlNode graphicXml(XmlElement blip) {
+ return element("a:graphic", list(
+ element("a:graphicData", list(
+ element("pic:pic", list(
+ element("pic:blipFill", list(blip))
+ ))
+ ))
+ ));
}
private XmlElement embeddedBlipXml(String relationshipId) {
diff --git a/src/test/java/org/zwobble/mammoth/tests/docx/DocumentMatchers.java b/src/test/java/org/zwobble/mammoth/tests/docx/DocumentMatchers.java
index 67e15f0..47c5d03 100644
--- a/src/test/java/org/zwobble/mammoth/tests/docx/DocumentMatchers.java
+++ b/src/test/java/org/zwobble/mammoth/tests/docx/DocumentMatchers.java
@@ -122,4 +122,9 @@ public class DocumentMatchers {
static Matcher<Hyperlink> hasNoTargetFrame() {
return hasProperty("targetFrame", equalTo(Optional.empty()));
}
+
+ @SafeVarargs
+ static Matcher<DocumentElement> isImage(Matcher<? super Image>... matchers) {
+ return cast(Image.class, allOf(matchers));
+ }
}