SubwayTooter-Android-App/_Emoji/src/main/java/com/android/ide/common/vectordrawable/VdParser.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;
}
}