/*******************************************************************************
* Copyright 2012-present Pixate, Inc.
*
* 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.
******************************************************************************/
/**
* Copyright (c) 2012 Pixate, Inc. All rights reserved.
*/
package com.pixate.freestyle.cg.parsing;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.graphics.Matrix;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Picture;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.shapes.Shape;
import android.net.Uri;
import com.pixate.freestyle.cg.paints.PXGradient;
import com.pixate.freestyle.cg.paints.PXGradient.PXGradientUnits;
import com.pixate.freestyle.cg.paints.PXLinearGradient;
import com.pixate.freestyle.cg.paints.PXPaint;
import com.pixate.freestyle.cg.paints.PXRadialGradient;
import com.pixate.freestyle.cg.paints.PXSolidPaint;
import com.pixate.freestyle.cg.shapes.PXArc;
import com.pixate.freestyle.cg.shapes.PXCircle;
import com.pixate.freestyle.cg.shapes.PXEllipse;
import com.pixate.freestyle.cg.shapes.PXLine;
import com.pixate.freestyle.cg.shapes.PXPath;
import com.pixate.freestyle.cg.shapes.PXPie;
import com.pixate.freestyle.cg.shapes.PXPolygon;
import com.pixate.freestyle.cg.shapes.PXRectangle;
import com.pixate.freestyle.cg.shapes.PXShape;
import com.pixate.freestyle.cg.shapes.PXShapeDocument;
import com.pixate.freestyle.cg.shapes.PXShapeGroup;
import com.pixate.freestyle.cg.shapes.PXShapeGroup.AlignViewPortType;
import com.pixate.freestyle.cg.shapes.PXShapeGroup.CropType;
import com.pixate.freestyle.cg.shapes.PXText;
import com.pixate.freestyle.cg.strokes.PXStroke;
import com.pixate.freestyle.cg.strokes.PXStroke.PXStrokeType;
import com.pixate.freestyle.styling.parsing.PXValueParser;
import com.pixate.freestyle.util.PXColorUtil;
import com.pixate.freestyle.util.PXLog;
import com.pixate.freestyle.util.Scanner;
import com.pixate.freestyle.util.Size;
import com.pixate.freestyle.util.StringUtil;
import com.pixate.freestyle.util.UrlStreamOpener;
/**
* PXSVGLoader
*
* @author Shalom Gibly
*/
public class PXSVGLoader {
// Used to time the SVG loading
private static final boolean TIME_LOGGING = false;
// TODO - Attach this to some verification mechanism
private static final boolean PXTEXT_SUPPORT = true;
private static final String TAG = PXSVGLoader.class.getSimpleName();
private static final Pattern DEFAULT_DELIMITER_PATTERN = Pattern.compile("[ ,\r\n]");
private static final Pattern COLON_SEPARATOR = Pattern.compile(":");
private static final Pattern SEMICOLON_SEPARATOR = Pattern.compile(";");
/**
* Returns a {@link PXShapeDocument} after parsing a SVG file (subset of
* SVG).
*
* @param url
* @return {@link PXShapeDocument}
* @throws IOException
*/
public static PXShapeDocument loadFromURL(Uri url) throws IOException {
return loadFromStream(UrlStreamOpener.open(url));
}
/**
* Returns a {@link PXShapeDocument} after parsing a SVG resource (subset of
* SVG).
*
* @param resources
* @param resourceId
* @return {@link PXShapeDocument}
* @throws IOException
* @throws NotFoundException
*/
public static PXShapeDocument loadFromResource(Resources resources, int resourceId)
throws NotFoundException, IOException {
return loadFromStream(resources.openRawResource(resourceId));
}
/**
* Returns a {@link PXShapeDocument} after parsing a SVG stream (subset of
* SVG).
*
* @param inputStream An {@link InputStream}. Will be closed by this method
* after the {@link PXShape} is loaded.
* @return {@link PXShapeDocument}
* @throws IOException
*/
public static PXShapeDocument loadFromStream(InputStream inputStream) throws IOException {
long start = System.currentTimeMillis();
PXSVGParser parser = new PXSVGParser();
PXShapeGroup result = parser.parse(inputStream);
PXShapeDocument scene = parser.getDocument();
if (result == null) {
PXLog.e(TAG, "Error parsing document from input stream");
} else {
scene.setShape(result);
}
if (TIME_LOGGING) {
PXLog.i(TAG, "Loading SVG (from stream) took " + (System.currentTimeMillis() - start)
+ "ms");
}
return scene;
}
/**
* Parses the SVG and returns a {@link Picture}.
*/
protected static class PXSVGParser extends DefaultHandler {
// Supported SVG elements.
private static final String SVG_ELEMENT = "svg";
private static final String GROUP_ELEMENT = "g";
private static final String PATH_ELEMENT = "path";
private static final String RECT_ELEMENT = "rect";
private static final String LINE_ELEMENT = "line";
private static final String CIRCLE_ELEMENT = "circle";
private static final String ELLIPSE_ELEMENT = "ellipse";
private static final String LINEAR_GRADIENT_ELEMENT = "linearGradient";
private static final String RADIAL_GRADIENT_ELEMENT = "radialGradient";
private static final String STOP_ELEMENT = "stop";
private static final String POLYGON_ELEMENT = "polygon";
private static final String POLYLINE_ELEMENT = "polyline";
private static final String TEXT_ELEMENT = "text";
private static final String ARC_ELEMENT = "arc";
private static final String PIE_ELEMENT = "pie";
// static parsers
private static PXTransformParser transformParser = new PXTransformParser();
private static PXValueParser valueParser = new PXValueParser();
// Use an ArrayDeque an a non-synchronized replacement for Stack.
private PXShapeDocument document;
private PXShapeGroup result;
private ArrayDeque<PXShapeGroup> stack;
private PXGradient currentGradient;
private Map<String, PXGradient> gradients;
private Map<String, PXShapeGroup.AlignViewPortType> alignTypes;
private PXText currentTextElement;
/**
* Constructs a parser
*/
protected PXSVGParser() {
document = new PXShapeDocument();
stack = new ArrayDeque<PXShapeGroup>();
gradients = new HashMap<String, PXGradient>();
// view port alignment type map
alignTypes = new HashMap<String, PXShapeGroup.AlignViewPortType>(10);
for (AlignViewPortType type : EnumSet.allOf(AlignViewPortType.class)) {
alignTypes.put(type.toString(), type);
}
}
/**
* Returns the root group, which is the result of the shape parsing.
*
* @return A {@link PXShapeGroup} instance.
*/
protected PXShapeGroup getResult() {
return result;
}
/**
* Returns the document (scene).
*
* @return A {@link PXShapeDocument}
*/
protected PXShapeDocument getDocument() {
return document;
}
/**
* Parses the stream and returns a {@link PXShape} out of it.
*
* @return A {@link PXShapeGroup}
* @throws PXSVGParseException
*/
protected PXShapeGroup parse(InputStream inputStream) throws PXSVGParseException {
try {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
XMLReader reader = parser.getXMLReader();
reader.setContentHandler(this);
reader.parse(new InputSource(new InputStreamReader(inputStream, "UTF-8")));
inputStream.close();
} catch (Throwable t) {
throw new PXSVGParseException("Error processing the shape", t);
}
return result;
}
/*
* (non-Javadoc)
* @see
* org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String,
* java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
if (localName == null) {
return;
}
// merge style properties with attribute values. Style declarations
// override attributes
attributes = getAttributesWithMergedStyles(attributes);
if (SVG_ELEMENT.equals(localName)) {
PXShapeGroup newGroup = new PXShapeGroup();
// set viewport, if we have one
Float width = numberFromString(attributes.getValue("width"), null);
Float height = numberFromString(attributes.getValue("height"), null);
if (width != null && height != null) {
newGroup.setViewport(new RectF(0, 0, width, height));
// set viewport settings, if we have those
applyViewport(attributes, newGroup);
}
// create top-level group
stack.push(newGroup);
} else if (GROUP_ELEMENT.equals(localName)) {
// create nested group
PXShapeGroup newGroup = new PXShapeGroup();
// TODO: set all inherited properties
newGroup.setOpacity(opacityFromString(attributes.getValue("opacity")));
// id
String ident = attributes.getValue("id");
if (ident != null) {
document.addShape(ident, newGroup);
}
// transform
newGroup.setTransform(transformFromString(attributes.getValue("transform")));
// set viewport settings, if we have those
applyViewport(attributes, newGroup);
// add group as child of active group
addShape(stack, newGroup);
// push new group as active group
stack.push(newGroup);
} else if (PATH_ELEMENT.equals(localName)) {
// add path to current group
String data = attributes.getValue("d");
if (data != null) {
PXPath path = PXPath.createPathFromPathData(data);
applyStyles(attributes, path, gradients, document);
addShape(stack, path);
}
} else if (RECT_ELEMENT.equals(localName)) {
// add path to current group
Float x = numberFromString(attributes.getValue("x"));
Float y = numberFromString(attributes.getValue("y"));
RectF rect = new RectF(x, y, x + numberFromString(attributes.getValue("width")), y
+ numberFromString(attributes.getValue("height")));
Float rx = numberFromString(attributes.getValue("rx"), null);
Float ry = numberFromString(attributes.getValue("ry"), null);
PXRectangle rectangle = new PXRectangle(rect);
if (rx != null && ry != null) {
rectangle.setCornerRadii(new Size(rx, ry));
}
applyStyles(attributes, rectangle, gradients, document);
addShape(stack, rectangle);
} else if (LINE_ELEMENT.equals(localName)) {
PXLine line = new PXLine(numberFromString(attributes.getValue("x1")),
numberFromString(attributes.getValue("y1")),
numberFromString(attributes.getValue("x2")),
numberFromString(attributes.getValue("y2")));
applyStyles(attributes, line, gradients, document);
addShape(stack, line);
} else if (CIRCLE_ELEMENT.equals(localName)) {
PXCircle circle = new PXCircle(new PointF(
numberFromString(attributes.getValue("cx")),
numberFromString(attributes.getValue("cy"))),
numberFromString(attributes.getValue("r")));
applyStyles(attributes, circle, gradients, document);
addShape(stack, circle);
} else if (ELLIPSE_ELEMENT.equals(localName)) {
PXEllipse ellipse = new PXEllipse(new PointF(
numberFromString(attributes.getValue("cx")),
numberFromString(attributes.getValue("cy"))),
numberFromString(attributes.getValue("rx")),
numberFromString(attributes.getValue("ry")));
applyStyles(attributes, ellipse, gradients, document);
addShape(stack, ellipse);
} else if (LINEAR_GRADIENT_ELEMENT.equals(localName)) {
String name = attributes.getValue("id");
if (name != null) {
Matrix transform = transformFromString(attributes.getValue("gradientTransform"));
PXLinearGradient gradient = new PXLinearGradient();
gradient.setP1(new PointF(numberFromString(attributes.getValue("x1")),
numberFromString(attributes.getValue("y1"))));
gradient.setP2(new PointF(numberFromString(attributes.getValue("x2")),
numberFromString(attributes.getValue("y2"))));
gradient.setTransform(transform);
String gradientUnits = attributes.getValue("gradientUnits");
if ("userSpaceOnUse".equals(gradientUnits)) {
gradient.setGradientUnits(PXGradientUnits.USER_SPACE);
} else {
// assume all non-valid values in addition to
// "objectBoundingBox" mean bounding box
gradient.setGradientUnits(PXGradientUnits.BOUNDING_BOX);
}
currentGradient = gradient;
gradients.put(name, currentGradient);
} else {
PXLog.i(TAG, "Skipping unnamed linear gradient");
}
} else if (RADIAL_GRADIENT_ELEMENT.equals(localName)) {
String name = attributes.getValue("id");
if (name != null) {
PXRadialGradient gradient = new PXRadialGradient();
gradient.setCenter(new PointF(numberFromString(attributes.getValue("cx")),
numberFromString(attributes.getValue("cy"))));
gradient.setRadius(numberFromString(attributes.getValue("r")));
String gradientUnits = attributes.getValue("gradientUnits");
gradient.setTransform(transformFromString(attributes
.getValue("gradientTransform")));
// TODO - Add (somehow) start and end centers for the
// gradient.
// Float fx = numberFromString(attributes.getValue("fx"));
// Float fy = numberFromString(attributes.getValue("fy"));
// if (fx != null && fy != null) {
// gradient.setStartCenter(new PointF(fx, fy));
// } else {
// gradient.setStartCenter(gradient.getEndCenter());
// }
if ("userSpaceOnUse".equals(gradientUnits)) {
gradient.setGradientUnits(PXGradientUnits.USER_SPACE);
} else {
// assume all non-valid values in addition to
// "objectBoundingBox" mean bounding box
gradient.setGradientUnits(PXGradientUnits.BOUNDING_BOX);
}
currentGradient = gradient;
gradients.put(name, currentGradient);
} else {
PXLog.i(TAG, "Skipping unnamed radial gradient");
}
} else if (STOP_ELEMENT.equals(localName)) {
if (currentGradient != null) {
Float offset = numberFromString(attributes.getValue("offset"), null);
String stopColorString = attributes.getValue("stop-color");
if (stopColorString != null) {
String stopOpacityString = attributes.getValue("stop-opacity");
Integer stopColor = valueParser.parseColor(PXValueParser
.lexemesForSource(stopColorString));
if (stopOpacityString != null && stopColor != null) {
stopColor = PXColorUtil.colorWithAlpha(stopColor,
opacityFromString(stopOpacityString));
}
if (offset != null) {
currentGradient.addOffset(offset);
}
currentGradient.addColor(stopColor);
} else {
PXLog.e(TAG, "Stop element is missing a stop-color");
}
} else {
PXLog.e(TAG,
"Skipping stop element since it is not contained within a gradient element");
}
} else if (POLYGON_ELEMENT.equals(localName)) {
PXPolygon polygon = makePolygon(attributes.getValue("points"));
polygon.setClosed(true);
applyStyles(attributes, polygon, gradients, document);
addShape(stack, polygon);
} else if (POLYLINE_ELEMENT.equals(localName)) {
PXPolygon polygon = makePolygon(attributes.getValue("points"));
polygon.setClosed(false);
applyStyles(attributes, polygon, gradients, document);
addShape(stack, polygon);
} else if (TEXT_ELEMENT.equals(localName)) {
if (PXTEXT_SUPPORT) {
float x = numberFromString(attributes.getValue("x"), 0F);
float y = numberFromString(attributes.getValue("y"), 0F);
PXText text = new PXText();
text.setOrigin(new PointF(x, y));
applyStyles(attributes, text, gradients, document);
addShape(stack, text);
currentTextElement = text;
}
} else if (ARC_ELEMENT.equals(localName)) {
float cx = numberFromString(attributes.getValue("cx"));
float cy = numberFromString(attributes.getValue("cy"));
float r = numberFromString(attributes.getValue("r"));
float startAngle = numberFromString(attributes.getValue("start-angle"));
float endAngle = numberFromString(attributes.getValue("end-angle"));
PXArc arc = new PXArc();
arc.setCenter(new PointF(cx, cy));
arc.setRadius(r);
arc.setStartingAngle(startAngle);
arc.setEndingAngle(endAngle);
applyStyles(attributes, arc, gradients, document);
addShape(stack, arc);
} else if (PIE_ELEMENT.equals(localName)) {
float cx = numberFromString(attributes.getValue("cx"));
float cy = numberFromString(attributes.getValue("cy"));
float r = numberFromString(attributes.getValue("r"));
float startAngle = numberFromString(attributes.getValue("start-angle"));
float endAngle = numberFromString(attributes.getValue("end-angle"));
PXPie pie = new PXPie();
pie.setCenter(new PointF(cx, cy));
pie.setRadius(r);
pie.setStartingAngle(startAngle);
pie.setEndingAngle(endAngle);
applyStyles(attributes, pie, gradients, document);
addShape(stack, pie);
}
}
/*
* (non-Javadoc)
* @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String,
* java.lang.String, java.lang.String)
*/
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (localName == null) {
return;
}
if (SVG_ELEMENT.equals(localName)) {
result = stack.pop();
} else if (GROUP_ELEMENT.equals(localName)) {
stack.pop();
} else if (LINEAR_GRADIENT_ELEMENT.equals(localName)
|| RADIAL_GRADIENT_ELEMENT.equals(localName)) {
currentGradient = null;
} else if (TEXT_ELEMENT.equals(localName)) {
if (PXTEXT_SUPPORT) {
// TODO: grab accumulated text and assigned to text element
currentTextElement.setText("Professional!");
currentTextElement = null;
}
}
}
/**
* Add a {@link Shape} to the stack.
*
* @param stack
* @param shape
*/
private static void addShape(ArrayDeque<PXShapeGroup> stack, PXShape shape) {
if (!stack.isEmpty()) {
stack.peek().addShape(shape);
}
}
/**
* Apply style attributes to the shape.
*
* @param attributes
* @param shape
* @param gradients
* @param scene
* @param transformParser
*/
private static void applyStyles(Attributes attributes, PXShape shape,
Map<String, PXGradient> gradients, PXShapeDocument scene) {
String strokeDashArray = attributes.getValue("stroke-dasharray");
String fillColor = attributes.getValue("fill");
shape.setOpacity(opacityFromString(attributes.getValue("opacity")));
// fill
if (fillColor == null) {
fillColor = "#000000";
}
// TODO - Check if in this case we can just update directly the
// fillColor field.
// Perhaps add another method that accepts a flag for the update.
shape.setFillColor(paintFromString(fillColor, attributes.getValue("fill-opacity"),
gradients));
// stroke
PXStroke stroke = new PXStroke();
String strokeType = attributes.getValue("stroke-type");
if (strokeType != null) {
if ("inner".equals(strokeType)) {
stroke.setType(PXStrokeType.INNER);
} else if ("outer".equals(strokeType)) {
stroke.setType(PXStrokeType.OUTER);
}
// else, use the default "center"
}
stroke.setColor(paintFromString(attributes.getValue("stroke"),
attributes.getValue("stroke-opacity"), gradients));
stroke.setWidth(numberFromString(attributes.getValue("stroke-width"), 1f));
if (strokeDashArray != null) {
stroke.setDashArray(numberArrayFromString(strokeDashArray));
}
stroke.setDashOffset(numberFromString(attributes.getValue("stroke-dashoffset"))
.intValue());
stroke.setLineCap(lineCapFromString(attributes.getValue("stroke-linecap")));
stroke.setLineJoin(lineJoinFromString(attributes.getValue("stroke-linejoin")));
String miterLimit = attributes.getValue("stroke-miterlimit");
stroke.setMiterLimit((miterLimit != null) ? numberFromString(miterLimit) : 4.0F);
// TODO - Check if in this case we can just update directly the
// stroke field. perhaps add another method that accepts a flag for
// the update.
shape.setStroke(stroke);
// visibility
String visibility = attributes.getValue("visibility");
if (visibility != null) {
shape.setVisible("visible".equals(visibility));
}
String ident = attributes.getValue("id");
if (ident != null) {
scene.addShape(ident, shape);
}
// transform
shape.setTransform(transformFromString(attributes.getValue("transform")));
}
private void applyViewport(Attributes attributes, PXShapeGroup group) {
String par = attributes.getValue("preserveAspectRatio");
if (par != null) {
String[] parts = par.split(" ");
int partCount = parts.length;
AlignViewPortType alignment = AlignViewPortType.XMID_YMID;
CropType crop = CropType.MEET;
if (1 <= partCount && partCount <= 2) {
String alignmentString = parts[0];
AlignViewPortType typeNumber = alignTypes.get(alignmentString);
if (typeNumber != null) {
alignment = typeNumber;
} else {
PXLog.e(TAG, "Unrecognized aspect ratio crop setting: " + alignmentString);
}
if (partCount == 2) {
String cropString = parts[1];
if ("meet".equals(cropString)) {
crop = CropType.MEET;
} else if ("slice".equals(cropString)) {
crop = CropType.SLICE;
} else {
PXLog.e(TAG, "Unrecognized crop type: " + cropString);
}
} else {
PXLog.e(TAG, "Unrecognized preserveAspectRatio value: " + par);
}
}
group.setViewportAlignment(alignment);
group.setViewportCrop(crop);
}
}
private static Cap lineCapFromString(String value) {
if (value == null) {
return Cap.BUTT;
}
Cap cap = Cap.valueOf(value.toUpperCase(Locale.US));
if (cap == null) {
PXLog.e(TAG, "Unrecognized line cap: " + value);
}
return cap;
}
private static Join lineJoinFromString(String value) {
if (value == null) {
return Join.MITER;
}
Join join = Join.valueOf(value.toUpperCase(Locale.US));
if (join == null) {
PXLog.e(TAG, "Unrecognized line join: " + value);
}
return join;
}
private static float opacityFromString(String value) {
return (value != null) ? Float.parseFloat(value) : 1.0F;
}
private static PXPolygon makePolygon(String pointsString) {
float[] coords = numberArrayFromString(pointsString);
int length = coords.length;
if ((length % 2) == 1) {
length--;
}
PointF[] points = new PointF[length / 2];
for (int i = 0, j = 0; i < length; i += 2, j++) {
points[j] = new PointF(coords[i], coords[i + 1]);
}
return new PXPolygon(points);
}
private static PXPaint paintFromString(String attributeValue, String opacity,
Map<String, PXGradient> gradients) {
PXPaint paint = null;
if (attributeValue != null) {
float alpha = opacityFromString(opacity);
if (attributeValue.equals("none")) {
// TODO - Test if this is a "clear" color
paint = new PXSolidPaint(0x00000000);
} else if (attributeValue.startsWith("#")) {
int color = PXColorUtil.colorFromHexString(attributeValue, alpha);
paint = new PXSolidPaint(color);
} else if (attributeValue.startsWith("url(#")) {
// locate the gradient color in the gradients map
paint = (PXPaint) gradients.get(attributeValue.substring(5,
attributeValue.length() - 1));
} else {
paint = valueParser.parsePaint(PXValueParser.lexemesForSource(attributeValue));
}
}
return paint;
}
private static Float numberFromString(String value) {
return numberFromString(value, 0F);
}
private static Float numberFromString(String value, Float defaultValue) {
if (value == null) {
return defaultValue;
} else {
if (value.endsWith("px")) {
return Float.parseFloat(value.substring(0, value.length() - 2));
} else if (value.endsWith("%")) {
return Float.parseFloat(value.substring(0, value.length() - 1)) / 100.0f;
}
return Float.parseFloat(value);
}
}
private static float[] numberArrayFromString(String value) {
List<Float> numbers = new ArrayList<Float>(5);
Scanner scanner = new Scanner(value);
scanner.useDelimiter(DEFAULT_DELIMITER_PATTERN);
while (scanner.hasNextFloat()) {
numbers.add(scanner.nextFloat());
}
scanner.close();
// have to convert the arraylist into float-array
return toPrimitiveArray(numbers);
}
private static float[] toPrimitiveArray(List<Float> values) {
float[] primitiveValues = new float[values.size()];
for (int i = 0; i < primitiveValues.length; i++) {
primitiveValues[i] = values.get(i);
}
return primitiveValues;
}
private static Matrix transformFromString(String value) {
if (value != null) {
return transformParser.parse(value);
}
return null;
}
/**
* Returns the Attributes after expanding the "style" values as separate
* attributes.
*
* @param attributes
* @return
*/
private static Attributes getAttributesWithMergedStyles(Attributes attributes) {
String styles = attributes.getValue("style");
if (!StringUtil.isEmpty(styles)) {
// break up the styles into additional attributes.
AttributesImpl newAttributes = new AttributesImpl(attributes);
String[] declarations = SEMICOLON_SEPARATOR.split(styles);
for (String declaration : declarations) {
String[] parts = COLON_SEPARATOR.split(declaration);
if (parts.length == 2) {
newAttributes.addAttribute(StringUtil.EMPTY, StringUtil.EMPTY,
parts[0].trim(), StringUtil.EMPTY, parts[1].trim());
} else {
PXLog.w(TAG, "Expected 2 parts in the style declaration, but got %d. '%s'",
parts.length, declaration);
}
}
attributes = newAttributes;
}
return attributes;
}
}
}