Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Autodetect image file extensions from user preferences and content-type

AuthorDaveJarvis <email>
Date2021-03-16 22:09:39 GMT-0700
Commitf4dfed9838d369dbe10788196102cdac71084f56
Parent6d4b12a
build.gradle
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}"
src/main/java/com/keenwrite/io/MediaType.java
/*
+ * PDF Media Type.
+ * https://www.rfc-editor.org/rfc/rfc3778.txt
+ */
+ APP_PDF(
+ APPLICATION, "pdf"
+ ),
+
+ /*
* Standard font types.
*/
src/main/java/com/keenwrite/io/MediaTypeExtension.java
*/
public enum MediaTypeExtension {
+ MEDIA_APP_PDF( APP_PDF ),
+
MEDIA_FONT_OTF( FONT_OTF ),
MEDIA_FONT_TTF( FONT_TTF ),
src/main/java/com/keenwrite/preview/RenderingSettings.java
-/* 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() {
- }
-}
src/main/java/com/keenwrite/preview/SvgRasterizer.java
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 );
}
src/main/java/com/keenwrite/processors/XhtmlProcessor.java
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();
}
}
src/main/java/com/keenwrite/processors/markdown/extensions/r/RExtension.java
}
- blockNode.appendChild( node );
+ if( node != null ) {
+ blockNode.appendChild( node );
+ }
}
}
src/test/java/com/keenwrite/tex/TeXRasterization.java
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 );
tex/setups.tex
\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}
tex/style.tex
\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]}
]
Delta174 lines added, 143 lines removed, 31-line increase