/* * 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; } }