Commit a70c775e authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Removes androidSvg lib as module dependency.

Now, using it througth jitpack.io
parent 7f25987f
......@@ -49,7 +49,6 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':player')
implementation project(':svg')
implementation libraries.kotlin
implementation libraries.coroutines
......@@ -93,6 +92,7 @@ dependencies {
implementation libraries.kotshiApi
implementation libraries.frescoImageViewer
implementation libraries.androidSvg
implementation libraries.aVLoadingIndicatorView
......
......@@ -27,7 +27,6 @@ allprojects {
jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
maven { url "https://jitpack.io" }
maven { url "http://dl.bintray.com/amulyakhare/maven" } // For TextDrawable.
}
apply from: rootProject.file('dependencies.gradle')
......
......@@ -25,6 +25,7 @@ ext {
fresco : '1.7.1',
kotshi : '0.3.0',
frescoImageViewer : '0.5.0',
androidSvg : 'master-SNAPSHOT',
aVLoadingIndicatorView : '2.1.3',
// For testing
......@@ -79,6 +80,7 @@ ext {
kotshiCompiler : "se.ansman.kotshi:compiler:${versions.kotshi}",
frescoImageViewer : "com.github.stfalcon:frescoimageviewer:${versions.frescoImageViewer}",
androidSvg : "com.github.BigBadaboom:androidsvg:${versions.androidSvg}",
aVLoadingIndicatorView : "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
......
include ':app', ':player', ':svg'
\ No newline at end of file
include ':app', ':player'
\ No newline at end of file
apply plugin: 'com.android.library'
android {
compileSdkVersion versions.compileSdk
defaultConfig {
minSdkVersion 21
targetSdkVersion versions.targetSdk
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.caverock.androidsvg"
android:versionCode="1"
android:versionName="1.2.3-beta-1">
<uses-sdk/>
</manifest>
\ No newline at end of file
/*
Copyright 2013 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
import android.util.Log;
import com.caverock.androidsvg.SVG.SvgContainer;
import com.caverock.androidsvg.SVG.SvgElementBase;
import com.caverock.androidsvg.SVG.SvgObject;
import com.caverock.androidsvg.SVGParser.TextScanner;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* A very simple CSS parser that is not very compliant with the CSS spec but
* hopefully parses almost all the CSS we are likely to strike in an SVG file.
* The main goals are to (a) be small, and (b) parse the CSS in a Corel Draw SVG file.
*/
public class CSSParser
{
private static final String TAG = "AndroidSVG CSSParser";
private static final String ID = "id";
private static final String CLASS = "class";
private MediaType rendererMediaType = null;
private boolean inMediaRule = false;
@SuppressWarnings("unused")
enum MediaType
{
all,
aural,
braille,
embossed,
handheld,
print,
projection,
screen,
tty,
tv
}
private enum Combinator
{
DESCENDANT, // E F
CHILD, // E > F
FOLLOWS // E + F
}
private enum AttribOp
{
EXISTS, // *[foo]
EQUALS, // *[foo=bar]
INCLUDES, // *[foo~=bar]
DASHMATCH, // *[foo|=bar]
}
private static class Attrib
{
final public String name;
final AttribOp operation;
final public String value;
Attrib(String name, AttribOp op, String value)
{
this.name = name;
this.operation = op;
this.value = value;
}
}
private static class SimpleSelector
{
Combinator combinator = null;
String tag = null; // null means "*"
List<Attrib> attribs = null;
List<String> pseudos = null;
SimpleSelector(Combinator combinator, String tag)
{
this.combinator = (combinator != null) ? combinator : Combinator.DESCENDANT;
this.tag = tag;
}
void addAttrib(String attrName, AttribOp op, String attrValue)
{
if (attribs == null)
attribs = new ArrayList<>();
attribs.add(new Attrib(attrName, op, attrValue));
}
void addPseudo(String pseudo)
{
if (pseudos == null)
pseudos = new ArrayList<>();
pseudos.add(pseudo);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if (combinator == Combinator.CHILD)
sb.append("> ");
else if (combinator == Combinator.FOLLOWS)
sb.append("+ ");
sb.append((tag == null) ? "*" : tag);
if (attribs != null) {
for (Attrib attr: attribs) {
sb.append('[').append(attr.name);
switch(attr.operation) {
case EQUALS: sb.append('=').append(attr.value); break;
case INCLUDES: sb.append("~=").append(attr.value); break;
case DASHMATCH: sb.append("|=").append(attr.value); break;
default: break;
}
sb.append(']');
}
}
if (pseudos != null) {
for (String pseu: pseudos)
sb.append(':').append(pseu);
}
return sb.toString();
}
}
static class Ruleset
{
private List<Rule> rules = null;
// Add a rule to the ruleset. The position at which it is inserted is determined by its specificity value.
void add(Rule rule)
{
if (this.rules == null)
this.rules = new ArrayList<>();
for (int i = 0; i < rules.size(); i++)
{
Rule nextRule = rules.get(i);
if (nextRule.selector.specificity > rule.selector.specificity) {
rules.add(i, rule);
return;
}
}
rules.add(rule);
}
void addAll(Ruleset rules)
{
if (rules.rules == null)
return;
if (this.rules == null)
this.rules = new ArrayList<>(rules.rules.size());
for (Rule rule: rules.rules) {
this.rules.add(rule);
}
}
List<Rule> getRules()
{
return this.rules;
}
boolean isEmpty()
{
return this.rules == null || this.rules.isEmpty();
}
@Override
public String toString()
{
if (rules == null)
return "";
StringBuilder sb = new StringBuilder();
for (Rule rule: rules)
sb.append(rule.toString()).append('\n');
return sb.toString();
}
}
static class Rule
{
Selector selector = null;
SVG.Style style = null;
Rule(Selector selector, SVG.Style style)
{
this.selector = selector;
this.style = style;
}
@Override
public String toString()
{
return String.valueOf(selector) + " {}";
}
}
private static class Selector
{
List<SimpleSelector> selector = null;
int specificity = 0;
void add(SimpleSelector part)
{
if (this.selector == null)
this.selector = new ArrayList<>();
this.selector.add(part);
}
int size()
{
return (this.selector == null) ? 0 : this.selector.size();
}
SimpleSelector get(int i)
{
return this.selector.get(i);
}
boolean isEmpty()
{
return (this.selector == null) || this.selector.isEmpty();
}
// Methods for accumulating a specificity value as SimpleSelector entries are added.
void addedIdAttribute()
{
specificity += 10000;
}
void addedAttributeOrPseudo()
{
specificity += 100;
}
void addedElement()
{
specificity += 1;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
for (SimpleSelector sel: selector)
sb.append(sel).append(' ');
return sb.append('(').append(specificity).append(')').toString();
}
}
//===========================================================================================
CSSParser(MediaType rendererMediaType)
{
this.rendererMediaType = rendererMediaType;
}
Ruleset parse(String sheet) throws SVGParseException
{
CSSTextScanner scan = new CSSTextScanner(sheet);
scan.skipWhitespace();
return parseRuleset(scan);
}
static boolean mediaMatches(String mediaListStr, MediaType rendererMediaType) throws SVGParseException
{
CSSTextScanner scan = new CSSTextScanner(mediaListStr);
scan.skipWhitespace();
List<MediaType> mediaList = parseMediaList(scan);
if (!scan.empty())
throw new SVGParseException("Invalid @media type list");
return mediaMatches(mediaList, rendererMediaType);
}
//==============================================================================
private static void warn(String format, Object... args)
{
Log.w(TAG, String.format(format, args));
}
/*
private static void error(String format, Object... args)
{
Log.e(TAG, String.format(format, args));
}
private static void debug(String format, Object... args)
{
if (LibConfig.DEBUG)
Log.d(TAG, String.format(format, args));
}
*/
//==============================================================================
private static class CSSTextScanner extends TextScanner
{
CSSTextScanner(String input)
{
super(input.replaceAll("(?s)/\\*.*?\\*/", "")); // strip all block comments
}
/*
* Scans for a CSS 'ident' identifier.
*/
String nextIdentifier()
{
int end = scanForIdentifier();
if (end == position)
return null;
String result = input.substring(position, end);
position = end;
return result;
}
private int scanForIdentifier()
{
if (empty())
return position;
int start = position;
int lastValidPos = position;
int ch = input.charAt(position);
if (ch == '-')
ch = advanceChar();
// nmstart
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch == '_'))
{
ch = advanceChar();
// nmchar
while ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch == '-') || (ch == '_')) {
ch = advanceChar();
}
lastValidPos = position;
}
position = start;
return lastValidPos;
}
/*
* Scans for a CSS 'simple selector'.
* Returns true if it found one.
* Returns false if there was an error or the input is empty.
*/
boolean nextSimpleSelector(Selector selector) throws SVGParseException
{
if (empty())
return false;
int start = position;
Combinator combinator = null;
SimpleSelector selectorPart = null;
if (!selector.isEmpty())
{
if (consume('>')) {
combinator = Combinator.CHILD;
skipWhitespace();
} else if (consume('+')) {
combinator = Combinator.FOLLOWS;
skipWhitespace();
}
}
if (consume('*')) {
selectorPart = new SimpleSelector(combinator, null);
} else {
String tag = nextIdentifier();
if (tag != null) {
selectorPart = new SimpleSelector(combinator, tag);
selector.addedElement();
}
}
while (!empty())
{
if (consume('.'))
{
// ".foo" is equivalent to *[class="foo"]
if (selectorPart == null)
selectorPart = new SimpleSelector(combinator, null);
String value = nextIdentifier();
if (value == null)
throw new SVGParseException("Invalid \".class\" selector in <style> element");
selectorPart.addAttrib(CLASS, AttribOp.EQUALS, value);
selector.addedAttributeOrPseudo();
continue;
}
if (consume('#'))
{
// "#foo" is equivalent to *[id="foo"]
if (selectorPart == null)
selectorPart = new SimpleSelector(combinator, null);
String value = nextIdentifier();
if (value == null)
throw new SVGParseException("Invalid \"#id\" selector in <style> element");
selectorPart.addAttrib(ID, AttribOp.EQUALS, value);
selector.addedIdAttribute();
}
if (selectorPart == null)
break;
// Now check for attribute selection and pseudo selectors
if (consume('['))
{
skipWhitespace();
String attrName = nextIdentifier();
String attrValue = null;
if (attrName == null)
throw new SVGParseException("Invalid attribute selector in <style> element");
skipWhitespace();
AttribOp op = null;
if (consume('='))
op = AttribOp.EQUALS;
else if (consume("~="))
op = AttribOp.INCLUDES;
else if (consume("|="))
op = AttribOp.DASHMATCH;
if (op != null) {
skipWhitespace();
attrValue = nextAttribValue();
if (attrValue == null)
throw new SVGParseException("Invalid attribute selector in <style> element");
skipWhitespace();
}
if (!consume(']'))
throw new SVGParseException("Invalid attribute selector in <style> element");
selectorPart.addAttrib(attrName, (op == null) ? AttribOp.EXISTS : op, attrValue);
selector.addedAttributeOrPseudo();
continue;
}
if (consume(':'))
{
// skip pseudo
int pseudoStart = position;
if (nextIdentifier() != null) {
if (consume('(')) {
skipWhitespace();
if (nextIdentifier() != null) {
skipWhitespace();
if (!consume(')')) {
position = pseudoStart - 1;
break;
}
}
}
selectorPart.addPseudo(input.substring(pseudoStart, position));
selector.addedAttributeOrPseudo();
}
}
break;
}
if (selectorPart != null)
{
selector.add(selectorPart);
return true;
}
// Otherwise 'fail'
position = start;
return false;
}
/*
* The value (bar) part of "[foo="bar"]".
*/
private String nextAttribValue()
{
if (empty())
return null;
String result = nextQuotedString();
if (result != null)
return result;
return nextIdentifier();
}
/*
* Scans for a CSS property value.
*/
String nextPropertyValue()
{
if (empty())
return null;
int start = position;
int lastValidPos = position;
int ch = input.charAt(position);
while (ch != -1 && ch != ';' && ch != '}' && ch != '!' && !isEOL(ch)) {
if (!isWhitespace(ch)) // don't include an spaces at the end
lastValidPos = position + 1;
ch = advanceChar();
}
if (position > start)
return input.substring(start, lastValidPos);
position = start;
return null;
}
}
//==============================================================================
// Returns true if 'rendererMediaType' matches one of the media types in 'mediaList'
private static boolean mediaMatches(List<MediaType> mediaList, MediaType rendererMediaType)
{
for (MediaType type: mediaList) {
if (type == MediaType.all || type == rendererMediaType)
return true;
}
return false;
}
private static List<MediaType> parseMediaList(CSSTextScanner scan) throws SVGParseException
{
ArrayList<MediaType> typeList = new ArrayList<>();
while (!scan.empty()) {
String type = scan.nextToken(',');
try {
typeList.add(MediaType.valueOf(type));
} catch (IllegalArgumentException e) {
throw new SVGParseException("Invalid @media type list");
}
// If there is a comma, keep looping, otherwise break
if (!scan.skipCommaWhitespace())
break;
}
return typeList;
}
private void parseAtRule(Ruleset ruleset, CSSTextScanner scan) throws SVGParseException
{
String atKeyword = scan.nextIdentifier();
scan.skipWhitespace();
if (atKeyword == null)
throw new SVGParseException("Invalid '@' rule in <style> element");
if (!inMediaRule && atKeyword.equals("media"))
{
List<MediaType> mediaList = parseMediaList(scan);
if (!scan.consume('{'))
throw new SVGParseException("Invalid @media rule: missing rule set");
scan.skipWhitespace();
if (mediaMatches(mediaList, rendererMediaType)) {
inMediaRule = true;
ruleset.addAll( parseRuleset(scan) );
inMediaRule = false;
} else {
parseRuleset(scan); // parse and ignore accompanying ruleset
}
if (!scan.consume('}'))
throw new SVGParseException("Invalid @media rule: expected '}' at end of rule set");
//} else if (atKeyword.equals("charset")) {
//} else if (atKeyword.equals("import")) {
}
else
{
// Unknown/unsupported at-rule
warn("Ignoring @%s rule", atKeyword);
skipAtRule(scan);
}
scan.skipWhitespace();
}
// Skip an unsupported at-rule: "ignore everything up to and including the next semicolon or block".
private void skipAtRule(CSSTextScanner scan)
{
int depth = 0;
while (!scan.empty())
{
int ch = scan.nextChar();
if (ch == ';' && depth == 0)
return;
if (ch == '{')
depth++;
else if (ch == '}' && depth > 0) {
if (--depth == 0)
return;
}
}
}
private Ruleset parseRuleset(CSSTextScanner scan) throws SVGParseException
{
Ruleset ruleset = new Ruleset();
while (!scan.empty())
{
if (scan.consume("<!--"))
continue;
if (scan.consume("-->"))
continue;
if (scan.consume('@')) {
parseAtRule(ruleset, scan);
continue;
}
if (parseRule(ruleset, scan))
continue;
// Nothing recognisable found. Could be end of rule set. Return.
break;
}
return ruleset;
}
private boolean parseRule(Ruleset ruleset, CSSTextScanner scan) throws SVGParseException
{
List<Selector> selectors = parseSelectorGroup(scan);
if (selectors != null && !selectors.isEmpty())
{
if (!scan.consume('{'))
throw new SVGParseException("Malformed rule block in <style> element: missing '{'");
scan.skipWhitespace();
SVG.Style ruleStyle = parseDeclarations(scan);
scan.skipWhitespace();
for (Selector selector: selectors) {
ruleset.add( new Rule(selector, ruleStyle) );
}
return true;
}
else
{
return false;
}
}
/*
* Parse a selector group (eg. E, F, G). In many/most cases there will be only one entry.
*/
private List<Selector> parseSelectorGroup(CSSTextScanner scan) throws SVGParseException
{
if (scan.empty())
return null;
ArrayList<Selector> selectorGroup = new ArrayList<>(1);
Selector selector = new Selector();
while (!scan.empty())
{
if (scan.nextSimpleSelector(selector))
{
// If there is a comma, keep looping, otherwise break
if (!scan.skipCommaWhitespace())
continue; // if not a comma, go back and check for next part of selector
selectorGroup.add(selector);
selector = new Selector();
}
else
break;
}
if (!selector.isEmpty())
selectorGroup.add(selector);
return selectorGroup;
}
// Parse a list of
private SVG.Style parseDeclarations(CSSTextScanner scan) throws SVGParseException
{
SVG.Style ruleStyle = new SVG.Style();
while (true)
{
String propertyName = scan.nextIdentifier();
scan.skipWhitespace();
if (!scan.consume(':'))
break; // Syntax error. Stop processing CSS rules.
scan.skipWhitespace();
String propertyValue = scan.nextPropertyValue();
if (propertyValue == null)
break; // Syntax error
// Check for !important flag.
scan.skipWhitespace();
if (scan.consume('!')) {
scan.skipWhitespace();
if (!scan.consume("important")) {
throw new SVGParseException("Malformed rule set in <style> element: found unexpected '!'");
}
// We don't do anything with these. We just ignore them.
scan.skipWhitespace();
}
scan.consume(';');
SVGParser.processStyleProperty(ruleStyle, propertyName, propertyValue);
scan.skipWhitespace();
if (scan.consume('}'))
return ruleStyle;
if (scan.empty())
break;
}
throw new SVGParseException("Malformed rule set in <style> element");
}
/*
* Used by SVGParser to parse the "class" attribute.
* Follows ordered set parser algorithm: https://dom.spec.whatwg.org/#concept-ordered-set-parser
*/
public static List<String> parseClassAttribute(String val)
{
CSSTextScanner scan = new CSSTextScanner(val);
List<String> classNameList = null;
while (!scan.empty())
{
String className = scan.nextToken();
if (className == null)
continue;
if (classNameList == null)
classNameList = new ArrayList<>();
classNameList.add(className);
scan.skipWhitespace();
}
return classNameList;
}
/*
* Used by renderer to check if a CSS rule matches the current element.
*/
static boolean ruleMatch(Selector selector, SvgElementBase obj)
{
// Build the list of ancestor objects
List<SvgContainer> ancestors = new ArrayList<>();
SvgContainer parent = obj.parent;
while (parent != null) {
ancestors.add(0, parent);
parent = ((SvgObject) parent).parent;
}
int ancestorsPos = ancestors.size() - 1;
// Check the most common case first as a shortcut.
if (selector.size() == 1)
return selectorMatch(selector.get(0), ancestors, ancestorsPos, obj);
// We start at the last part of the selector and loop back through the parts
// Get the next selector part
return ruleMatch(selector, selector.size() - 1, ancestors, ancestorsPos, obj);
}
private static boolean ruleMatch(Selector selector, int selPartPos, List<SvgContainer> ancestors, int ancestorsPos, SvgElementBase obj)
{
// We start at the last part of the selector and loop back through the parts
// Get the next selector part
SimpleSelector sel = selector.get(selPartPos);
if (!selectorMatch(sel, ancestors, ancestorsPos, obj))
return false;
// Selector part matched, check its combinator
if (sel.combinator == Combinator.DESCENDANT)
{
if (selPartPos == 0)
return true;
// Search up the ancestors list for a node that matches the next selector
while (ancestorsPos >= 0) {
if (ruleMatchOnAncestors(selector, selPartPos - 1, ancestors, ancestorsPos))
return true;
ancestorsPos--;
}
return false;
}
else if (sel.combinator == Combinator.CHILD)
{
return ruleMatchOnAncestors(selector, selPartPos - 1, ancestors, ancestorsPos);
}
else //if (sel.combinator == Combinator.FOLLOWS)
{
int childPos = getChildPosition(ancestors, ancestorsPos, obj);
if (childPos <= 0)
return false;
SvgElementBase prevSibling = (SvgElementBase) obj.parent.getChildren().get(childPos - 1);
return ruleMatch(selector, selPartPos - 1, ancestors, ancestorsPos, prevSibling);
}
}
private static boolean ruleMatchOnAncestors(Selector selector, int selPartPos, List<SvgContainer> ancestors, int ancestorsPos)
{
SimpleSelector sel = selector.get(selPartPos);
SvgElementBase obj = (SvgElementBase) ancestors.get(ancestorsPos);
if (!selectorMatch(sel, ancestors, ancestorsPos, obj))
return false;
// Selector part matched, check its combinator
if (sel.combinator == Combinator.DESCENDANT)
{
if (selPartPos == 0)
return true;
// Search up the ancestors list for a node that matches the next selector
while (ancestorsPos > 0) {
if (ruleMatchOnAncestors(selector, selPartPos - 1, ancestors, --ancestorsPos))
return true;
}
return false;
}
else if (sel.combinator == Combinator.CHILD)
{
return ruleMatchOnAncestors(selector, selPartPos - 1, ancestors, ancestorsPos - 1);
}
else //if (sel.combinator == Combinator.FOLLOWS)
{
int childPos = getChildPosition(ancestors, ancestorsPos, obj);
if (childPos <= 0)
return false;
SvgElementBase prevSibling = (SvgElementBase) obj.parent.getChildren().get(childPos - 1);
return ruleMatch(selector, selPartPos - 1, ancestors, ancestorsPos, prevSibling);
}
}
private static int getChildPosition(List<SvgContainer> ancestors, int ancestorsPos, SvgElementBase obj)
{
if (ancestorsPos < 0) // Has no parent, so can't have a sibling
return -1;
if (ancestors.get(ancestorsPos) != obj.parent) // parent doesn't match, so obj must be an indirect reference (eg. from a <use>)
return -1;
int childPos = 0;
for (SvgObject child: obj.parent.getChildren())
{
if (child == obj)
return childPos;
childPos++;
}
return -1;
}
private static boolean selectorMatch(SimpleSelector sel, List<SvgContainer> ancestors, int ancestorsPos, SvgElementBase obj)
{
// Check tag name. tag==null means tag is "*" which matches everything.
if (sel.tag != null) {
// The Group object does not match its tag ("<g>"), so we have to handle it as a special case.
if (sel.tag.equalsIgnoreCase("G"))
{
if (!(obj instanceof SVG.Group))
return false;
}
// all other element classes should match their tag names
else if (!sel.tag.equals(obj.getClass().getSimpleName().toLowerCase(Locale.US)))
{
return false;
}
}
// If here, then tag part matched
// Check the attributes
if (sel.attribs != null)
{
for (Attrib attr: sel.attribs)
{
switch (attr.name) {
case ID:
if (!attr.value.equals(obj.id))
return false;
break;
case CLASS:
if (obj.classNames == null)
return false;
if (!obj.classNames.contains(attr.value))
return false;
break;
default:
// Other attribute selector not yet supported
return false;
}
}
}
// Check the pseudo classes
if (sel.pseudos != null) {
for (String pseudo: sel.pseudos) {
if (pseudo.equals("first-child")) {
if (getChildPosition(ancestors, ancestorsPos, obj) != 0)
return false;
} else {
return false;
}
}
}
// If w reached this point, the selector matched
return true;
}
}
/*
Copyright 2014 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
/**
* Parse a SVG/CSS 'integer' or hex number from a String.
*
* We use our own parser to gain a bit of speed. This routine is
* around twice as fast as the system one.
*/
class IntegerParser
{
private int pos;
private long value;
private IntegerParser(long value, int pos)
{
this.value = value;
this.pos = pos;
}
/*
* Return the value of pos after the parse.
*/
int getEndPos()
{
return this.pos;
}
/*
* Scan the string for an SVG integer.
* Assumes maxPos will not be greater than input.length().
*/
/*
private static IntegerParser parseInt(String input, int startpos, int len)
{
int pos = startpos;
boolean isNegative = false;
long value = 0;
if (pos >= len)
return null; // String is empty - no number found
char ch = input.charAt(pos);
switch (ch) {
case '-': isNegative = true;
// fall through
case '+': pos++;
}
int sigStart = pos;
while (pos < len)
{
ch = input.charAt(pos);
if (ch >= '0' && ch <= '9')
{
if (isNegative) {
value = value * 10 - ((int)ch - (int)'0');
if (value < Integer.MIN_VALUE)
return null;
} else {
value = value * 10 + ((int)ch - (int)'0');
if (value > Integer.MAX_VALUE)
return null;
}
}
else
break;
pos++;
}
// Have we seen anything number-ish at all so far?
if (pos == sigStart) {
return null;
}
return new IntegerParser(isNegative, value, pos);
}
*/
/*
* Return the parsed value as an actual float.
*/
public int value()
{
return (int)value;
}
/*
* Scan the string for an SVG hex integer.
* Assumes maxPos will not be greater than input.length().
*/
static IntegerParser parseHex(String input, int startpos, int len)
{
int pos = startpos;
long value = 0;
char ch;
if (pos >= len)
return null; // String is empty - no number found
while (pos < len)
{
ch = input.charAt(pos);
if (ch >= '0' && ch <= '9')
{
value = value * 16 + ((int)ch - (int)'0');
}
else if (ch >= 'A' && ch <= 'F')
{
value = value * 16 + ((int)ch - (int)'A') + 10;
}
else if (ch >= 'a' && ch <= 'f')
{
value = value * 16 + ((int)ch - (int)'a') + 10;
}
else
break;
if (value > 0xffffffffL)
return null;
pos++;
}
// Have we seen anything number-ish at all so far?
if (pos == startpos) {
return null;
}
return new IntegerParser(value, pos);
}
}
/*
Copyright 2013 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
/**
* Configure debugging on or off.
*/
class LibConfig
{
static final boolean DEBUG = false;
}
/*
Copyright 2014 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
/**
* Parse a SVG 'number' or a CSS 'number' from a String.
*
* We use our own parser because the one in Android (from Harmony I think) is slow.
*
* An SVG 'number' is defined as
* integer ([Ee] integer)?
* | [+-]? [0-9]* "." [0-9]+ ([Ee] integer)?
* Where 'integer' is
* [+-]? [0-9]+
* CSS numbers were different, but have now been updated to a compatible definition (see 2.1 Errata)
* [+-]?([0-9]+|[0-9]*\.[0-9]+)(e[+-]?[0-9]+)?
*
*/
class NumberParser
{
private int pos;
/*
* Return the value of pos after the parse.
*/
int getEndPos()
{
return this.pos;
}
/*
* Scan the string for an SVG number.
* Assumes maxPos will not be greater than str.length().
*/
float parseNumber(String input, int startpos, int len)
{
boolean isNegative = false;
long significand = 0;
int numDigits = 0;
int numLeadingZeroes = 0;
int numTrailingZeroes = 0;
boolean decimalSeen = false;
int sigStart;
int decimalPos = 0;
int exponent;
long TOO_BIG = Long.MAX_VALUE / 10;
pos = startpos;
if (pos >= len)
return Float.NaN; // String is empty - no number found
char ch = input.charAt(pos);
switch (ch) {
case '-': isNegative = true;
// fall through
case '+': pos++;
}
sigStart = pos;
while (pos < len)
{
ch = input.charAt(pos);
if (ch == '0')
{
if (numDigits == 0) {
numLeadingZeroes++;
} else {
// We potentially skip trailing zeroes. Keep count for now.
numTrailingZeroes++;
}
}
else if (ch >= '1' && ch <= '9')
{
// Multiply any skipped zeroes into buffer
numDigits += numTrailingZeroes;
while (numTrailingZeroes > 0) {
if (significand > TOO_BIG) {
//Log.e("Number is too large");
return Float.NaN;
}
significand *= 10;
numTrailingZeroes--;
}
if (significand > TOO_BIG) {
// We will overflow if we continue...
//Log.e("Number is too large");
return Float.NaN;
}
significand = significand * 10 + ((int)ch - (int)'0');
numDigits++;
if (significand < 0)
return Float.NaN; // overflowed from +ve to -ve
}
else if (ch == '.')
{
if (decimalSeen) {
// Stop parsing here. We may be looking at a new number.
break;
}
decimalPos = pos - sigStart;
decimalSeen = true;
}
else
break;
pos++;
}
if (decimalSeen && pos == (decimalPos + 1)) {
// No digits following decimal point (eg. "1.")
//Log.e("Missing fraction part of number");
return Float.NaN;
}
// Have we seen anything number-ish at all so far?
if (numDigits == 0) {
if (numLeadingZeroes == 0) {
//Log.e("Number not found");
return Float.NaN;
}
// Leading zeroes have been seen though, so we
// treat that as a '0'.
numDigits = 1;
}
if (decimalSeen) {
exponent = decimalPos - numLeadingZeroes - numDigits;
} else {
exponent = numTrailingZeroes;
}
// Now look for exponent
if (pos < len)
{
ch = input.charAt(pos);
if (ch == 'E' || ch == 'e')
{
boolean expIsNegative = false;
int expVal = 0;
boolean abortExponent = false;
pos++;
if (pos == len) {
// Incomplete exponent.
//Log.e("Incomplete exponent of number");
return Float.NaN;
}
switch (input.charAt(pos)) {
case '-': expIsNegative = true;
// fall through
case '+': pos++;
break;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
break; // acceptable next char
default:
// any other character is a failure, ie no exponent.
// Could be something legal like "em" though.
abortExponent = true;
pos--; // reset pos to position of 'E'/'e'
}
if (!abortExponent)
{
int expStart = pos;
while (pos < len)
{
ch = input.charAt(pos);
if (ch >= '0' && ch <= '9')
{
if (expVal > TOO_BIG) {
// We will overflow if we continue...
//Log.e("Exponent of number is too large");
return Float.NaN;
}
expVal = expVal * 10 + ((int)ch - (int)'0');
pos++;
}
else
break;
}
// Check that at least some exponent digits were read
if (pos == expStart) {
//Log.e(""Incomplete exponent of number"");
return Float.NaN;
}
if (expIsNegative)
exponent -= expVal;
else
exponent += expVal;
}
}
}
// Quick check to eliminate huge exponents.
// Biggest float is (2 - 2^23) . 2^127 ~== 3.4e38
// Biggest negative float is 2^-149 ~== 1.4e-45
// Some numbers that will overflow will get through the scan
// and be returned as 'valid', yet fail when value() is called.
// However they will be very rare and not worth slowing down
// the parse for.
if ((exponent + numDigits) > 39 || (exponent + numDigits) < -44)
return Float.NaN;
float f = (float) significand;
if (significand != 0)
{
// Do exponents > 0
if (exponent > 0)
{
f *= positivePowersOf10[exponent];
}
else if (exponent < 0)
{
// Some valid numbers can have an exponent greater than the max (ie. < -38)
// for a float. For example, significand=123, exponent=-40
// If that's the case, we need to apply the exponent in two steps.
if (exponent < -38) {
// Long.MAX_VALUE is 19 digits, so taking 20 off the exponent should be enough.
f *= 1e-20;
exponent += 20;
}
// Do exponents < 0
f *= negativePowersOf10[-exponent];
}
}
return (isNegative) ? -f : f;
}
private static final float positivePowersOf10[] = {
1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f, 1e7f, 1e8f, 1e9f,
1e10f, 1e11f, 1e12f, 1e13f, 1e14f, 1e15f, 1e16f, 1e17f, 1e18f, 1e19f,
1e20f, 1e21f, 1e22f, 1e23f, 1e24f, 1e25f, 1e26f, 1e27f, 1e28f, 1e29f,
1e30f, 1e31f, 1e32f, 1e33f, 1e34f, 1e35f, 1e36f, 1e37f, 1e38f
};
private static final float negativePowersOf10[] = {
1e0f, 1e-1f, 1e-2f, 1e-3f, 1e-4f, 1e-5f, 1e-6f, 1e-7f, 1e-8f, 1e-9f,
1e-10f, 1e-11f, 1e-12f, 1e-13f, 1e-14f, 1e-15f, 1e-16f, 1e-17f, 1e-18f, 1e-19f,
1e-20f, 1e-21f, 1e-22f, 1e-23f, 1e-24f, 1e-25f, 1e-26f, 1e-27f, 1e-28f, 1e-29f,
1e-30f, 1e-31f, 1e-32f, 1e-33f, 1e-34f, 1e-35f, 1e-36f, 1e-37f, 1e-38f
};
}
/*
Copyright 2013 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
/**
* The SVGPositioning class tells the renderer how to scale and position the
* SVG document in the current viewport. It is roughly equivalent to the
* {@code preserveAspectRatio} attribute of an {@code <svg>} element.
* <p>
* In order for scaling to happen, the SVG document must have a viewBox attribute set.
* For example:
*
* <pre>
* {@code
* <svg version="1.1" viewBox="0 0 200 100">
* }
* </pre>
*
*/
public class PreserveAspectRatio
{
private Alignment alignment;
private Scale scale;
/**
* Draw doucment at its natural position and scale.
*/
@SuppressWarnings("unused")
public static final PreserveAspectRatio UNSCALED = new PreserveAspectRatio(null, null);
/**
* Stretch horizontally and vertically to fill the viewport.
*/
@SuppressWarnings("WeakerAccess")
public static final PreserveAspectRatio STRETCH = new PreserveAspectRatio(Alignment.None, null);
/**
* Keep the document's aspect ratio, but scale it so that it fits neatly inside the viewport.
* <p>
* The document will be centred in the viewport and may have blank strips at either the top and
* bottom of the viewport or at the sides.
*/
@SuppressWarnings("WeakerAccess")
public static final PreserveAspectRatio LETTERBOX = new PreserveAspectRatio(Alignment.XMidYMid, Scale.Meet);
/**
* Keep the document's aspect ratio, but scale it so that it fits neatly inside the viewport.
* <p>
* The document will be positioned at the top of tall and narrow viewports, and at the left of short
* and wide viewports.
*/
@SuppressWarnings("unused")
public static final PreserveAspectRatio START = new PreserveAspectRatio(Alignment.XMinYMin, Scale.Meet);
/**
* Keep the document's aspect ratio, but scale it so that it fits neatly inside the viewport.
* <p>
* The document will be positioned at the bottom of tall and narrow viewports, and at the right of short
* and wide viewports.
*/
@SuppressWarnings("unused")
public static final PreserveAspectRatio END = new PreserveAspectRatio(Alignment.XMaxYMax, Scale.Meet);
/**
* Keep the document's aspect ratio, but scale it so that it fits neatly inside the viewport.
* <p>
* The document will be positioned at the top of tall and narrow viewports, and at the centre of
* short and wide viewports.
*/
@SuppressWarnings("unused")
public static final PreserveAspectRatio TOP = new PreserveAspectRatio(Alignment.XMidYMin, Scale.Meet);
/**
* Keep the document's aspect ratio, but scale it so that it fits neatly inside the viewport.
* <p>
* The document will be positioned at the bottom of tall and narrow viewports, and at the centre of
* short and wide viewports.
*/
@SuppressWarnings("unused")
public static final PreserveAspectRatio BOTTOM = new PreserveAspectRatio(Alignment.XMidYMax, Scale.Meet);
/**
* Keep the document's aspect ratio, but scale it so that it fills the entire viewport.
* This may result in some of the document falling outside the viewport.
* <p>
* The document will be positioned so that the centre of the document will always be visible,
* but the edges of the document may not.
*/
@SuppressWarnings("unused")
public static final PreserveAspectRatio FULLSCREEN = new PreserveAspectRatio(Alignment.XMidYMid, Scale.Slice);
/**
* Keep the document's aspect ratio, but scale it so that it fills the entire viewport.
* This may result in some of the document falling outside the viewport.
* <p>
* The document will be positioned so that the top left of the document will always be visible,
* but the right hand or bottom edge may not.
*/
@SuppressWarnings("unused")
public static final PreserveAspectRatio FULLSCREEN_START = new PreserveAspectRatio(Alignment.XMinYMin, Scale.Slice);
/**
* Determines how the document is to me positioned relative to the viewport (normally the canvas).
* <p>
* For the value {@code none}, the document is stretched to fit the viewport dimensions. For all
* other values, the aspect ratio of the document is kept the same but the document is scaled to
* fit the viewport.
*/
public enum Alignment
{
/** Document is stretched to fit both the width and height of the viewport. When using this Alignment value, the value of Scale is not used and will be ignored. */
None,
/** Document is positioned at the top left of the viewport. */
XMinYMin,
/** Document is positioned at the centre top of the viewport. */
XMidYMin,
/** Document is positioned at the top right of the viewport. */
XMaxYMin,
/** Document is positioned at the middle left of the viewport. */
XMinYMid,
/** Document is centred in the viewport both vertically and horizontally. */
XMidYMid,
/** Document is positioned at the middle right of the viewport. */
XMaxYMid,
/** Document is positioned at the bottom left of the viewport. */
XMinYMax,
/** Document is positioned at the bottom centre of the viewport. */
XMidYMax,
/** Document is positioned at the bottom right of the viewport. */
XMaxYMax
}
/**
* Determine whether the scaled document fills the viewport entirely or is scaled to
* fill the viewport without overflowing.
*/
public enum Scale
{
/**
* The document is scaled so that it is as large as possible without overflowing the viewport.
* There may be blank areas on one or more sides of the document.
*/
Meet,
/**
* The document is scaled so that entirely fills the viewport. That means that some of the
* document may fall outside the viewport and will not be rendered.
*/
Slice
}
PreserveAspectRatio(Alignment alignment, Scale scale)
{
this.alignment = alignment;
this.scale = scale;
}
/**
* Returns the alignment value of this instance.
* @return the alignment
*/
@SuppressWarnings("WeakerAccess")
public Alignment getAlignment()
{
return alignment;
}
/**
* Returns the scale value of this instance.
* @return the scale
*/
@SuppressWarnings("WeakerAccess")
public Scale getScale()
{
return scale;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PreserveAspectRatio other = (PreserveAspectRatio) obj;
return (alignment == other.alignment && scale == other.scale);
}
}
/*
Copyright 2013 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Picture;
import android.graphics.RectF;
import android.util.Log;
import com.caverock.androidsvg.CSSParser.Ruleset;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* AndroidSVG is a library for reading, parsing and rendering SVG documents on Android devices.
* <p>
* All interaction with AndroidSVG is via this class.
* <p>
* Typically, you will call one of the SVG loading and parsing classes then call the renderer,
* passing it a canvas to draw upon.
*
* <h4>Usage summary</h4>
*
* <ul>
* <li>Use one of the static {@code getFromX()} methods to read and parse the SVG file. They will
* return an instance of this class.
* <li>Call one of the {@code renderToX()} methods to render the document.
* </ul>
*
* <h4>Usage example</h4>
*
* <pre>
* {@code
* SVG svg = SVG.getFromAsset(getContext().getAssets(), svgPath);
* svg.registerExternalFileResolver(myResolver);
*
* Bitmap newBM = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
* Canvas bmcanvas = new Canvas(newBM);
* bmcanvas.drawRGB(255, 255, 255); // Clear background to white
*
* svg.renderToCanvas(bmcanvas);
* }
* </pre>
*
* For more detailed information on how to use this library, see the documentation at {@code http://code.google.com/p/androidsvg/}
*/
public class SVG
{
private static final String TAG = "AndroidSVG";
private static final String VERSION = "1.2.3-beta-1";
private static final int DEFAULT_PICTURE_WIDTH = 512;
private static final int DEFAULT_PICTURE_HEIGHT = 512;
private static final double SQRT2 = 1.414213562373095;
// Parser configuration
private static boolean enableInternalEntities = true;
// The root svg element
private Svg rootElement = null;
// Metadata
private String title = "";
private String desc = "";
// Resolver
private SVGExternalFileResolver fileResolver = null;
// DPI to use for rendering
private float renderDPI = 96f; // default is 96
// CSS rules
private Ruleset cssRules = new Ruleset();
// Map from id attribute to element
private Map<String, SvgElementBase> idToElementMap = new HashMap<>();
enum Unit
{
px,
em,
ex,
in,
cm,
mm,
pt,
pc,
percent
}
@SuppressWarnings("unused")
enum GradientSpread
{
pad,
reflect,
repeat
}
/* package private */
SVG()
{
}
/**
* Read and parse an SVG from the given {@code InputStream}.
*
* @param is the input stream from which to read the file.
* @return an SVG instance on which you can call one of the render methods.
* @throws SVGParseException if there is an error parsing the document.
*/
@SuppressWarnings("WeakerAccess")
public static SVG getFromInputStream(InputStream is) throws SVGParseException
{
SVGParser parser = new SVGParser();
return parser.parse(is, enableInternalEntities);
}
/**
* Read and parse an SVG from the given {@code String}.
*
* @param svg the String instance containing the SVG document.
* @return an SVG instance on which you can call one of the render methods.
* @throws SVGParseException if there is an error parsing the document.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public static SVG getFromString(String svg) throws SVGParseException
{
SVGParser parser = new SVGParser();
return parser.parse(new ByteArrayInputStream(svg.getBytes()), enableInternalEntities);
}
/**
* Read and parse an SVG from the given resource location.
*
* @param context the Android context of the resource.
* @param resourceId the resource identifier of the SVG document.
* @return an SVG instance on which you can call one of the render methods.
* @throws SVGParseException if there is an error parsing the document.
*/
@SuppressWarnings("WeakerAccess")
public static SVG getFromResource(Context context, int resourceId) throws SVGParseException
{
return getFromResource(context.getResources(), resourceId);
}
/**
* Read and parse an SVG from the given resource location.
*
* @param resources the set of Resources in which to locate the file.
* @param resourceId the resource identifier of the SVG document.
* @return an SVG instance on which you can call one of the render methods.
* @throws SVGParseException if there is an error parsing the document.
*/
@SuppressWarnings("WeakerAccess")
public static SVG getFromResource(Resources resources, int resourceId) throws SVGParseException
{
SVGParser parser = new SVGParser();
InputStream is = resources.openRawResource(resourceId);
try {
return parser.parse(is, enableInternalEntities);
} finally {
try {
is.close();
} catch (IOException e) {
// Do nothing
}
}
}
/**
* Read and parse an SVG from the assets folder.
*
* @param assetManager the AssetManager instance to use when reading the file.
* @param filename the filename of the SVG document within assets.
* @return an SVG instance on which you can call one of the render methods.
* @throws SVGParseException if there is an error parsing the document.
* @throws IOException if there is some IO error while reading the file.
*/
@SuppressWarnings("WeakerAccess")
public static SVG getFromAsset(AssetManager assetManager, String filename) throws SVGParseException, IOException
{
SVGParser parser = new SVGParser();
InputStream is = assetManager.open(filename);
try {
return parser.parse(is, enableInternalEntities);
} finally {
try {
is.close();
} catch (IOException e) {
// Do nothing
}
}
}
//===============================================================================
/**
* Tells the parser whether to allow the expansion of internal entities.
* An example of document containing an internal entities is:
*
* {@code
* <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [
* <!ENTITY hello "Hello World!">
* ]>
* <svg>
* <text>&hello;</text>
* </svg>
* }
*
* Entities are useful in some circumstances, but SVG files that use them are quite rare. Note
* also that enabling entity expansion makes you vulnerable to the
* <a href="https://en.wikipedia.org/wiki/Billion_laughs_attack">Billion Laughs Attack</a>
*
* Entity expansion is enabled by default.
*
* @param enable Set true if you want to enable entity expansion by the parser.
* @since 1.3
*/
public static void setInternalEntitiesEnabled(boolean enable)
{
enableInternalEntities = enable;
}
/**
* @return true if internal entity expansion is enabled in the parser
* @since 1.3
*/
public static boolean isInternalEntitiesEnabled()
{
return enableInternalEntities;
}
/**
* Register an {@link SVGExternalFileResolver} instance that the renderer should use when resolving
* external references such as images and fonts.
*
* @param fileResolver the resolver to use.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void registerExternalFileResolver(SVGExternalFileResolver fileResolver)
{
this.fileResolver = fileResolver;
}
/**
* Set the DPI (dots-per-inch) value to use when rendering. The DPI setting is used in the
* conversion of "physical" units - such an "pt" or "cm" - to pixel values. The default DPI is 96.
* <p>
* You should not normally need to alter the DPI from the default of 96 as recommended by the SVG
* and CSS specifications.
*
* @param dpi the DPI value that the renderer should use.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void setRenderDPI(float dpi)
{
this.renderDPI = dpi;
}
/**
* Get the current render DPI setting.
* @return the DPI value
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public float getRenderDPI()
{
return renderDPI;
}
//===============================================================================
// SVG document rendering to a Picture object (indirect rendering)
/**
* Renders this SVG document to a Picture object.
* <p>
* An attempt will be made to determine a suitable initial viewport from the contents of the SVG file.
* If an appropriate viewport can't be determined, a default viewport of 512x512 will be used.
*
* @return a Picture object suitable for later rendering using {@code Canvas.drawPicture()}
*/
@SuppressWarnings("WeakerAccess")
public Picture renderToPicture()
{
// Determine the initial viewport. See SVG spec section 7.2.
Length width = rootElement.width;
if (width != null)
{
float w = width.floatValue(this.renderDPI);
float h;
Box rootViewBox = rootElement.viewBox;
if (rootViewBox != null) {
h = w * rootViewBox.height / rootViewBox.width;
} else {
Length height = rootElement.height;
if (height != null) {
h = height.floatValue(this.renderDPI);
} else {
h = w;
}
}
return renderToPicture( (int) Math.ceil(w), (int) Math.ceil(h) );
}
else
{
return renderToPicture(DEFAULT_PICTURE_WIDTH, DEFAULT_PICTURE_HEIGHT);
}
}
/**
* Renders this SVG document to a Picture object.
*
* @param widthInPixels the width of the initial viewport
* @param heightInPixels the height of the initial viewport
* @return a Picture object suitable for later rendering using {@code Canvas.darwPicture()}
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public Picture renderToPicture(int widthInPixels, int heightInPixels)
{
Picture picture = new Picture();
Canvas canvas = picture.beginRecording(widthInPixels, heightInPixels);
Box viewPort = new Box(0f, 0f, (float) widthInPixels, (float) heightInPixels);
SVGAndroidRenderer renderer = new SVGAndroidRenderer(canvas, this.renderDPI);
renderer.renderDocument(this, viewPort, null, null);
picture.endRecording();
return picture;
}
/**
* Renders this SVG document to a Picture object using the specified view defined in the document.
* <p>
* A View is an special element in a SVG document that describes a rectangular area in the document.
* Calling this method with a {@code viewId} will result in the specified view being positioned and scaled
* to the viewport. In other words, use {@link #renderToPicture()} to render the whole document, or use this
* method instead to render just a part of it.
*
* @param viewId the id of a view element in the document that defines which section of the document is to be visible.
* @param widthInPixels the width of the initial viewport
* @param heightInPixels the height of the initial viewport
* @return a Picture object suitable for later rendering using {@code Canvas.drawPicture()}, or null if the viewId was not found.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public Picture renderViewToPicture(String viewId, int widthInPixels, int heightInPixels)
{
SvgObject obj = this.getElementById(viewId);
if (obj == null)
return null;
if (!(obj instanceof SVG.View))
return null;
SVG.View view = (SVG.View) obj;
if (view.viewBox == null) {
Log.w(TAG, "View element is missing a viewBox attribute.");
return null;
}
Picture picture = new Picture();
Canvas canvas = picture.beginRecording(widthInPixels, heightInPixels);
Box viewPort = new Box(0f, 0f, (float) widthInPixels, (float) heightInPixels);
SVGAndroidRenderer renderer = new SVGAndroidRenderer(canvas, this.renderDPI);
renderer.renderDocument(this, viewPort, view.viewBox, view.preserveAspectRatio);
picture.endRecording();
return picture;
}
//===============================================================================
// SVG document rendering to a canvas object (direct rendering)
/**
* Renders this SVG document to a Canvas object. The full width and height of the canvas
* will be used as the viewport into which the document will be rendered.
*
* @param canvas the canvas to which the document should be rendered.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void renderToCanvas(Canvas canvas)
{
renderToCanvas(canvas, null);
}
/**
* Renders this SVG document to a Canvas object.
*
* @param canvas the canvas to which the document should be rendered.
* @param viewPort the bounds of the area on the canvas you want the SVG rendered, or null for the whole canvas.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void renderToCanvas(Canvas canvas, RectF viewPort)
{
Box canvasViewPort;
if (viewPort != null) {
canvasViewPort = Box.fromLimits(viewPort.left, viewPort.top, viewPort.right, viewPort.bottom);
} else {
canvasViewPort = new Box(0f, 0f, (float) canvas.getWidth(), (float) canvas.getHeight());
}
SVGAndroidRenderer renderer = new SVGAndroidRenderer(canvas, this.renderDPI);
renderer.renderDocument(this, canvasViewPort, null, null);
}
/**
* Renders this SVG document to a Canvas using the specified view defined in the document.
* <p>
* A View is an special element in a SVG documents that describes a rectangular area in the document.
* Calling this method with a {@code viewId} will result in the specified view being positioned and scaled
* to the viewport. In other words, use {@link #renderToPicture()} to render the whole document, or use this
* method instead to render just a part of it.
* <p>
* If the {@code <view>} could not be found, nothing will be drawn.
*
* @param viewId the id of a view element in the document that defines which section of the document is to be visible.
* @param canvas the canvas to which the document should be rendered.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void renderViewToCanvas(String viewId, Canvas canvas)
{
renderViewToCanvas(viewId, canvas, null);
}
/**
* Renders this SVG document to a Canvas using the specified view defined in the document.
* <p>
* A View is an special element in a SVG documents that describes a rectangular area in the document.
* Calling this method with a {@code viewId} will result in the specified view being positioned and scaled
* to the viewport. In other words, use {@link #renderToPicture()} to render the whole document, or use this
* method instead to render just a part of it.
* <p>
* If the {@code <view>} could not be found, nothing will be drawn.
*
* @param viewId the id of a view element in the document that defines which section of the document is to be visible.
* @param canvas the canvas to which the document should be rendered.
* @param viewPort the bounds of the area on the canvas you want the SVG rendered, or null for the whole canvas.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void renderViewToCanvas(String viewId, Canvas canvas, RectF viewPort)
{
SvgObject obj = this.getElementById(viewId);
if (obj == null)
return;
if (!(obj instanceof SVG.View))
return;
SVG.View view = (SVG.View) obj;
if (view.viewBox == null) {
Log.w(TAG, "View element is missing a viewBox attribute.");
return;
}
Box canvasViewPort;
if (viewPort != null) {
canvasViewPort = Box.fromLimits(viewPort.left, viewPort.top, viewPort.right, viewPort.bottom);
} else {
canvasViewPort = new Box(0f, 0f, (float) canvas.getWidth(), (float) canvas.getHeight());
}
SVGAndroidRenderer renderer = new SVGAndroidRenderer(canvas, this.renderDPI);
renderer.renderDocument(this, canvasViewPort, view.viewBox, view.preserveAspectRatio);
}
//===============================================================================
// Other document utility API functions
/**
* Returns the version number of this library.
*
* @return the version number in string format
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public static String getVersion()
{
return VERSION;
}
/**
* Returns the contents of the {@code <title>} element in the SVG document.
*
* @return title contents if available, otherwise an empty string.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public String getDocumentTitle()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
return title;
}
/**
* Returns the contents of the {@code <desc>} element in the SVG document.
*
* @return desc contents if available, otherwise an empty string.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public String getDocumentDescription()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
return desc;
}
/**
* Returns the SVG version number as provided in the root {@code <svg>} tag of the document.
*
* @return the version string if declared, otherwise an empty string.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public String getDocumentSVGVersion()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
return rootElement.version;
}
/**
* Returns a list of ids for all {@code <view>} elements in this SVG document.
* <p>
* The returned view ids could be used when calling and of the {@code renderViewToX()} methods.
*
* @return the list of id strings.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public Set<String> getViewList()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
List<SvgObject> viewElems = getElementsByTagName(View.class);
Set<String> viewIds = new HashSet<>(viewElems.size());
for (SvgObject elem: viewElems)
{
View view = (View) elem;
if (view.id != null)
viewIds.add(view.id);
else
Log.w("AndroidSVG", "getViewList(): found a <view> without an id attribute");
}
return viewIds;
}
/**
* Returns the width of the document as specified in the SVG file.
* <p>
* If the width in the document is specified in pixels, that value will be returned.
* If the value is listed with a physical unit such as "cm", then the current
* {@code RenderDPI} value will be used to convert that value to pixels. If the width
* is missing, or in a form which can't be converted to pixels, such as "100%" for
* example, -1 will be returned.
*
* @return the width in pixels, or -1 if there is no width available.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public float getDocumentWidth()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
return getDocumentDimensions(this.renderDPI).width;
}
/**
* Change the width of the document by altering the "width" attribute
* of the root {@code <svg>} element.
*
* @param pixels The new value of width in pixels.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void setDocumentWidth(float pixels)
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
this.rootElement.width = new Length(pixels);
}
/**
* Change the width of the document by altering the "width" attribute
* of the root {@code <svg>} element.
*
* @param value A valid SVG 'length' attribute, such as "100px" or "10cm".
* @throws SVGParseException if {@code value} cannot be parsed successfully.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void setDocumentWidth(String value) throws SVGParseException
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
this.rootElement.width = SVGParser.parseLength(value);
}
/**
* Returns the height of the document as specified in the SVG file.
* <p>
* If the height in the document is specified in pixels, that value will be returned.
* If the value is listed with a physical unit such as "cm", then the current
* {@code RenderDPI} value will be used to convert that value to pixels. If the height
* is missing, or in a form which can't be converted to pixels, such as "100%" for
* example, -1 will be returned.
*
* @return the height in pixels, or -1 if there is no height available.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public float getDocumentHeight()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
return getDocumentDimensions(this.renderDPI).height;
}
/**
* Change the height of the document by altering the "height" attribute
* of the root {@code <svg>} element.
*
* @param pixels The new value of height in pixels.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void setDocumentHeight(float pixels)
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
this.rootElement.height = new Length(pixels);
}
/**
* Change the height of the document by altering the "height" attribute
* of the root {@code <svg>} element.
*
* @param value A valid SVG 'length' attribute, such as "100px" or "10cm".
* @throws SVGParseException if {@code value} cannot be parsed successfully.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void setDocumentHeight(String value) throws SVGParseException
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
this.rootElement.height = SVGParser.parseLength(value);
}
/**
* Change the document view box by altering the "viewBox" attribute
* of the root {@code <svg>} element.
* <p>
* The viewBox generally describes the bounding box dimensions of the
* document contents. A valid viewBox is necessary if you want the
* document scaled to fit the canvas or viewport the document is to be
* rendered into.
* <p>
* By setting a viewBox that describes only a portion of the document,
* you can reproduce the effect of image sprites.
*
* @param minX the left coordinate of the viewBox in pixels
* @param minY the top coordinate of the viewBox in pixels.
* @param width the width of the viewBox in pixels
* @param height the height of the viewBox in pixels
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void setDocumentViewBox(float minX, float minY, float width, float height)
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
this.rootElement.viewBox = new Box(minX, minY, width, height);
}
/**
* Returns the viewBox attribute of the current SVG document.
*
* @return the document's viewBox attribute as a {@code android.graphics.RectF} object, or null if not set.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public RectF getDocumentViewBox()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
if (this.rootElement.viewBox == null)
return null;
return this.rootElement.viewBox.toRectF();
}
/**
* Change the document positioning by altering the "preserveAspectRatio"
* attribute of the root {@code <svg>} element. See the
* documentation for {@link PreserveAspectRatio} for more information
* on how positioning works.
*
* @param preserveAspectRatio the new {@code preserveAspectRatio} setting for the root {@code <svg>} element.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public void setDocumentPreserveAspectRatio(PreserveAspectRatio preserveAspectRatio)
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
this.rootElement.preserveAspectRatio = preserveAspectRatio;
}
/**
* Return the "preserveAspectRatio" attribute of the root {@code <svg>}
* element in the form of an {@link PreserveAspectRatio} object.
*
* @return the preserveAspectRatio setting of the document's root {@code <svg>} element.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public PreserveAspectRatio getDocumentPreserveAspectRatio()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
if (this.rootElement.preserveAspectRatio == null)
return null;
return this.rootElement.preserveAspectRatio;
}
/**
* Returns the aspect ratio of the document as a width/height fraction.
* <p>
* If the width or height of the document are listed with a physical unit such as "cm",
* then the current {@code renderDPI} setting will be used to convert that value to pixels.
* <p>
* If the width or height cannot be determined, -1 will be returned.
*
* @return the aspect ratio as a width/height fraction, or -1 if the ratio cannot be determined.
* @throws IllegalArgumentException if there is no current SVG document loaded.
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public float getDocumentAspectRatio()
{
if (this.rootElement == null)
throw new IllegalArgumentException("SVG document is empty");
Length w = this.rootElement.width;
Length h = this.rootElement.height;
// If width and height are both specified and are not percentages, aspect ratio is calculated from these (SVG1.1 sect 7.12)
if (w != null && h != null && w.unit!=Unit.percent && h.unit!=Unit.percent)
{
if (w.isZero() || h.isZero())
return -1f;
return w.floatValue(this.renderDPI) / h.floatValue(this.renderDPI);
}
// Otherwise, get the ratio from the viewBox
if (this.rootElement.viewBox != null && this.rootElement.viewBox.width != 0f && this.rootElement.viewBox.height != 0f) {
return this.rootElement.viewBox.width / this.rootElement.viewBox.height;
}
// Could not determine aspect ratio
return -1f;
}
//===============================================================================
SVG.Svg getRootElement()
{
return rootElement;
}
void setRootElement(SVG.Svg rootElement)
{
this.rootElement = rootElement;
}
SvgObject resolveIRI(String iri)
{
if (iri == null)
return null;
iri = cssQuotedString(iri);
if (iri.length() > 1 && iri.startsWith("#"))
{
return getElementById(iri.substring(1));
}
return null;
}
private String cssQuotedString(String str)
{
if (str.startsWith("\"") && str.endsWith("\""))
{
// Remove quotes and replace escaped double-quote
str = str.substring(1, str.length()-1).replace("\\\"", "\"");
}
else if (str.startsWith("'") && str.endsWith("'"))
{
// Remove quotes and replace escaped single-quote
str = str.substring(1, str.length()-1).replace("\\'", "'");
}
// Remove escaped newline. Replace escape seq representing newline
return str.replace("\\\n", "").replace("\\A", "\n");
}
private Box getDocumentDimensions(float dpi)
{
Length w = this.rootElement.width;
Length h = this.rootElement.height;
if (w == null || w.isZero() || w.unit==Unit.percent || w.unit==Unit.em || w.unit==Unit.ex)
return new Box(-1,-1,-1,-1);
float wOut = w.floatValue(dpi);
float hOut;
if (h != null) {
if (h.isZero() || h.unit==Unit.percent || h.unit==Unit.em || h.unit==Unit.ex) {
return new Box(-1,-1,-1,-1);
}
hOut = h.floatValue(dpi);
} else {
// height is not specified. SVG spec says this is okay. If there is a viewBox, we use
// that to calculate the height. Otherwise we set height equal to width.
if (this.rootElement.viewBox != null) {
hOut = (wOut * this.rootElement.viewBox.height) / this.rootElement.viewBox.width;
} else {
hOut = wOut;
}
}
return new Box(0,0, wOut,hOut);
}
//===============================================================================
// CSS support methods
void addCSSRules(Ruleset ruleset)
{
this.cssRules.addAll(ruleset);
}
List<CSSParser.Rule> getCSSRules()
{
return this.cssRules.getRules();
}
boolean hasCSSRules()
{
return !this.cssRules.isEmpty();
}
//===============================================================================
// Object sub-types used in the SVG object tree
static class Box implements Cloneable
{
float minX, minY, width, height;
Box(float minX, float minY, float width, float height)
{
this.minX = minX;
this.minY = minY;
this.width = width;
this.height = height;
}
Box(Box copy)
{
this.minX = copy.minX;
this.minY = copy.minY;
this.width = copy.width;
this.height = copy.height;
}
static Box fromLimits(float minX, float minY, float maxX, float maxY)
{
return new Box(minX, minY, maxX-minX, maxY-minY);
}
RectF toRectF()
{
return new RectF(minX, minY, maxX(), maxY());
}
float maxX() { return minX + width; }
float maxY() { return minY + height; }
void union(Box other)
{
if (other.minX < minX) minX = other.minX;
if (other.minY < minY) minY = other.minY;
if (other.maxX() > maxX()) width = other.maxX() - minX;
if (other.maxY() > maxY()) height = other.maxY() - minY;
}
public String toString() { return "["+minX+" "+minY+" "+width+" "+height+"]"; }
}
static final long SPECIFIED_FILL = 1;
static final long SPECIFIED_FILL_RULE = (1<<1);
static final long SPECIFIED_FILL_OPACITY = (1<<2);
static final long SPECIFIED_STROKE = (1<<3);
static final long SPECIFIED_STROKE_OPACITY = (1<<4);
static final long SPECIFIED_STROKE_WIDTH = (1<<5);
static final long SPECIFIED_STROKE_LINECAP = (1<<6);
static final long SPECIFIED_STROKE_LINEJOIN = (1<<7);
static final long SPECIFIED_STROKE_MITERLIMIT = (1<<8);
static final long SPECIFIED_STROKE_DASHARRAY = (1<<9);
static final long SPECIFIED_STROKE_DASHOFFSET = (1<<10);
static final long SPECIFIED_OPACITY = (1<<11);
static final long SPECIFIED_COLOR = (1<<12);
static final long SPECIFIED_FONT_FAMILY = (1<<13);
static final long SPECIFIED_FONT_SIZE = (1<<14);
static final long SPECIFIED_FONT_WEIGHT = (1<<15);
static final long SPECIFIED_FONT_STYLE = (1<<16);
static final long SPECIFIED_TEXT_DECORATION = (1<<17);
static final long SPECIFIED_TEXT_ANCHOR = (1<<18);
static final long SPECIFIED_OVERFLOW = (1<<19);
static final long SPECIFIED_CLIP = (1<<20);
static final long SPECIFIED_MARKER_START = (1<<21);
static final long SPECIFIED_MARKER_MID = (1<<22);
static final long SPECIFIED_MARKER_END = (1<<23);
static final long SPECIFIED_DISPLAY = (1<<24);
static final long SPECIFIED_VISIBILITY = (1<<25);
static final long SPECIFIED_STOP_COLOR = (1<<26);
static final long SPECIFIED_STOP_OPACITY = (1<<27);
static final long SPECIFIED_CLIP_PATH = (1<<28);
static final long SPECIFIED_CLIP_RULE = (1<<29);
static final long SPECIFIED_MASK = (1<<30);
static final long SPECIFIED_SOLID_COLOR = (1L<<31);
static final long SPECIFIED_SOLID_OPACITY = (1L<<32);
static final long SPECIFIED_VIEWPORT_FILL = (1L<<33);
static final long SPECIFIED_VIEWPORT_FILL_OPACITY = (1L<<34);
static final long SPECIFIED_VECTOR_EFFECT = (1L<<35);
static final long SPECIFIED_DIRECTION = (1L<<36);
static final long SPECIFIED_IMAGE_RENDERING = (1L<<37);
private static final long SPECIFIED_ALL = 0xffffffff;
/*
protected static final long SPECIFIED_NON_INHERITING = SPECIFIED_DISPLAY | SPECIFIED_OVERFLOW | SPECIFIED_CLIP
| SPECIFIED_CLIP_PATH | SPECIFIED_OPACITY | SPECIFIED_STOP_COLOR
| SPECIFIED_STOP_OPACITY | SPECIFIED_MASK | SPECIFIED_SOLID_COLOR
| SPECIFIED_SOLID_OPACITY | SPECIFIED_VIEWPORT_FILL
| SPECIFIED_VIEWPORT_FILL_OPACITY | SPECIFIED_VECTOR_EFFECT;
*/
protected static class Style implements Cloneable
{
// Which properties have been explicitly specified by this element
long specifiedFlags = 0;
SvgPaint fill;
FillRule fillRule;
Float fillOpacity;
SvgPaint stroke;
Float strokeOpacity;
Length strokeWidth;
LineCaps strokeLineCap;
LineJoin strokeLineJoin;
Float strokeMiterLimit;
Length[] strokeDashArray;
Length strokeDashOffset;
Float opacity; // master opacity of both stroke and fill
Colour color;
List<String> fontFamily;
Length fontSize;
Integer fontWeight;
FontStyle fontStyle;
TextDecoration textDecoration;
TextDirection direction;
TextAnchor textAnchor;
Boolean overflow; // true if overflow visible
CSSClipRect clip;
String markerStart;
String markerMid;
String markerEnd;
Boolean display; // true if we should display
Boolean visibility; // true if visible
SvgPaint stopColor;
Float stopOpacity;
String clipPath;
FillRule clipRule;
String mask;
SvgPaint solidColor;
Float solidOpacity;
SvgPaint viewportFill;
Float viewportFillOpacity;
VectorEffect vectorEffect;
RenderQuality imageRendering;
static final int FONT_WEIGHT_NORMAL = 400;
static final int FONT_WEIGHT_BOLD = 700;
static final int FONT_WEIGHT_LIGHTER = -1;
static final int FONT_WEIGHT_BOLDER = +1;
public enum FillRule
{
NonZero,
EvenOdd
}
public enum LineCaps
{
Butt,
Round,
Square
}
public enum LineJoin
{
Miter,
Round,
Bevel
}
public enum FontStyle
{
Normal,
Italic,
Oblique
}
public enum TextAnchor
{
Start,
Middle,
End
}
public enum TextDecoration
{
None,
Underline,
Overline,
LineThrough,
Blink
}
public enum TextDirection
{
LTR,
RTL
}
public enum VectorEffect
{
None,
NonScalingStroke
}
public enum RenderQuality
{
auto,
optimizeQuality,
optimizeSpeed
}
static Style getDefaultStyle()
{
Style def = new Style();
def.specifiedFlags = SPECIFIED_ALL;
//def.inheritFlags = 0;
def.fill = Colour.BLACK;
def.fillRule = FillRule.NonZero;
def.fillOpacity = 1f;
def.stroke = null; // none
def.strokeOpacity = 1f;
def.strokeWidth = new Length(1f);
def.strokeLineCap = LineCaps.Butt;
def.strokeLineJoin = LineJoin.Miter;
def.strokeMiterLimit = 4f;
def.strokeDashArray = null;
def.strokeDashOffset = new Length(0f);
def.opacity = 1f;
def.color = Colour.BLACK; // currentColor defaults to black
def.fontFamily = null;
def.fontSize = new Length(12, Unit.pt);
def.fontWeight = FONT_WEIGHT_NORMAL;
def.fontStyle = FontStyle.Normal;
def.textDecoration = TextDecoration.None;
def.direction = TextDirection.LTR;
def.textAnchor = TextAnchor.Start;
def.overflow = true; // Overflow shown/visible for root, but not for other elements (see section 14.3.3).
def.clip = null;
def.markerStart = null;
def.markerMid = null;
def.markerEnd = null;
def.display = Boolean.TRUE;
def.visibility = Boolean.TRUE;
def.stopColor = Colour.BLACK;
def.stopOpacity = 1f;
def.clipPath = null;
def.clipRule = FillRule.NonZero;
def.mask = null;
def.solidColor = null;
def.solidOpacity = 1f;
def.viewportFill = null;
def.viewportFillOpacity = 1f;
def.vectorEffect = VectorEffect.None;
def.imageRendering = RenderQuality.auto;
return def;
}
// Called on the state.style object to reset the properties that don't inherit
// from the parent style.
void resetNonInheritingProperties(boolean isRootSVG)
{
this.display = Boolean.TRUE;
this.overflow = isRootSVG ? Boolean.TRUE : Boolean.FALSE;
this.clip = null;
this.clipPath = null;
this.opacity = 1f;
this.stopColor = Colour.BLACK;
this.stopOpacity = 1f;
this.mask = null;
this.solidColor = null;
this.solidOpacity = 1f;
this.viewportFill = null;
this.viewportFillOpacity = 1f;
this.vectorEffect = VectorEffect.None;
}
@Override
protected Object clone() throws CloneNotSupportedException
{
Style obj = (Style) super.clone();
if (strokeDashArray != null) {
obj.strokeDashArray = strokeDashArray.clone();
}
return obj;
}
}
// What fill or stroke is
abstract static class SvgPaint implements Cloneable
{
}
static class Colour extends SvgPaint
{
int colour;
static final Colour BLACK = new Colour(0xff000000); // Black singleton - a common default value.
Colour(int val)
{
this.colour = val;
}
public String toString()
{
return String.format("#%08x", colour);
}
}
// Special version of Colour that indicates use of 'currentColor' keyword
static class CurrentColor extends SvgPaint
{
private static CurrentColor instance = new CurrentColor();
private CurrentColor()
{
}
static CurrentColor getInstance()
{
return instance;
}
}
static class PaintReference extends SvgPaint
{
String href;
SvgPaint fallback;
PaintReference(String href, SvgPaint fallback)
{
this.href = href;
this.fallback = fallback;
}
public String toString()
{
return href + " " + fallback;
}
}
static class Length implements Cloneable
{
float value = 0;
Unit unit = Unit.px;
Length(float value, Unit unit)
{
this.value = value;
this.unit = unit;
}
Length(float value)
{
this.value = value;
this.unit = Unit.px;
}
float floatValue()
{
return value;
}
// Convert length to user units for a horizontally-related context.
float floatValueX(SVGAndroidRenderer renderer)
{
switch (unit)
{
case px:
return value;
case em:
return value * renderer.getCurrentFontSize();
case ex:
return value * renderer.getCurrentFontXHeight();
case in:
return value * renderer.getDPI();
case cm:
return value * renderer.getDPI() / 2.54f;
case mm:
return value * renderer.getDPI() / 25.4f;
case pt: // 1 point = 1/72 in
return value * renderer.getDPI() / 72f;
case pc: // 1 pica = 1/6 in
return value * renderer.getDPI() / 6f;
case percent:
Box viewPortUser = renderer.getCurrentViewPortInUserUnits();
if (viewPortUser == null)
return value; // Undefined in this situation - so just return value to avoid an NPE
return value * viewPortUser.width / 100f;
default:
return value;
}
}
// Convert length to user units for a vertically-related context.
float floatValueY(SVGAndroidRenderer renderer)
{
if (unit == Unit.percent) {
Box viewPortUser = renderer.getCurrentViewPortInUserUnits();
if (viewPortUser == null)
return value; // Undefined in this situation - so just return value to avoid an NPE
return value * viewPortUser.height / 100f;
}
return floatValueX(renderer);
}
// Convert length to user units for a context that is not orientation specific.
// For example, stroke width.
float floatValue(SVGAndroidRenderer renderer)
{
if (unit == Unit.percent)
{
Box viewPortUser = renderer.getCurrentViewPortInUserUnits();
if (viewPortUser == null)
return value; // Undefined in this situation - so just return value to avoid an NPE
float w = viewPortUser.width;
float h = viewPortUser.height;
if (w == h)
return value * w / 100f;
float n = (float) (Math.sqrt(w*w+h*h) / SQRT2); // see spec section 7.10
return value * n / 100f;
}
return floatValueX(renderer);
}
// Convert length to user units for a context that is not orientation specific.
// For percentage values, use the given 'max' parameter to represent the 100% value.
float floatValue(SVGAndroidRenderer renderer, float max)
{
if (unit == Unit.percent)
{
return value * max / 100f;
}
return floatValueX(renderer);
}
// For situations (like calculating the initial viewport) when we can only rely on
// physical real world units.
float floatValue(float dpi)
{
switch (unit)
{
case px:
return value;
case in:
return value * dpi;
case cm:
return value * dpi / 2.54f;
case mm:
return value * dpi / 25.4f;
case pt: // 1 point = 1/72 in
return value * dpi / 72f;
case pc: // 1 pica = 1/6 in
return value * dpi / 6f;
case em:
case ex:
case percent:
default:
return value;
}
}
boolean isZero()
{
return value == 0f;
}
boolean isNegative()
{
return value < 0f;
}
@Override
public String toString()
{
return String.valueOf(value) + unit;
}
}
static class CSSClipRect
{
Length top;
Length right;
Length bottom;
Length left;
CSSClipRect(Length top, Length right, Length bottom, Length left)
{
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
}
}
//===============================================================================
// The objects in the SVG object tree
//===============================================================================
// Any object that can be part of the tree
static class SvgObject
{
SVG document;
SvgContainer parent;
public String toString()
{
return this.getClass().getSimpleName();
//return super.toString();
}
}
// Any object in the tree that corresponds to an SVG element
static class SvgElementBase extends SvgObject
{
String id = null;
Boolean spacePreserve = null;
Style baseStyle = null; // style defined by explicit style attributes in the element (eg. fill="black")
Style style = null; // style expressed in a 'style' attribute (eg. style="fill:black")
List<String> classNames = null; // contents of the 'class' attribute
}
// Any object in the tree that corresponds to an SVG element
static class SvgElement extends SvgElementBase
{
Box boundingBox = null;
}
// Any element that can appear inside a <switch> element.
interface SvgConditional
{
void setRequiredFeatures(Set<String> features);
Set<String> getRequiredFeatures();
void setRequiredExtensions(String extensions);
String getRequiredExtensions();
void setSystemLanguage(Set<String> languages);
Set<String> getSystemLanguage();
void setRequiredFormats(Set<String> mimeTypes);
Set<String> getRequiredFormats();
void setRequiredFonts(Set<String> fontNames);
Set<String> getRequiredFonts();
}
// Any element that can appear inside a <switch> element.
static class SvgConditionalElement extends SvgElement implements SvgConditional
{
Set<String> requiredFeatures = null;
String requiredExtensions = null;
Set<String> systemLanguage = null;
Set<String> requiredFormats = null;
Set<String> requiredFonts = null;
@Override
public void setRequiredFeatures(Set<String> features) { this.requiredFeatures = features; }
@Override
public Set<String> getRequiredFeatures() { return this.requiredFeatures; }
@Override
public void setRequiredExtensions(String extensions) { this.requiredExtensions = extensions; }
@Override
public String getRequiredExtensions() { return this.requiredExtensions; }
@Override
public void setSystemLanguage(Set<String> languages) { this.systemLanguage = languages; }
@Override
public Set<String> getSystemLanguage() { return this.systemLanguage; }
@Override
public void setRequiredFormats(Set<String> mimeTypes) { this.requiredFormats = mimeTypes; }
@Override
public Set<String> getRequiredFormats() { return this.requiredFormats; }
@Override
public void setRequiredFonts(Set<String> fontNames) { this.requiredFonts = fontNames; }
@Override
public Set<String> getRequiredFonts() { return this.requiredFonts; }
}
interface SvgContainer
{
List<SvgObject> getChildren();
void addChild(SvgObject elem) throws SVGParseException;
}
static class SvgConditionalContainer extends SvgElement implements SvgContainer, SvgConditional
{
List<SvgObject> children = new ArrayList<>();
Set<String> requiredFeatures = null;
String requiredExtensions = null;
Set<String> systemLanguage = null;
Set<String> requiredFormats = null;
Set<String> requiredFonts = null;
@Override
public List<SvgObject> getChildren() { return children; }
@Override
public void addChild(SvgObject elem) throws SVGParseException { children.add(elem); }
@Override
public void setRequiredFeatures(Set<String> features) { this.requiredFeatures = features; }
@Override
public Set<String> getRequiredFeatures() { return this.requiredFeatures; }
@Override
public void setRequiredExtensions(String extensions) { this.requiredExtensions = extensions; }
@Override
public String getRequiredExtensions() { return this.requiredExtensions; }
@Override
public void setSystemLanguage(Set<String> languages) { this.systemLanguage = languages; }
@Override
public Set<String> getSystemLanguage() { return null; }
@Override
public void setRequiredFormats(Set<String> mimeTypes) { this.requiredFormats = mimeTypes; }
@Override
public Set<String> getRequiredFormats() { return this.requiredFormats; }
@Override
public void setRequiredFonts(Set<String> fontNames) { this.requiredFonts = fontNames; }
@Override
public Set<String> getRequiredFonts() { return this.requiredFonts; }
}
interface HasTransform
{
void setTransform(Matrix matrix);
}
static class SvgPreserveAspectRatioContainer extends SvgConditionalContainer
{
PreserveAspectRatio preserveAspectRatio = null;
}
static class SvgViewBoxContainer extends SvgPreserveAspectRatioContainer
{
Box viewBox;
}
static class Svg extends SvgViewBoxContainer
{
Length x;
Length y;
Length width;
Length height;
public String version;
}
// An SVG element that can contain other elements.
static class Group extends SvgConditionalContainer implements HasTransform
{
Matrix transform;
@Override
public void setTransform(Matrix transform) { this.transform = transform; }
}
interface NotDirectlyRendered
{
}
// A <defs> object contains objects that are not rendered directly, but are instead
// referenced from other parts of the file.
static class Defs extends Group implements NotDirectlyRendered
{
}
// One of the element types that can cause graphics to be drawn onto the target canvas.
// Specifically: 'circle', 'ellipse', 'image', 'line', 'path', 'polygon', 'polyline', 'rect', 'text' and 'use'.
static abstract class GraphicsElement extends SvgConditionalElement implements HasTransform
{
Matrix transform;
@Override
public void setTransform(Matrix transform) { this.transform = transform; }
}
protected static class Use extends Group
{
String href;
Length x;
Length y;
Length width;
Length height;
}
static class Path extends GraphicsElement
{
PathDefinition d;
Float pathLength;
}
static class Rect extends GraphicsElement
{
Length x;
Length y;
Length width;
Length height;
Length rx;
Length ry;
}
static class Circle extends GraphicsElement
{
Length cx;
Length cy;
Length r;
}
static class Ellipse extends GraphicsElement
{
Length cx;
Length cy;
Length rx;
Length ry;
}
static class Line extends GraphicsElement
{
Length x1;
Length y1;
Length x2;
Length y2;
}
static class PolyLine extends GraphicsElement
{
float[] points;
}
static class Polygon extends PolyLine
{
}
// A root text container such as <text> or <textPath>
interface TextRoot
{
}
interface TextChild
{
void setTextRoot(TextRoot obj);
TextRoot getTextRoot();
}
static class TextContainer extends SvgConditionalContainer
{
@Override
public void addChild(SvgObject elem) throws SVGParseException
{
if (elem instanceof TextChild)
children.add(elem);
else
throw new SVGParseException("Text content elements cannot contain "+elem+" elements.");
}
}
static class TextPositionedContainer extends TextContainer
{
List<Length> x;
List<Length> y;
List<Length> dx;
List<Length> dy;
}
protected static class Text extends TextPositionedContainer implements TextRoot, HasTransform
{
Matrix transform;
@Override
public void setTransform(Matrix transform) { this.transform = transform; }
}
static class TSpan extends TextPositionedContainer implements TextChild
{
private TextRoot textRoot;
@Override
public void setTextRoot(TextRoot obj) { this.textRoot = obj; }
@Override
public TextRoot getTextRoot() { return this.textRoot; }
}
static class TextSequence extends SvgObject implements TextChild
{
String text;
private TextRoot textRoot;
TextSequence(String text)
{
this.text = text;
}
public String toString()
{
return this.getClass().getSimpleName() + " '"+text+"'";
}
@Override
public void setTextRoot(TextRoot obj) { this.textRoot = obj; }
@Override
public TextRoot getTextRoot() { return this.textRoot; }
}
static class TRef extends TextContainer implements TextChild
{
String href;
private TextRoot textRoot;
@Override
public void setTextRoot(TextRoot obj) { this.textRoot = obj; }
@Override
public TextRoot getTextRoot() { return this.textRoot; }
}
static class TextPath extends TextContainer implements TextChild
{
String href;
Length startOffset;
private TextRoot textRoot;
@Override
public void setTextRoot(TextRoot obj) { this.textRoot = obj; }
@Override
public TextRoot getTextRoot() { return this.textRoot; }
}
// An SVG element that can contain other elements.
static class Switch extends Group
{
}
static class Symbol extends SvgViewBoxContainer implements NotDirectlyRendered
{
}
static class Marker extends SvgViewBoxContainer implements NotDirectlyRendered
{
boolean markerUnitsAreUser;
Length refX;
Length refY;
Length markerWidth;
Length markerHeight;
Float orient;
}
static class GradientElement extends SvgElementBase implements SvgContainer
{
List<SvgObject> children = new ArrayList<>();
Boolean gradientUnitsAreUser;
Matrix gradientTransform;
GradientSpread spreadMethod;
String href;
@Override
public List<SvgObject> getChildren()
{
return children;
}
@Override
public void addChild(SvgObject elem) throws SVGParseException
{
if (elem instanceof Stop)
children.add(elem);
else
throw new SVGParseException("Gradient elements cannot contain "+elem+" elements.");
}
}
static class Stop extends SvgElementBase implements SvgContainer
{
Float offset;
// Dummy container methods. Stop is officially a container, but we
// are not interested in any of its possible child elements.
@Override
public List<SvgObject> getChildren() { return Collections.emptyList(); }
@Override
public void addChild(SvgObject elem) throws SVGParseException { /* do nothing */ }
}
static class SvgLinearGradient extends GradientElement
{
Length x1;
Length y1;
Length x2;
Length y2;
}
static class SvgRadialGradient extends GradientElement
{
Length cx;
Length cy;
Length r;
Length fx;
Length fy;
}
static class ClipPath extends Group implements NotDirectlyRendered
{
Boolean clipPathUnitsAreUser;
}
static class Pattern extends SvgViewBoxContainer implements NotDirectlyRendered
{
Boolean patternUnitsAreUser;
Boolean patternContentUnitsAreUser;
Matrix patternTransform;
Length x;
Length y;
Length width;
Length height;
String href;
}
protected static class Image extends SvgPreserveAspectRatioContainer implements HasTransform
{
String href;
Length x;
Length y;
Length width;
Length height;
Matrix transform;
@Override
public void setTransform(Matrix transform) { this.transform = transform; }
}
protected static class View extends SvgViewBoxContainer implements NotDirectlyRendered
{
}
static class Mask extends SvgConditionalContainer implements NotDirectlyRendered
{
Boolean maskUnitsAreUser;
Boolean maskContentUnitsAreUser;
Length x;
Length y;
Length width;
Length height;
}
static class SolidColor extends SvgElementBase implements SvgContainer
{
// Not needed right now. Colour is set in this.baseStyle.
//public Length solidColor;
//public Length solidOpacity;
// Dummy container methods. Stop is officially a container, but we
// are not interested in any of its possible child elements.
@Override
public List<SvgObject> getChildren() { return Collections.emptyList(); }
@Override
public void addChild(SvgObject elem) throws SVGParseException { /* do nothing */ }
}
//===============================================================================
// Protected setters for internal use
void setTitle(String title)
{
this.title = title;
}
void setDesc(String desc)
{
this.desc = desc;
}
SVGExternalFileResolver getFileResolver()
{
return fileResolver;
}
//===============================================================================
// Path definition
interface PathInterface
{
void moveTo(float x, float y);
void lineTo(float x, float y);
void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3);
void quadTo(float x1, float y1, float x2, float y2);
void arcTo(float rx, float ry, float xAxisRotation, boolean largeArcFlag, boolean sweepFlag, float x, float y);
void close();
}
static class PathDefinition implements PathInterface
{
private byte[] commands = null;
private int commandsLength = 0;
private float[] coords = null;
private int coordsLength = 0;
private static final byte MOVETO = 0;
private static final byte LINETO = 1;
private static final byte CUBICTO = 2;
private static final byte QUADTO = 3;
private static final byte ARCTO = 4; // 4-7
private static final byte CLOSE = 8;
PathDefinition()
{
this.commands = new byte[8];
this.coords = new float[16];
}
boolean isEmpty()
{
return commandsLength == 0;
}
private void addCommand(byte value)
{
if (commandsLength == commands.length) {
byte[] newCommands = new byte[commands.length * 2];
System.arraycopy(commands, 0, newCommands, 0, commands.length);
commands = newCommands;
}
commands[commandsLength++] = value;
}
private void coordsEnsure(int num)
{
if (coords.length < (coordsLength + num)) {
float[] newCoords = new float[coords.length * 2];
System.arraycopy(coords, 0, newCoords, 0, coords.length);
coords = newCoords;
}
}
@Override
public void moveTo(float x, float y)
{
addCommand(MOVETO);
coordsEnsure(2);
coords[coordsLength++] = x;
coords[coordsLength++] = y;
}
@Override
public void lineTo(float x, float y)
{
addCommand(LINETO);
coordsEnsure(2);
coords[coordsLength++] = x;
coords[coordsLength++] = y;
}
@Override
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)
{
addCommand(CUBICTO);
coordsEnsure(6);
coords[coordsLength++] = x1;
coords[coordsLength++] = y1;
coords[coordsLength++] = x2;
coords[coordsLength++] = y2;
coords[coordsLength++] = x3;
coords[coordsLength++] = y3;
}
@Override
public void quadTo(float x1, float y1, float x2, float y2)
{
addCommand(QUADTO);
coordsEnsure(4);
coords[coordsLength++] = x1;
coords[coordsLength++] = y1;
coords[coordsLength++] = x2;
coords[coordsLength++] = y2;
}
@Override
public void arcTo(float rx, float ry, float xAxisRotation, boolean largeArcFlag, boolean sweepFlag, float x, float y)
{
int arc = ARCTO | (largeArcFlag?2:0) | (sweepFlag?1:0);
addCommand((byte) arc);
coordsEnsure(5);
coords[coordsLength++] = rx;
coords[coordsLength++] = ry;
coords[coordsLength++] = xAxisRotation;
coords[coordsLength++] = x;
coords[coordsLength++] = y;
}
@Override
public void close()
{
addCommand(CLOSE);
}
void enumeratePath(PathInterface handler)
{
int coordsPos = 0;
for (int commandPos = 0; commandPos < commandsLength; commandPos++)
{
byte command = commands[commandPos];
switch (command)
{
case MOVETO:
handler.moveTo(coords[coordsPos++], coords[coordsPos++]);
break;
case LINETO:
handler.lineTo(coords[coordsPos++], coords[coordsPos++]);
break;
case CUBICTO:
handler.cubicTo(coords[coordsPos++], coords[coordsPos++], coords[coordsPos++], coords[coordsPos++],coords[coordsPos++], coords[coordsPos++]);
break;
case QUADTO:
handler.quadTo(coords[coordsPos++], coords[coordsPos++], coords[coordsPos++], coords[coordsPos++]);
break;
case CLOSE:
handler.close();
break;
default:
boolean largeArcFlag = (command & 2) != 0;
boolean sweepFlag = (command & 1) != 0;
handler.arcTo(coords[coordsPos++], coords[coordsPos++], coords[coordsPos++], largeArcFlag, sweepFlag, coords[coordsPos++], coords[coordsPos++]);
}
}
}
}
private SvgObject getElementById(String id)
{
if (id == null || id.length() == 0)
return null;
if (id.equals(rootElement.id))
return rootElement;
if (idToElementMap.containsKey(id))
return idToElementMap.get(id);
// Search the object tree for a node with id property that matches 'id'
SvgElementBase result = getElementById(rootElement, id);
idToElementMap.put(id, result);
return result;
}
private SvgElementBase getElementById(SvgContainer obj, String id)
{
SvgElementBase elem = (SvgElementBase) obj;
if (id.equals(elem.id))
return elem;
for (SvgObject child: obj.getChildren())
{
if (!(child instanceof SvgElementBase))
continue;
SvgElementBase childElem = (SvgElementBase) child;
if (id.equals(childElem.id))
return childElem;
if (child instanceof SvgContainer)
{
SvgElementBase found = getElementById((SvgContainer) child, id);
if (found != null)
return found;
}
}
return null;
}
@SuppressWarnings("rawtypes")
private List<SvgObject> getElementsByTagName(Class clazz)
{
// Search the object tree for nodes with the give element class
return getElementsByTagName(rootElement, clazz);
}
@SuppressWarnings("rawtypes")
private List<SvgObject> getElementsByTagName(SvgContainer obj, Class clazz)
{
List<SvgObject> result = new ArrayList<>();
if (obj.getClass() == clazz)
result.add((SvgObject) obj);
for (SvgObject child: obj.getChildren())
{
if (child.getClass() == clazz)
result.add(child);
if (child instanceof SvgContainer)
getElementsByTagName((SvgContainer) child, clazz);
}
return result;
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
/*
Copyright 2013 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
import android.graphics.Bitmap;
import android.graphics.Typeface;
/**
* Resolver class used by the renderer when processing Text and Image elements.
* <p>
* The default behaviour is to tell AndroidSVG that the reference could not be found.
* <p>
* Extend this class and override the methods if you want to customise how AndroidSVG treats font and image references.
*/
public abstract class SVGExternalFileResolver
{
/**
* Called by renderer to resolve font references in &lt;text&gt; elements.
* <p>
* Return a {@code Typeface} instance, or null if you want the renderer to ignore
* this font and use the default Android font instead.
* <p>
* Note that AndroidSVG does not attempt to cache Typeface references. If you want
* them cached, for speed or memory reasons, you should do so yourself.
*
* @param fontFamily Font family as specified in a font-family style attribute.
* @param fontWeight Font weight as specified in a font-weight style attribute.
* @param fontStyle Font style as specified in a font-style style attribute.
* @return an Android Typeface instance, or null
*/
public Typeface resolveFont(String fontFamily, int fontWeight, String fontStyle)
{
return null;
}
/**
* Called by renderer to resolve image file references in &lt;image&gt; elements.
* <p>
* Return a {@code Bitmap} instance, or null if you want the renderer to ignore
* this image.
* <p>
* Note that AndroidSVG does not attempt to cache Bitmap references. If you want
* them cached, for speed or memory reasons, you should do so yourself.
*
* @param filename the filename as provided in the xlink:href attribute of a &lt;image&gt; element.
* @return an Android Bitmap object, or null if the image could not be found.
*/
public Bitmap resolveImage(String filename)
{
return null;
}
/**
* Called by renderer to determine whether a particular format is supported. In particular,
* this method is used in &lt;switch&gt; elements when processing {@code requiredFormats}
* conditionals.
*
* @param mimeType A MIME type (such as "image/jpeg").
* @return true if your {@code resolveImage()} implementation supports this file format.
*/
public boolean isFormatSupported(String mimeType)
{
return false;
}
}
/*
Copyright 2013 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.drawable.PictureDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
/**
* SVGImageView is a View widget that allows users to include SVG images in their layouts.
*
* It is implemented as a thin layer over {@code android.widget.ImageView}.
* <p>
* In its present form it has one significant limitation. It uses the {@link SVG#renderToPicture()}
* method. That means that SVG documents that use {@code <mask>} elements will not display correctly.
*
* @attr ref R.styleable#SVGImageView_svg
*/
@SuppressWarnings("JavaDoc")
public class SVGImageView extends ImageView
{
private static Method setLayerTypeMethod = null;
static {
try
{
setLayerTypeMethod = View.class.getMethod("setLayerType", Integer.TYPE, Paint.class);
}
catch (NoSuchMethodException e) { /* do nothing */ }
}
public SVGImageView(Context context)
{
super(context);
}
public SVGImageView(Context context, AttributeSet attrs)
{
super(context, attrs, 0);
init(attrs, 0);
}
public SVGImageView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
init(attrs, defStyle);
}
private void init(AttributeSet attrs, int defStyle)
{
if (isInEditMode())
return;
TypedArray a = getContext().getTheme()
.obtainStyledAttributes(attrs, R.styleable.SVGImageView, defStyle, 0);
try
{
int resourceId = a.getResourceId(R.styleable.SVGImageView_svg, -1);
if (resourceId != -1) {
setImageResource(resourceId);
return;
}
String url = a.getString(R.styleable.SVGImageView_svg);
if (url != null)
{
Uri uri = Uri.parse(url);
if (internalSetImageURI(uri, false))
return;
// Last chance, try loading it as an asset filename
setImageAsset(url);
}
} finally {
a.recycle();
}
}
/**
* Directly set the SVG.
*/
public void setSVG(SVG mysvg)
{
if (mysvg == null)
throw new IllegalArgumentException("Null value passed to setSVG()");
setSoftwareLayerType();
setImageDrawable(new PictureDrawable(mysvg.renderToPicture()));
}
/**
* Load an SVG image from the given resource id.
*/
@Override
public void setImageResource(int resourceId)
{
new LoadResourceTask(getContext(), resourceId).execute();
}
/**
* Load an SVG image from the given resource URI.
*/
@Override
public void setImageURI(Uri uri)
{
internalSetImageURI(uri, true);
}
/**
* Load an SVG image from the given asset filename.
*/
public void setImageAsset(String filename)
{
new LoadAssetTask(getContext(), filename).execute();
}
/*
* Attempt to set a picture from a Uri. Return true if it worked.
*/
private boolean internalSetImageURI(Uri uri, boolean isDirectRequestFromUser)
{
InputStream is;
try
{
is = getContext().getContentResolver().openInputStream(uri);
}
catch (FileNotFoundException e)
{
if (isDirectRequestFromUser)
Log.e("SVGImageView", "File not found: " + uri);
return false;
}
new LoadURITask().execute(is);
return true;
}
//===============================================================================================
private class LoadResourceTask extends AsyncTask<Integer, Integer, Picture>
{
private Context context;
private int resourceId;
LoadResourceTask(Context context, int resourceId)
{
this.context = context;
this.resourceId = resourceId;
}
protected Picture doInBackground(Integer... params)
{
try
{
SVG svg = SVG.getFromResource(context, resourceId);
return svg.renderToPicture();
}
catch (SVGParseException e)
{
Log.e("SVGImageView", String.format("Error loading resource 0x%x: %s", resourceId, e.getMessage()));
}
return null;
}
protected void onPostExecute(Picture picture)
{
if (picture != null) {
setSoftwareLayerType();
setImageDrawable(new PictureDrawable(picture));
}
}
}
private class LoadAssetTask extends AsyncTask<String, Integer, Picture>
{
private Context context;
private String filename;
LoadAssetTask(Context context, String filename)
{
this.context = context;
this.filename = filename;
}
protected Picture doInBackground(String... params)
{
try
{
SVG svg = SVG.getFromAsset(context.getAssets(), filename);
return svg.renderToPicture();
}
catch (SVGParseException e)
{
Log.e("SVGImageView", "Error loading file " + filename + ": " + e.getMessage());
}
catch (FileNotFoundException e)
{
Log.e("SVGImageView", "File not found: " + filename);
}
catch (IOException e)
{
Log.e("SVGImageView", "Unable to load asset file: " + filename, e);
}
return null;
}
protected void onPostExecute(Picture picture)
{
if (picture != null) {
setSoftwareLayerType();
setImageDrawable(new PictureDrawable(picture));
}
}
}
private class LoadURITask extends AsyncTask<InputStream, Integer, Picture>
{
protected Picture doInBackground(InputStream... is)
{
try
{
SVG svg = SVG.getFromInputStream(is[0]);
return svg.renderToPicture();
}
catch (SVGParseException e)
{
Log.e("SVGImageView", "Parse error loading URI: " + e.getMessage());
}
finally
{
try
{
is[0].close();
}
catch (IOException e) { /* do nothing */ }
}
return null;
}
protected void onPostExecute(Picture picture)
{
if (picture != null) {
setSoftwareLayerType();
setImageDrawable(new PictureDrawable(picture));
}
}
}
//===============================================================================================
/*
* Use reflection to call an API 11 method from this library (which is configured with a minSdkVersion of 8)
*/
private void setSoftwareLayerType()
{
if (setLayerTypeMethod == null)
return;
try
{
int LAYER_TYPE_SOFTWARE = View.class.getField("LAYER_TYPE_SOFTWARE").getInt(new View(getContext()));
setLayerTypeMethod.invoke(this, LAYER_TYPE_SOFTWARE, null);
}
catch (Exception e)
{
Log.w("SVGImageView", "Unexpected failure calling setLayerType", e);
}
}
}
/*
Copyright 2013 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
import org.xml.sax.SAXException;
/**
* Thrown by the parser if a problem is found in the SVG file.
* Extends SAXException rather than Exception to avoid unnecessary casts in the SAX parser handling code.
*/
public class SVGParseException extends SAXException
{
SVGParseException(String msg)
{
super(msg);
}
SVGParseException(String msg, Exception cause)
{
super(msg, cause);
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
/*
Copyright 2013 Paul LeBeau, Cave Rock Software Ltd.
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.caverock.androidsvg;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Typeface;
import android.util.Log;
/**
* A sample implementation of {@link SVGExternalFileResolver} that retrieves files from
* an application's "assets" folder.
*/
public class SimpleAssetResolver extends SVGExternalFileResolver
{
private static final String TAG = SimpleAssetResolver.class.getSimpleName();
private AssetManager assetManager;
public SimpleAssetResolver(AssetManager assetManager)
{
super();
this.assetManager = assetManager;
}
private static final Set<String> supportedFormats = new HashSet<>(8);
// Static initialiser
static {
// PNG, JPEG and SVG are required by the SVG 1.2 spec
supportedFormats.add("image/svg+xml");
supportedFormats.add("image/jpeg");
supportedFormats.add("image/png");
// Other image formats supported by Android BitmapFactory
supportedFormats.add("image/pjpeg");
supportedFormats.add("image/gif");
supportedFormats.add("image/bmp");
supportedFormats.add("image/x-windows-bmp");
// .webp supported in 4.0+ (ICE_CREAM_SANDWICH)
if (android.os.Build.VERSION.SDK_INT >= 14) {
supportedFormats.add("image/webp");
}
}
/**
* Attempt to find the specified font in the "assets" folder and return a Typeface object.
* For the font name "Foo", first the file "Foo.ttf" will be tried and if that fails, "Foo.otf".
*/
@Override
public Typeface resolveFont(String fontFamily, int fontWeight, String fontStyle)
{
Log.i(TAG, "resolveFont("+fontFamily+","+fontWeight+","+fontStyle+")");
// Try font name with suffix ".ttf"
try
{
return Typeface.createFromAsset(assetManager, fontFamily + ".ttf");
}
catch (RuntimeException ignored) {}
// That failed, so try ".otf"
try
{
return Typeface.createFromAsset(assetManager, fontFamily + ".otf");
}
catch (RuntimeException e)
{
return null;
}
}
/**
* Attempt to find the specified image file in the "assets" folder and return a decoded Bitmap.
*/
@Override
public Bitmap resolveImage(String filename)
{
Log.i(TAG, "resolveImage("+filename+")");
try
{
InputStream istream = assetManager.open(filename);
return BitmapFactory.decodeStream(istream);
}
catch (IOException e1)
{
return null;
}
}
/**
* Returns true when passed the MIME types for SVG, JPEG, PNG or any of the
* other bitmap image formats supported by Android's BitmapFactory class.
*/
@Override
public boolean isFormatSupported(String mimeType)
{
return supportedFormats.contains(mimeType);
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SVGImageView">
<!-- Location of the SVG document. -->
<attr name="svg" format="reference|string"/>
</declare-styleable>
</resources>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment