544 lines
20 KiB
Java
544 lines
20 KiB
Java
/*
|
|
* 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.SdkConstants;
|
|
import org.xml.sax.Attributes;
|
|
import org.xml.sax.ContentHandler;
|
|
import org.xml.sax.InputSource;
|
|
import org.xml.sax.Locator;
|
|
import org.xml.sax.SAXException;
|
|
import org.xml.sax.XMLReader;
|
|
|
|
import java.io.InputStream;
|
|
import java.net.URL;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.Locale;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import javax.xml.parsers.SAXParser;
|
|
import javax.xml.parsers.SAXParserFactory;
|
|
|
|
/**
|
|
* Parse a VectorDrawble's XML file, and generate an internal tree representation,
|
|
* which can be used for drawing / previewing.
|
|
*/
|
|
class VdParser {
|
|
private static Logger logger = Logger.getLogger(VdParser.class.getSimpleName());
|
|
|
|
private static final String PATH_SHIFT_X = "shift-x";
|
|
private static final String PATH_SHIFT_Y = "shift-y";
|
|
|
|
private static final String SHAPE_VECTOR = "vector";
|
|
private static final String SHAPE_PATH = "path";
|
|
private static final String SHAPE_GROUP = "group";
|
|
|
|
private static final String PATH_ID = "android:name";
|
|
private static final String PATH_DESCRIPTION = "android:pathData";
|
|
private static final String PATH_FILL = "android:fillColor";
|
|
private static final String PATH_FILL_OPACTIY = "android:fillAlpha";
|
|
private static final String PATH_STROKE = "android:strokeColor";
|
|
private static final String PATH_STROKE_OPACTIY = "android:strokeAlpha";
|
|
|
|
private static final String PATH_STROKE_WIDTH = "android:strokeWidth";
|
|
private static final String PATH_ROTATION = "android:rotation";
|
|
private static final String PATH_ROTATION_X = "android:pivotX";
|
|
private static final String PATH_ROTATION_Y = "android:pivotY";
|
|
private static final String PATH_TRIM_START = "android:trimPathStart";
|
|
private static final String PATH_TRIM_END = "android:trimPathEnd";
|
|
private static final String PATH_TRIM_OFFSET = "android:trimPathOffset";
|
|
private static final String PATH_STROKE_LINECAP = "android:strokeLinecap";
|
|
private static final String PATH_STROKE_LINEJOIN = "android:strokeLinejoin";
|
|
private static final String PATH_STROKE_MITERLIMIT = "android:strokeMiterlimit";
|
|
private static final String PATH_CLIP = "android:clipToPath";
|
|
private static final String LINECAP_BUTT = "butt";
|
|
private static final String LINECAP_ROUND = "round";
|
|
private static final String LINECAP_SQUARE = "square";
|
|
private static final String LINEJOIN_MITER = "miter";
|
|
private static final String LINEJOIN_ROUND = "round";
|
|
private static final String LINEJOIN_BEVEL = "bevel";
|
|
|
|
interface ElemParser {
|
|
void parse(VdTree path, Attributes attributes);
|
|
}
|
|
|
|
ElemParser mParseSize = new ElemParser() {
|
|
@Override
|
|
public void parse(VdTree tree, Attributes attributes) {
|
|
parseSize(tree, attributes);
|
|
}
|
|
};
|
|
|
|
ElemParser mParsePath = new ElemParser() {
|
|
@Override
|
|
public void parse(VdTree tree, Attributes attributes) {
|
|
VdPath p = parsePathAttributes(attributes);
|
|
tree.add(p);
|
|
}
|
|
};
|
|
|
|
ElemParser mParseGroup = new ElemParser() {
|
|
@Override
|
|
public void parse(VdTree tree, Attributes attributes) {
|
|
VdGroup g = parseGroupAttributes(attributes);
|
|
tree.add(g);
|
|
}
|
|
};
|
|
|
|
HashMap<String, ElemParser> tagSwitch = new HashMap<String, ElemParser>();
|
|
{
|
|
tagSwitch.put(SHAPE_VECTOR, mParseSize);
|
|
tagSwitch.put(SHAPE_PATH, mParsePath);
|
|
tagSwitch.put(SHAPE_GROUP, mParseGroup);
|
|
// TODO: add <g> tag and start to build the tree.
|
|
}
|
|
|
|
// Note that the incoming file is the VectorDrawable's XML file, not the SVG.
|
|
// TODO: Use Document to parse and make sure no big performance difference.
|
|
public VdTree parse(InputStream is, StringBuilder vdErrorLog) {
|
|
try {
|
|
final VdTree tree = new VdTree();
|
|
SAXParserFactory spf = SAXParserFactory.newInstance();
|
|
SAXParser sp = spf.newSAXParser();
|
|
XMLReader xr = sp.getXMLReader();
|
|
|
|
xr.setContentHandler(new ContentHandler() {
|
|
String space = " ";
|
|
|
|
@Override
|
|
public void setDocumentLocator(Locator locator) {
|
|
}
|
|
|
|
@Override
|
|
public void startDocument() throws SAXException {
|
|
}
|
|
|
|
@Override
|
|
public void endDocument() throws SAXException {
|
|
}
|
|
|
|
@Override
|
|
public void startPrefixMapping(String s, String s2) throws SAXException {
|
|
}
|
|
|
|
@Override
|
|
public void endPrefixMapping(String s) throws SAXException {
|
|
}
|
|
|
|
@Override
|
|
public void startElement(String s, String s2, String s3, Attributes attributes)
|
|
throws SAXException {
|
|
String name = s3;
|
|
if (tagSwitch.containsKey(name)) {
|
|
tagSwitch.get(name).parse(tree, attributes);
|
|
}
|
|
space += " ";
|
|
}
|
|
|
|
@Override
|
|
public void endElement(String s, String s2, String s3) throws SAXException {
|
|
space = space.substring(1);
|
|
}
|
|
|
|
@Override
|
|
public void characters(char[] chars, int i, int i2) throws SAXException {
|
|
}
|
|
|
|
@Override
|
|
public void ignorableWhitespace(char[] chars, int i, int i2) throws SAXException {
|
|
}
|
|
|
|
@Override
|
|
public void processingInstruction(String s, String s2) throws SAXException {
|
|
}
|
|
|
|
@Override
|
|
public void skippedEntity(String s) throws SAXException {
|
|
}
|
|
});
|
|
xr.parse(new InputSource(is));
|
|
tree.parseFinish();
|
|
return tree;
|
|
} catch (Exception e) {
|
|
vdErrorLog.append("Exception while parsing XML file:\n" + e.getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public VdParser() {
|
|
}
|
|
|
|
private static int nextStart(String s, int end) {
|
|
char c;
|
|
|
|
while (end < s.length()) {
|
|
c = s.charAt(end);
|
|
// Note that 'e' or 'E' are not valid path commands, but could be
|
|
// used for floating point numbers' scientific notation.
|
|
// Therefore, when searching for next command, we should ignore 'e'
|
|
// and 'E'.
|
|
if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
|
|
&& c != 'e' && c != 'E') {
|
|
return end;
|
|
}
|
|
end++;
|
|
}
|
|
return end;
|
|
}
|
|
|
|
public static VdPath.Node[] parsePath(String value) {
|
|
int start = 0;
|
|
int end = 1;
|
|
|
|
ArrayList<VdPath.Node> list = new ArrayList<VdPath.Node>();
|
|
while (end < value.length()) {
|
|
end = nextStart(value, end);
|
|
String s = value.substring(start, end);
|
|
float[] val = getFloats(s);
|
|
|
|
addNode(list, s.charAt(0), val);
|
|
|
|
start = end;
|
|
end++;
|
|
}
|
|
if ((end - start) == 1 && start < value.length()) {
|
|
|
|
addNode(list, value.charAt(start), new float[0]);
|
|
}
|
|
return list.toArray(new VdPath.Node[list.size()]);
|
|
}
|
|
|
|
private static class ExtractFloatResult {
|
|
// We need to return the position of the next separator and whether the
|
|
// next float starts with a '-' or a '.'.
|
|
int mEndPosition;
|
|
boolean mEndWithNegOrDot;
|
|
}
|
|
|
|
/**
|
|
* Copies elements from {@code original} into a new array, from indexes start (inclusive) to
|
|
* end (exclusive). The original order of elements is preserved.
|
|
* If {@code end} is greater than {@code original.length}, the result is padded
|
|
* with the value {@code 0.0f}.
|
|
*
|
|
* @param original the original array
|
|
* @param start the start index, inclusive
|
|
* @param end the end index, exclusive
|
|
* @return the new array
|
|
* @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length}
|
|
* @throws IllegalArgumentException if {@code start > end}
|
|
* @throws NullPointerException if {@code original == null}
|
|
*/
|
|
private static float[] copyOfRange(float[] original, int start, int end) {
|
|
if (start > end) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
int originalLength = original.length;
|
|
if (start < 0 || start > originalLength) {
|
|
throw new ArrayIndexOutOfBoundsException();
|
|
}
|
|
int resultLength = end - start;
|
|
int copyLength = Math.min(resultLength, originalLength - start);
|
|
float[] result = new float[resultLength];
|
|
System.arraycopy(original, start, result, 0, copyLength);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculate the position of the next comma or space or negative sign
|
|
* @param s the string to search
|
|
* @param start the position to start searching
|
|
* @param result the result of the extraction, including the position of the
|
|
* the starting position of next number, whether it is ending with a '-'.
|
|
*/
|
|
private static void extract(String s, int start, ExtractFloatResult result) {
|
|
// Now looking for ' ', ',', '.' or '-' from the start.
|
|
int currentIndex = start;
|
|
boolean foundSeparator = false;
|
|
result.mEndWithNegOrDot = false;
|
|
boolean secondDot = false;
|
|
boolean isExponential = false;
|
|
for (; currentIndex < s.length(); currentIndex++) {
|
|
boolean isPrevExponential = isExponential;
|
|
isExponential = false;
|
|
char currentChar = s.charAt(currentIndex);
|
|
switch (currentChar) {
|
|
case ' ':
|
|
case ',':
|
|
foundSeparator = true;
|
|
break;
|
|
case '-':
|
|
// The negative sign following a 'e' or 'E' is not a separator.
|
|
if (currentIndex != start && !isPrevExponential) {
|
|
foundSeparator = true;
|
|
result.mEndWithNegOrDot = true;
|
|
}
|
|
break;
|
|
case '.':
|
|
if (!secondDot) {
|
|
secondDot = true;
|
|
} else {
|
|
// This is the second dot, and it is considered as a separator.
|
|
foundSeparator = true;
|
|
result.mEndWithNegOrDot = true;
|
|
}
|
|
break;
|
|
case 'e':
|
|
case 'E':
|
|
isExponential = true;
|
|
break;
|
|
}
|
|
if (foundSeparator) {
|
|
break;
|
|
}
|
|
}
|
|
// When there is nothing found, then we put the end position to the end
|
|
// of the string.
|
|
result.mEndPosition = currentIndex;
|
|
}
|
|
|
|
/**
|
|
* parse the floats in the string this is an optimized version of parseFloat(s.split(",|\\s"));
|
|
*
|
|
* @param s the string containing a command and list of floats
|
|
* @return array of floats
|
|
*/
|
|
private static float[] getFloats(String s) {
|
|
if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
|
|
return new float[0];
|
|
}
|
|
try {
|
|
float[] results = new float[s.length()];
|
|
int count = 0;
|
|
int startPosition = 1;
|
|
int endPosition = 0;
|
|
|
|
ExtractFloatResult result = new ExtractFloatResult();
|
|
int totalLength = s.length();
|
|
|
|
// The startPosition should always be the first character of the
|
|
// current number, and endPosition is the character after the current
|
|
// number.
|
|
while (startPosition < totalLength) {
|
|
extract(s, startPosition, result);
|
|
endPosition = result.mEndPosition;
|
|
|
|
if (startPosition < endPosition) {
|
|
results[count++] = Float.parseFloat(
|
|
s.substring(startPosition, endPosition));
|
|
}
|
|
|
|
if (result.mEndWithNegOrDot) {
|
|
// Keep the '-' or '.' sign with next number.
|
|
startPosition = endPosition;
|
|
} else {
|
|
startPosition = endPosition + 1;
|
|
}
|
|
}
|
|
return copyOfRange(results, 0, count);
|
|
} catch (NumberFormatException e) {
|
|
throw new RuntimeException("error in parsing \"" + s + "\"", e);
|
|
}
|
|
}
|
|
// End of copy from PathParser.java
|
|
////////////////////////////////////////////////////////////////
|
|
private static void addNode(ArrayList<VdPath.Node> list, char cmd, float[] val) {
|
|
list.add(new VdPath.Node(cmd, val));
|
|
}
|
|
|
|
public VdTree parse(URL r, StringBuilder vdErrorLog) throws Exception {
|
|
return parse(r.openStream(), vdErrorLog);
|
|
}
|
|
|
|
private void parseSize(VdTree vdTree, Attributes attributes) {
|
|
|
|
Pattern pattern = Pattern.compile("^\\s*(\\d+(\\.\\d+)*)\\s*([a-zA-Z]+)\\s*$");
|
|
HashMap<String, Integer> m = new HashMap<String, Integer>();
|
|
m.put(SdkConstants.UNIT_PX, 1);
|
|
m.put(SdkConstants.UNIT_DIP, 1);
|
|
m.put(SdkConstants.UNIT_DP, 1);
|
|
m.put(SdkConstants.UNIT_SP, 1);
|
|
m.put(SdkConstants.UNIT_PT, 1);
|
|
m.put(SdkConstants.UNIT_IN, 1);
|
|
m.put(SdkConstants.UNIT_MM, 1);
|
|
int len = attributes.getLength();
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
String name = attributes.getQName(i);
|
|
String value = attributes.getValue(i);
|
|
Matcher matcher = pattern.matcher(value);
|
|
float size = 0;
|
|
if (matcher.matches()) {
|
|
float v = Float.parseFloat(matcher.group(1));
|
|
String unit = matcher.group(3).toLowerCase(Locale.getDefault());
|
|
size = v;
|
|
}
|
|
// -- Extract dimension units.
|
|
|
|
if ("android:width".equals(name)) {
|
|
vdTree.mBaseWidth = size;
|
|
} else if ("android:height".equals(name)) {
|
|
vdTree.mBaseHeight = size;
|
|
} else if ("android:viewportWidth".equals(name)) {
|
|
vdTree.mPortWidth = Float.parseFloat(value);
|
|
} else if ("android:viewportHeight".equals(name)) {
|
|
vdTree.mPortHeight = Float.parseFloat(value);
|
|
} else if ("android:alpha".equals(name)) {
|
|
vdTree.mRootAlpha = Float.parseFloat(value);
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
private VdPath parsePathAttributes(Attributes attributes) {
|
|
int len = attributes.getLength();
|
|
VdPath vgPath = new VdPath();
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
String name = attributes.getQName(i);
|
|
String value = attributes.getValue(i);
|
|
logger.log(Level.FINE, "name " + name + "value " + value);
|
|
setNameValue(vgPath, name, value);
|
|
}
|
|
return vgPath;
|
|
}
|
|
|
|
private VdGroup parseGroupAttributes(Attributes attributes) {
|
|
int len = attributes.getLength();
|
|
VdGroup vgGroup = new VdGroup();
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
String name = attributes.getQName(i);
|
|
String value = attributes.getValue(i);
|
|
logger.log(Level.FINE, "name " + name + "value " + value);
|
|
}
|
|
return vgGroup;
|
|
}
|
|
|
|
public void setNameValue(VdPath vgPath, String name, String value) {
|
|
if (PATH_DESCRIPTION.equals(name)) {
|
|
vgPath.mNode = parsePath(value);
|
|
} else if (PATH_ID.equals(name)) {
|
|
vgPath.mName = value;
|
|
} else if (PATH_FILL.equals(name)) {
|
|
vgPath.mFillColor = calculateColor(value);
|
|
if (!Float.isNaN(vgPath.mFillOpacity)) {
|
|
vgPath.mFillColor &= 0x00FFFFFF;
|
|
vgPath.mFillColor |= ((int) (0xFF * vgPath.mFillOpacity)) << 24;
|
|
}
|
|
} else if (PATH_STROKE.equals(name)) {
|
|
vgPath.mStrokeColor = calculateColor(value);
|
|
if (!Float.isNaN(vgPath.mStrokeOpacity)) {
|
|
vgPath.mStrokeColor &= 0x00FFFFFF;
|
|
vgPath.mStrokeColor |= ((int) (0xFF * vgPath.mStrokeOpacity)) << 24;
|
|
}
|
|
} else if (PATH_FILL_OPACTIY.equals(name)) {
|
|
vgPath.mFillOpacity = Float.parseFloat(value);
|
|
vgPath.mFillColor &= 0x00FFFFFF;
|
|
vgPath.mFillColor |= ((int) (0xFF * vgPath.mFillOpacity)) << 24;
|
|
} else if (PATH_STROKE_OPACTIY.equals(name)) {
|
|
vgPath.mStrokeOpacity = Float.parseFloat(value);
|
|
vgPath.mStrokeColor &= 0x00FFFFFF;
|
|
vgPath.mStrokeColor |= ((int) (0xFF * vgPath.mStrokeOpacity)) << 24;
|
|
} else if (PATH_STROKE_WIDTH.equals(name)) {
|
|
vgPath.mStrokeWidth = Float.parseFloat(value);
|
|
} else if (PATH_ROTATION.equals(name)) {
|
|
vgPath.mRotate = Float.parseFloat(value);
|
|
} else if (PATH_SHIFT_X.equals(name)) {
|
|
vgPath.mShiftX = Float.parseFloat(value);
|
|
} else if (PATH_SHIFT_Y.equals(name)) {
|
|
vgPath.mShiftY = Float.parseFloat(value);
|
|
} else if (PATH_ROTATION_Y.equals(name)) {
|
|
vgPath.mRotateY = Float.parseFloat(value);
|
|
} else if (PATH_ROTATION_X.equals(name)) {
|
|
vgPath.mRotateX = Float.parseFloat(value);
|
|
} else if (PATH_CLIP.equals(name)) {
|
|
|
|
vgPath.mClip = Boolean.parseBoolean(value);
|
|
|
|
} else if (PATH_TRIM_START.equals(name)) {
|
|
vgPath.mTrimPathStart = Float.parseFloat(value);
|
|
} else if (PATH_TRIM_END.equals(name)) {
|
|
vgPath.mTrimPathEnd = Float.parseFloat(value);
|
|
} else if (PATH_TRIM_OFFSET.equals(name)) {
|
|
vgPath.mTrimPathOffset = Float.parseFloat(value);
|
|
} else if (PATH_STROKE_LINECAP.equals(name)) {
|
|
if (LINECAP_BUTT.equals(value)) {
|
|
vgPath.mStrokeLineCap = 0;
|
|
} else if (LINECAP_ROUND.equals(value)) {
|
|
vgPath.mStrokeLineCap = 1;
|
|
} else if (LINECAP_SQUARE.equals(value)) {
|
|
vgPath.mStrokeLineCap = 2;
|
|
}
|
|
} else if (PATH_STROKE_LINEJOIN.equals(name)) {
|
|
if (LINEJOIN_MITER.equals(value)) {
|
|
vgPath.mStrokeLineJoin = 0;
|
|
} else if (LINEJOIN_ROUND.equals(value)) {
|
|
vgPath.mStrokeLineJoin = 1;
|
|
} else if (LINEJOIN_BEVEL.equals(value)) {
|
|
vgPath.mStrokeLineJoin = 2;
|
|
}
|
|
} else if (PATH_STROKE_MITERLIMIT.equals(name)) {
|
|
vgPath.mStrokeMiterlimit = Float.parseFloat(value);
|
|
} else {
|
|
logger.log(Level.FINE, ">>>>>> DID NOT UNDERSTAND ! \"" + name + "\" <<<<");
|
|
}
|
|
|
|
}
|
|
|
|
private int calculateColor(String value) {
|
|
int len = value.length();
|
|
int ret;
|
|
int k = 0;
|
|
switch (len) {
|
|
case 7: // #RRGGBB
|
|
ret = (int) Long.parseLong(value.substring(1), 16);
|
|
ret |= 0xFF000000;
|
|
break;
|
|
case 9: // #AARRGGBB
|
|
ret = (int) Long.parseLong(value.substring(1), 16);
|
|
break;
|
|
case 4: // #RGB
|
|
ret = (int) Long.parseLong(value.substring(1), 16);
|
|
|
|
k |= ((ret >> 8) & 0xF) * 0x110000;
|
|
k |= ((ret >> 4) & 0xF) * 0x1100;
|
|
k |= ((ret) & 0xF) * 0x11;
|
|
ret = k | 0xFF000000;
|
|
break;
|
|
case 5: // #ARGB
|
|
ret = (int) Long.parseLong(value.substring(1), 16);
|
|
k |= ((ret >> 16) & 0xF) * 0x11000000;
|
|
k |= ((ret >> 8) & 0xF) * 0x110000;
|
|
k |= ((ret >> 4) & 0xF) * 0x1100;
|
|
k |= ((ret) & 0xF) * 0x11;
|
|
break;
|
|
default:
|
|
return 0xFF000000;
|
|
}
|
|
|
|
logger.log(Level.FINE, "color = " + value + " = " + Integer.toHexString(ret));
|
|
return ret;
|
|
}
|
|
}
|