/*

 ============================================================================
                   The Apache Software License, Version 1.1
 ============================================================================

 Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.

 Redistribution and use in source and binary forms, with or without modifica-
 tion, are permitted provided that the following conditions are met:

 1. Redistributions of  source code must  retain the above copyright  notice,
    this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

 3. The end-user documentation included with the redistribution, if any, must
    include  the following  acknowledgment:  "This product includes  software
    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
    Alternately, this  acknowledgment may  appear in the software itself,  if
    and wherever such third-party acknowledgments normally appear.

 4. The names "Batik" and  "Apache Software Foundation" must  not  be
    used to  endorse or promote  products derived from  this software without
    prior written permission. For written permission, please contact
    apache@apache.org.

 5. Products  derived from this software may not  be called "Apache", nor may
    "Apache" appear  in their name,  without prior written permission  of the
    Apache Software Foundation.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
 APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
 DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
 ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
 (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 This software  consists of voluntary contributions made  by many individuals
 on  behalf of the Apache Software  Foundation. For more  information on the
 Apache Software Foundation, please see <http://www.apache.org/>.

*/

package org.apache.batik.apps.rasterizer;

import java.awt.Color;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import org.apache.batik.transcoder.Transcoder;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.ImageTranscoder;
import org.apache.batik.transcoder.image.JPEGTranscoder;
import org.apache.batik.transcoder.image.PNGTranscoder;

/**
 * This application can be used to convert SVG images to raster images.
 * <br />
 * Possible result raster image formats are PNG, JPEG, TIFF, and PDF. 
 * The Batik Transcoder API is used to execute the conversion. FOP is 
 * needed to be able to transcode to the PDF format<br />
 *
 * The source has to be list of files or URL (set by the <tt>setSources</tt> 
 * method). <br />
 *
 * The destination can be:<br /><ul>
 * <li><b>unspecified</b>. In that case, only file sources can be converted and 
 * a file in the same directory as the source will be created.</li>
 * <li><b>a directory</b>, set by the <tt>setDst</tt> method. In that case,
 * the output files are created in that destination directory</li>
 * <li><b>a file</b>. In case there is a <i>single 
 * source</i>, the destination can be a single named file 
 * (set with the <tt>setDst</tt> method.</li>)<br />
 * </ul>
 *
 * <hr />
 *
 * There are a number of options which control the way the image is
 * converted to the destination format:<br /><ul>
 * <li>destinationType: controls the type of conversion which should be done. 
 *     see the {@link DestinationType} documentation.</li>
 * <li>width/height: they control the desired width and height, in user space,
 *     for the output image.</li>
 * <li>maxWidth/maxHeight: control the maximum width and height, 
 *     in user space, of the output image.</li>
 * <li>area: controls the specific sub-area of the image which should be 
 *     rendered.</li>
 * <li>backgroundColor: controls the color which is used to fill the 
 *     background before rendering the image</li>
 * <li>quality: relevant only for JPEG destinations, this controls the 
 *     encoding quality.</li>
 * <li>indexed: relevant only for PNG, controls the number of bits
 *              used in writting of a palletized files.</li>
 * <li>mediaType: controls the CSS media, or list of media, for which the 
 *     image should be rendered.</li>
 * <li>alternate: controls the alternate CSS stylesheet to activate, 
 *     if any.</li>
 * <li>language: controls the user language with which the SVG document 
 *     should be converted.</li>
 * <li>userStylesheet: defines the user stylesheet to apply to SVG documents 
 *     in addition to other stylesheets referenced by or embedded in the 
 *     SVG documents.</li>
 * <li>pixelUnitToMillimeter: defines the size of a pixel in millimeters 
 *     to use when processing the SVG documents.</li>
 * </ul>
 *
 * @version $Id: SVGConverter.java,v 1.20 2003/08/08 11:38:49 vhardy Exp $
 * @author <a href="Henri.Ruini@nokia.com">Henri Ruini</a>
 * @author <a href="mailto:vhardy@apache.org">Vincent Hardy</a>
 */
public class SVGConverter {
    // 
    // Error codes reported by the SVGConverter
    //

    //
    // Reported when no source file has been specified.
    //
    public static final String ERROR_NO_SOURCES_SPECIFIED
        = "SVGConverter.error.no.sources.specified";

    //
    // Reported when there is more than one valid input source
    // and no output directory has been set and the source is
    // not a file.
    //
    public static final String ERROR_CANNOT_COMPUTE_DESTINATION
        = "SVGConverter.error.cannot.compute.destination";

    //
    // Reported when the dst is a file and there are multiple 
    // sources.
    //
    public static final String ERROR_CANNOT_USE_DST_FILE
        = "SVGConverter.error.cannot.use.dst.file";

    //
    // Reported when the <tt>Transcoder</tt> for the requested
    // <tt>destinationType</tt> cannot be found.
    //
    public static final String ERROR_CANNOT_ACCESS_TRANSCODER 
        = "SVGConverter.error.cannot.access.transcoder";

    //
    // Reported when the source is found to be the same as
    // the destination. Note that it is not guaranteed that
    // this error condition will always be detected.
    //
    public static final String ERROR_SOURCE_SAME_AS_DESTINATION
        = "SVGConverter.error.source.same.as.destination";

    //
    // Reported when one of the sources cannot be read.
    //
    public static final String ERROR_CANNOT_READ_SOURCE
        = "SVGConverter.error.cannot.read.source";

    //
    // Reported when an error happens while opening a source
    // file.
    //
    public static final String ERROR_CANNOT_OPEN_SOURCE
        = "SVGConverter.error.cannot.open.source";

    //
    // Reported if the output is not writeable. This may 
    // happen if the output file already exists and does not
    // have write permission.
    //
    public static final String ERROR_OUTPUT_NOT_WRITEABLE
        = "SVGConverter.error.output.not.writeable";

    //
    // Reported when an error happens while trying to open
    // the output file for writing.
    //
    public static final String ERROR_CANNOT_OPEN_OUTPUT_FILE
        = "SVGConverter.error.cannot.open.output.file";

    //
    // Reported when the converter was not able to create 
    // the destination directory for the files.
    //
    public static final String ERROR_UNABLE_TO_CREATE_OUTPUT_DIR
        = "SVGConverter.error.unable.to.create.output.dir";

    //
    // Reported when an error occurs while convertion the 
    // source file.
    //
    public static final String ERROR_WHILE_RASTERIZING_FILE
        = "SVGConverter.error.while.rasterizing.file";

    //
    // Class variables and constants 
    //

    /** SVG file extension */
    protected static final String SVG_EXTENSION = ".svg";

    /** Default quality value. -1 means disabled*/
    protected static final float DEFAULT_QUALITY 
        = -1f;

    /** Maximum quality value */
    protected static final float MAXIMUM_QUALITY
        = .99F;

    /** Default result type */
    protected static final DestinationType DEFAULT_RESULT_TYPE 
        = DestinationType.PNG;

    /** Default width */
    protected static final float DEFAULT_WIDTH = -1;

    /** Default height */
    protected static final float DEFAULT_HEIGHT = -1;

    /** Result type */
    protected DestinationType destinationType = DEFAULT_RESULT_TYPE;

    /** Output image height. */
    protected float height = DEFAULT_HEIGHT;

    /** Output image width. */
    protected float width = DEFAULT_WIDTH;

    /** Maximum output image height. */
    protected float maxHeight = DEFAULT_HEIGHT;

    /** Maximum output image width. */
    protected float maxWidth = DEFAULT_WIDTH;

    /** Output image quality. */
    protected float quality = DEFAULT_QUALITY;

    /** Should output Image be indexed . */
    protected int indexed = -1;

    /** Output AOI area. */
    protected Rectangle2D area = null;

    /** Language */
    protected String language = null;

    /** User stylesheet */
    protected String userStylesheet = null;

    /** Millimeters Per Pixel */
    protected float pixelUnitToMillimeter = -1f;

    /** Validation flag */
    protected boolean validate = false;

    /** Execute the 'onload' scripts flag */
    protected boolean executeOnload = false;

    /** Set of allowed script types. */
    protected String allowedScriptTypes = null;

    /** Controls whether scripts can only have the same origin as 
        the document which references them. */
    protected boolean constrainScriptOrigin = true;

    /** Controls whether scripts should be run securely or not */
    protected boolean securityOff = false;

    /** Sources files or URLs */
    protected Vector sources = null;

    /** 
     * Destination image path. Can be a file (for single source) or
     *  a directory 
     */
    protected File dst;

    /** Background color for the output images. */
    protected Color backgroundColor = null;

    /** Media type for which the SVG image should be rendered */
    protected String mediaType = null;

    /** Default value for the font-family when it is unspecified */
    protected String defaultFontFamily = null;

    /** Alternate stylesheet for which should be applied to the SVG */
    protected String alternateStylesheet = null;

    /** Contents of <tt>fileset</tt> elements. */
    protected Vector files = new Vector();

    /**
     * Controls some aspects of the converter's operation,
     *  such as whether or not it should proceed in some
     *  error situations. See {@link SVGConverterController}
     */
    protected SVGConverterController controller;

    //
    // Default constructor
    //
    public SVGConverter(){
        this(new DefaultSVGConverterController());
    }

    //
    // Constructor
    //
    public SVGConverter(SVGConverterController controller){
        if (controller == null){
            throw new IllegalArgumentException();
        }

        this.controller = controller;
    }

    // 
    // Property get/set methods 
    // 

    /**
     * Sets the <tt>destinationType</tt> attribute value. 
     * Should not be null.
     */
    public void setDestinationType(DestinationType destinationType) {
        if(destinationType == null){
            throw new IllegalArgumentException();
        }
        this.destinationType = destinationType;
    }

    public DestinationType getDestinationType(){
        return destinationType;
    }

    /**
     * In less than or equal to zero, the height is not
     * constrained on the output image. The height is in
     * user space.
     */
    public void setHeight(float height) {
        this.height = height;
    }

    public float getHeight(){
        return height;
    }

    /**
     * In less than or equal to zero, the width is not
     * constrained on the output image. The width is in
     * user space.
     */
    public void setWidth(float width) {
        this.width = width;
    }

    public float getWidth(){
        return width;
    }

    /**
     * If less than or equal to zero, the maximum height 
     * does not have any effect on the output image. 
     * The maximum height is in user space.
     */
    public void setMaxHeight(float height) {
        this.maxHeight = height;
    }

    public float getMaxHeight(){
        return maxHeight;
    }

    /**
     * If less than or equal to zero, the maximum width 
     * does not have any effect on the output image. 
     * The maximum width is in user space.
     */
    public void setMaxWidth(float width) {
        this.maxWidth = width;
    }

    public float getMaxWidth(){
        return maxWidth;
    }

    /**
     * Sets the JPEG encoding quality. The value should be strictly
     * less than 1. If the value is less than zero, then the maximum
     * encoding quality is used.
     */
    public void setQuality(float quality) throws IllegalArgumentException {
        if(quality >= 1){
            throw new IllegalArgumentException();
        }

        this.quality = quality;
    }

    public float getQuality(){
        return quality;
    }

    /**
     * Tells the PNG encoder to reduce the image to 256 colors, so the
     * PNG file is indexed.
     */
    public void setIndexed(int bits) throws IllegalArgumentException {
        this.indexed = bits;
    }

    public int getIndexed(){
        return indexed;
    }

    /**
     * Sets the user language. If the value is null, then the default (see 
     * {@link org.apache.batik.bridge.UserAgent#getLanguages})
     * is used.
     */
    public void setLanguage(String language){
        this.language = language;
    }

    public String getLanguage(){
        return language;
    }

    /**
     * Sets the user stylesheet. May be null.
     */
    public void setUserStylesheet(String userStylesheet){
        this.userStylesheet = userStylesheet;
    }

    public String getUserStylesheet(){
        return userStylesheet;
    }

    /**
     * Sets the millimeters per pixel constant. A negative
     * value will cause the default value 
     * (see {@link org.apache.batik.bridge.UserAgent#getPixelUnitToMillimeter})
     * to be used.
     */
    public void setPixelUnitToMillimeter(float pixelUnitToMillimeter){
        this.pixelUnitToMillimeter = pixelUnitToMillimeter;
    }

    public float getPixelUnitToMillimeter(){
        return pixelUnitToMillimeter;
    }

    /**
     * Sets the <tt>area</tt> as a Rectangle. This value can
     * be null in which case the whole image will be rendered. If the 
     * area is not null, then only the portion of the image it
     * defines will be rendered.
     */
    public void setArea(Rectangle2D area){
        this.area = area;
    }

    public Rectangle2D getArea(){
        return area;
    }

    /**
     * Sets the list of individual SVG sources. The strings 
     * can be either URLs or file names. Note that invalid
     * sources (e.g., read-protected files or invalid URLs)
     * will cause <tt>SVGConverterExceptions</tt> to be 
     * thrown during the transcoding process (see {@link #execute});
     */
    public void setSources(String[] sources) {
        if(sources == null){
            this.sources = null;
        }
        else{
            this.sources = new Vector();
            for (int i=0; i<sources.length; i++){
                if (sources[i] != null){
                    this.sources.addElement(sources[i]);
                }
            }

            if (this.sources.size() == 0){
                this.sources = null;
            }
        }
    }

    public Vector getSources(){
        return sources;
    }

    /**
     * When converting a single source, dst can be a file.
     * Othewise, it should be a directory.
     */
    public void setDst(File dst) {
        this.dst = dst;
    }

    public File getDst(){
        return dst;
    }

    /**
     * Sets the <tt>backgroundColor</tt> value. This can be
     * null in which case no color will be used to fill the 
     * background before rendering this SVG image.
     */
    public void setBackgroundColor(Color backgroundColor){
        this.backgroundColor = backgroundColor;
    }

    public Color getBackgroundColor(){
        return backgroundColor;
    }

    /**
     * Sets the <tt>mediaType</tt> value. This value controls
     * the CSS media for which the image should be rendered. It 
     * can be null, in which case no specific media selectors will
     * apply. If it is not null, it can contain space separated values
     * of the medias for which the image should be rendered. For example,
     * "screen", "print" or "scree projection" are valid values.
     */
    public void setMediaType(String mediaType){
        this.mediaType = mediaType;
    }

    public String getMediaType(){
        return mediaType;
    }

    /**
     * Sets the <tt>defaultFontFamily</tt> value. This value controls
     * the default value for the font-family CSS property when that
     * property is unspecified.
     */
    public void setDefaultFontFamily(String defaultFontFamily) {
        this.defaultFontFamily = defaultFontFamily;
    }

    public String getDefaultFontFamily() {
        return defaultFontFamily;
    }

    /**
     * Sets the <tt>alternateStyleSheet</tt> value. This value
     * controls the CSS alternate stylesheet to select in the 
     * rendered SVG file(s). It may be null, in which case no alternate
     * stylesheet will be selected.
     */
    public void setAlternateStylesheet(String alternateStylesheet){
        this.alternateStylesheet = alternateStylesheet;
    }

    public String getAlternateStylesheet(){
        return alternateStylesheet;
    }

    /**
     * Defines whether or not input sources should be validated in
     * the conversion process
     */
    public void setValidate(boolean validate){
        this.validate = validate;
    }

    public boolean getValidate(){
        return validate;
    }

    /**
     * Sets whether or not scripts attached to the DOM using 'onload'
     * event attribute must be executed before rasterizing.
     *
     * @param b true means scripts will be executed
     */
    public void setExecuteOnload(boolean b){
        this.executeOnload = b;
    }

    /**
     * Returns true if the scripts attached to the DOM using 'onload'
     * event attribute is going to be executed before rasterizing,
     * false otherwise.
     */
    public boolean getExecuteOnload(){
        return executeOnload;
    }
    
    /**
     * Sets the set of allowed script types (i.e., the set of possible
     * values for the type attribute in the &lt;script&gt; element),
     * as a comma separated list of allowed values.
     */
    public void setAllowedScriptTypes(String allowedScriptTypes){
        this.allowedScriptTypes = allowedScriptTypes;
    }

    /**
     * Returns the list of allowed script types.
     *
     * @see #setAllowedScriptTypes
     */
    public String getAllowedScriptTypes(){
        return allowedScriptTypes;
    }

    /**
     * Sets whether scripts should only be loaded from the same
     * location as the documents referencing them.
     */
    public void setConstrainScriptOrigin(boolean constrainScriptOrigin){
        this.constrainScriptOrigin = constrainScriptOrigin;
    }

    /**
     * Returns whether scripts can only be loaded from the same
     * origin as the documents referencing them.
     */
    public boolean getConstrainScriptOrigin(){
        return constrainScriptOrigin;
    }

    /**
     * Sets whether or not scripts should be run securely
     */
    public void setSecurityOff(boolean securityOff){
        this.securityOff = securityOff;
    }

    /**
     * Returns whether or not scripts will be run securely
     */
    public boolean getSecurityOff(){
        return securityOff;
    }

    /**
     * Returns true if f is a File. f is found to be a file if
     * it exists and is a file. If it does not exist, it is declared
     * to be a file if it has the same extension as the DestinationType.
     */
    protected boolean isFile(File f){
        if (f.exists()){
            return f.isFile();
        } else {
            if (f.toString().toLowerCase().endsWith(destinationType.getExtension())){
                return true;
            }
        }

        return false;
    }

    /**
     * Starts the conversion process.
     * @throws SVGConverterException thrown if parameters are not set correctly.
     */
    public void execute() throws SVGConverterException {
        // Compute the set of SVGConverterSource from the source properties
        // (srcDir and srcFile);
        // This throws an exception if there is not at least one src file.
        Vector sources = computeSources();

        // Compute the destination files from dest
        Vector dstFiles = null;
        if(sources.size() == 1 && dst != null && isFile(dst)){
            dstFiles = new Vector();
            dstFiles.addElement(dst);
        }
        else{
            dstFiles = computeDstFiles(sources);
        }

        // Now, get the transcoder to use for the operation
        Transcoder transcoder = destinationType.getTranscoder();
        if(transcoder == null) {
            throw new SVGConverterException(ERROR_CANNOT_ACCESS_TRANSCODER,
                                             new Object[]{destinationType.toString()},
                                             true /* fatal error */);
        }

        // Now, compute the set of transcoding hints to use
        Map hints = computeTranscodingHints();
        transcoder.setTranscodingHints(hints);

        // Notify listener that task has been computed
        if(!controller.proceedWithComputedTask(transcoder,
                                               hints,
                                               sources,
                                               dstFiles)){
            return;
        }

        // Convert files one by one
        for(int i = 0 ; i < sources.size() ; i++) {
            // Get the file from the vector.
            SVGConverterSource currentFile 
                = (SVGConverterSource)sources.elementAt(i);
            File outputFile  = (File)dstFiles.elementAt(i);

            createOutputDir(outputFile);
            transcode(currentFile, outputFile, transcoder);
        }
    }
    
    /**
     * Populates a vector with destination files names
     * computed from the names of the files in the sources vector
     * and the value of the dst property
     */
    protected Vector computeDstFiles(Vector sources) 
    throws SVGConverterException {
        Vector dstFiles = new Vector();
        if (dst != null) {
            if (dst.exists() && dst.isFile()) {
                throw new SVGConverterException(ERROR_CANNOT_USE_DST_FILE);
            }

            //
            // Either dst exist and is a directory or dst does not
            // exist and we may fail later on in createOutputDir
            //
            int n = sources.size();
            for(int i=0; i<n; i++){
                SVGConverterSource src = (SVGConverterSource)sources.elementAt(i);
                // Generate output filename from input filename.
                File outputName = new File(dst.getPath(), 
                                           getDestinationFile(src.getName()));
                dstFiles.addElement(outputName);
                
            }
        } else {
            //
            // No destination directory has been specified.
            // Try and create files in the same directory as the 
            // sources. This only work if sources are files.
            //
            int n = sources.size();
            for(int i=0; i<n; i++){
                SVGConverterSource src = (SVGConverterSource)sources.elementAt(i);
                if (!(src instanceof SVGConverterFileSource)) {
                    throw new SVGConverterException(ERROR_CANNOT_COMPUTE_DESTINATION,
                                                     new Object[]{src});
                }

                // Generate output filename from input filename.
                SVGConverterFileSource fs = (SVGConverterFileSource)src;
                File outputName = new File(fs.getFile().getParent(),
                                           getDestinationFile(src.getName()));
                dstFiles.addElement(outputName);
            }
            
        }

        return dstFiles;
    }

    /**
     * Populates a vector with the set of SVG files from the 
     * srcDir if it is not null and with the sources (files or URLs)
     * if any.
     */
    protected Vector computeSources() throws SVGConverterException{
        Vector sources = new Vector();

        // Check that at least one source has been specified.
        if (this.sources == null){
            throw new SVGConverterException(ERROR_NO_SOURCES_SPECIFIED);
        }

        int n = this.sources.size();
        for (int i=0; i<n; i++){
            String sourceString = (String)(this.sources.elementAt(i));
            File file = new File(sourceString);
            if (file.exists()) {
                sources.addElement(new SVGConverterFileSource(file));
            } else {
                String[] fileNRef = getFileNRef(sourceString);
                file = new File(fileNRef[0]);
                if (file.exists()){
                    sources.addElement(new SVGConverterFileSource(file, fileNRef[1]));
                } else{
                    sources.addElement(new SVGConverterURLSource(sourceString));
                }
            }
        }
        
        return sources;
    }

    public String[] getFileNRef(String fileName){
        int n = fileName.lastIndexOf("#");
        String[] result = {fileName, ""};
        if (n > -1){
            result[0] = fileName.substring(0, n);
            if (n+1 < fileName.length()){
                result[1] = fileName.substring(n+1);
            }
        }

        return result;
    }

    // -----------------------------------------------------------------------
    //   Internal methods
    // -----------------------------------------------------------------------

    /**
     * Computes the set of transcoding hints to use for the operation
     */
    protected Map computeTranscodingHints(){
        HashMap map = new HashMap();

        // Set AOI. ----------------------------------------------------------
        if (area != null) {
            map.put(ImageTranscoder.KEY_AOI, area);           
        }

        // Set image quality. ------------------------------------------------
        if (quality > 0) {
            map.put(JPEGTranscoder.KEY_QUALITY, new Float(this.quality));
        } 

        // Set image indexed. ------------------------------------------------
        if (indexed != -1) {
            map.put(PNGTranscoder.KEY_INDEXED, new Integer(indexed));
        } 

        // Set image background color -----------------------------------------
        if (backgroundColor != null){
            map.put(ImageTranscoder.KEY_BACKGROUND_COLOR, backgroundColor);
        }

        // Set image height and width. ----------------------------------------
        if (height > 0) {
            map.put(ImageTranscoder.KEY_HEIGHT, new Float(this.height));
        }
        if (width > 0){
            map.put(ImageTranscoder.KEY_WIDTH, new Float(this.width));
        }

        // Set maximum height and width ---------------------------------------
        if (maxHeight > 0) {
            map.put(ImageTranscoder.KEY_MAX_HEIGHT, new Float(this.maxHeight));
        }
        if (maxWidth > 0){
            map.put(ImageTranscoder.KEY_MAX_WIDTH, new Float(this.maxWidth));
        }

        // Set CSS Media
        if (mediaType != null){
            map.put(ImageTranscoder.KEY_MEDIA, mediaType);
        }

        // Set default font-family
        if (defaultFontFamily != null) {
            map.put(ImageTranscoder.KEY_DEFAULT_FONT_FAMILY, defaultFontFamily);
        }

        // Set alternateStylesheet
        if (alternateStylesheet != null){
            map.put(ImageTranscoder.KEY_ALTERNATE_STYLESHEET, alternateStylesheet);
        }

        // Set user stylesheet
        if (userStylesheet != null){
            map.put(ImageTranscoder.KEY_USER_STYLESHEET_URI, userStylesheet);
        }

        // Set the user language
        if (language != null){
            map.put(ImageTranscoder.KEY_LANGUAGE, language);
        }

        // Sets the millimeters per pixel
        if (pixelUnitToMillimeter > 0){
            map.put(ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER, 
                    new Float(pixelUnitToMillimeter));
        }

        // Set validation
        if (validate){
            map.put(ImageTranscoder.KEY_XML_PARSER_VALIDATING,
                    new Boolean(validate));
        }

        // Set onload
        if (executeOnload) {
            map.put(ImageTranscoder.KEY_EXECUTE_ONLOAD, new Boolean(executeOnload));
        }
        
        // Set allowed scripts
        if (allowedScriptTypes != null) {
            map.put(ImageTranscoder.KEY_ALLOWED_SCRIPT_TYPES, allowedScriptTypes);
        }

        // Set constrain script origin
        if (!constrainScriptOrigin) {
            map.put(ImageTranscoder.KEY_CONSTRAIN_SCRIPT_ORIGIN, 
                    new Boolean(constrainScriptOrigin));
        }

        return map;
    }

    /**
     * Converts the input image to the result image.
     * with the given transcoder. If a failure happens, the 
     * controller is notified and decides whether to proceed
     * or not. If it decides to proceed, the converter will
     * continue processing other files. Otherwise, it will
     * throw an exception.
     */
    protected void transcode(SVGConverterSource inputFile, 
                             File outputFile,
                             Transcoder transcoder)
        throws SVGConverterException {
        TranscoderInput input = null;
        TranscoderOutput output = null;
        OutputStream outputStream = null;

        if (!controller.proceedWithSourceTranscoding(inputFile, 
                                                     outputFile)){
            return;
        }

        try {
            if (inputFile.isSameAs(outputFile.getPath())) {
                throw new SVGConverterException(ERROR_SOURCE_SAME_AS_DESTINATION,
                                                 true /* fatal error */);
            }
            
            // Compute transcoder input.
            if (!inputFile.isReadable()) {
                throw new SVGConverterException(ERROR_CANNOT_READ_SOURCE,
                                                 new Object[]{inputFile.getName()});
            }

            try {
                InputStream in = inputFile.openStream();
                in.close();
            } catch(IOException ioe) {
                throw new SVGConverterException(ERROR_CANNOT_OPEN_SOURCE,
                                                 new Object[] {inputFile.getName(),
                                                               ioe.toString()});
                                                               } 
            
            input = new TranscoderInput(inputFile.getURI());

            // Compute transcoder output.
            if (!isWriteable(outputFile)) {
                throw new SVGConverterException(ERROR_OUTPUT_NOT_WRITEABLE,
                                                 new Object[] {outputFile.getName()});
            }
            try {
                outputStream = new FileOutputStream(outputFile);
            } catch(FileNotFoundException fnfe) {
                throw new SVGConverterException(ERROR_CANNOT_OPEN_OUTPUT_FILE,
                                                 new Object[] {outputFile.getName()});
            }
            
            output = new TranscoderOutput(outputStream);
        } catch(SVGConverterException e){
            boolean proceed = controller.proceedOnSourceTranscodingFailure
                (inputFile, outputFile, e.getErrorCode());
            if (proceed){
                return;
            } else {
                throw e;
            }
        }

        // Transcode now
        boolean success = false;
        try {
            transcoder.transcode(input, output);
            success = true;
        } catch(Exception te) {
            te.printStackTrace();
            try {
                outputStream.flush();
                outputStream.close();
            } catch(IOException ioe) {}
            
            // Report error to the controller. If controller decides
            // to stop, throw an exception
            boolean proceed = controller.proceedOnSourceTranscodingFailure
                (inputFile, outputFile, ERROR_WHILE_RASTERIZING_FILE);

            if (!proceed){
                throw new SVGConverterException(ERROR_WHILE_RASTERIZING_FILE,
                                                 new Object[] {outputFile.getName(),
                                                               te.getMessage()});
            }
        }

        // Close streams and clean up.
        try {
            outputStream.flush();
            outputStream.close();
        } catch(IOException ioe) {
            return;
        }

        if (success){
            controller.onSourceTranscodingSuccess(inputFile, outputFile);
        }
    }

    /**
     * Get the name of the result image file.
     *
     * <P>This method modifies the result filename, it changes the existing 
     * suffix to correspong the result file type. It also adds the suffix 
     * if the file doesn't have one.</P>
     *
     * @param file Result file name as a String object.
     *
     * @return Name of the file. The directory of the file is not returned. 
     *         The returned string is empty if the parameter is not a file.
     */
    protected String getDestinationFile(String file) {
        int suffixStart;            // Location of the first char of 
                                    // the suffix in a String.
        String oldName;             // Existing filename.
        String newSuffix = destinationType.getExtension();
                                    // New suffix.

        oldName = file;
        // Find the first char of the suffix.
        suffixStart = oldName.lastIndexOf(".");
        String dest = null;
        if (suffixStart != -1) {
            // Replace existing suffix.
            dest = new String(oldName.substring(0, suffixStart) + newSuffix);
        } else {
            // Add new suffix.
            dest = new String(oldName + newSuffix);
        }

        return dest;
    }

    /**
     * Creates directories for output files if needed.
     *
     * @param output Output file with path.
     *
     * @throws SVGConverterException Output directory doesn't exist and it can't be created.
     */
    protected void createOutputDir(File output)
        throws SVGConverterException {

        File outputDir;             // Output directory object.
        boolean success = true;     // false if the output directory 
                                    // doesn't exist and it can't be created
                                    // true otherwise


        // Create object from output directory.
        String parentDir = output.getParent();
        if (parentDir != null){
            outputDir = new File(output.getParent());
            if (outputDir.exists() == false) {
                // Output directory doesn't exist, so create it.
                success = outputDir.mkdirs();
            } else {
                if (outputDir.isDirectory() == false) {
                    // File, which have a same name as the output directory, exists.
                    // Create output directory.
                    success = outputDir.mkdirs();
                }
            }
        }

        if (!success) {
            throw new SVGConverterException(ERROR_UNABLE_TO_CREATE_OUTPUT_DIR);
        }
    }

    /**
     * Checks if the application is allowed to write to the file.
     *
     * @param file File to be checked.
     *
     * @return <tt>true</tt> if the file is writeable and <tt>false</tt> otherwise.
     */
    protected boolean isWriteable(File file) {
        if (file.exists()) {
            // Check the existing file.
            if (!file.canWrite()) {
                return false;
            }
        } else {
            // Check the file that doesn't exist yet.
            // Create a new file. The file is writeable if 
            // the creation succeeds.
            try {
                file.createNewFile();
            } catch(IOException ioe) {
                return false;
            }
        }
        return true;
    }

    // -----------------------------------------------------------------------
    //   Inner classes
    // -----------------------------------------------------------------------

    /**
     * Convenience class to filter svg files
     */
    public static class SVGFileFilter implements FileFilter {
        public static final String SVG_EXTENSION = ".svg";
        
        public boolean accept(File file){
            if (file != null && file.getName().toLowerCase().endsWith(SVG_EXTENSION)){
                return true;
            }
            
            return false;
        }
    }

}

