/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.novella; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import javax.imageio.ImageIO; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLResolver; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import com.google.common.base.Preconditions; import org.apache.commons.lang.ClassUtils; import org.apache.commons.lang.StringUtils; import org.novelang.common.FileTools; import org.novelang.common.Problem; import org.novelang.common.ProblemCollector; import org.novelang.common.SimpleTree; import org.novelang.common.SyntacticTree; import org.novelang.common.tree.Treepath; import org.novelang.common.tree.TreepathTools; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; import org.novelang.parser.NodeKind; /** * Transforms the path of embeddable resources (like {@link NodeKind#RASTER_IMAGE} or * {@link NodeKind#VECTOR_IMAGE}) that are initially relative to the Novella they are referenced from, * into a path relative to the base directory of the content. * This is done by changing the text of the {@link NodeKind#RESOURCE_LOCATION} node. * Also reads resolution of bitmap images and adds corresponding nodes. * * @author Laurent Caillette */ public class ImageFixer { private static final Logger LOGGER = LoggerFactory.getLogger( ImageFixer.class ); private final File baseDirectory; private final File referrerDirectory ; private final ProblemCollector problemCollector ; public ImageFixer( final File baseDirectory, final File referrerDirectory, final ProblemCollector problemCollector ) { Preconditions.checkNotNull( baseDirectory ) ; Preconditions.checkArgument( baseDirectory.exists(), "Does not exist: '%s'", baseDirectory ) ; Preconditions.checkArgument( baseDirectory.isDirectory() ) ; Preconditions.checkNotNull( referrerDirectory ) ; Preconditions.checkArgument( referrerDirectory.exists() ) ; Preconditions.checkArgument( referrerDirectory.isDirectory() ) ; Preconditions.checkArgument( FileTools.isParentOfOrSameAs( baseDirectory, referrerDirectory ), "Base directory '%s' should be parent of referrer directory '%s'", baseDirectory, referrerDirectory ) ; this.baseDirectory = baseDirectory; this.referrerDirectory = referrerDirectory ; this.problemCollector = problemCollector ; LOGGER.debug( "Created ", ClassUtils.getShortClassName( getClass() ), " contentRoot: '", baseDirectory.getAbsolutePath(), "', referrerDirectory: '", referrerDirectory.getAbsolutePath(), "'" ) ; } public SyntacticTree relocateResources( final SyntacticTree tree ) { return relocateAllResources( Treepath.create( tree ) ).getTreeAtEnd() ; } private Treepath<SyntacticTree> relocateAllResources( final Treepath< SyntacticTree > treepath ) { Treepath<SyntacticTree> treepath1 = treepath; final SyntacticTree tree = treepath1.getTreeAtEnd(); if( tree.isOneOf( NodeKind.RASTER_IMAGE, NodeKind.VECTOR_IMAGE ) ) { treepath1 = fixImage( treepath1 ); } else { final int childCount = tree.getChildCount(); for( int i = 0 ; i < childCount ; i++ ) { treepath1 = relocateAllResources( Treepath.create( treepath1, i ) ).getPrevious(); } } return treepath1; } private Treepath< SyntacticTree > fixImage( final Treepath< SyntacticTree > treepathToImage ) { Treepath< SyntacticTree > newTreepath ; final SyntacticTree imageTree = treepathToImage.getTreeAtEnd() ; for( int i = 0 ; i < imageTree.getChildCount() ; i++ ) { final SyntacticTree child = imageTree.getChildAt( i ) ; if( child.isOneOf( NodeKind.RESOURCE_LOCATION ) ) { final String oldLocation = child.getChildAt( 0 ).getText() ; final String newLocation ; try { newLocation = relocate( oldLocation ) ; } catch ( ImageFixerException e ) { LOGGER.debug( ClassUtils.getShortClassName( getClass() ), " got exception: ", // Just debug level, exception will raise later. e.getMessage() ) ; problemCollector.collect( Problem.createProblem( e ) ) ; return treepathToImage ; // Leave unchanged. } LOGGER.debug( "Replacing '", oldLocation, "' by '", newLocation, "'" ) ; final Treepath< SyntacticTree > treepathToResourceLocation = Treepath.create( treepathToImage, i, 0 ) ; newTreepath = TreepathTools.replaceTreepathEnd( treepathToResourceLocation, new SimpleTree( newLocation ) ).getPrevious().getPrevious() ; newTreepath = addImageMetadata( newTreepath, newLocation ) ; return newTreepath ; } } throw new IllegalArgumentException( "Missing child " + NodeKind.RESOURCE_LOCATION + " in " + imageTree.toStringTree() ) ; } private Treepath< SyntacticTree > addImageMetadata( final Treepath< SyntacticTree > treepathToImage, final String imageLocation ) { final File imageFile = new File( baseDirectory, imageLocation ) ; final NodeKind nodeKind = NodeKind.valueOf( treepathToImage.getTreeAtEnd().getText() ) ; Treepath< SyntacticTree > newTreepath = treepathToImage ; try { //noinspection EnumSwitchStatementWhichMissesCases switch( nodeKind ) { case RASTER_IMAGE : newTreepath = addRasterImageMetadata( newTreepath, imageFile ) ; break ; case VECTOR_IMAGE : newTreepath = addVectorImageMetadata( newTreepath, imageFile ) ; break ; default : break ; } } catch( Exception e ) { final String message = "Could not read '" + imageLocation + "'"; problemCollector.collect( Problem.createProblem( message ) ) ; LOGGER.warn( e, message ) ; } return newTreepath ; } private static Treepath< SyntacticTree > addRasterImageMetadata( final Treepath< SyntacticTree > treepathToImage, final File imageFile ) throws IOException { LOGGER.debug( "Extracting raster image metadata from '", imageFile.getAbsolutePath(), "'..." ) ; Treepath< SyntacticTree > newTreepath = treepathToImage ; final BufferedImage bufferedImage = ImageIO.read( imageFile ) ; newTreepath = addImageMetadata( newTreepath, NodeKind._IMAGE_WIDTH, bufferedImage.getWidth() + "px" ) ; newTreepath = addImageMetadata( newTreepath, NodeKind._IMAGE_HEIGHT, bufferedImage.getHeight() + "px" ) ; return newTreepath ; } /** * Decorates the referenced {@link NodeKind#VECTOR_IMAGE} with {@link NodeKind#_IMAGE_WIDTH } * and {@link NodeKind#_IMAGE_HEIGHT}. * TODO: Seems that loading XML document triggers a connection to Internet (w3c.org). */ private static Treepath< SyntacticTree > addVectorImageMetadata( final Treepath< SyntacticTree > treepathToImage, final File imageFile ) throws IOException, XMLStreamException { LOGGER.debug( "Extracting vector image metadata from '", imageFile.getAbsolutePath(), "'..." ) ; String width = null ; String height = null ; final XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance() ; final InputStream inputStream = new FileInputStream( imageFile ) ; try { xmlInputFactory.setProperty( "javax.xml.stream.resolver", ENTITY_RESOLVER ); final XMLStreamReader reader = xmlInputFactory.createXMLStreamReader( inputStream ) ; for( int event = reader.next() ; event != XMLStreamConstants.END_DOCUMENT ; event = reader.next() ) { if( event == XMLStreamConstants.START_ELEMENT && "svg".equals( reader.getName().getLocalPart() ) ) { width = reader.getAttributeValue( "", "width" ) ; height = reader.getAttributeValue( "", "height" ) ; break ; } } } finally { inputStream.close() ; } return addImageMetadata( treepathToImage, width, height ); } /** * @param width maybe null. * @param height maybe null. */ private static Treepath< SyntacticTree > addImageMetadata( final Treepath< SyntacticTree > treepathToImage, final String width, final String height ) { Treepath< SyntacticTree > newTreepath = treepathToImage ; if( ! StringUtils.isBlank( width ) ) { newTreepath = addImageMetadata( newTreepath, NodeKind._IMAGE_WIDTH, width ) ; } if( ! StringUtils.isBlank( height ) ) { newTreepath = addImageMetadata( newTreepath, NodeKind._IMAGE_HEIGHT, height ) ; } return newTreepath ; } private static Treepath<SyntacticTree> addImageMetadata( final Treepath<SyntacticTree> treepathToImage, final NodeKind sizeNodeKind, final String value ) { final SyntacticTree withTree = new SimpleTree( sizeNodeKind, new SimpleTree( value ) ); return TreepathTools.addChildLast( treepathToImage, withTree ).getPrevious(); } /** * Returns an absolute file, given a directory and a relative name. * * @param nameRelativeToReferrer a non-null, non-empty String which may start by a {@code ./}. * File separator is a solidus. * @return a non-null object representing an existing resource file. * * @throws IllegalArgumentException if one of the preconditions on arguments is violated. * @throws ImageFixerException if the resource does not exist, or if the resulting file * is not located under given {@code directory}. */ protected String relocate( final String nameRelativeToReferrer ) throws ImageFixerException { Preconditions.checkArgument( ! StringUtils.isBlank( nameRelativeToReferrer ), "Not expecting: '%s'", nameRelativeToReferrer ) ; final File absoluteResourceFile ; { final File relocated ; if( nameRelativeToReferrer.startsWith( "/" ) ) { relocated = new File( baseDirectory, nameRelativeToReferrer ) ; } else { relocated = new File( referrerDirectory, nameRelativeToReferrer ) ; } try { absoluteResourceFile = relocated.getCanonicalFile() ; } catch ( IOException e ) { throw new RuntimeException( "Should not happen: " + e.getMessage(), e ) ; } } if( ! FileTools.isParentOf( baseDirectory, absoluteResourceFile ) ) { throw new ImageFixerException( "Given resource '" + nameRelativeToReferrer + "' " + "resolved outside of '" + baseDirectory + "'" ) ; } if( ! absoluteResourceFile.exists() ) { throw new ImageFixerException( "Does not exist: '" + absoluteResourceFile.getAbsolutePath() + "'" ) ; } final String resourceNameRelativeToBase = "/" + FileTools.urlifyPath( FileTools.relativizePath( baseDirectory, absoluteResourceFile ) ) ; return resourceNameRelativeToBase ; } /** * Always returns an empty {@code InputStream} with the effect of disabling any entity inclusion. */ private static final XMLResolver ENTITY_RESOLVER = new XMLResolver() { @Override public InputStream resolveEntity( final String publicId, final String systemId, final String baseURI, final String namespace ) { return new ByteArrayInputStream( new byte[] { } ) ; } } ; }