| 9 | 9 | import org.apache.batik.css.parser.Parser; |
| 10 | 10 | import org.apache.batik.gvt.renderer.ImageRenderer; |
| 11 | | import org.apache.batik.transcoder.TranscoderException; |
| 12 | | import org.apache.batik.transcoder.TranscoderInput; |
| 13 | | import org.apache.batik.transcoder.TranscoderOutput; |
| 14 | | import org.apache.batik.transcoder.image.ImageTranscoder; |
| 15 | | import org.apache.batik.util.XMLResourceDescriptor; |
| 16 | | import org.w3c.css.sac.CSSException; |
| 17 | | import org.w3c.dom.Document; |
| 18 | | import org.w3c.dom.Element; |
| 19 | | |
| 20 | | import java.awt.*; |
| 21 | | import java.awt.image.BufferedImage; |
| 22 | | import java.io.File; |
| 23 | | import java.io.IOException; |
| 24 | | import java.io.InputStream; |
| 25 | | import java.io.StringReader; |
| 26 | | import java.net.URI; |
| 27 | | import java.nio.file.Path; |
| 28 | | import java.text.NumberFormat; |
| 29 | | import java.text.ParseException; |
| 30 | | |
| 31 | | import static com.keenwrite.dom.DocumentParser.transform; |
| 32 | | import static com.keenwrite.events.StatusEvent.clue; |
| 33 | | import static com.keenwrite.preview.HighQualityRenderingHints.RENDERING_HINTS; |
| 34 | | import static java.awt.image.BufferedImage.TYPE_INT_RGB; |
| 35 | | import static java.text.NumberFormat.getIntegerInstance; |
| 36 | | import static org.apache.batik.bridge.UnitProcessor.createContext; |
| 37 | | import static org.apache.batik.bridge.UnitProcessor.svgHorizontalLengthToUserSpace; |
| 38 | | import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH; |
| 39 | | import static org.apache.batik.transcoder.TranscodingHints.Key; |
| 40 | | import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER; |
| 41 | | import static org.apache.batik.util.SVGConstants.SVG_WIDTH_ATTRIBUTE; |
| 42 | | import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName; |
| 43 | | |
| 44 | | /** |
| 45 | | * Responsible for converting SVG images into rasterized PNG images. |
| 46 | | */ |
| 47 | | public final class SvgRasterizer { |
| 48 | | /** |
| 49 | | * <a href="https://issues.apache.org/jira/browse/BATIK-1112">Bug fix</a> |
| 50 | | */ |
| 51 | | public static final class InkscapeCssParser extends Parser { |
| 52 | | public void parseStyleDeclaration( final String source ) |
| 53 | | throws CSSException, IOException { |
| 54 | | super.parseStyleDeclaration( |
| 55 | | source.replaceAll( "-inkscape-font-specification:[^;\"]*;", "" ) |
| 56 | | ); |
| 57 | | } |
| 58 | | } |
| 59 | | |
| 60 | | static { |
| 61 | | XMLResourceDescriptor.setCSSParserClassName( |
| 62 | | InkscapeCssParser.class.getName() |
| 63 | | ); |
| 64 | | } |
| 65 | | |
| 66 | | private static final UserAgent USER_AGENT = new UserAgentAdapter(); |
| 67 | | private static final BridgeContext BRIDGE_CONTEXT = new BridgeContext( |
| 68 | | USER_AGENT, new DocumentLoader( USER_AGENT ) |
| 69 | | ); |
| 70 | | |
| 71 | | private static final SAXSVGDocumentFactory FACTORY_DOM = |
| 72 | | new SAXSVGDocumentFactory( getXMLParserClassName() ); |
| 73 | | |
| 74 | | private static final NumberFormat INT_FORMAT = getIntegerInstance(); |
| 75 | | |
| 76 | | public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER; |
| 77 | | |
| 78 | | /** |
| 79 | | * A FontAwesome camera icon, cleft asunder. |
| 80 | | */ |
| 81 | | public static final String BROKEN_IMAGE_SVG = |
| 82 | | "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" + |
| 83 | | ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" + |
| 84 | | ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " + |
| 85 | | "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" + |
| 86 | | ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" + |
| 87 | | ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" + |
| 88 | | ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" + |
| 89 | | ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " + |
| 90 | | "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" + |
| 91 | | ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" + |
| 92 | | ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" + |
| 93 | | ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" + |
| 94 | | ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" + |
| 95 | | ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" + |
| 96 | | ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" + |
| 97 | | ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" + |
| 98 | | ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " + |
| 99 | | ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" + |
| 100 | | ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" + |
| 101 | | ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" + |
| 102 | | ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" + |
| 103 | | ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" + |
| 104 | | ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" + |
| 105 | | ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " + |
| 106 | | "0'/></g></svg>"; |
| 107 | | |
| 108 | | static { |
| 109 | | // The width and height cannot be embedded in the SVG above because the |
| 110 | | // path element values are relative to the viewBox dimensions. |
| 111 | | final int w = 75; |
| 112 | | final int h = 75; |
| 113 | | BufferedImage image; |
| 114 | | |
| 115 | | try { |
| 116 | | image = rasterizeString( BROKEN_IMAGE_SVG, w ); |
| 117 | | } catch( final Exception ex ) { |
| 118 | | image = new BufferedImage( w, h, TYPE_INT_RGB ); |
| 119 | | final var graphics = (Graphics2D) image.getGraphics(); |
| 120 | | graphics.setRenderingHints( RENDERING_HINTS ); |
| 121 | | |
| 122 | | // Fall back to a (\) symbol. |
| 123 | | graphics.setColor( new Color( 204, 204, 204 ) ); |
| 124 | | graphics.fillRect( 0, 0, w, h ); |
| 125 | | graphics.setColor( new Color( 255, 204, 204 ) ); |
| 126 | | graphics.setStroke( new BasicStroke( 4 ) ); |
| 127 | | graphics.drawOval( w / 4, h / 4, w / 2, h / 2 ); |
| 128 | | graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI), |
| 129 | | h / 4 + (int) (w / 4 / Math.PI), |
| 130 | | w / 2 + w / 4 - (int) (w / 4 / Math.PI), |
| 131 | | h / 2 + h / 4 - (int) (w / 4 / Math.PI) ); |
| 132 | | } |
| 133 | | |
| 134 | | BROKEN_IMAGE_PLACEHOLDER = image; |
| 135 | | } |
| 136 | | |
| 137 | | /** |
| 138 | | * Responsible for creating a new {@link ImageRenderer} implementation that |
| 139 | | * can render a DOM as an SVG image. |
| 140 | | */ |
| 141 | | private static class BufferedImageTranscoder extends ImageTranscoder { |
| 142 | | private BufferedImage mImage; |
| 143 | | |
| 144 | | @Override |
| 145 | | public BufferedImage createImage( final int w, final int h ) { |
| 146 | | return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB ); |
| 147 | | } |
| 148 | | |
| 149 | | @Override |
| 150 | | public void writeImage( |
| 151 | | final BufferedImage image, final TranscoderOutput output ) { |
| 152 | | mImage = image; |
| 153 | | } |
| 154 | | |
| 155 | | public BufferedImage getImage() { |
| 156 | | return mImage; |
| 157 | | } |
| 158 | | |
| 159 | | @Override |
| 160 | | protected ImageRenderer createRenderer() { |
| 161 | | final ImageRenderer renderer = super.createRenderer(); |
| 162 | | final RenderingHints hints = renderer.getRenderingHints(); |
| 163 | | hints.putAll( RENDERING_HINTS ); |
| 164 | | renderer.setRenderingHints( hints ); |
| 165 | | |
| 166 | | return renderer; |
| 167 | | } |
| 168 | | } |
| 169 | | |
| 170 | | /** |
| 171 | | * Rasterizes the given SVG input stream into an image at 96 DPI. |
| 172 | | * |
| 173 | | * @param svg The SVG data to rasterize, must be closed by caller. |
| 174 | | * @return The given input stream converted to a rasterized image. |
| 175 | | */ |
| 176 | | public static BufferedImage rasterize( final InputStream svg ) |
| 177 | | throws TranscoderException { |
| 178 | | return rasterize( svg, 96 ); |
| 179 | | } |
| 180 | | |
| 181 | | /** |
| 182 | | * Rasterizes the given SVG input stream into an image. |
| 183 | | * |
| 184 | | * @param svg The SVG data to rasterize, must be closed by caller. |
| 185 | | * @param dpi Resolution to use when rasterizing (default is 96 DPI). |
| 186 | | * @return The given input stream converted to a rasterized image at the |
| 187 | | * given resolution. |
| 188 | | */ |
| 189 | | public static BufferedImage rasterize( |
| 190 | | final InputStream svg, final float dpi ) throws TranscoderException { |
| 191 | | return rasterize( |
| 192 | | new TranscoderInput( svg ), |
| 193 | | KEY_PIXEL_UNIT_TO_MILLIMETER, |
| 194 | | 1f / dpi * 25.4f |
| 195 | | ); |
| 196 | | } |
| 197 | | |
| 198 | | /** |
| 199 | | * Rasterizes the given document into an image. |
| 200 | | * |
| 201 | | * @param svg The SVG {@link Document} to rasterize. |
| 202 | | * @param width The rasterized image's width (in pixels). |
| 203 | | * @return The rasterized image. |
| 204 | | */ |
| 205 | | public static BufferedImage rasterize( |
| 206 | | final Document svg, final int width ) throws TranscoderException { |
| 207 | | return rasterize( |
| 208 | | new TranscoderInput( svg ), |
| 209 | | KEY_WIDTH, |
| 210 | | fit( svg.getDocumentElement(), width ) |
| 211 | | ); |
| 212 | | } |
| 213 | | |
| 214 | | /** |
| 215 | | * Rasterizes the given vector graphic file using the width dimension |
| 216 | | * specified by the document's width attribute. |
| 217 | | * |
| 218 | | * @param document The {@link Document} containing a vector graphic. |
| 219 | | * @return A rasterized image as an instance of {@link BufferedImage}, or |
| 220 | | * {@link #BROKEN_IMAGE_PLACEHOLDER} if the graphic could not be rasterized. |
| 221 | | */ |
| 222 | | public static BufferedImage rasterize( final Document document ) |
| 223 | | throws ParseException, TranscoderException { |
| 224 | | final var root = document.getDocumentElement(); |
| 225 | | final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE ); |
| 226 | | return rasterize( document, INT_FORMAT.parse( width ).intValue() ); |
| 227 | | } |
| 228 | | |
| 229 | | /** |
| 230 | | * Rasterizes the vector graphic file at the given URI. If any exception |
| 231 | | * happens, a broken image icon is returned instead. |
| 232 | | * |
| 233 | | * @param path The {@link Path} to a vector graphic file. |
| 234 | | * @param width Scale the image to the given width (px); aspect ratio is |
| 235 | | * maintained. |
| 236 | | * @return A rasterized image as an instance of {@link BufferedImage}. |
| 237 | | */ |
| 238 | | public static BufferedImage rasterize( final Path path, final int width ) { |
| 239 | | return rasterize( path.toUri(), width ); |
| 240 | | } |
| 241 | | |
| 242 | | /** |
| 243 | | * Rasterizes the vector graphic file at the given URI. If any exception |
| 244 | | * happens, a broken image icon is returned instead. |
| 245 | | * |
| 246 | | * @param uri The URI to a vector graphic file, which must include the |
| 247 | | * protocol scheme (such as file:// or https://). |
| 248 | | * @param width Scale the image to the given width (px); aspect ratio is |
| 249 | | * maintained. |
| 250 | | * @return A rasterized image as an instance of {@link BufferedImage}. |
| 251 | | */ |
| 252 | | public static BufferedImage rasterize( final String uri, final int width ) { |
| 253 | | return rasterize( new File( uri ).toURI(), width ); |
| 254 | | } |
| 255 | | |
| 256 | | /** |
| 257 | | * Converts an SVG drawing into a rasterized image that can be drawn on |
| 258 | | * a graphics context. |
| 259 | | * |
| 260 | | * @param uri The path to the image (can be web address). |
| 261 | | * @param width Scale the image to the given width (px); aspect ratio is |
| 262 | | * maintained. |
| 263 | | * @return The vector graphic transcoded into a raster image format. |
| 264 | | */ |
| 265 | | public static BufferedImage rasterize( final URI uri, final int width ) { |
| 266 | | try { |
| 267 | | return rasterize( FACTORY_DOM.createDocument( uri.toString() ), width ); |
| 268 | | } catch( final Exception ex ) { |
| 269 | | clue( ex ); |
| 270 | | } |
| 271 | | |
| 272 | | return BROKEN_IMAGE_PLACEHOLDER; |
| 273 | | } |
| 274 | | |
| 275 | | /** |
| 276 | | * Converts an SVG string into a rasterized image that can be drawn on |
| 277 | | * a graphics context. The dimensions are determined from the document. |
| 278 | | * |
| 279 | | * @param xml The SVG xml document. |
| 280 | | * @return The vector graphic transcoded into a raster image format. |
| 281 | | */ |
| 282 | | public static BufferedImage rasterizeString( final String xml ) |
| 283 | | throws ParseException, TranscoderException { |
| 284 | | final var document = toDocument( xml ); |
| 285 | | final var root = document.getDocumentElement(); |
| 286 | | final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE ); |
| 287 | | return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() ); |
| 288 | | } |
| 289 | | |
| 290 | | /** |
| 291 | | * Converts an SVG string into a rasterized image that can be drawn on |
| 292 | | * a graphics context. |
| 293 | | * |
| 294 | | * @param svg The SVG xml document. |
| 295 | | * @param w Scale the image width to this size (aspect ratio is |
| 296 | | * maintained). |
| 297 | | * @return The vector graphic transcoded into a raster image format. |
| 298 | | */ |
| 299 | | public static BufferedImage rasterizeString( final String svg, final int w ) |
| 300 | | throws TranscoderException { |
| 301 | | return rasterize( toDocument( svg ), w ); |
| 302 | | } |
| 303 | | |
| 304 | | /** |
| 305 | | * Given a document object model (DOM) {@link Element}, this will convert that |
| 306 | | * element to a string. |
| 307 | | * |
| 308 | | * @param root The DOM node to convert to a string. |
| 309 | | * @return The DOM node as an escaped, plain text string. |
| 310 | | */ |
| 311 | | public static String toSvg( final Element root ) { |
| 312 | | try { |
| 313 | | return transform( root ).replaceAll( "xmlns=\"\" ", "" ); |
| 314 | | } catch( final Exception ex ) { |
| 315 | | clue( ex ); |
| 316 | | } |
| 317 | | |
| 318 | | return BROKEN_IMAGE_SVG; |
| 319 | | } |
| 320 | | |
| 321 | | /** |
| 322 | | * Converts an SVG XML string into a new {@link Document} instance. |
| 323 | | * |
| 324 | | * @param xml The XML containing SVG elements. |
| 325 | | * @return The SVG contents parsed into a {@link Document} object model. |
| 326 | | */ |
| 327 | | private static Document toDocument( final String xml ) { |
| 328 | | try( final var reader = new StringReader( xml ) ) { |
| 329 | | return FACTORY_DOM.createSVGDocument( |
| 330 | | "http://www.w3.org/2000/svg", reader ); |
| 331 | | } catch( final Exception ex ) { |
| 332 | | throw new IllegalArgumentException( ex ); |
| 333 | | } |
| 334 | | } |
| 335 | | |
| 336 | | /** |
| 337 | | * Creates a rasterized image of the given source document. |
| 338 | | * |
| 339 | | * @param input The source document to transcode. |
| 340 | | * @param key Transcoding hint key. |
| 341 | | * @param width Transcoding hint value. |
| 342 | | * @return A new {@link BufferedImageTranscoder} instance with the given |
| 343 | | * transcoding hint applied. |
| 344 | | */ |
| 345 | | private static BufferedImage rasterize( |
| 346 | | final TranscoderInput input, final Key key, final float width ) |
| 347 | | throws TranscoderException { |
| 348 | | final var transcoder = new BufferedImageTranscoder(); |
| 349 | | |
| 11 | import org.apache.batik.transcoder.*; |
| 12 | import org.apache.batik.transcoder.image.ImageTranscoder; |
| 13 | import org.apache.batik.util.XMLResourceDescriptor; |
| 14 | import org.w3c.css.sac.CSSException; |
| 15 | import org.w3c.dom.Document; |
| 16 | import org.w3c.dom.Element; |
| 17 | |
| 18 | import java.awt.*; |
| 19 | import java.awt.image.BufferedImage; |
| 20 | import java.io.File; |
| 21 | import java.io.IOException; |
| 22 | import java.io.InputStream; |
| 23 | import java.io.StringReader; |
| 24 | import java.net.URI; |
| 25 | import java.nio.file.Path; |
| 26 | import java.text.NumberFormat; |
| 27 | import java.text.ParseException; |
| 28 | |
| 29 | import static com.keenwrite.dom.DocumentParser.transform; |
| 30 | import static com.keenwrite.events.StatusEvent.clue; |
| 31 | import static com.keenwrite.preview.HighQualityRenderingHints.RENDERING_HINTS; |
| 32 | import static java.awt.image.BufferedImage.TYPE_INT_RGB; |
| 33 | import static java.text.NumberFormat.getIntegerInstance; |
| 34 | import static org.apache.batik.bridge.UnitProcessor.createContext; |
| 35 | import static org.apache.batik.bridge.UnitProcessor.svgHorizontalLengthToUserSpace; |
| 36 | import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH; |
| 37 | import static org.apache.batik.transcoder.TranscodingHints.Key; |
| 38 | import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER; |
| 39 | import static org.apache.batik.util.SVGConstants.SVG_WIDTH_ATTRIBUTE; |
| 40 | import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName; |
| 41 | |
| 42 | /** |
| 43 | * Responsible for converting SVG images into rasterized PNG images. |
| 44 | */ |
| 45 | public final class SvgRasterizer { |
| 46 | /** |
| 47 | * <a href="https://issues.apache.org/jira/browse/BATIK-1112">Bug fix</a> |
| 48 | */ |
| 49 | public static final class InkscapeCssParser extends Parser { |
| 50 | public void parseStyleDeclaration( final String source ) |
| 51 | throws CSSException, IOException { |
| 52 | super.parseStyleDeclaration( |
| 53 | source.replaceAll( "-inkscape-font-specification:[^;\"]*;", "" ) |
| 54 | ); |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | /** |
| 59 | * Prevent rudely barfing stack traces to the console. |
| 60 | */ |
| 61 | private static final class SvgErrorHandler implements ErrorHandler { |
| 62 | @Override |
| 63 | public void error( final TranscoderException ex ) { |
| 64 | clue( ex ); |
| 65 | } |
| 66 | |
| 67 | @Override |
| 68 | public void fatalError( final TranscoderException ex ) { |
| 69 | clue( ex ); |
| 70 | } |
| 71 | |
| 72 | @Override |
| 73 | public void warning( final TranscoderException ex ) { |
| 74 | clue( ex ); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | static { |
| 79 | XMLResourceDescriptor.setCSSParserClassName( |
| 80 | InkscapeCssParser.class.getName() |
| 81 | ); |
| 82 | } |
| 83 | |
| 84 | private static final UserAgent USER_AGENT = new UserAgentAdapter(); |
| 85 | private static final BridgeContext BRIDGE_CONTEXT = new BridgeContext( |
| 86 | USER_AGENT, new DocumentLoader( USER_AGENT ) |
| 87 | ); |
| 88 | private static final ErrorHandler sErrorHandler = new SvgErrorHandler(); |
| 89 | |
| 90 | private static final SAXSVGDocumentFactory FACTORY_DOM = |
| 91 | new SAXSVGDocumentFactory( getXMLParserClassName() ); |
| 92 | |
| 93 | private static final NumberFormat INT_FORMAT = getIntegerInstance(); |
| 94 | |
| 95 | public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER; |
| 96 | |
| 97 | /** |
| 98 | * A FontAwesome camera icon, cleft asunder. |
| 99 | */ |
| 100 | public static final String BROKEN_IMAGE_SVG = |
| 101 | "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" + |
| 102 | ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" + |
| 103 | ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " + |
| 104 | "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" + |
| 105 | ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" + |
| 106 | ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" + |
| 107 | ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" + |
| 108 | ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " + |
| 109 | "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" + |
| 110 | ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" + |
| 111 | ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" + |
| 112 | ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" + |
| 113 | ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" + |
| 114 | ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" + |
| 115 | ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" + |
| 116 | ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" + |
| 117 | ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " + |
| 118 | ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" + |
| 119 | ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" + |
| 120 | ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" + |
| 121 | ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" + |
| 122 | ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" + |
| 123 | ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" + |
| 124 | ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " + |
| 125 | "0'/></g></svg>"; |
| 126 | |
| 127 | static { |
| 128 | // The width and height cannot be embedded in the SVG above because the |
| 129 | // path element values are relative to the viewBox dimensions. |
| 130 | final int w = 75; |
| 131 | final int h = 75; |
| 132 | BufferedImage image; |
| 133 | |
| 134 | try { |
| 135 | image = rasterizeString( BROKEN_IMAGE_SVG, w ); |
| 136 | } catch( final Exception ex ) { |
| 137 | image = new BufferedImage( w, h, TYPE_INT_RGB ); |
| 138 | final var graphics = (Graphics2D) image.getGraphics(); |
| 139 | graphics.setRenderingHints( RENDERING_HINTS ); |
| 140 | |
| 141 | // Fall back to a (\) symbol. |
| 142 | graphics.setColor( new Color( 204, 204, 204 ) ); |
| 143 | graphics.fillRect( 0, 0, w, h ); |
| 144 | graphics.setColor( new Color( 255, 204, 204 ) ); |
| 145 | graphics.setStroke( new BasicStroke( 4 ) ); |
| 146 | graphics.drawOval( w / 4, h / 4, w / 2, h / 2 ); |
| 147 | graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI), |
| 148 | h / 4 + (int) (w / 4 / Math.PI), |
| 149 | w / 2 + w / 4 - (int) (w / 4 / Math.PI), |
| 150 | h / 2 + h / 4 - (int) (w / 4 / Math.PI) ); |
| 151 | } |
| 152 | |
| 153 | BROKEN_IMAGE_PLACEHOLDER = image; |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Responsible for creating a new {@link ImageRenderer} implementation that |
| 158 | * can render a DOM as an SVG image. |
| 159 | */ |
| 160 | private static class BufferedImageTranscoder extends ImageTranscoder { |
| 161 | private BufferedImage mImage; |
| 162 | |
| 163 | /** |
| 164 | * Prevent barfing a stack trace when the transcoder encounters problems |
| 165 | * parsing SVG contents. |
| 166 | */ |
| 167 | @Override |
| 168 | protected UserAgent createUserAgent() { |
| 169 | return new SVGAbstractTranscoderUserAgent() { |
| 170 | @Override |
| 171 | public void displayError( final Exception ex ) { |
| 172 | clue( ex ); |
| 173 | } |
| 174 | }; |
| 175 | } |
| 176 | |
| 177 | @Override |
| 178 | public BufferedImage createImage( final int w, final int h ) { |
| 179 | return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB ); |
| 180 | } |
| 181 | |
| 182 | @Override |
| 183 | public void writeImage( |
| 184 | final BufferedImage image, final TranscoderOutput output ) { |
| 185 | mImage = image; |
| 186 | } |
| 187 | |
| 188 | public BufferedImage getImage() { |
| 189 | return mImage; |
| 190 | } |
| 191 | |
| 192 | @Override |
| 193 | protected ImageRenderer createRenderer() { |
| 194 | final ImageRenderer renderer = super.createRenderer(); |
| 195 | final RenderingHints hints = renderer.getRenderingHints(); |
| 196 | hints.putAll( RENDERING_HINTS ); |
| 197 | renderer.setRenderingHints( hints ); |
| 198 | |
| 199 | return renderer; |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | /** |
| 204 | * Rasterizes the given SVG input stream into an image at 96 DPI. |
| 205 | * |
| 206 | * @param svg The SVG data to rasterize, must be closed by caller. |
| 207 | * @return The given input stream converted to a rasterized image. |
| 208 | */ |
| 209 | public static BufferedImage rasterize( final InputStream svg ) |
| 210 | throws TranscoderException { |
| 211 | return rasterize( svg, 96 ); |
| 212 | } |
| 213 | |
| 214 | /** |
| 215 | * Rasterizes the given SVG input stream into an image. |
| 216 | * |
| 217 | * @param svg The SVG data to rasterize, must be closed by caller. |
| 218 | * @param dpi Resolution to use when rasterizing (default is 96 DPI). |
| 219 | * @return The given input stream converted to a rasterized image at the |
| 220 | * given resolution. |
| 221 | */ |
| 222 | public static BufferedImage rasterize( |
| 223 | final InputStream svg, final float dpi ) throws TranscoderException { |
| 224 | return rasterize( |
| 225 | new TranscoderInput( svg ), |
| 226 | KEY_PIXEL_UNIT_TO_MILLIMETER, |
| 227 | 1f / dpi * 25.4f |
| 228 | ); |
| 229 | } |
| 230 | |
| 231 | /** |
| 232 | * Rasterizes the given document into an image. |
| 233 | * |
| 234 | * @param svg The SVG {@link Document} to rasterize. |
| 235 | * @param width The rasterized image's width (in pixels). |
| 236 | * @return The rasterized image. |
| 237 | */ |
| 238 | public static BufferedImage rasterize( |
| 239 | final Document svg, final int width ) throws TranscoderException { |
| 240 | return rasterize( |
| 241 | new TranscoderInput( svg ), |
| 242 | KEY_WIDTH, |
| 243 | fit( svg.getDocumentElement(), width ) |
| 244 | ); |
| 245 | } |
| 246 | |
| 247 | /** |
| 248 | * Rasterizes the given vector graphic file using the width dimension |
| 249 | * specified by the document's width attribute. |
| 250 | * |
| 251 | * @param document The {@link Document} containing a vector graphic. |
| 252 | * @return A rasterized image as an instance of {@link BufferedImage}, or |
| 253 | * {@link #BROKEN_IMAGE_PLACEHOLDER} if the graphic could not be rasterized. |
| 254 | */ |
| 255 | public static BufferedImage rasterize( final Document document ) |
| 256 | throws ParseException, TranscoderException { |
| 257 | final var root = document.getDocumentElement(); |
| 258 | final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE ); |
| 259 | return rasterize( document, INT_FORMAT.parse( width ).intValue() ); |
| 260 | } |
| 261 | |
| 262 | /** |
| 263 | * Rasterizes the vector graphic file at the given URI. If any exception |
| 264 | * happens, a broken image icon is returned instead. |
| 265 | * |
| 266 | * @param path The {@link Path} to a vector graphic file. |
| 267 | * @param width Scale the image to the given width (px); aspect ratio is |
| 268 | * maintained. |
| 269 | * @return A rasterized image as an instance of {@link BufferedImage}. |
| 270 | */ |
| 271 | public static BufferedImage rasterize( final Path path, final int width ) { |
| 272 | return rasterize( path.toUri(), width ); |
| 273 | } |
| 274 | |
| 275 | /** |
| 276 | * Rasterizes the vector graphic file at the given URI. If any exception |
| 277 | * happens, a broken image icon is returned instead. |
| 278 | * |
| 279 | * @param uri The URI to a vector graphic file, which must include the |
| 280 | * protocol scheme (such as <code>file://</code> or |
| 281 | * <code>https://</code>). |
| 282 | * @param width Scale the image to the given width (px); aspect ratio is |
| 283 | * maintained. |
| 284 | * @return A rasterized image as an instance of {@link BufferedImage}. |
| 285 | */ |
| 286 | public static BufferedImage rasterize( final String uri, final int width ) { |
| 287 | return rasterize( new File( uri ).toURI(), width ); |
| 288 | } |
| 289 | |
| 290 | /** |
| 291 | * Converts an SVG drawing into a rasterized image that can be drawn on |
| 292 | * a graphics context. |
| 293 | * |
| 294 | * @param uri The path to the image (can be web address). |
| 295 | * @param width Scale the image to the given width (px); aspect ratio is |
| 296 | * maintained. |
| 297 | * @return The vector graphic transcoded into a raster image format. |
| 298 | */ |
| 299 | public static BufferedImage rasterize( final URI uri, final int width ) { |
| 300 | try { |
| 301 | return rasterize( FACTORY_DOM.createDocument( uri.toString() ), width ); |
| 302 | } catch( final Exception ex ) { |
| 303 | clue( ex ); |
| 304 | } |
| 305 | |
| 306 | return BROKEN_IMAGE_PLACEHOLDER; |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * Converts an SVG string into a rasterized image that can be drawn on |
| 311 | * a graphics context. The dimensions are determined from the document. |
| 312 | * |
| 313 | * @param xml The SVG xml document. |
| 314 | * @return The vector graphic transcoded into a raster image format. |
| 315 | */ |
| 316 | public static BufferedImage rasterizeString( final String xml ) |
| 317 | throws ParseException, TranscoderException { |
| 318 | final var document = toDocument( xml ); |
| 319 | final var root = document.getDocumentElement(); |
| 320 | final var width = root.getAttribute( SVG_WIDTH_ATTRIBUTE ); |
| 321 | return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() ); |
| 322 | } |
| 323 | |
| 324 | /** |
| 325 | * Converts an SVG string into a rasterized image that can be drawn on |
| 326 | * a graphics context. |
| 327 | * |
| 328 | * @param svg The SVG xml document. |
| 329 | * @param w Scale the image width to this size (aspect ratio is |
| 330 | * maintained). |
| 331 | * @return The vector graphic transcoded into a raster image format. |
| 332 | */ |
| 333 | public static BufferedImage rasterizeString( final String svg, final int w ) |
| 334 | throws TranscoderException { |
| 335 | return rasterize( toDocument( svg ), w ); |
| 336 | } |
| 337 | |
| 338 | /** |
| 339 | * Given a document object model (DOM) {@link Element}, this will convert that |
| 340 | * element to a string. |
| 341 | * |
| 342 | * @param root The DOM node to convert to a string. |
| 343 | * @return The DOM node as an escaped, plain text string. |
| 344 | */ |
| 345 | public static String toSvg( final Element root ) { |
| 346 | try { |
| 347 | return transform( root ).replaceAll( "xmlns=\"\" ", "" ); |
| 348 | } catch( final Exception ex ) { |
| 349 | clue( ex ); |
| 350 | } |
| 351 | |
| 352 | return BROKEN_IMAGE_SVG; |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Converts an SVG XML string into a new {@link Document} instance. |
| 357 | * |
| 358 | * @param xml The XML containing SVG elements. |
| 359 | * @return The SVG contents parsed into a {@link Document} object model. |
| 360 | */ |
| 361 | private static Document toDocument( final String xml ) { |
| 362 | try( final var reader = new StringReader( xml ) ) { |
| 363 | return FACTORY_DOM.createSVGDocument( |
| 364 | "http://www.w3.org/2000/svg", reader ); |
| 365 | } catch( final Exception ex ) { |
| 366 | throw new IllegalArgumentException( ex ); |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | /** |
| 371 | * Creates a rasterized image of the given source document. |
| 372 | * |
| 373 | * @param input The source document to transcode. |
| 374 | * @param key Transcoding hint key. |
| 375 | * @param width Transcoding hint value. |
| 376 | * @return A new {@link BufferedImageTranscoder} instance with the given |
| 377 | * transcoding hint applied. |
| 378 | */ |
| 379 | private static BufferedImage rasterize( |
| 380 | final TranscoderInput input, final Key key, final float width ) |
| 381 | throws TranscoderException { |
| 382 | final var transcoder = new BufferedImageTranscoder(); |
| 383 | |
| 384 | transcoder.setErrorHandler( sErrorHandler ); |
| 350 | 385 | transcoder.addTranscodingHint( key, width ); |
| 351 | 386 | transcoder.transcode( input, null ); |