| Author | DaveJarvis <email> |
|---|---|
| Date | 2021-03-16 22:09:39 GMT-0700 |
| Commit | f4dfed9838d369dbe10788196102cdac71084f56 |
| Parent | 6d4b12a |
| implementation "org.apache.xmlgraphics:batik-svggen:${v_batik}" | ||
| implementation "org.apache.xmlgraphics:batik-transcoder:${v_batik}" | ||
| + implementation "org.apache.xmlgraphics:batik-rasterizer:${v_batik}" | ||
| implementation "org.apache.xmlgraphics:batik-util:${v_batik}" | ||
| implementation "org.apache.xmlgraphics:batik-xml:${v_batik}" |
| /* | ||
| + * PDF Media Type. | ||
| + * https://www.rfc-editor.org/rfc/rfc3778.txt | ||
| + */ | ||
| + APP_PDF( | ||
| + APPLICATION, "pdf" | ||
| + ), | ||
| + | ||
| + /* | ||
| * Standard font types. | ||
| */ |
| */ | ||
| public enum MediaTypeExtension { | ||
| + MEDIA_APP_PDF( APP_PDF ), | ||
| + | ||
| MEDIA_FONT_OTF( FONT_OTF ), | ||
| MEDIA_FONT_TTF( FONT_TTF ), |
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.preview; | ||
| - | ||
| -import java.util.HashMap; | ||
| -import java.util.Map; | ||
| - | ||
| -import static java.awt.RenderingHints.*; | ||
| -import static java.awt.Toolkit.getDefaultToolkit; | ||
| - | ||
| -/** | ||
| - * Responsible for supplying consistent rendering hints throughout the | ||
| - * application, such as image rendering for {@link SvgRasterizer}. | ||
| - */ | ||
| -@SuppressWarnings("rawtypes") | ||
| -public final class RenderingSettings { | ||
| - | ||
| - /** | ||
| - * Default hints for high-quality rendering that may be changed by | ||
| - * the system's rendering hints. | ||
| - */ | ||
| - private static final Map<Object, Object> DEFAULT_HINTS = Map.of( | ||
| - KEY_ANTIALIASING, | ||
| - VALUE_ANTIALIAS_ON, | ||
| - KEY_ALPHA_INTERPOLATION, | ||
| - VALUE_ALPHA_INTERPOLATION_QUALITY, | ||
| - KEY_COLOR_RENDERING, | ||
| - VALUE_COLOR_RENDER_QUALITY, | ||
| - KEY_DITHERING, | ||
| - VALUE_DITHER_DISABLE, | ||
| - KEY_FRACTIONALMETRICS, | ||
| - VALUE_FRACTIONALMETRICS_ON, | ||
| - KEY_INTERPOLATION, | ||
| - VALUE_INTERPOLATION_BICUBIC, | ||
| - KEY_RENDERING, | ||
| - VALUE_RENDER_QUALITY, | ||
| - KEY_STROKE_CONTROL, | ||
| - VALUE_STROKE_PURE, | ||
| - KEY_TEXT_ANTIALIASING, | ||
| - VALUE_TEXT_ANTIALIAS_ON | ||
| - ); | ||
| - | ||
| - /** | ||
| - * Shared hints for high-quality rendering. | ||
| - */ | ||
| - public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>( | ||
| - DEFAULT_HINTS | ||
| - ); | ||
| - | ||
| - static { | ||
| - final var toolkit = getDefaultToolkit(); | ||
| - final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" ); | ||
| - | ||
| - if( hints instanceof Map ) { | ||
| - final var map = (Map) hints; | ||
| - for( final var key : map.keySet() ) { | ||
| - final var hint = map.get( key ); | ||
| - RENDERING_HINTS.put( key, hint ); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Prevent instantiation as per Joshua Bloch's recommendation. | ||
| - */ | ||
| - private RenderingSettings() { | ||
| - } | ||
| -} | ||
| import java.awt.*; | ||
| import java.awt.image.BufferedImage; | ||
| -import java.io.File; | ||
| -import java.io.InputStream; | ||
| -import java.io.StringReader; | ||
| -import java.io.StringWriter; | ||
| +import java.io.*; | ||
| import java.net.URI; | ||
| import java.nio.file.Path; | ||
| import java.text.NumberFormat; | ||
| +import java.text.ParseException; | ||
| +import java.util.HashMap; | ||
| +import java.util.Map; | ||
| import static com.keenwrite.events.StatusEvent.clue; | ||
| -import static com.keenwrite.preview.RenderingSettings.RENDERING_HINTS; | ||
| +import static java.awt.RenderingHints.*; | ||
| +import static java.awt.Toolkit.getDefaultToolkit; | ||
| import static java.awt.image.BufferedImage.TYPE_INT_RGB; | ||
| import static java.nio.charset.StandardCharsets.UTF_8; | ||
| * Responsible for converting SVG images into rasterized PNG images. | ||
| */ | ||
| +@SuppressWarnings( "rawtypes" ) | ||
| public final class SvgRasterizer { | ||
| + /** | ||
| + * Default hints for high-quality rendering that may be changed by | ||
| + * the system's rendering hints. | ||
| + */ | ||
| + private static final Map<Object, Object> DEFAULT_HINTS = Map.of( | ||
| + KEY_ANTIALIASING, VALUE_ANTIALIAS_ON, | ||
| + KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY, | ||
| + KEY_COLOR_RENDERING, VALUE_COLOR_RENDER_QUALITY, | ||
| + KEY_DITHERING, VALUE_DITHER_DISABLE, | ||
| + KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON, | ||
| + KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC, | ||
| + KEY_RENDERING, VALUE_RENDER_QUALITY, | ||
| + KEY_STROKE_CONTROL, VALUE_STROKE_PURE, | ||
| + KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON | ||
| + ); | ||
| + | ||
| + /** | ||
| + * Shared hints for high-quality rendering. | ||
| + */ | ||
| + public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>( | ||
| + DEFAULT_HINTS | ||
| + ); | ||
| + | ||
| + static { | ||
| + final var toolkit = getDefaultToolkit(); | ||
| + final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" ); | ||
| + | ||
| + if( hints instanceof Map ) { | ||
| + final var map = (Map) hints; | ||
| + | ||
| + for( final var key : map.keySet() ) { | ||
| + final var hint = map.get( key ); | ||
| + RENDERING_HINTS.put( key, hint ); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| private static final SAXSVGDocumentFactory FACTORY_DOM = | ||
| new SAXSVGDocumentFactory( getXMLParserClassName() ); | ||
| * @return The rasterized image. | ||
| */ | ||
| - public static BufferedImage rasterize( final Document svg, final int width ) { | ||
| - try { | ||
| - final var transcoder = new BufferedImageTranscoder(); | ||
| - final var input = new TranscoderInput( svg ); | ||
| - | ||
| - transcoder.addTranscodingHint( KEY_WIDTH, (float) width ); | ||
| - transcoder.transcode( input, null ); | ||
| - return transcoder.getImage(); | ||
| - } catch( final Exception ex ) { | ||
| - clue( ex ); | ||
| - } | ||
| + public static BufferedImage rasterize( final Document svg, final int width ) | ||
| + throws TranscoderException { | ||
| + final var transcoder = new BufferedImageTranscoder(); | ||
| + final var input = new TranscoderInput( svg ); | ||
| - return BROKEN_IMAGE_PLACEHOLDER; | ||
| + transcoder.addTranscodingHint( KEY_WIDTH, (float) width ); | ||
| + transcoder.transcode( input, null ); | ||
| + return transcoder.getImage(); | ||
| } | ||
| * {@link #BROKEN_IMAGE_PLACEHOLDER} if the graphic could not be rasterized. | ||
| */ | ||
| - public static BufferedImage rasterize( final Document document ) { | ||
| - try { | ||
| - final var root = document.getDocumentElement(); | ||
| - final var width = root.getAttribute( "width" ); | ||
| - return rasterize( document, INT_FORMAT.parse( width ).intValue() ); | ||
| - } catch( final Exception ex ) { | ||
| - clue( ex ); | ||
| - } | ||
| - | ||
| - return BROKEN_IMAGE_PLACEHOLDER; | ||
| + public static BufferedImage rasterize( final Document document ) | ||
| + throws ParseException, TranscoderException { | ||
| + final var root = document.getDocumentElement(); | ||
| + final var width = root.getAttribute( "width" ); | ||
| + return rasterize( document, INT_FORMAT.parse( width ).intValue() ); | ||
| } | ||
| * @return The vector graphic transcoded into a raster image format. | ||
| */ | ||
| - public static BufferedImage rasterizeString( final String xml ) { | ||
| - try { | ||
| - final var document = toDocument( xml ); | ||
| - final var root = document.getDocumentElement(); | ||
| - final var width = root.getAttribute( "width" ); | ||
| - return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() ); | ||
| - } catch( final Exception ex ) { | ||
| - clue( ex ); | ||
| - } | ||
| - | ||
| - return BROKEN_IMAGE_PLACEHOLDER; | ||
| + public static BufferedImage rasterizeString( final String xml ) | ||
| + throws ParseException, TranscoderException { | ||
| + final var document = toDocument( xml ); | ||
| + final var root = document.getDocumentElement(); | ||
| + final var width = root.getAttribute( "width" ); | ||
| + return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() ); | ||
| } | ||
| * @return The vector graphic transcoded into a raster image format. | ||
| */ | ||
| - public static BufferedImage rasterizeString( final String svg, final int w ) { | ||
| + public static BufferedImage rasterizeString( final String svg, final int w ) | ||
| + throws TranscoderException { | ||
| return rasterize( toDocument( svg ), w ); | ||
| } | ||
| import com.keenwrite.util.ProtocolScheme; | ||
| -import java.io.File; | ||
| +import java.io.FileNotFoundException; | ||
| import java.io.IOException; | ||
| +import java.io.InputStream; | ||
| import java.net.URL; | ||
| import java.nio.file.Path; | ||
| +import java.util.regex.Pattern; | ||
| import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | ||
| import static com.keenwrite.events.StatusEvent.clue; | ||
| +import static com.keenwrite.io.MediaType.IMAGE_SVG_XML; | ||
| import static com.keenwrite.io.MediaTypeExtension.valueFrom; | ||
| import static com.keenwrite.preferences.WorkspaceKeys.KEY_IMAGES_DIR; | ||
| -import static com.keenwrite.util.ProtocolScheme.isRemote; | ||
| +import static com.keenwrite.preferences.WorkspaceKeys.KEY_IMAGES_ORDER; | ||
| import static java.io.File.createTempFile; | ||
| +import static java.lang.String.format; | ||
| import static java.nio.file.Files.copy; | ||
| import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; | ||
| +import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; | ||
| +import static java.util.regex.Pattern.compile; | ||
| import static org.jsoup.Jsoup.parse; | ||
| import static org.jsoup.nodes.Document.OutputSettings.Syntax; | ||
| /** | ||
| * Responsible for making the body of an HTML document complete by wrapping | ||
| * it with html and body elements. This doesn't have to be super-efficient | ||
| * because it's not run in real-time. | ||
| */ | ||
| public final class XhtmlProcessor extends ExecutorProcessor<String> { | ||
| + private final static Pattern BLANK = | ||
| + compile( "\\p{Blank}", UNICODE_CHARACTER_CLASS ); | ||
| private final Workspace mWorkspace; | ||
| for( final var img : doc.getElementsByTag( "img" ) ) { | ||
| - final var src = img.attr( "src" ); | ||
| - | ||
| try { | ||
| - final var protocol = ProtocolScheme.getProtocol( src ); | ||
| - final File imageFile; | ||
| + final var imageFile = exportImage( img.attr( "src" ) ); | ||
| - if( protocol.isRemote() ) { | ||
| - final var url = new URL( src ); | ||
| - final var conn = url.openConnection(); | ||
| - conn.setUseCaches( false ); | ||
| + img.attr( "src", imageFile.toString() ); | ||
| + } catch( final Exception ex ) { | ||
| + clue( ex ); | ||
| + } | ||
| + } | ||
| - final var type = conn.getContentType(); | ||
| - final var media = MediaType.valueFrom( type ); | ||
| + return doc.html(); | ||
| + } | ||
| - try( final var in = conn.getInputStream() ) { | ||
| - imageFile = createTemporaryFile( media ); | ||
| - copy( in, imageFile.toPath(), REPLACE_EXISTING ); | ||
| - } | ||
| - } | ||
| - else { | ||
| - imageFile = Path.of( getImagePath(), src ).toFile(); | ||
| + /** | ||
| + * For a given src URI, this method will attempt to normalize it such that a | ||
| + * third-party application can find the file. Normalization could entail | ||
| + * downloading from the Internet or finding a suitable file name extension. | ||
| + * | ||
| + * @param src A path, local or remote, to a partial or complete file name. | ||
| + * @return A local file system path to the source path. | ||
| + * @throws Exception Could not read from, write to, or find a file. | ||
| + */ | ||
| + private Path exportImage( final String src ) throws Exception { | ||
| + MediaType mediaType; | ||
| + Path imageFile = null; | ||
| + InputStream svgIn ; | ||
| + | ||
| + final var protocol = ProtocolScheme.getProtocol( src ); | ||
| + | ||
| + if( protocol.isRemote() ) { | ||
| + final var url = new URL( src ); | ||
| + final var conn = url.openConnection(); | ||
| + conn.setUseCaches( false ); | ||
| + | ||
| + final var type = conn.getContentType(); | ||
| + mediaType = MediaType.valueFrom( type ); | ||
| + svgIn = conn.getInputStream(); | ||
| + | ||
| + if( mediaType != IMAGE_SVG_XML ) { | ||
| + // Download into temporary directory. | ||
| + imageFile = createTemporaryFile( mediaType ); | ||
| + copy( svgIn, imageFile, REPLACE_EXISTING ); | ||
| + svgIn.close(); | ||
| + } | ||
| + } | ||
| + else { | ||
| + final var extensions = " " + getImageOrder().trim(); | ||
| + final var imagePath = getImagePath(); | ||
| + | ||
| + // By including " " in the extensions, the first element returned | ||
| + // will be the empty string. Thus the first extension to try is the | ||
| + // file's default extension. Subsequent iterations will try to find | ||
| + // a file that has a name matching one of the preferred extensions. | ||
| + for( final var extension : BLANK.split( extensions ) ) { | ||
| + final var filename = format( | ||
| + "%s%s%s", src, extension.isBlank() ? "" : ".", extension ); | ||
| + imageFile = Path.of( imagePath, filename ); | ||
| + | ||
| + if( imageFile.toFile().exists() ) { | ||
| + break; | ||
| } | ||
| + } | ||
| - img.attr( "src", imageFile.getAbsolutePath() ); | ||
| - } catch( final Exception ex ) { | ||
| - clue( ex ); | ||
| + // If a file name and extension combo could not be found, tell the user. | ||
| + if( imageFile == null ) { | ||
| + imageFile = Path.of( imagePath, src ); | ||
| + throw new FileNotFoundException( imageFile.toString() ); | ||
| } | ||
| } | ||
| - return doc.html(); | ||
| + return imageFile; | ||
| } | ||
| private String getImagePath() { | ||
| return mWorkspace.fileProperty( KEY_IMAGES_DIR ).get().toString(); | ||
| } | ||
| - private static File createTemporaryFile( final MediaType media ) | ||
| + private String getImageOrder() { | ||
| + return mWorkspace.stringProperty( KEY_IMAGES_ORDER ).get(); | ||
| + } | ||
| + | ||
| + private static Path createTemporaryFile( final MediaType media ) | ||
| throws IOException { | ||
| final var file = createTempFile( | ||
| APP_TITLE_LOWERCASE, '.' + valueFrom( media ).getExtension() ); | ||
| file.deleteOnExit(); | ||
| - return file; | ||
| + return file.toPath(); | ||
| } | ||
| } | ||
| } | ||
| - blockNode.appendChild( node ); | ||
| + if( node != null ) { | ||
| + blockNode.appendChild( node ); | ||
| + } | ||
| } | ||
| } |
| import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D; | ||
| import com.whitemagicsoftware.tex.graphics.SvgGraphics2D; | ||
| +import org.apache.batik.transcoder.TranscoderException; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.xml.sax.SAXException; | ||
| import java.io.IOException; | ||
| import java.nio.file.Path; | ||
| +import java.text.ParseException; | ||
| import static com.keenwrite.preview.SvgRasterizer.*; | ||
| @Test | ||
| public void test_Rasterize_SimpleFormula_CorrectImageSize() | ||
| - throws IOException { | ||
| + throws IOException, ParseException, TranscoderException { | ||
| final var g = new SvgGraphics2D(); | ||
| drawGraphics( g ); | ||
| @Test | ||
| public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage() | ||
| - throws ParserConfigurationException, IOException, SAXException { | ||
| + throws ParserConfigurationException, IOException, SAXException, | ||
| + ParseException, TranscoderException { | ||
| final var g = new SvgGraphics2D(); | ||
| drawGraphics( g ); | ||
| @Test | ||
| public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage() | ||
| - throws IOException { | ||
| + throws IOException, ParseException, TranscoderException { | ||
| final var g = new SvgDomGraphics2D(); | ||
| drawGraphics( g ); | ||
| \stopxmlsetups | ||
| -\xmlregistersetup{xml:xhtml} | ||
| \startxmlsetups xml:html | ||
| \startxmlsetups xml:img | ||
| - \placefigure{}{\externalfigure[\xmlatt{#1}{src}]} | ||
| + \starttexcode | ||
| + \placefigure{}{% | ||
| + \externalfigure[\xmlatt{#1}{src}][conversion=mp]% | ||
| + } | ||
| + \stoptexcode | ||
| \stopxmlsetups | ||
| \stop | ||
| \stopxmlsetups | ||
| + | ||
| +\xmlregistersetup{xml:xhtml} | ||
| \setuphead[chapter][ | ||
| page=yes, | ||
| - header=empty, | ||
| - align=middle, | ||
| - after={\blank[line]} | ||
| + header=empty, | ||
| + align=middle, | ||
| + after={\blank[line]} | ||
| ] | ||
| \setuphead[section,subsection][ | ||
| page=no, | ||
| - align=middle, | ||
| - number=no, | ||
| - before={\blank[big]}, | ||
| + align=middle, | ||
| + number=no, | ||
| + before={\blank[big]}, | ||
| after={\blank[line]} | ||
| ] |
| Delta | 174 lines added, 143 lines removed, 31-line increase |
|---|