All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fr.avianey.androidsvgdrawable.SvgDrawablePlugin Maven / Gradle / Ivy

There is a newer version: 3.1.1
Show newest version
/*
 * Copyright 2013, 2014, 2015 Antoine Vianey
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fr.avianey.androidsvgdrawable;

import com.google.common.annotations.VisibleForTesting;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import fr.avianey.androidsvgdrawable.NinePatch.Zone;
import fr.avianey.androidsvgdrawable.util.Log;
import fr.avianey.androidsvgdrawable.util.QualifiedResourceFilter;
import org.apache.batik.transcoder.TranscoderException;
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.commons.io.FilenameUtils;
import org.xml.sax.SAXException;

import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Set;

import static com.google.common.base.Joiner.on;
import static com.google.common.base.Preconditions.checkNotNull;
import static fr.avianey.androidsvgdrawable.util.Constants.MM_PER_INCH;
import static java.awt.Color.BLACK;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.apache.batik.transcoder.SVGAbstractTranscoder.*;
import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_BACKGROUND_COLOR;
import static org.apache.batik.transcoder.image.JPEGTranscoder.KEY_QUALITY;
import static org.apache.commons.io.FileUtils.listFiles;
import static org.apache.commons.io.filefilter.TrueFileFilter.INSTANCE;

/**
 * Generates drawable from Scalable Vector Graphics (SVG) files.
 *
 * @author antoine vianey
 */
public class SvgDrawablePlugin {

    private static final String SVG_EXTENSION = "svg";
    private static final String SVGMASK_EXTENSION = "svgmask";
    private static final String PNG_EXTENSION = "png";

    public interface Parameters {

        Integer DEFAULT_JPG_BACKGROUND_COLOR = -1;
        Integer DEFAULT_JPG_QUALITY = 85;
        OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PNG;
        OutputType DEFAULT_OUTPUT_TYPE = OutputType.drawable;
        BoundsType DEFAULT_BOUNDS_TYPE = BoundsType.sensitive;
        OverwriteMode DEFAULT_OVERRIDE_MODE = OverwriteMode.always;
        Boolean DEFAULT_CREATE_MISSING_DIRECTORIES = true;

        Iterable getFiles();

        File getTo();

        boolean isCreateMissingDirectories();

        OverwriteMode getOverwriteMode();

        @Nullable
        Density.Value[] getTargetedDensities();

        @Nullable
        File getNinePatchConfig();

        Iterable getSvgMaskFiles();

        Iterable getSvgMaskResourceFiles();

        File getSvgMaskedSvgOutputDirectory();

        boolean isUseSameSvgOnlyOnceInMask();

        OutputFormat getOutputFormat();

        OutputType getOutputType();

        int getJpgQuality();

        int getJpgBackgroundColor();

        BoundsType getSvgBoundsType();

    }

    // log
    private final Log log;
    private final Parameters parameters;
    private final QualifiedSVGResourceFactory qualifiedSVGResourceFactory;

    public SvgDrawablePlugin(final Parameters parameters, final Log log) {
        this.parameters = parameters;
        this.log = log;
        this.qualifiedSVGResourceFactory = new QualifiedSVGResourceFactory(getLog(), parameters.getSvgBoundsType());
    }

    private Log getLog() {
        return this.log;
    }

    public void execute() {

        /**********************
         * Targeted densities *
         **********************/

        // validating targeted densities
        // un-targeted densities will be ignored
        // if the output type is 'raw' then targeted densities are ignored
        final Set targetDensities = EnumSet.noneOf(Density.Value.class);
        if (parameters.getOutputType() != OutputType.raw) {
            if (parameters.getTargetedDensities() != null) {
                targetDensities.addAll(asList(parameters.getTargetedDensities()));
            }
            if (targetDensities.isEmpty()) {
                targetDensities.addAll(EnumSet.allOf(Density.Value.class));
            }
            getLog().info("Targeted densities : " + on(", ").join(targetDensities));
        } else {
            getLog().info("Ignoring targeted densities for 'raw' output type...");
        }

        /********************************
         * Load NinePatch configuration *
         ********************************/

        NinePatchMap ninePatchMap = new NinePatchMap();
        if (parameters.getNinePatchConfig() != null && parameters.getNinePatchConfig().isFile()) {
            if (parameters.getOutputType() == OutputType.mipmap) {
                getLog().warn("NinePatch is not supported by the Android platform. " +
                        "Skipping NinePatch configuration file " + parameters.getNinePatchConfig().getAbsolutePath());
            } else {
                getLog().info("Loading NinePatch configuration file " + parameters.getNinePatchConfig().getAbsolutePath());
                try (final Reader reader = new FileReader(parameters.getNinePatchConfig())) {
                    Type t = new TypeToken>() {
                    }.getType();
                    Set ninePathSet = new GsonBuilder().create().fromJson(reader, t);
                    ninePatchMap = NinePatch.init(ninePathSet);
                } catch (IOException e) {
                    getLog().error("Error loading NinePatch configuration file", e);
                }
            }
        } else {
            getLog().info("No NinePatch configuration file specified");
        }

        /*****************************
         * List input SVG to convert *
         *****************************/

        getLog().info("Listing SVG files : " + on(", ").join(parameters.getFiles()));
        final Collection svgToConvert = listQualifiedResources(parameters.getFiles(), SVG_EXTENSION);
        getLog().info("SVG files found : " + on(", ").join(svgToConvert));

        /*****************************
         * List input SVGMASK to use *
         *****************************/

        Iterable svgMaskFiles = parameters.getSvgMaskFiles() == null ? parameters.getFiles() : parameters.getSvgMaskFiles();

        getLog().info("Listing SVGMASK files : " + on(", ").join(svgMaskFiles));
        final Collection svgMasks = listQualifiedResources(svgMaskFiles, SVGMASK_EXTENSION);
        getLog().info("SVGMASK files found : " + on(", ").join(svgMasks));
        if (!svgMasks.isEmpty()) {
            // list resources to mask
            Iterable svgMaskedResourcesFiles = parameters.getSvgMaskResourceFiles() == null ? svgMaskFiles : parameters.getSvgMaskResourceFiles();
            getLog().info("Listing SVG files to mask : " + on(", ").join(svgMaskedResourcesFiles));
            final Collection svgMaskResources = listQualifiedResources(svgMaskedResourcesFiles, SVG_EXTENSION);
            getLog().info("SVG files to mask found : " + on(", ").join(svgMasks));
            // generate masked svg
            svgToConvert.addAll(generateMaskedSvg(svgMasks, svgMaskResources));
        } else {
            getLog().info("No SVGMASK file found.");
        }

        /*********************************
         * Create svg in res/* folder(s) *
         *********************************/

        for (QualifiedResource svg : svgToConvert) {
            try {
                getLog().info("Transcoding " + FilenameUtils.getName(svg.getAbsolutePath()) + " to targeted densities");
                Collection _targetedDensities = parameters.getOutputType() == OutputType.raw ? singletonList(svg.getDensity().getValue()) : targetDensities;
                for (Density.Value d : _targetedDensities) {
                    NinePatch ninePatch = ninePatchMap.getBestMatch(svg);
                    File destination = parameters.getOutputType() == OutputType.raw ? parameters.getTo() : svg.getOutputFor(d, parameters.getTo(), parameters.getOutputType());
                    if (!destination.exists() && parameters.isCreateMissingDirectories()) {
                        destination.mkdirs();
                    }
                    if (destination.exists()) {
                        getLog().debug("+ transcoding " + svg.getName() + " into " + destination.getName());
                        transcode(svg, d, destination, ninePatch);
                    } else {
                        getLog().info("Qualified output " + destination.getName() + " does not exists. " +
                        		"Set 'createMissingDirectories' to true if you want it to be created if missing...");
                    }
                }
            } catch (Exception e) {
                getLog().error("Error while converting " + svg, e);
			}
        }

    }

    /**
     * Generate masked SVG files to be handle like regular SVG files
     * @param svgMasks SVGMASK files
     * @param svgMaskResources SVG files to mask
     * @return masked qualified resources
     */
    private Collection generateMaskedSvg(Collection svgMasks, Collection svgMaskResources) {
        Collection maskedFiles = new ArrayList<>();
        for (QualifiedResource maskFile : svgMasks) {
            getLog().info("Generating masked files for " + maskFile);
            try {
                Collection generatedResources = new SvgMask(maskFile).generatesMaskedResources(
                        qualifiedSVGResourceFactory,
                        parameters.getSvgMaskedSvgOutputDirectory(), svgMaskResources,
                        parameters.isUseSameSvgOnlyOnceInMask(), parameters.getOverwriteMode());
                if (!generatedResources.isEmpty()) {
                    getLog().debug("+ " + on(", ").join(generatedResources));
                } else {
                    getLog().debug("+ no matching masked resource file was found");
                }
                maskedFiles.addAll(generatedResources);
            } catch (XPathExpressionException | TransformerException | ParserConfigurationException | SAXException | IOException e) {
                getLog().error(e);
            }
        }
        return maskedFiles;
    }

    /**
     * Given it's bounds, transcodes a svg file to a raster image for the desired density
     * @param svg the svg to transcode
     * @param targetDensity the density to transcode to
     * @param destination where the transcoded files should be generated
     * @param ninePatch the nine patch configuration for the svg to transcode (if any)
     * @throws IOException
     * @throws TranscoderException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    @VisibleForTesting
    void transcode(QualifiedResource svg, Density.Value targetDensity, File destination, @Nullable NinePatch ninePatch) throws IOException, TranscoderException, InstantiationException, IllegalAccessException {
        final Rectangle outputBounds = svg.getScaledBounds(targetDensity);
        if (getLog().isDebugEnabled()) {
            getLog().debug("+ target dimensions [width=" + outputBounds.getWidth() + " - height=" + outputBounds.getHeight() +"]");
        }
        try (FileInputStream svgInputStream = new FileInputStream(svg)) {
            TranscoderInput input = new TranscoderInput(svgInputStream);

            // final name
            final String finalName = new StringBuilder(destination.getAbsolutePath())                              //
                    .append(System.getProperty("file.separator"))                                                  //
                    .append(svg.getName())                                                                         //
                    .append(ninePatch != null && parameters.getOutputFormat().hasNinePatchSupport() ? ".9" : "")   //
                    .append(".")                                                                                   //
                    .append(parameters.getOutputFormat().name().toLowerCase()).toString();                         //

            final File finalFile = new File(finalName);

            if (parameters.getOverwriteMode().shouldOverride(svg, finalFile, parameters.getNinePatchConfig())) {
                // unit conversion for size not in pixel (in, mm, ...)

                ImageTranscoder t = parameters.getOutputFormat().getTranscoderClass().newInstance();
                if (t instanceof JPEGTranscoder) {
                    // custom jpg hints
                    t.addTranscodingHint(KEY_QUALITY, min(1, max(0, parameters.getJpgQuality() / 100f)));
                    t.addTranscodingHint(KEY_BACKGROUND_COLOR, new Color(parameters.getJpgBackgroundColor()));
                }
                t.addTranscodingHint(KEY_WIDTH, new Float(outputBounds.getWidth()));
                t.addTranscodingHint(KEY_HEIGHT, new Float(outputBounds.getHeight()));
                t.addTranscodingHint(KEY_PIXEL_UNIT_TO_MILLIMETER, MM_PER_INCH / svg.getDensity().getDpi());

                if (ninePatch == null || !parameters.getOutputFormat().hasNinePatchSupport()) {
                    if (ninePatch != null) {
                        getLog().warn("skipping the nine-patch configuration for the JPG output format !!!");
                    }
                    // write file directly
                    OutputStream os = new FileOutputStream(finalName);
                    TranscoderOutput output = new TranscoderOutput(os);
                    t.transcode(input, output);
                    os.flush();
                    os.close();
                } else {
                    // write in memory
                    try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
                        TranscoderOutput output = new TranscoderOutput(os);
                        t.transcode(input, output);
                        os.flush();
                        try (InputStream is = new ByteArrayInputStream(os.toByteArray())) {
                            // fill the patch
                            toNinePatch(is, finalName, ninePatch, svg.getBounds(), outputBounds);
                        }
                    }
                }
            } else {
                getLog().debug(finalName + " already exists and is up to date... skipping generation!");
                getLog().debug("+ " + finalName + " last modified on " + new File(finalName).lastModified());
                getLog().debug("+ " + svg.getAbsolutePath() + " last modified on " + svg.lastModified());
                if (ninePatch != null && parameters.getNinePatchConfig() != null /* for tests */) {
                    getLog().debug("+ " + parameters.getNinePatchConfig().getAbsolutePath() + " last modified on " + parameters.getNinePatchConfig().lastModified());
                }
            }
        }
    }

    /**
     * Draw the stretch and content area defined by the {@link NinePatch} around the given image
     * @param is the generated PNG input file
     * @param finalName the targeted filename
     * @param ninePatch the nine patch configuration
     * @param svgBounds original svg bounds
     * @param outputBounds targeted bounds
     * @throws IOException
     */
    private void toNinePatch(final InputStream is, final String finalName, final NinePatch ninePatch, final Rectangle svgBounds, final Rectangle outputBounds) throws IOException {
        BufferedImage image = ImageIO.read(is);
        final double wRatio = outputBounds.getWidth() / svgBounds.getWidth();
        final double hRatio = outputBounds.getHeight() / svgBounds.getHeight();
        final int w = image.getWidth();
        final int h = image.getHeight();
        BufferedImage ninePatchImage = new BufferedImage(
                w + 2,
                h + 2,
                TYPE_INT_ARGB);
        Graphics g = ninePatchImage.getGraphics();
        g.drawImage(image, 1, 1, null);

        // draw patch
        g.setColor(BLACK);

        Zone stretch = ninePatch.getStretch();
        Zone content = ninePatch.getContent();

        if (stretch.getX() == null) {
            if (getLog().isDebugEnabled()) {
                getLog().debug("+ ninepatch stretch(x) [start=0 - size=" + w + "]");
            }
        	g.fillRect(1, 0, w, 1);
        } else {
	        for (int[] seg : stretch.getX()) {
	            final int start = NinePatch.start(seg[0], w, wRatio);
	            final int size = NinePatch.size(seg[0], seg[1], w, wRatio);
	            if (getLog().isDebugEnabled()) {
	                getLog().debug("+ ninepatch stretch(x) [start=" + start + " - size=" + size + "]");
	            }
	            g.fillRect(start + 1, 0, size, 1);
	        }
        }

        if (stretch.getY() == null) {
            if (getLog().isDebugEnabled()) {
                getLog().debug("+ ninepatch stretch(y) [start=0 - size=" + h + "]");
            }
        	g.fillRect(0, 1, 1, h);
        } else {
	        for (int[] seg : stretch.getY()) {
	            final int start = NinePatch.start(seg[0], h, hRatio);
	            final int size = NinePatch.size(seg[0], seg[1], h, hRatio);
	            if (getLog().isDebugEnabled()) {
	                getLog().debug("+ ninepatch stretch(y) [start=" + start + " - size=" + size + "]");
	            }
	            g.fillRect(0, start + 1, 1, size);
	        }
        }

        if (content.getX() == null) {
            if (getLog().isDebugEnabled()) {
                getLog().debug("+ ninepatch content(x) [start=0 - size=" + w + "]");
            }
        	g.fillRect(1, h + 1, w, 1);
        } else {
	        for (int[] seg : content.getX()) {
	            final int start = NinePatch.start(seg[0], w, hRatio);
	            final int size = NinePatch.size(seg[0], seg[1], w, hRatio);
	            if (getLog().isDebugEnabled()) {
	                getLog().debug("+ ninepatch content(x) [start=" + start + " - size=" + size + "]");
	            }
	            g.fillRect(start + 1, h + 1, size, 1);
	        }
        }

        if (content.getY() == null) {
            if (getLog().isDebugEnabled()) {
                getLog().debug("+ ninepatch content(y) [start=0 - size=" + h + "]");
            }
        	g.fillRect(w + 1, 1, 1, h);
        } else {
	        for (int[] seg : content.getY()) {
	            final int start = NinePatch.start(seg[0], h, hRatio);
	            final int size = NinePatch.size(seg[0], seg[1], h, hRatio);
	            if (getLog().isDebugEnabled()) {
	                getLog().debug("+ ninepatch content(y) [start=" + start + " - size=" + size + "]");
	            }
	            g.fillRect(w + 1, start + 1, 1, size);
	        }
        }

        ImageIO.write(ninePatchImage, PNG_EXTENSION, new File(finalName));
    }

    /**
     * List {@link QualifiedResource} from various input files / directories.
     * @param files files where to pick svg to convert from
     * @param extension the extension from which qualified resources should be extracted
     * @return qualified resource from the specified files (recursively)
     */
    private Collection listQualifiedResources(final Iterable files, final String extension) {
        checkNotNull(extension);
        QualifiedResourceFilter filter = new QualifiedResourceFilter(getLog(), qualifiedSVGResourceFactory, extension);
        for (File from : files) {
            if (from.isDirectory()) {
                listFiles(from, filter, INSTANCE);
            } else {
                filter.accept(from);
            }
        }
        return filter.filteredResources();
    }

    @VisibleForTesting
    QualifiedSVGResourceFactory getQualifiedSVGResourceFactory() {
        return qualifiedSVGResourceFactory;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy