Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M src/main/java/com/keenwrite/preview/SvgRasterizer.java
99
import org.apache.batik.css.parser.Parser;
1010
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 );
350385
    transcoder.addTranscodingHint( key, width );
351386
    transcoder.transcode( input, null );
M src/main/java/com/keenwrite/processors/VariableProcessor.java
106106
    return result;
107107
  }
108
109
  protected ProcessorContext getContext() {
110
    return mContext;
111
  }
112108
}
113109
M src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
1212
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
1313
import com.keenwrite.processors.markdown.extensions.tex.TeXExtension;
14
import com.keenwrite.processors.r.RInlineEvaluator;
1415
import com.vladsch.flexmark.util.misc.Extension;
1516
1617
import java.util.ArrayList;
1718
import java.util.List;
19
import java.util.function.Function;
1820
1921
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
...
5759
    final var mediaType = MediaType.valueFrom( inputPath );
5860
    final Processor<String> processor;
61
    final Function<String, String> evaluator;
5962
    final List<Extension> extensions = new ArrayList<>();
6063
6164
    if( mediaType == TEXT_R_MARKDOWN ) {
6265
      extensions.add( RInlineExtension.create( context ) );
6366
      processor = IDENTITY;
67
      evaluator = new RInlineEvaluator( context );
6468
    }
6569
    else {
6670
      processor = new VariableProcessor( IDENTITY, context );
71
      evaluator = processor;
6772
    }
6873
6974
    // Add typographic, table, strikethrough, and similar extensions.
7075
    extensions.addAll( super.createExtensions( context ) );
7176
7277
    extensions.add( ImageLinkExtension.create( context ) );
7378
    extensions.add( TeXExtension.create( processor, context ) );
74
    extensions.add( FencedBlockExtension.create( processor, context ) );
79
    extensions.add( FencedBlockExtension.create( processor, evaluator, context ) );
7580
    extensions.add( CaretExtension.create( context ) );
7681
    extensions.add( DocumentOutlineExtension.create( processor ) );
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedBlockExtension.java
2222
import java.util.HashSet;
2323
import java.util.Set;
24
import java.util.function.Function;
2425
2526
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
...
5455
  }
5556
56
  private final RChunkEvaluator mEvaluator;
57
  private final Processor<String> mProcessor;
57
  private final RChunkEvaluator mRChunkEvaluator;
58
  private final Function<String, String> mInlineEvaluator;
59
5860
  private final Processor<String> mRVariableProcessor;
5961
  private final ProcessorContext mContext;
6062
6163
  public FencedBlockExtension(
62
    final Processor<String> processor, final ProcessorContext context ) {
64
    final Processor<String> processor,
65
    final Function<String, String> evaluator,
66
    final ProcessorContext context ) {
6367
    assert processor != null;
6468
    assert context != null;
65
    mProcessor = processor;
6669
    mContext = context;
67
    mEvaluator = new RChunkEvaluator( context );
70
    mRChunkEvaluator = new RChunkEvaluator( context );
71
    mInlineEvaluator = evaluator;
6872
    mRVariableProcessor = new VerbatimRVariableProcessor( IDENTITY, context );
6973
  }
...
8892
   */
8993
  public static FencedBlockExtension create(
90
    final Processor<String> processor, final ProcessorContext context ) {
94
    final Processor<String> processor,
95
    final Function<String, String> evaluator,
96
    final ProcessorContext context ) {
9197
    assert processor != null;
9298
    assert context != null;
93
    return new FencedBlockExtension( processor, context );
99
    return new FencedBlockExtension( processor, evaluator, context );
94100
  }
95101
...
158164
      final var type = style.substring( STYLE_DIAGRAM_LEN );
159165
      final var content = node.getContentChars().normalizeEOL();
160
      final var text = mProcessor.apply( content );
166
      final var text = mInlineEvaluator.apply( content );
161167
      final var server = mContext.getImageServer();
162168
      final var source = DiagramUrlGenerator.toUrl( server, type, text );
...
177183
      final var link = context.resolveLink( LINK, source, false );
178184
      final var r = format( "svg('%s')%n%s%ndev.off()%n", source, text );
179
      final var result = mEvaluator.apply( r );
185
      final var result = mRChunkEvaluator.apply( r );
180186
181187
      return new Pair<>( source, link );
M src/main/java/com/keenwrite/processors/r/RInlineEvaluator.java
2121
2222
  private static final int PREFIX_LENGTH = PREFIX.length();
23
  private static final int SUFFIX_LENGTH = SUFFIX.length();
2423
2524
  private final Processor<String> mProcessor;
25
  private final ProcessorContext mContext;
2626
2727
  /**
2828
   * Constructs an evaluator capable of executing R statements.
2929
   */
3030
  public RInlineEvaluator( final ProcessorContext context ) {
3131
    mProcessor = new RVariableProcessor( IDENTITY, context );
32
    mContext = context;
3233
  }
3334
...
4445
  public String apply( final String text ) {
4546
    try {
46
      final var len = text.length();
47
      final var r = mProcessor.apply(
48
        text.substring( PREFIX_LENGTH, len - SUFFIX_LENGTH )
49
      );
47
      final var buffer = new StringBuilder( text.length() );
5048
51
      // Return the evaluated R expression for insertion back into the text.
52
      return Engine.eval( r );
49
      int index = 0;
50
      int began;
51
      int ended;
52
53
      RBootstrapController.init( mContext );
54
55
      while( (began = text.indexOf( PREFIX, index )) >= 0 ) {
56
        buffer.append( text, index, began );
57
58
        ended = text.indexOf( SUFFIX, began + 1 );
59
60
        if( ended > began ) {
61
          final var r = mProcessor.apply(
62
            text.substring( began + PREFIX_LENGTH, ended )
63
          );
64
65
          // Return the evaluated R expression for insertion back into the text.
66
          buffer.append( Engine.eval( r ) );
67
68
          index = ended + 1;
69
        }
70
      }
71
72
      buffer.append( text.substring( index ) );
73
74
      return buffer.toString();
5375
    } catch( final Exception ex ) {
5476
      clue( STATUS_PARSE_ERROR, ex.getMessage() );