From 244ed7482aedf44a8afccc7b2f7f77c977974f82 Mon Sep 17 00:00:00 2001 From: Michael Williamson Date: Mon, 27 Mar 2017 19:20:19 +0100 Subject: Add custom image converters --- NEWS | 2 + README.md | 59 +++++++++++++++++++++- .../org/zwobble/mammoth/DocumentConverter.java | 9 ++++ .../java/org/zwobble/mammoth/images/Image.java | 11 ++++ .../org/zwobble/mammoth/images/ImageConverter.java | 13 +++++ .../internal/conversion/DocumentToHtml.java | 28 +++++++--- .../internal/conversion/DocumentToHtmlOptions.java | 44 +++++++++++++--- .../org/zwobble/mammoth/tests/MammothTests.java | 23 ++++++++- 8 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/zwobble/mammoth/images/Image.java create mode 100644 src/main/java/org/zwobble/mammoth/images/ImageConverter.java diff --git a/NEWS b/NEWS index 47db2c6..340dd9b 100644 --- a/NEWS +++ b/NEWS @@ -29,6 +29,8 @@ * Handle hyperlinks created with complex fields. +* Support custom image converters. + # 0.0.4 * Add support for comments. diff --git a/README.md b/README.md index 614e43c..ad3dfe3 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,26 @@ DocumentConverter converter = new DocumentConverter() .disableDefaultStyleMap(); ``` +#### Custom image handlers + +By default, images are converted to `` elements with the source included inline in the `src` attribute. +This behaviour can be changed by calling `imageConverter()` with an [image converter](#image-converters) . + +For instance, the following would replicate the default behaviour: + +```java +DocumentConverter converter = new DocumentConverter() + .imageConverter(image -> { + String base64 = streamToBase64(image::getInputStream); + String src = "data:" + image.getContentType() + ";base64," + base64; + Map attributes = new HashMap<>(); + attributes.put("src", src); + return attributes; + }); +``` + +where `streamToBase64` is a function that reads an input stream and encodes it as a Base64 string. + #### Bold By default, bold text is wrapped in `` tags. @@ -239,6 +259,10 @@ Methods: such as those used by bookmarks, footnotes and endnotes. Defaults to the empty string. +* `DocumentConverter imageConverter(ImageConverter.ImgElement imageConverter)`: + By default, images are converted to `` elements with the source included inline in the `src` attribute. + Call this to change how images are converted. + #### `Result` Represents the result of a conversion. Methods: @@ -247,6 +271,40 @@ Represents the result of a conversion. Methods: * `Set getWarnings()`: any warnings generated during the conversion. +#### Image converters + +An image converter can be created by implementing `ImageConverter.ImgElement`. +This creates an `` element for each image in the original docx. +The interface has a single method, `Map convert(Image image)`. +The `image` argument is the image element being converted, +and has the following methods: + +* `InputStream getInputStream()`: open the image file. + +* `String getContentType()`: the content type of the image, such as `image/png`. + +* `Optional getAltText()`: the alt text of the image, if any. + +`convert()` should return a `Map` of attributes for the `` element. +At a minimum, this should include the `src` attribute. +If any alt text is found for the image, +this will be automatically added to the element's attributes. + +For instance, the following replicates the default image conversion: + +```java +DocumentConverter converter = new DocumentConverter() + .imageConverter(image -> { + String base64 = streamToBase64(image::getInputStream); + String src = "data:" + image.getContentType() + ";base64," + base64; + Map attributes = new HashMap<>(); + attributes.put("src", src); + return attributes; + }); +``` + +where `streamToBase64` is a function that reads an input stream and encodes it as a Base64 string. + ## Writing style maps A style map is made up of a number of style mappings separated by new lines. @@ -441,7 +499,6 @@ You can nest elements to any depth. Compared to the JavaScript and Python implementations, the following features are currently missing: -* Custom image handlers * CLI * Writing embedded style maps * Markdown support diff --git a/src/main/java/org/zwobble/mammoth/DocumentConverter.java b/src/main/java/org/zwobble/mammoth/DocumentConverter.java index feba5ac..2dea8e8 100644 --- a/src/main/java/org/zwobble/mammoth/DocumentConverter.java +++ b/src/main/java/org/zwobble/mammoth/DocumentConverter.java @@ -1,5 +1,6 @@ package org.zwobble.mammoth; +import org.zwobble.mammoth.images.ImageConverter; import org.zwobble.mammoth.internal.InternalDocumentConverter; import org.zwobble.mammoth.internal.conversion.DocumentToHtmlOptions; @@ -59,6 +60,14 @@ public class DocumentConverter { return new DocumentConverter(options.disableEmbeddedStyleMap()); } + /** + * By default, images are converted to {@code } elements with the source included inline in the {@code src} attribute. + * Call this to change how images are converted. + */ + public DocumentConverter imageConverter(ImageConverter.ImgElement imageConverter) { + return new DocumentConverter(options.imageConverter(imageConverter)); + } + /** * Converts {@code stream} into an HTML string. * Note that using this method instead of {@link #convertToHtml(File file)} diff --git a/src/main/java/org/zwobble/mammoth/images/Image.java b/src/main/java/org/zwobble/mammoth/images/Image.java new file mode 100644 index 0000000..977e27d --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/images/Image.java @@ -0,0 +1,11 @@ +package org.zwobble.mammoth.images; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +public interface Image { + Optional getAltText(); + String getContentType(); + InputStream getInputStream() throws IOException; +} diff --git a/src/main/java/org/zwobble/mammoth/images/ImageConverter.java b/src/main/java/org/zwobble/mammoth/images/ImageConverter.java new file mode 100644 index 0000000..83480f2 --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/images/ImageConverter.java @@ -0,0 +1,13 @@ +package org.zwobble.mammoth.images; + +import java.io.IOException; +import java.util.Map; + +public class ImageConverter { + private ImageConverter() { + } + + public interface ImgElement { + Map convert(Image image) throws IOException; + } +} diff --git a/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java b/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java index cfa4f56..bdb3d01 100644 --- a/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java +++ b/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtml.java @@ -1,16 +1,17 @@ package org.zwobble.mammoth.internal.conversion; +import org.zwobble.mammoth.images.ImageConverter; import org.zwobble.mammoth.internal.documents.*; import org.zwobble.mammoth.internal.html.Html; import org.zwobble.mammoth.internal.html.HtmlNode; import org.zwobble.mammoth.internal.results.InternalResult; import org.zwobble.mammoth.internal.styles.HtmlPath; import org.zwobble.mammoth.internal.styles.StyleMap; -import org.zwobble.mammoth.internal.util.Base64Encoding; import org.zwobble.mammoth.internal.util.Lists; import org.zwobble.mammoth.internal.util.Maps; import java.io.IOException; +import java.io.InputStream; import java.util.*; import java.util.function.Supplier; @@ -53,6 +54,7 @@ public class DocumentToHtml { private final String idPrefix; private final boolean preserveEmptyParagraphs; private final StyleMap styleMap; + private final ImageConverter.ImgElement imageConverter; private final Map comments; private final List noteReferences = new ArrayList<>(); private final List referencedComments = new ArrayList<>(); @@ -62,6 +64,7 @@ public class DocumentToHtml { this.idPrefix = options.idPrefix(); this.preserveEmptyParagraphs = options.shouldPreserveEmptyParagraphs(); this.styleMap = options.styleMap(); + this.imageConverter = options.imageConverter(); this.comments = Maps.toMapWithKey(comments, Comment::getCommentId); } @@ -278,14 +281,23 @@ public class DocumentToHtml { return image.getContentType() .map(contentType -> { try { - Map attributes = new HashMap<>(); - - String base64 = Base64Encoding.streamToBase64(image::open); - String src = "data:" + contentType + ";base64," + base64; - attributes.put("src", src); - + Map attributes = new HashMap<>(imageConverter.convert(new org.zwobble.mammoth.images.Image() { + @Override + public Optional getAltText() { + return image.getAltText(); + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public InputStream getInputStream() throws IOException { + return image.open(); + } + })); image.getAltText().ifPresent(altText -> attributes.put("alt", altText)); - return list(Html.element("img", attributes)); } catch (IOException exception) { warnings.add(exception.getMessage()); diff --git a/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtmlOptions.java b/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtmlOptions.java index 8a709af..52b2558 100644 --- a/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtmlOptions.java +++ b/src/main/java/org/zwobble/mammoth/internal/conversion/DocumentToHtmlOptions.java @@ -1,11 +1,28 @@ package org.zwobble.mammoth.internal.conversion; +import org.zwobble.mammoth.images.ImageConverter; import org.zwobble.mammoth.internal.styles.DefaultStyles; import org.zwobble.mammoth.internal.styles.StyleMap; import org.zwobble.mammoth.internal.styles.parsing.StyleMapParser; +import org.zwobble.mammoth.internal.util.Base64Encoding; + +import static org.zwobble.mammoth.internal.util.Maps.map; public class DocumentToHtmlOptions { - public static final DocumentToHtmlOptions DEFAULT = new DocumentToHtmlOptions("", false, StyleMap.EMPTY, StyleMap.EMPTY, false, false); + public static final DocumentToHtmlOptions DEFAULT = new DocumentToHtmlOptions( + "", + false, + StyleMap.EMPTY, + StyleMap.EMPTY, + false, + false, + image -> { + String base64 = Base64Encoding.streamToBase64(image::getInputStream); + String src = "data:" + image.getContentType() + ";base64," + base64; + return map("src", src); + } + + ); private final String idPrefix; private final boolean preserveEmptyParagraphs; @@ -13,6 +30,7 @@ public class DocumentToHtmlOptions { private final StyleMap embeddedStyleMap; private final boolean disableDefaultStyleMap; private final boolean disableEmbeddedStyleMap; + private final ImageConverter.ImgElement imageConverter; public DocumentToHtmlOptions( String idPrefix, @@ -20,7 +38,8 @@ public class DocumentToHtmlOptions { StyleMap styleMap, StyleMap embeddedStyleMap, boolean disableDefaultStyleMap, - boolean disableEmbeddedStyleMap + boolean disableEmbeddedStyleMap, + ImageConverter.ImgElement imageConverter ) { this.idPrefix = idPrefix; this.preserveEmptyParagraphs = preserveEmptyParagraphs; @@ -28,14 +47,15 @@ public class DocumentToHtmlOptions { this.embeddedStyleMap = embeddedStyleMap; this.disableDefaultStyleMap = disableDefaultStyleMap; this.disableEmbeddedStyleMap = disableEmbeddedStyleMap; + this.imageConverter = imageConverter; } public DocumentToHtmlOptions idPrefix(String prefix) { - return new DocumentToHtmlOptions(prefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap); + return new DocumentToHtmlOptions(prefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); } public DocumentToHtmlOptions preserveEmptyParagraphs() { - return new DocumentToHtmlOptions(idPrefix, true, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap); + return new DocumentToHtmlOptions(idPrefix, true, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); } public DocumentToHtmlOptions addStyleMap(String styleMap) { @@ -43,19 +63,23 @@ public class DocumentToHtmlOptions { } public DocumentToHtmlOptions addStyleMap(StyleMap styleMap) { - return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, this.styleMap.update(styleMap), embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap); + return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, this.styleMap.update(styleMap), embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); } public DocumentToHtmlOptions disableDefaultStyleMap() { - return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, true, disableEmbeddedStyleMap); + return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, true, disableEmbeddedStyleMap, imageConverter); } public DocumentToHtmlOptions disableEmbeddedStyleMap() { - return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, true); + return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, true, imageConverter); } public DocumentToHtmlOptions addEmbeddedStyleMap(StyleMap embeddedStyleMap) { - return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap); + return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); + } + + public DocumentToHtmlOptions imageConverter(ImageConverter.ImgElement imageConverter) { + return new DocumentToHtmlOptions(idPrefix, preserveEmptyParagraphs, styleMap, embeddedStyleMap, disableDefaultStyleMap, disableEmbeddedStyleMap, imageConverter); } public String idPrefix() { @@ -77,4 +101,8 @@ public class DocumentToHtmlOptions { styleMap = styleMap.update(this.styleMap); return styleMap; } + + public ImageConverter.ImgElement imageConverter() { + return imageConverter; + } } diff --git a/src/test/java/org/zwobble/mammoth/tests/MammothTests.java b/src/test/java/org/zwobble/mammoth/tests/MammothTests.java index ac81c76..393f1a7 100644 --- a/src/test/java/org/zwobble/mammoth/tests/MammothTests.java +++ b/src/test/java/org/zwobble/mammoth/tests/MammothTests.java @@ -3,11 +3,16 @@ package org.zwobble.mammoth.tests; import org.junit.Test; import org.zwobble.mammoth.DocumentConverter; import org.zwobble.mammoth.Result; -import org.zwobble.mammoth.internal.docx.EmbeddedStyleMap; +import org.zwobble.mammoth.images.ImageConverter; import org.zwobble.mammoth.internal.archives.InMemoryArchive; +import org.zwobble.mammoth.internal.docx.EmbeddedStyleMap; import org.zwobble.mammoth.internal.styles.parsing.ParseException; +import org.zwobble.mammoth.internal.util.Base64Encoding; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Function; @@ -15,6 +20,7 @@ import java.util.function.Function; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.zwobble.mammoth.internal.util.Lists.list; +import static org.zwobble.mammoth.internal.util.Maps.map; import static org.zwobble.mammoth.tests.ResultMatchers.isResult; import static org.zwobble.mammoth.tests.ResultMatchers.isSuccess; import static org.zwobble.mammoth.tests.util.MammothAsserts.assertThrows; @@ -131,6 +137,19 @@ public class MammothTests { } } + @Test + public void imageConversionCanBeCustomised() throws IOException { + ImageConverter.ImgElement imageConverter = image -> { + String base64 = Base64Encoding.streamToBase64(image::getInputStream); + String src = base64.substring(0, 2) + "," + image.getContentType(); + return map("src", src); + }; + assertThat( + convertToHtml("tiny-picture.docx", converter -> converter.imageConverter(imageConverter)), + isSuccess("

") + ); + } + @Test public void contentTypesAreRead() throws IOException { assertThat( -- cgit v1.2.3