package org.ggp.base.util.ui; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import nu.validator.htmlparser.dom.HtmlDocumentBuilder; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xhtmlrenderer.resource.XMLResource; import org.xhtmlrenderer.swing.Java2DRenderer; import org.xhtmlrenderer.swing.NaiveUserAgent; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * GameStateRenderer generates an image that represents the current state * of a match, based on the current state of the match (in XML) and an XSLT * that converts that XML match state into HTML. After rendering the match * state in HTML as a DOM, it renders that DOM into a BufferedImage which * can be displayed to the user. * * TODO: This class is still pretty rough, and I suspect there's much room * for improvement. Furthermore, improving this class will yield immediate * visible benefits, in terms of better visualizations and such. For example, * when rendering games that don't take up the full 600x600 image, there's an * empty black space on the final image, which looks bad. That could be fixed. * * @author Ethan Dreyfuss and Sam Schreiber */ public class GameStateRenderer { private static final Dimension defaultSize = new Dimension(600,600); /* Note: NaiveUserAgent is not thread safe, so whenever the architecture * of this class is modified in a way that enables concurrent rendering, * this must be changed to another UserAgentCallback implementation that * uses a thread safe image cache */ private static NaiveUserAgent userAgent = new NaiveUserAgent(128); public static Dimension getDefaultSize() { return defaultSize; } public static synchronized void renderImagefromGameXML(String gameXML, String XSL, BufferedImage backimage) { String xhtml = getXHTMLfromGameXML(gameXML, XSL); InputSource is = new InputSource(new BufferedReader(new StringReader(xhtml))); Document dom; try { dom = new HtmlDocumentBuilder().parse(is); // Many existing visualization stylesheets have style elements // deep within html body content, where compliant renderers interpret // them as text, and not as styles. So we have to pull them out. NodeList styles = dom.getElementsByTagName("style"); Node head = dom.getElementsByTagName("head").item(0); for (int i = 0; i < styles.getLength(); i += 1) { Node parent = styles.item(i).getParentNode(); if (!parent.equals(head) && parent.getNamespaceURI().contains("html")) { head.appendChild(styles.item(i)); } } Node style = dom.createElement("style"); String bodyStyle = String.format("body { width: %dpx; height: %dpx; overflow:hidden; margin:auto;}", defaultSize.width, defaultSize.height); style.appendChild(dom.createTextNode(bodyStyle)); head.appendChild(style); } catch (SAXException | IOException ex) { xhtml = "<html><head><title>Error</title></head><body><h1>Error parsing visualization</h1><pre id='pre'></pre></body></html>"; dom = XMLResource.load(new StringReader(xhtml)).getDocument(); dom.getElementById("pre").appendChild(dom.createTextNode(ex.toString())); ex.printStackTrace(); } Java2DRenderer r = new Java2DRenderer(dom, backimage.getWidth(), backimage.getHeight()); r.getSharedContext().setUserAgentCallback(userAgent); ChainingReplacedElementFactory chainingReplacedElementFactory = new ChainingReplacedElementFactory(); chainingReplacedElementFactory.addReplacedElementFactory(r.getSharedContext().getReplacedElementFactory()); chainingReplacedElementFactory.addReplacedElementFactory(new SVGReplacedElementFactory()); r.getSharedContext().setReplacedElementFactory(chainingReplacedElementFactory); backimage.setData(r.getImage().getData()); } public static synchronized void shrinkCache() { userAgent.shrinkImageCache(); } private static String getXHTMLfromGameXML(String gameXML, String XSL) { IOString game = new IOString(gameXML); IOString xslIOString = new IOString(XSL); IOString content = new IOString(""); try { TransformerFactory tFactory = TransformerFactory.newInstance(); Transformer transformer = tFactory.newTransformer(new StreamSource(xslIOString.getInputStream())); transformer.setParameter("width", defaultSize.getWidth()-40); transformer.setParameter("height", defaultSize.getHeight()-40); transformer.transform(new StreamSource(game.getInputStream()), new StreamResult(content.getOutputStream())); } catch (Exception ex) { ex.printStackTrace(); } return content.getString(); } //========IOstring code======== private static class IOString { private StringBuilder buf; public IOString(String s) { buf = new StringBuilder(s); } public String getString() { return buf.toString(); } public InputStream getInputStream() { return new IOString.IOStringInputStream(); } public OutputStream getOutputStream() { return new IOString.IOStringOutputStream(); } class IOStringInputStream extends java.io.InputStream { private int position = 0; @Override public int read() throws java.io.IOException { if (position < buf.length()) { return buf.charAt(position++); } else { return -1; } } } class IOStringOutputStream extends java.io.OutputStream { @Override public void write(int character) throws java.io.IOException { buf.append((char)character); } } } }