Skip to content

Commit

Permalink
danfickle#23 Add the possibility to also render SVGs when drawing wit…
Browse files Browse the repository at this point in the history
…h Graphics2D
  • Loading branch information
rototor committed Feb 26, 2017
1 parent 20f6d87 commit c2a59cf
Show file tree
Hide file tree
Showing 4 changed files with 443 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -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<AddedFont> _fonts = new ArrayList<AddedFont>();

private static class AddedFont {
private final FSSupplier<InputStream> supplier;
private final Integer weight;
private final String family;
private final boolean subset;
private final PrimitivePropertyBuilders.FontStyle style;

private AddedFont(FSSupplier<InputStream> 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.
* <p>
* 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
* <code>word-wrap: break-word</code> 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
* <code>String::toUpperCase(Locale.US)</code> 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
* <code>String::toLowerCase(Locale.US)</code> 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);
}
}

}
Loading

0 comments on commit c2a59cf

Please sign in to comment.