Dave Jarvis' Repositories

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

Add rasterization upon export

AuthorDaveJarvis <email>
Date2021-03-07 23:15:25 GMT-0800
Commitbfef24b3221246f4b618546281edfb1d508aa90e
Parentb368ed8
src/main/java/com/keenwrite/MainPane.java
.stream()
.collect(
- groupingBy( path -> bin.apply( MediaType.valueFrom( path ) ) )
+ groupingBy( path -> bin.apply( MediaType.fromFilename( path ) ) )
);
src/main/java/com/keenwrite/io/HttpMediaType.java
map.forEach( ( key, values ) -> {
if( "content-type".equalsIgnoreCase( key ) ) {
- var header = values.get( 0 );
- // Trim off the character encoding.
- var i = header.indexOf( ';' );
- header = header.substring( 0, i == -1 ? header.length() : i );
-
- // Split the type and subtype.
- i = header.indexOf( '/' );
- i = i == -1 ? header.length() : i;
- final var type = header.substring( 0, i );
- final var subtype = header.substring( i + 1 );
-
- mediaType[ 0 ] = MediaType.valueFrom( type, subtype );
+ mediaType[ 0 ] = MediaType.valueFrom( values.get( 0 ) );
clue( "Main.status.image.request.success", mediaType[ 0 ] );
}
return mediaType[ 0 ];
}
+
}
src/main/java/com/keenwrite/io/MediaType.java
*/
public static MediaType valueFrom( final File file ) {
- return valueFrom( file.getName() );
+ return fromFilename( file.getName() );
+ }
+
+ /**
+ * Returns the {@link MediaType} associated with the given file name.
+ *
+ * @param filename The file name that may contain an extension associated
+ * with a known {@link MediaType}.
+ * @return {@link MediaType#UNDEFINED} if the extension has not been
+ * assigned, otherwise the {@link MediaType} associated with this
+ * URL's file name extension.
+ */
+ public static MediaType fromFilename( final String filename ) {
+ return getMediaType( getExtension( filename ) );
}
/**
- * Returns the {@link MediaType} associated with the given file name.
+ * Determines the media type an IANA-defined, semi-colon-separated string.
+ * This is often used after making an HTTP request to extract the type
+ * and subtype from the content-type.
*
- * @param filename The file name that may contain an extension associated
- * with a known {@link MediaType}.
- * @return {@link MediaType#UNDEFINED} if the extension has not been
- * assigned, otherwise the {@link MediaType} associated with this
- * URL's file name extension.
+ * @param header The content-type header value.
+ * @return The data type for the resource or {@link MediaType#UNDEFINED} if
+ * unmapped.
*/
- public static MediaType valueFrom( final String filename ) {
- return getMediaType( getExtension( filename ) );
+ public static MediaType valueFrom( String header ) {
+ // Trim off the character encoding.
+ var i = header.indexOf( ';' );
+ header = header.substring( 0, i == -1 ? header.length() : i );
+
+ // Split the type and subtype.
+ i = header.indexOf( '/' );
+ i = i == -1 ? header.length() : i;
+ final var type = header.substring( 0, i );
+ final var subtype = header.substring( i + 1 );
+
+ return valueFrom( type, subtype );
}
/**
- * Used by {@link MediaTypeExtension} to initialize associations where the
- * subtype name and the file name extension have a 1:1 mapping.
+ * Returns the IANA-defined subtype classification. Primarily used by
+ * {@link MediaTypeExtension} to initialize associations where the subtype
+ * name and the file name extension have a 1:1 mapping.
*
* @return The IANA subtype value.
*/
- String getSubtype() {
+ public String getSubtype() {
return mSubtype;
}
src/main/java/com/keenwrite/preview/SvgRasterizer.java
package com.keenwrite.preview;
-import javafx.scene.image.ImageView;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.gvt.renderer.ImageRenderer;
import static javax.xml.transform.OutputKeys.*;
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
+import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER;
import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
/**
- * Rasterizes the resource specified by the path into an image.
+ * Rasterizes the given SVG input stream into an image at 96 DPI.
*
- * @param svg The SVG data to rasterize.
- * @return The resource at the given path as an {@link ImageView}.
+ * @param svg The SVG data to rasterize, must be closed by caller.
+ * @return The given input stream converted to a rasterized image.
*/
public static BufferedImage rasterize( final InputStream svg )
throws TranscoderException {
- final var in = new TranscoderInput( svg );
+ return rasterize( svg, 96 );
+ }
+
+ /**
+ * Rasterizes the given SVG input stream into an image.
+ *
+ * @param svg The SVG data to rasterize, must be closed by caller.
+ * @param dpi Resolution to use when rasterizing (default is 96 DPI).
+ * @return The given input stream converted to a rasterized image at the
+ * given resolution.
+ */
+ public static BufferedImage rasterize(
+ final InputStream svg, final float dpi )
+ throws TranscoderException {
final var transcoder = new BufferedImageTranscoder();
- transcoder.transcode( in, null );
+ transcoder.addTranscodingHint(
+ KEY_PIXEL_UNIT_TO_MILLIMETER, 1f / dpi * 25.4f );
+ transcoder.transcode( new TranscoderInput( svg ), null );
return transcoder.getImage();
}
}
+ /**
+ * Rasterizes the given vector graphic file using the width dimension
+ * specified by the document's width attribute.
+ *
+ * @param document The {@link Document} containing a vector graphic.
+ * @return A rasterized image as an instance of {@link BufferedImage}, or
+ * {@link #BROKEN_IMAGE_PLACEHOLDER} if the graphic could not be rasterized.
+ */
public static BufferedImage rasterize( final Document document ) {
try {
src/main/java/com/keenwrite/preview/SvgReplacedElementFactory.java
if( getProtocol( source ).isHttp() ) {
- var mediaType = MediaType.valueFrom( source );
+ var mediaType = MediaType.fromFilename( source );
if( isSvg( mediaType ) || mediaType == UNDEFINED ) {
}
}
- else if( isSvg( MediaType.valueFrom( source ) ) ) {
+ else if( isSvg( MediaType.fromFilename( source ) ) ) {
// Attempt to rasterize based on file name.
final var path = Path.of( new URI( source ).getPath() );
src/main/java/com/keenwrite/processors/ProcessorFactory.java
import com.keenwrite.AbstractFileFactory;
+import com.keenwrite.preferences.Workspace;
import com.keenwrite.preview.HtmlPreview;
import com.keenwrite.processors.markdown.MarkdownProcessor;
? createHtmlPreviewProcessor()
: context.isExportFormat( XHTML_TEX )
- ? createXhtmlProcessor()
+ ? createXhtmlProcessor( context.getWorkspace() )
: createIdentityProcessor();
* @return An instance of {@link Processor} that completes an HTML document.
*/
- private Processor<String> createXhtmlProcessor() {
- return new XhtmlProcessor();
+ private Processor<String> createXhtmlProcessor( final Workspace workspace ) {
+ return new XhtmlProcessor( workspace );
}
src/main/java/com/keenwrite/processors/XhtmlProcessor.java
package com.keenwrite.processors;
+import com.keenwrite.io.MediaType;
+import com.keenwrite.preferences.Workspace;
+import com.keenwrite.util.ProtocolScheme;
+
+import javax.imageio.ImageIO;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+
+import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
+import static com.keenwrite.events.StatusEvent.clue;
+import static com.keenwrite.io.MediaTypeExtension.valueFrom;
+import static com.keenwrite.preview.SvgRasterizer.rasterize;
+import static java.io.File.createTempFile;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+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.
+ * 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 Workspace mWorkspace;
+
+ public XhtmlProcessor( final Workspace workspace ) {
+ mWorkspace = workspace;
+ }
+
@Override
public String apply( final String html ) {
- return "<html><body>" + html + "</body></html>";
+ final var doc = parse( html );
+ doc.outputSettings().syntax( Syntax.xml );
+
+ for( final var img : doc.getElementsByTag( "img" ) ) {
+ final var src = img.absUrl( "src" );
+
+ try {
+ final var url = new URL( src );
+ final var protocol = ProtocolScheme.valueFrom( url );
+
+ if( protocol.isRemote() ) {
+ final var conn = url.openConnection();
+ conn.setUseCaches( false );
+
+ final var type = conn.getContentType();
+ final var media = MediaType.valueFrom( type );
+
+ try( final var in = conn.getInputStream() ) {
+ File imageFile;
+
+ if( media == MediaType.IMAGE_SVG_XML ) {
+ // Rasterize.
+ final var image = rasterize( in, 300f );
+ final var mt = MediaType.IMAGE_PNG;
+ imageFile = createTemporaryFile( mt );
+ ImageIO.write( image, mt.getSubtype(), imageFile );
+ }
+ else {
+ // Download into temporary directory.
+ imageFile = createTemporaryFile( media );
+ Files.copy( in, imageFile.toPath(), REPLACE_EXISTING );
+ }
+
+ img.attr( "src", imageFile.getAbsolutePath() );
+ }
+ }
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ return doc.html();
+ }
+
+ private static File createTemporaryFile( final MediaType media )
+ throws IOException {
+ final var file = createTempFile(
+ APP_TITLE_LOWERCASE, '.' + valueFrom( media ).getExtension() );
+ file.deleteOnExit();
+ return file;
}
}
src/main/java/com/keenwrite/util/BoundedCache.java
@Override
- protected boolean removeEldestEntry(
- final Map.Entry<K, V> eldest ) {
+ protected boolean removeEldestEntry( final Map.Entry<K, V> eldest ) {
return size() > mCacheSize;
}
src/test/java/com/keenwrite/io/MediaTypeTest.java
/**
- * Test that {@link MediaType#valueFrom(String)} can lookup by file name.
+ * Test that {@link MediaType#fromFilename(String)} can lookup by file name.
*/
@Test
);
- map.forEach( ( k, v ) -> assertEquals( v, valueFrom( "f." + k ) ) );
+ map.forEach( ( k, v ) -> assertEquals( v, fromFilename( "f." + k ) ) );
}
tex/build.sh
+#!/usr/bin/env bash
+
+#
+# Example command-line to be invoked directly by the application.
+#
+
+context --environment="setups,entities,style,classes" "$1.xml"
+
tex/classes.tex
+\defineframedtext[projection][
+ style=tt,
+ width=\textwidth,
+]
+
tex/entities.tex
+% Map XHTML document entities to ConTeXt symbols and TeX macros.
+
+\xmltexentity{ldquo}{\symbol[leftquotation]{}}
+\xmltexentity{rdquo}{\symbol[rightquotation]{}}
+\xmltexentity{rsquo}{\quotesingle{}}
+\xmltexentity{mdash}{\emdash{}}
+\xmltexentity{ndash}{\endash{}}
+\xmltexentity{hellip}{\dots{}}
+
tex/setups.tex
-\startxmlsetups xml:xhtmlsetups
+% XML setups map ConTeXt commands to HTML elements.
+
+\startxmlsetups xml:xhtml
% Do not typeset the HTML document's header title element.
\xmlsetsetup{\xmldocument}{*}{-}
- % Typeset the elements that follow.
- \xmlsetsetup{\xmldocument}{html|body|h1|h2|h3|p|em|strong|q|div}{xml:*}
+ % Only elements included in this list are typeset.
+ \xmlsetsetup{\xmldocument}{%
+ html|body|div|h1|h2|h3|h4|h5|h6|p|blockquote|span|em|q|b|strong|%
+ a|ul|ol|li|dl|dt|dd|hr|br|sup|sub|code|img%
+ }{xml:*}
\stopxmlsetups
-\xmlregistersetup{xml:xhtmlsetups}
+\xmlregistersetup{xml:xhtml}
\startxmlsetups xml:html
\stopxmlsetups
+% Paragraphs are followed by a paragraph break.
\startxmlsetups xml:p
\xmlflush{#1}\par
\stopxmlsetups
+% Emphasized text is italicized, typically.
\startxmlsetups xml:em
\dontleavehmode{\em\xmlflush{#1}}
\stopxmlsetups
+% Strong text is bolded, typically.
\startxmlsetups xml:strong
\dontleavehmode{\bf\xmlflush{#1}}
+\stopxmlsetups
+
+\startxmlsetups xml:img
+ \placefigure[none]{}{%
+ \externalfigure[\xmlatt{#1}{src}]
+ }
\stopxmlsetups
\startxmlsetups xml:q
\quotation{\xmlflush{#1}}
\stopxmlsetups
+% Map arbitrary div classes, defined by fenced divs.
\startxmlsetups xml:div
- GOT DIV?
+ \start[\xmlatt{#1}{class}]
+ \xmlflush{#1}
+ \stop
\stopxmlsetups
tex/style.tex
\setupindenting[medium, yes]
+\setupexternalfigures[
+ %order={svg,pdf,png},
+ %directory={images},
+ %maxwidth=\makeupwidth,
+ width=\makeupwidth,
+]
+
+% Force images to flow exactly where they fall in the text.
+\setupfloat[figure][default=force]
+
+% Indent the paragraph following each image.
+\setupfloats[indentnext=yes]
+
Delta211 lines added, 47 lines removed, 164-line increase