com.android.ide.common.vectordrawable.VdPreview Maven / Gradle / Ivy
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 com.android.ide.common.vectordrawable;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.util.AssetUtil;
import com.google.common.base.Charsets;
import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Locale;
/**
* Generate a Image based on the VectorDrawable's XML content.
*
* This class also contains a main method, which can be used to preview a vector drawable file.
*/
public class VdPreview {
private static final String ANDROID_ALPHA = "android:alpha";
private static final String ANDROID_AUTO_MIRRORED = "android:autoMirrored";
private static final String ANDROID_HEIGHT = "android:height";
private static final String ANDROID_WIDTH = "android:width";
public static final int MAX_PREVIEW_IMAGE_SIZE = 4096;
public static final int MIN_PREVIEW_IMAGE_SIZE = 1;
/**
* Parse the VectorDrawable's XML file into a document object.
*
* @param xmlFileContent the content of the VectorDrawable's XML file.
* @param errorLog when errors were found, log them in this builder if it is not null.
* @return parsed document or null if errors happened.
*/
@Nullable
public static Document parseVdStringIntoDocument(@NonNull String xmlFileContent,
@Nullable StringBuilder errorLog) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db;
Document document;
try {
db = dbf.newDocumentBuilder();
document = db.parse(new InputSource(new StringReader(xmlFileContent)));
}
catch (Exception e) {
if (errorLog != null) {
errorLog.append("Exception while parsing XML file:\n").append(e.getMessage());
}
return null;
}
return document;
}
/**
* This encapsulates the information used to determine the preview image size.
* The reason we have different ways here is that both Studio UI and build process need
* to use this common code path to generate images for vectordrawable.
* When {@code mUseWidth} is true, use {@code mImageMaxDimension} as the maximum
* dimension value while keeping the aspect ratio.
* Otherwise, use {@code mImageScale} to scale the image based on the XML's size information.
*/
public static class TargetSize {
private boolean mUseWidth;
private int mImageMaxDimension;
private float mImageScale;
private TargetSize(boolean useWidth, int imageWidth, float imageScale) {
mUseWidth = useWidth;
mImageMaxDimension = imageWidth;
mImageScale = imageScale;
}
public static TargetSize createSizeFromWidth(int imageWidth) {
return new TargetSize(true, imageWidth, 0.0f);
}
public static TargetSize createSizeFromScale(float imageScale) {
return new TargetSize(false, 0, imageScale);
}
}
/**
* Since we allow overriding the vector drawable's size, we also need to keep
* the original size and aspect ratio.
*/
public static class SourceSize {
public int getHeight() {
return mSourceHeight;
}
public int getWidth() {
return mSourceWidth;
}
private int mSourceWidth;
private int mSourceHeight;
}
/**
* @return a format object for XML formatting.
*/
@NonNull
private static OutputFormat getPrettyPrintFormat() {
OutputFormat format = new OutputFormat();
format.setLineWidth(120);
format.setIndenting(true);
format.setIndent(4);
format.setEncoding("UTF-8");
format.setOmitComments(true);
format.setOmitXMLDeclaration(true);
return format;
}
/**
* Get the vector drawable's original size.
*/
public static SourceSize getVdOriginalSize(@NonNull Document document) {
Element root = document.getDocumentElement();
SourceSize srcSize = new SourceSize();
// Update attributes, note that attributes as width and height are required,
// while others are optional.
NamedNodeMap attr = root.getAttributes();
Node nodeAttr = attr.getNamedItem(ANDROID_WIDTH);
assert nodeAttr != null;
srcSize.mSourceWidth = parseDimension(0, nodeAttr, false);
nodeAttr = attr.getNamedItem(ANDROID_HEIGHT);
assert nodeAttr != null;
srcSize.mSourceHeight = parseDimension(0, nodeAttr, false);
return srcSize;
}
/**
* The UI can override some properties of the Vector drawable.
* In order to override in an uniform way, we re-parse the XML file
* and pick the appropriate attributes to override.
*
* @param document the parsed document of original VectorDrawable's XML file.
* @param info incoming override information for VectorDrawable.
* @param errorLog log for the parsing errors and warnings.
* @return the overridden XML file in one string. If exception happens
* or no attributes needs to be overriden, return null.
*/
@Nullable
public static String overrideXmlContent(@NonNull Document document,
@NonNull VdOverrideInfo info,
@Nullable StringBuilder errorLog) {
boolean isXmlFileContentChanged = false;
Element root = document.getDocumentElement();
// Update attributes, note that attributes as width and height are required,
// while others are optional.
NamedNodeMap attr = root.getAttributes();
if (info.needsOverrideWidth()) {
Node nodeAttr = attr.getNamedItem(ANDROID_WIDTH);
int overrideValue = info.getWidth();
int originalValue = parseDimension(overrideValue, nodeAttr, true);
if (originalValue != overrideValue) {
isXmlFileContentChanged = true;
}
}
if (info.needsOverrideHeight()) {
Node nodeAttr = attr.getNamedItem(ANDROID_HEIGHT);
int overrideValue = info.getHeight();
int originalValue = parseDimension(overrideValue, nodeAttr, true);
if (originalValue != overrideValue) {
isXmlFileContentChanged = true;
}
}
if (info.needsOverrideOpacity()) {
Node nodeAttr = attr.getNamedItem(ANDROID_ALPHA);
String opacityValue = String.format((Locale) null, "%.2f", info.getOpacity() / 100.0f);
if (nodeAttr != null) {
nodeAttr.setTextContent(opacityValue);
}
else {
root.setAttribute(ANDROID_ALPHA, opacityValue);
}
isXmlFileContentChanged = true;
}
// When auto mirror is set to true, then we always need to set it.
// Because SVG has no such attribute at all.
if (info.needsOverrideAutoMirrored()) {
Node nodeAttr = attr.getNamedItem(ANDROID_AUTO_MIRRORED);
if (nodeAttr != null) {
nodeAttr.setTextContent("true");
}
else {
root.setAttribute(ANDROID_AUTO_MIRRORED, "true");
}
isXmlFileContentChanged = true;
}
if (isXmlFileContentChanged) {
// Prettify the XML string from the document.
StringWriter stringOut = new StringWriter();
XMLSerializer serial = new XMLSerializer(stringOut, getPrettyPrintFormat());
try {
serial.serialize(document);
}
catch (IOException e) {
if (errorLog != null) {
errorLog.append("Exception while parsing XML file:\n").append(e.getMessage());
}
}
return stringOut.toString();
} else {
return null;
}
}
/**
* Query the dimension info and override it if needed.
*
* @param overrideValue the dimension value to override with.
* @param nodeAttr the node who contains dimension info.
* @param override if true then override the dimension.
* @return the original dimension value.
*/
private static int parseDimension(int overrideValue, Node nodeAttr, boolean override) {
assert nodeAttr != null;
String content = nodeAttr.getTextContent();
assert content.endsWith("dp");
int originalValue = Integer.parseInt(content.substring(0, content.length() - 2));
if (override) {
nodeAttr.setTextContent(overrideValue + "dp");
}
return originalValue;
}
/**
* This generates an image according to the VectorDrawable's content {@code xmlFileContent}.
* At the same time, {@code vdErrorLog} captures all the errors found during parsing.
* The size of image is determined by the {@code size}.
*
* @param targetSize the size of result image.
* @param xmlFileContent VectorDrawable's XML file's content.
* @param vdErrorLog log for the parsing errors and warnings.
* @return an preview image according to the VectorDrawable's XML
*/
@Nullable
public static BufferedImage getPreviewFromVectorXml(@NonNull TargetSize targetSize,
@Nullable String xmlFileContent,
@Nullable StringBuilder vdErrorLog) {
if (xmlFileContent == null || xmlFileContent.isEmpty()) {
return null;
}
VdParser p = new VdParser();
VdTree vdTree;
InputStream inputStream = new ByteArrayInputStream(
xmlFileContent.getBytes(Charsets.UTF_8));
vdTree = p.parse(inputStream, vdErrorLog);
if (vdTree == null) {
return null;
}
// If the forceImageSize is set (>0), then we honor that.
// Otherwise, we will ask the vectorDrawable for the prefer size, then apply the imageScale.
float vdWidth = vdTree.getBaseWidth();
float vdHeight = vdTree.getBaseHeight();
float imageWidth;
float imageHeight;
int forceImageSize = targetSize.mImageMaxDimension;
float imageScale = targetSize.mImageScale;
if (forceImageSize > 0) {
// The goal here is to generate an image within certain size, while keeping the
// aspect ration as much as we can.
// If it is scaling too much to fit in, we log an error.
float maxVdSize = Math.max(vdWidth, vdHeight);
float ratioToForceImageSize = forceImageSize / maxVdSize;
float scaledWidth = ratioToForceImageSize * vdWidth;
float scaledHeight = ratioToForceImageSize * vdHeight;
imageWidth = Math.max(MIN_PREVIEW_IMAGE_SIZE, Math.min(MAX_PREVIEW_IMAGE_SIZE, scaledWidth));
imageHeight = Math.max(MIN_PREVIEW_IMAGE_SIZE, Math.min(MAX_PREVIEW_IMAGE_SIZE, scaledHeight));
if (scaledWidth != imageWidth || scaledHeight != imageHeight) {
vdErrorLog.append("Invalid image size, can't fit in a square whose size is" + forceImageSize);
}
} else {
imageWidth = vdWidth * imageScale;
imageHeight = vdHeight * imageScale;
}
// Create the image according to the vectorDrawable's aspect ratio.
BufferedImage image = AssetUtil.newArgbBufferedImage((int)imageWidth, (int)imageHeight);
vdTree.drawIntoImage(image);
return image;
}
}