From c2a59cf722f23d7f114e73716620ff17c7ffb9f6 Mon Sep 17 00:00:00 2001 From: Emmeran Seehuber Date: Sun, 26 Feb 2017 15:58:07 +0100 Subject: [PATCH] #23 Add the possibility to also render SVGs when drawing with Graphics2D --- .../simple/Java2DRendererBuilder.java | 397 ++++++++++++++++++ .../swing/Java2DOutputDevice.java | 11 +- .../testcases/TestcaseRunner.java | 66 +-- .../main/resources/testcases/moonbase.html | 1 + 4 files changed, 443 insertions(+), 32 deletions(-) create mode 100644 openhtmltopdf-core/src/main/java/com/openhtmltopdf/simple/Java2DRendererBuilder.java create mode 100644 openhtmltopdf-examples/src/main/resources/testcases/moonbase.html diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/simple/Java2DRendererBuilder.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/simple/Java2DRendererBuilder.java new file mode 100644 index 000000000..88f77428d --- /dev/null +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/simple/Java2DRendererBuilder.java @@ -0,0 +1,397 @@ +package com.openhtmltopdf.simple; + +import com.openhtmltopdf.css.parser.property.PrimitivePropertyBuilders; +import com.openhtmltopdf.extend.*; +import com.openhtmltopdf.layout.LayoutContext; +import com.openhtmltopdf.render.BlockBox; +import com.openhtmltopdf.render.RenderingContext; +import com.openhtmltopdf.simple.extend.XhtmlNamespaceHandler; +import com.openhtmltopdf.swing.EmptyReplacedElement; +import com.openhtmltopdf.swing.NaiveUserAgent; +import com.openhtmltopdf.swing.SwingReplacedElementFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Build a Java2D renderer for a given + */ +public class Java2DRendererBuilder { + private HttpStreamFactory _httpStreamFactory; + private FSCache _cache; + private FSUriResolver _resolver; + private String _html; + private String _baseUri; + private Document _document; + private SVGDrawer _svgImpl; + private String _replacementText; + private FSTextBreaker _lineBreaker; + private FSTextBreaker _charBreaker; + private FSTextTransformer _unicodeToUpperTransformer; + private FSTextTransformer _unicodeToLowerTransformer; + private FSTextTransformer _unicodeToTitleTransformer; + private List _fonts = new ArrayList(); + + private static class AddedFont { + private final FSSupplier supplier; + private final Integer weight; + private final String family; + private final boolean subset; + private final PrimitivePropertyBuilders.FontStyle style; + + private AddedFont(FSSupplier supplier, Integer weight, String family, boolean subset, + PrimitivePropertyBuilders.FontStyle style) { + this.supplier = supplier; + this.weight = weight; + this.family = family; + this.subset = subset; + this.style = style; + } + } + + /** + * Provides an HttpStreamFactory implementation if the user desires to use + * an external HTTP/HTTPS implementation. Uses URL::openStream by default. + * + * @param factory + * @return + */ + public Java2DRendererBuilder useHttpStreamImplementation(HttpStreamFactory factory) { + this._httpStreamFactory = factory; + return this; + } + + /** + * Provides a uri resolver to resolve relative uris or private uri schemes. + * + * @param resolver + * @return + */ + public Java2DRendererBuilder useUriResolver(FSUriResolver resolver) { + this._resolver = resolver; + return this; + } + + /** + * Provides an external cache which can choose to cache items between runs, + * such as fonts or logo images. + * + * @param cache + * @return + */ + public Java2DRendererBuilder useCache(FSCache cache) { + this._cache = cache; + return this; + } + + /** + * Provides a string containing XHTML/XML to convert to PDF. + * + * @param html + * @param baseUri + * @return + */ + public Java2DRendererBuilder withHtmlContent(String html, String baseUri) { + this._html = html; + this._baseUri = baseUri; + return this; + } + + /** + * Provides a w3c DOM Document acquired from an external source. + * + * @param doc + * @param baseUri + * @return + */ + public Java2DRendererBuilder withW3cDocument(org.w3c.dom.Document doc, String baseUri) { + this._document = doc; + this._baseUri = baseUri; + return this; + } + + /** + * Uses the specified SVG drawer implementation. + * + * @param svgImpl + * @return + */ + public Java2DRendererBuilder useSVGDrawer(SVGDrawer svgImpl) { + this._svgImpl = svgImpl; + return this; + } + + /** + * The replacement text to use if a character is cannot be renderered by any + * of the specified fonts. This is not broken across lines so should be one + * or zero characters for best results. Also, make sure it can be rendered + * by at least one of your specified fonts! The default is the # character. + * + * @param replacement + * @return + */ + public Java2DRendererBuilder useReplacementText(String replacement) { + this._replacementText = replacement; + return this; + } + + /** + * Specify the line breaker. By default a Java default BreakIterator line + * instance is used with US locale. Additionally, this is wrapped with + * UrlAwareLineBreakIterator to also break before the forward slash (/) + * character so that long URIs can be broken on to multiple lines. + *

+ * You may want to use a BreakIterator with a different locale (wrapped by + * UrlAwareLineBreakIterator or not) or a more advanced BreakIterator from + * icu4j (see the rtl-support module for an example). + * + * @param breaker + * @return + */ + public Java2DRendererBuilder useUnicodeLineBreaker(FSTextBreaker breaker) { + this._lineBreaker = breaker; + return this; + } + + /** + * Specify the character breaker. By default a break iterator character + * instance is used with US locale. Currently this is used when + * word-wrap: break-word is in effect. + * + * @param breaker + * @return + */ + public Java2DRendererBuilder useUnicodeCharacterBreaker(FSTextBreaker breaker) { + this._charBreaker = breaker; + return this; + } + + /** + * Specify a transformer to use to upper case strings. By default + * String::toUpperCase(Locale.US) is used. + * + * @param tr + * @return + */ + public Java2DRendererBuilder useUnicodeToUpperTransformer(FSTextTransformer tr) { + this._unicodeToUpperTransformer = tr; + return this; + } + + /** + * Specify a transformer to use to lower case strings. By default + * String::toLowerCase(Locale.US) is used. + * + * @param tr + * @return + */ + public Java2DRendererBuilder useUnicodeToLowerTransformer(FSTextTransformer tr) { + this._unicodeToLowerTransformer = tr; + return this; + } + + /** + * Specify a transformer to title case strings. By default a best effort + * implementation (non locale aware) is used. + * + * @param tr + * @return + */ + public Java2DRendererBuilder useUnicodeToTitleTransformer(FSTextTransformer tr) { + this._unicodeToTitleTransformer = tr; + return this; + } + + /** + * Built renderer, which can be used to render the document as image or to a + * Graphics2D + */ + public interface IJava2DRenderer { + /** + * Builds an Image + */ + BufferedImage renderToImage(int width, int bufferedImageType); + + /** + * Layout the HTML for the given width + */ + void layout(int width); + + /** + * Draw the HTML on the given Graphics2D. It will be drawn on (0,0). So + * if you want the rendering somewhere else you should first apply the + * needed transforms and or clippings on gfx before calling this method. + * + * Note: You must call layout() at least once before calling this + * method! + * + * @param gfx + * graphics 2D to draw to. + */ + void render(Graphics2D gfx); + + } + + /** + * Build a renderer + * + * @return a renderer which can be used to create images or draw to a + * Graphics2D + */ + public IJava2DRenderer build() { + final XHTMLPanel panel = new XHTMLPanel(); + panel.setInteractive(false); + + NaiveUserAgent userAgent = new NaiveUserAgent(); + userAgent.setBaseURL(_baseUri); + if (_httpStreamFactory != null) + userAgent.setHttpStreamFactory(_httpStreamFactory); + + if (_resolver != null) + userAgent.setUriResolver(_resolver); + + if (_cache != null) + userAgent.setExternalCache(_cache); + + panel.getSharedContext().setUserAgentCallback(userAgent); + Java2DReplacedElementFactory ref = new Java2DReplacedElementFactory(); + ref._svgImpl = _svgImpl; + panel.getSharedContext().setReplacedElementFactory(ref); + + if (_html != null) + panel.setDocumentFromString(_html, _baseUri, new XhtmlNamespaceHandler()); + if (_document != null) + panel.setDocument(_document, _baseUri, new XhtmlNamespaceHandler()); + + return new IJava2DRenderer() { + + @Override + public BufferedImage renderToImage(int width, int bufferedImageType) { + layout(width); + + // get size + Rectangle rect; + if (panel.getPreferredSize() != null) { + rect = new Rectangle(0, 0, (int) panel.getPreferredSize().getWidth(), + (int) panel.getPreferredSize().getHeight()); + } else { + rect = new Rectangle(0, 0, panel.getWidth(), panel.getHeight()); + } + + // render into real buffer + BufferedImage buff = new BufferedImage((int) rect.getWidth(), (int) rect.getHeight(), + bufferedImageType); + Graphics2D g = (Graphics2D) buff.getGraphics(); + if (buff.getColorModel().hasAlpha()) { + g.clearRect(0, 0, (int) rect.getWidth(), (int) rect.getHeight()); + } else { + g.setColor(Color.WHITE); + g.fillRect(0, 0, (int) rect.getWidth(), (int) rect.getHeight()); + } + render(g); + g.dispose(); + + return buff; + } + + @Override + public void layout(int width) { + Dimension dim = new Dimension(width, 100); + + // do layout with temp buffer + BufferedImage buff = new BufferedImage((int) dim.getWidth(), (int) dim.getHeight(), + BufferedImage.TYPE_3BYTE_BGR); + Graphics2D g = (Graphics2D) buff.getGraphics(); + panel.setSize(dim); + panel.doDocumentLayout(g); + g.dispose(); + + } + + @Override + public void render(Graphics2D gfx) { + panel.paintComponent(gfx); + } + }; + } + + public static abstract class Graphics2DPaintingReplacedElement extends EmptyReplacedElement { + protected Graphics2DPaintingReplacedElement(int width, int height) { + super(width, height); + } + + public abstract void paint(OutputDevice outputDevice, RenderingContext ctx, double x, double y, double width, + double height); + + public static double DOTS_PER_INCH = 72.0; + } + + private static class Java2DSVGReplacedElement extends Graphics2DPaintingReplacedElement { + private final SVGDrawer _svgImpl; + private final Element e; + + public Java2DSVGReplacedElement(Element e, SVGDrawer svgImpl, int width, int height) { + super(width, height); + this.e = e; + this._svgImpl = svgImpl; + } + + @Override + public void paint(OutputDevice outputDevice, RenderingContext ctx, double x, double y, double width, + double height) { + _svgImpl.drawSVG(e, outputDevice, ctx, x, y, width, height, DOTS_PER_INCH); + } + + @Override + public int getIntrinsicWidth() { + if (super.getIntrinsicWidth() >= 0) { + // CSS takes precedence over width and height defined on + // element. + return super.getIntrinsicWidth(); + } else { + // Seems to need dots rather than pixels. + return this._svgImpl.getSVGWidth(e); + } + } + + @Override + public int getIntrinsicHeight() { + if (super.getIntrinsicHeight() >= 0) { + // CSS takes precedence over width and height defined on + // element. + return super.getIntrinsicHeight(); + } else { + // Seems to need dots rather than pixels. + return this._svgImpl.getSVGHeight(e); + } + } + } + + public static class Java2DReplacedElementFactory extends SwingReplacedElementFactory { + private SVGDrawer _svgImpl; + + @Override + public ReplacedElement createReplacedElement(LayoutContext context, BlockBox box, UserAgentCallback uac, + int cssWidth, int cssHeight) { + Element e = box.getElement(); + if (e == null) { + return null; + } + + String nodeName = e.getNodeName(); + if (nodeName.equals("svg") && _svgImpl != null) { + return new Java2DSVGReplacedElement(e, _svgImpl, cssWidth, cssHeight); + /* + * Default: Just let the base class handle everything + */ + } + return super.createReplacedElement(context, box, uac, cssWidth, cssHeight); + } + } + +} diff --git a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/swing/Java2DOutputDevice.java b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/swing/Java2DOutputDevice.java index 2fe5131c0..bf3160aee 100644 --- a/openhtmltopdf-core/src/main/java/com/openhtmltopdf/swing/Java2DOutputDevice.java +++ b/openhtmltopdf-core/src/main/java/com/openhtmltopdf/swing/Java2DOutputDevice.java @@ -23,17 +23,18 @@ import com.openhtmltopdf.css.parser.FSRGBColor; import com.openhtmltopdf.extend.*; import com.openhtmltopdf.render.*; +import com.openhtmltopdf.simple.Java2DRendererBuilder; import javax.swing.*; - import java.awt.*; import java.awt.RenderingHints.Key; import java.awt.font.GlyphVector; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; -import java.util.*; +import java.util.Collections; import java.util.List; +import java.util.Stack; public class Java2DOutputDevice extends AbstractOutputDevice implements OutputDevice { private final Graphics2D _graphics; @@ -179,7 +180,11 @@ public void paintReplacedElement(RenderingContext c, BlockBox box) { Point location = replaced.getLocation(); _graphics.drawImage( image, (int)location.getX(), (int)location.getY(), null); - } + } else if (replaced instanceof Java2DRendererBuilder.Graphics2DPaintingReplacedElement) { + Rectangle contentBounds = box.getContentAreaEdge(box.getAbsX(), box.getAbsY(), c); + ((Java2DRendererBuilder.Graphics2DPaintingReplacedElement) replaced).paint(this, c, contentBounds.x, + contentBounds.y, contentBounds.width, contentBounds.height); + } } public void setColor(FSColor color) { diff --git a/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/testcases/TestcaseRunner.java b/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/testcases/TestcaseRunner.java index 3baa0bdda..996f9e6c6 100644 --- a/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/testcases/TestcaseRunner.java +++ b/openhtmltopdf-examples/src/main/java/com/openhtmltopdf/testcases/TestcaseRunner.java @@ -4,7 +4,7 @@ import com.openhtmltopdf.bidi.support.ICUBidiSplitter; import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; import com.openhtmltopdf.pdfboxout.PdfRendererBuilder.TextDirection; -import com.openhtmltopdf.simple.Graphics2DRenderer; +import com.openhtmltopdf.simple.Java2DRendererBuilder; import com.openhtmltopdf.svgsupport.BatikSVGDrawer; import com.openhtmltopdf.util.JDKXRLogger; import com.openhtmltopdf.util.XRLog; @@ -18,27 +18,28 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.net.URL; import java.util.ArrayList; import java.util.logging.Level; public class TestcaseRunner { /** - * Runs our set of manual test cases. You can specify an output directory with - * -DOUT_DIRECTORY=./output - * for example. Otherwise, the current working directory is used. - * Test cases must be placed in src/main/resources/testcases/ + * Runs our set of manual test cases. You can specify an output directory + * with -DOUT_DIRECTORY=./output for example. Otherwise, the current working + * directory is used. Test cases must be placed in + * src/main/resources/testcases/ + * * @param args * @throws Exception */ public static void main(String[] args) throws Exception { - + /* - * Note: The RepeatedTableSample optionally requires the font file NotoSans-Regular.ttf - * to be placed in the resources directory. + * Note: The RepeatedTableSample optionally requires the font file + * NotoSans-Regular.ttf to be placed in the resources directory. * - * This sample demonstrates the failing repeated table header on each page. + * This sample demonstrates the failing repeated table header on each + * page. */ runTestCase("RepeatedTableSample"); @@ -46,7 +47,7 @@ public static void main(String[] args) throws Exception { * This sample demonstrates the -fs-pagebreak-min-height css property */ runTestCase("FSPageBreakMinHeightSample"); - + runTestCase("color"); runTestCase("background-color"); runTestCase("background-image"); @@ -60,20 +61,27 @@ public static void main(String[] args) throws Exception { */ runTestCase("svg-inline"); + /* + * Graphics2D Texterror Case + */ + runTestCase("moonbase"); + /* Add additional test cases here. */ } - + /** * Will throw an exception if a SEVERE or WARNING message is logged. + * * @param testCaseFile * @throws Exception */ public static void runTestWithoutOutput(String testCaseFile) throws Exception { runTestWithoutOutput(testCaseFile, false); } - + /** * Will silently let ALL log messages through. + * * @param testCaseFile * @throws Exception */ @@ -83,12 +91,12 @@ public static void runTestWithoutOutputAndAllowWarnings(String testCaseFile) thr private static void runTestWithoutOutput(String testCaseFile, boolean allowWarnings) throws Exception { System.out.println("Trying to run: " + testCaseFile); - - byte[] htmlBytes = IOUtils.toByteArray(TestcaseRunner.class - .getResourceAsStream("/testcases/" + testCaseFile + ".html")); + + byte[] htmlBytes = IOUtils + .toByteArray(TestcaseRunner.class.getResourceAsStream("/testcases/" + testCaseFile + ".html")); String html = new String(htmlBytes, Charsets.UTF_8); OutputStream outputStream = new ByteArrayOutputStream(4096); - + // We wan't to throw if we get a warning or severe log message. final XRLogger delegate = new JDKXRLogger(); final java.util.List warnings = new ArrayList(); @@ -96,20 +104,18 @@ private static void runTestWithoutOutput(String testCaseFile, boolean allowWarni @Override public void setLevel(String logger, Level level) { } - + @Override public void log(String where, Level level, String msg, Throwable th) { - if (level.equals(Level.WARNING) || - level.equals(Level.SEVERE)) { + if (level.equals(Level.WARNING) || level.equals(Level.SEVERE)) { warnings.add(new RuntimeException(where + ": " + msg, th)); } delegate.log(where, level, msg, th); } - + @Override public void log(String where, Level level, String msg) { - if (level.equals(Level.WARNING) || - level.equals(Level.SEVERE)) { + if (level.equals(Level.WARNING) || level.equals(Level.SEVERE)) { warnings.add(new RuntimeException(where + ": " + msg)); } delegate.log(where, level, msg); @@ -138,12 +144,14 @@ private static void renderPDF(String html, OutputStream outputStream) throws Exc } } - private static void renderPNG(URL f, OutputStream outputStream) throws IOException { - BufferedImage image = Graphics2DRenderer.renderToImageAutoSize(f.toExternalForm(), - 512, BufferedImage.TYPE_INT_ARGB); - ImageIO.write(image,"PNG",outputStream); + private static void renderPNG(String html, OutputStream outputStream) throws IOException { + Java2DRendererBuilder builder = new Java2DRendererBuilder(); + builder.useSVGDrawer(new BatikSVGDrawer()); + builder.withHtmlContent(html, TestcaseRunner.class.getResource("/testcases/").toString()); + BufferedImage image = builder.build().renderToImage(512, BufferedImage.TYPE_3BYTE_BGR); + ImageIO.write(image, "PNG", outputStream); outputStream.close(); - + } public static void runTestCase(String testCaseFile) throws Exception { @@ -158,6 +166,6 @@ public static void runTestCase(String testCaseFile) throws Exception { System.out.println("Wrote " + testCaseOutputFile); FileOutputStream outputStreamPNG = new FileOutputStream(testCaseOutputPNGFile); - renderPNG(TestcaseRunner.class.getResource("/testcases/" + testCaseFile + ".html"), outputStreamPNG); + renderPNG(html, outputStreamPNG); } } diff --git a/openhtmltopdf-examples/src/main/resources/testcases/moonbase.html b/openhtmltopdf-examples/src/main/resources/testcases/moonbase.html new file mode 100644 index 000000000..013be9ac2 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/testcases/moonbase.html @@ -0,0 +1 @@ +Moonbase \ No newline at end of file