| Author | DaveJarvis <email> |
|---|---|
| Date | 2021-03-07 23:15:25 GMT-0800 |
| Commit | bfef24b3221246f4b618546281edfb1d508aa90e |
| Parent | b368ed8 |
| .stream() | ||
| .collect( | ||
| - groupingBy( path -> bin.apply( MediaType.valueFrom( path ) ) ) | ||
| + groupingBy( path -> bin.apply( MediaType.fromFilename( path ) ) ) | ||
| ); | ||
| 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 ]; | ||
| } | ||
| + | ||
| } | ||
| */ | ||
| 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; | ||
| } | ||
| 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 { | ||
| 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() ); | ||
| 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 ); | ||
| } | ||
| 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; | ||
| } | ||
| } |
| @Override | ||
| - protected boolean removeEldestEntry( | ||
| - final Map.Entry<K, V> eldest ) { | ||
| + protected boolean removeEldestEntry( final Map.Entry<K, V> eldest ) { | ||
| return size() > mCacheSize; | ||
| } |
| /** | ||
| - * 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 ) ) ); | ||
| } | ||
| +#!/usr/bin/env bash | ||
| + | ||
| +# | ||
| +# Example command-line to be invoked directly by the application. | ||
| +# | ||
| + | ||
| +context --environment="setups,entities,style,classes" "$1.xml" | ||
| + | ||
| +\defineframedtext[projection][ | ||
| + style=tt, | ||
| + width=\textwidth, | ||
| +] | ||
| + | ||
| +% 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{}} | ||
| + | ||
| -\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 | ||
| \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] | ||
| + | ||
| Delta | 211 lines added, 47 lines removed, 164-line increase |
|---|