Helpers.java
package io.brunoborges.jairosvg.util;
import java.awt.geom.AffineTransform;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.brunoborges.jairosvg.dom.Node;
import io.brunoborges.jairosvg.surface.Surface;
/**
* Surface helpers: size parsing, transforms, normalize, point, etc.
*/
public final class Helpers {
public static final Map<String, Double> UNITS = Map.of("mm", 1.0 / 25.4, "cm", 1.0 / 2.54, "in", 1.0, "pt",
1.0 / 72.0, "pc", 1.0 / 6.0);
public static final Pattern PAINT_URL = Pattern.compile("(url\\(.+\\))\\s*(.*)");
public static final String PATH_LETTERS = "achlmqstvzACHLMQSTVZ";
private static final Pattern RECT_PATTERN = Pattern.compile("rect\\(\\s*(.+?)\\s*\\)");
private static final Pattern NEGATIVE_SIGN = Pattern.compile("(?<!e)-");
private static final Pattern WHITESPACE_COMMA = Pattern.compile("[\\s,]+");
private static final Pattern DECIMAL_SPLIT = Pattern.compile("(\\.[0-9-]+)(?=\\.)");
private static final Pattern POINT_PATTERN = Pattern.compile("^(\\S+?)\\s+(\\S+?)(?:\\s+|$)");
private static final Pattern TRANSFORM_PATTERN = Pattern.compile("(\\w+)\\s*\\(\\s*(.*?)\\s*\\)");
private static final Pattern WHITESPACE = Pattern.compile("\\s+");
private static final String[] EMPTY_PAINT = {null, null};
private static final double[] DEFAULT_RATIO = {1, 1, 0, 0};
private static final int CALC_PREFIX_LENGTH = 5; // length of "calc("
public static class PointError extends RuntimeException {
public PointError() {
super();
}
public PointError(String msg) {
super(msg);
}
}
// ── Geometry ───────────────────────────────────────────────────────────
// ── Units & sizing ──────────────────────────────────────────────────────
/** Distance between two points. */
public static double distance(double x1, double y1, double x2, double y2) {
return Math.hypot(x2 - x1, y2 - y1);
}
/** Extract URI and color from a paint value. Returns [source, color]. */
public static String[] paint(String value) {
if (value == null || value.isBlank()) {
return EMPTY_PAINT;
}
value = value.strip();
if (value.startsWith("url")) {
Matcher m = PAINT_URL.matcher(value);
if (m.find()) {
String source = UrlHelper.parseUrl(m.group(1)).fragment();
String color = m.group(2).isEmpty() ? null : m.group(2);
return new String[]{source, color};
}
}
return new String[]{null, value.isEmpty() ? null : value};
}
/** Return (width, height, viewbox) of a node. */
public static double[] nodeFormat(Surface surface, Node node, boolean reference) {
String refSize = reference ? "xy" : null;
double width = size(surface, node.get("width", "100%"), refSize != null ? "x" : null);
double height = size(surface, node.get("height", "100%"), refSize != null ? "y" : null);
String viewboxStr = node.get("viewBox");
double[] viewbox = null;
if (viewboxStr != null && !viewboxStr.isEmpty()) {
viewboxStr = WHITESPACE_COMMA.matcher(viewboxStr).replaceAll(" ").strip();
String[] parts = viewboxStr.split(" ");
if (parts.length == 4) {
viewbox = new double[]{Double.parseDouble(parts[0]), Double.parseDouble(parts[1]),
Double.parseDouble(parts[2]), Double.parseDouble(parts[3])};
if (width == 0)
width = viewbox[2];
if (height == 0)
height = viewbox[3];
}
}
return new double[]{width, height, viewbox != null ? viewbox[0] : Double.NaN,
viewbox != null ? viewbox[1] : Double.NaN, viewbox != null ? viewbox[2] : Double.NaN,
viewbox != null ? viewbox[3] : Double.NaN};
}
public static double[] nodeFormat(Surface surface, Node node) {
return nodeFormat(surface, node, true);
}
/** Check if viewbox is present (not NaN). */
public static boolean hasViewbox(double[] nodeFormat) {
return !Double.isNaN(nodeFormat[2]);
}
/** Get viewbox part from nodeFormat result. Returns null if no viewbox. */
public static double[] getViewbox(double[] nodeFormat) {
if (!hasViewbox(nodeFormat))
return null;
return new double[]{nodeFormat[2], nodeFormat[3], nodeFormat[4], nodeFormat[5]};
}
// ── String normalization ─────────────────────────────────────────────
/** Normalize a string corresponding to an array of various values. */
public static String normalize(String string) {
if (string == null || string.isEmpty())
return "";
string = string.replace('E', 'e');
string = NEGATIVE_SIGN.matcher(string).replaceAll(" -");
string = WHITESPACE_COMMA.matcher(string).replaceAll(" ");
string = DECIMAL_SPLIT.matcher(string).replaceAll("$1 ");
return string.strip();
}
/** Return (x, y, trailing_text) from string. */
public static double[] point(Surface surface, String string) {
string = string.strip();
Matcher m = POINT_PATTERN.matcher(string);
if (m.find()) {
double x = size(surface, m.group(1), "x");
double y = size(surface, m.group(2), "y");
return new double[]{x, y};
}
throw new PointError("Cannot parse point from: " + string);
}
/** Return (x, y, remaining_string). */
public static ParsedPoint pointWithRemainder(Surface surface, String string) {
string = string.strip();
Matcher m = POINT_PATTERN.matcher(string);
if (m.find()) {
double x = size(surface, m.group(1), "x");
double y = size(surface, m.group(2), "y");
String remainder = string.substring(m.end()).strip();
return new ParsedPoint(x, y, remainder);
}
throw new PointError("Cannot parse point from: " + string);
}
/** Return angle between x axis and point knowing given center. */
public static double pointAngle(double cx, double cy, double px, double py) {
return Math.atan2(py - cy, px - cx);
}
// ── Transforms ─────────────────────────────────────────────────────────
/**
* Manage the ratio preservation. Returns [scaleX, scaleY, translateX,
* translateY].
*/
public static double[] preserveRatio(Surface surface, Node node, double width, double height) {
double viewboxWidth, viewboxHeight;
if ("marker".equals(node.tag)) {
if (width == 0)
width = size(surface, node.get("markerWidth", "3"), "x");
if (height == 0)
height = size(surface, node.get("markerHeight", "3"), "y");
double[] nf = nodeFormat(surface, node);
double[] vb = getViewbox(nf);
if (vb == null)
return DEFAULT_RATIO;
viewboxWidth = vb[2];
viewboxHeight = vb[3];
} else if ("svg".equals(node.tag) || "image".equals(node.tag) || "g".equals(node.tag)) {
double[] nf = nodeFormat(surface, node);
if (width == 0)
width = nf[0];
if (height == 0)
height = nf[1];
viewboxWidth = node.imageWidth;
viewboxHeight = node.imageHeight;
} else {
throw new IllegalArgumentException(
"Root node is " + node.tag + ". Should be one of marker, svg, image, or g.");
}
double translateX = 0, translateY = 0;
double scaleX = viewboxWidth > 0 ? width / viewboxWidth : 1;
double scaleY = viewboxHeight > 0 ? height / viewboxHeight : 1;
String par = node.get("preserveAspectRatio", "xMidYMid");
String[] parts = WHITESPACE.split(par);
String align = parts[0];
String xPosition, yPosition;
if ("none".equals(align)) {
xPosition = "min";
yPosition = "min";
} else {
String meetOrSlice = parts.length > 1 ? parts[1] : null;
double scaleValue;
if ("slice".equals(meetOrSlice)) {
scaleValue = Math.max(scaleX, scaleY);
} else {
scaleValue = Math.min(scaleX, scaleY);
}
scaleX = scaleY = scaleValue;
xPosition = align.substring(1, 4).toLowerCase();
yPosition = align.substring(5).toLowerCase();
}
if ("marker".equals(node.tag)) {
translateX = -size(surface, node.get("refX", "0"), "x");
translateY = -size(surface, node.get("refY", "0"), "y");
} else {
if ("mid".equals(xPosition)) {
translateX = (width / scaleX - viewboxWidth) / 2;
} else if ("max".equals(xPosition)) {
translateX = width / scaleX - viewboxWidth;
}
if ("mid".equals(yPosition)) {
translateY = (height / scaleY - viewboxHeight) / 2;
} else if ("max".equals(yPosition)) {
translateY = height / scaleY - viewboxHeight;
}
}
return new double[]{scaleX, scaleY, translateX, translateY};
}
public static double[] preserveRatio(Surface surface, Node node) {
return preserveRatio(surface, node, 0, 0);
}
/** Get clip (x, y, width, height) of marker box. */
public static double[] clipMarkerBox(Surface surface, Node node, double scaleX, double scaleY) {
double mw = size(surface, node.get("markerWidth", "3"), "x");
double mh = size(surface, node.get("markerHeight", "3"), "y");
double[] nf = nodeFormat(surface, node);
double[] vb = getViewbox(nf);
if (vb == null)
return new double[]{0, 0, mw, mh};
double vbW = vb[2], vbH = vb[3];
String align = WHITESPACE.split(node.get("preserveAspectRatio", "xMidYMid"))[0];
String xPos = "none".equals(align) ? "min" : align.substring(1, 4).toLowerCase();
String yPos = "none".equals(align) ? "min" : align.substring(5).toLowerCase();
double clipX = vb[0];
if ("mid".equals(xPos))
clipX += (vbW - mw / scaleX) / 2.0;
else if ("max".equals(xPos))
clipX += vbW - mw / scaleX;
double clipY = vb[1];
if ("mid".equals(yPos))
clipY += (vbH - mh / scaleY) / 2.0;
else if ("max".equals(yPos))
clipY += vbH - mh / scaleY;
return new double[]{clipX, clipY, mw / scaleX, mh / scaleY};
}
/** Return quadratic points for cubic curve approximation. */
public static double[] quadraticPoints(double x1, double y1, double x2, double y2, double x3, double y3) {
double xq1 = x2 * 2.0 / 3 + x1 / 3.0;
double yq1 = y2 * 2.0 / 3 + y1 / 3.0;
double xq2 = x2 * 2.0 / 3 + x3 / 3.0;
double yq2 = y2 * 2.0 / 3 + y3 / 3.0;
return new double[]{xq1, yq1, xq2, yq2, x3, y3};
}
/** Rotate a point by angle around origin. */
public static double[] rotate(double x, double y, double angle) {
return new double[]{x * Math.cos(angle) - y * Math.sin(angle), y * Math.cos(angle) + x * Math.sin(angle)};
}
/** Parse an SVG transform string into an AffineTransform. */
// Reusable AffineTransform for matrix() transform function — avoids per-call
// allocation
private static final ThreadLocal<AffineTransform> TEMP_AT = ThreadLocal.withInitial(AffineTransform::new);
public static AffineTransform parseTransform(Surface surface, String transformString) {
if (transformString == null || transformString.isEmpty())
return new AffineTransform();
String normalized = normalize(transformString);
Matcher tm = TRANSFORM_PATTERN.matcher(normalized);
AffineTransform matrix = new AffineTransform();
while (tm.find()) {
String type = tm.group(1);
String[] valStrs = WHITESPACE.split(tm.group(2).strip());
double[] values = new double[valStrs.length];
for (int i = 0; i < valStrs.length; i++) {
values[i] = size(surface, valStrs[i]);
}
switch (type) {
case "matrix" -> {
if (values.length >= 6) {
AffineTransform m = TEMP_AT.get();
m.setTransform(values[0], values[1], values[2], values[3], values[4], values[5]);
matrix.concatenate(m);
}
}
case "rotate" -> {
double angle = Math.toRadians(values[0]);
double cx = values.length > 1 ? values[1] : 0;
double cy = values.length > 2 ? values[2] : 0;
matrix.translate(cx, cy);
matrix.rotate(angle);
matrix.translate(-cx, -cy);
}
case "skewX" -> matrix.shear(Math.tan(Math.toRadians(values[0])), 0);
case "skewY" -> matrix.shear(0, Math.tan(Math.toRadians(values[0])));
case "translate" -> {
double tx = values[0];
double ty = values.length > 1 ? values[1] : 0;
matrix.translate(tx, ty);
}
case "scale" -> {
double sx = values[0];
double sy = values.length > 1 ? values[1] : sx;
matrix.scale(sx, sy);
}
}
}
return matrix;
}
/**
* Apply SVG transform string to the surface Graphics2D, with Node-level
* caching.
*/
public static void transform(Surface surface, Node node, String transformString, String transformOrigin) {
if (transformString == null || transformString.isEmpty())
return;
AffineTransform matrix;
if (transformOrigin == null && node.cachedTransformStr != null
&& node.cachedTransformStr.equals(transformString)) {
matrix = node.cachedTransform;
} else {
matrix = parseTransform(surface, transformString);
if (transformOrigin == null) {
node.cachedTransform = matrix;
node.cachedTransformStr = transformString;
}
}
if (transformOrigin != null && !transformOrigin.isEmpty()) {
String[] origin = WHITESPACE.split(transformOrigin.strip());
double originX = parseOriginComponent(surface, origin[0], true);
double originY = origin.length > 1
? parseOriginComponent(surface, origin[1], false)
: surface.contextHeight / 2;
AffineTransform withOrigin = new AffineTransform();
withOrigin.translate(originX, originY);
withOrigin.concatenate(matrix);
withOrigin.translate(-originX, -originY);
matrix = withOrigin;
}
surface.context.transform(matrix);
}
// ── Clip helpers ────────────────────────────────────────────────────
/** Parse clip rect values. */
public static String[] clipRect(String string) {
if (string == null || string.isEmpty())
return new String[0];
Matcher m = RECT_PATTERN.matcher(normalize(string));
if (m.find()) {
return WHITESPACE.split(m.group(1));
}
return new String[0];
}
// ── Units and size parsing ──────────────────────────────────────────
/**
* Check if a string contains only plain numeric characters (digits, dot, minus,
* plus, e/E).
*/
private static boolean isPlainNumber(String s) {
for (int i = 0, len = s.length(); i < len; i++) {
char c = s.charAt(i);
if (!((c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E')) {
return false;
}
}
return true;
}
/**
* Replace a string with units by a float value. Reference: 'x' = viewport
* width, 'y' = viewport height, 'xy' = diagonal
*/
public static double size(Surface surface, String string, String reference) {
if (string == null || string.isEmpty())
return 0;
if (isPlainNumber(string)) {
return Double.parseDouble(string);
}
// Handle CSS calc() expressions
String stripped = string.strip();
if (stripped.length() > CALC_PREFIX_LENGTH
&& stripped.substring(0, CALC_PREFIX_LENGTH).equalsIgnoreCase("calc(")) {
return evalCalc(surface, stripped, reference);
}
if (surface == null)
return 0;
// Quick unit suffix check — avoids normalize() for simple "100px", "50%", etc.
String numPart = null;
String unit = null;
if (string.endsWith("%")) {
numPart = string.substring(0, string.length() - 1);
unit = "%";
} else if (string.endsWith("px")) {
numPart = string.substring(0, string.length() - 2);
unit = "px";
} else if (string.endsWith("em")) {
numPart = string.substring(0, string.length() - 2);
unit = "em";
} else if (string.endsWith("ex")) {
numPart = string.substring(0, string.length() - 2);
unit = "ex";
} else if (string.endsWith("ch")) {
numPart = string.substring(0, string.length() - 2);
unit = "ch";
} else if (string.endsWith("pt")) {
numPart = string.substring(0, string.length() - 2);
unit = "pt";
} else {
for (var entry : UNITS.entrySet()) {
if (string.endsWith(entry.getKey())) {
numPart = string.substring(0, string.length() - entry.getKey().length());
unit = entry.getKey();
break;
}
}
}
if (numPart != null && isPlainNumber(numPart)) {
return sizeWithUnit(surface, numPart, unit, reference);
}
// Fallback: normalize for complex strings (e.g. containing whitespace or
// multiple values)
String normalized = normalize(string);
int spaceIdx = normalized.indexOf(' ');
string = spaceIdx > 0 ? normalized.substring(0, spaceIdx) : normalized;
if (string.endsWith("%")) {
numPart = string.substring(0, string.length() - 1);
unit = "%";
} else if (string.endsWith("em")) {
numPart = string.substring(0, string.length() - 2);
unit = "em";
} else if (string.endsWith("ex")) {
numPart = string.substring(0, string.length() - 2);
unit = "ex";
} else if (string.endsWith("ch")) {
numPart = string.substring(0, string.length() - 2);
unit = "ch";
} else if (string.endsWith("pt")) {
numPart = string.substring(0, string.length() - 2);
unit = "pt";
} else if (string.endsWith("px")) {
numPart = string.substring(0, string.length() - 2);
unit = "px";
} else {
for (var entry : UNITS.entrySet()) {
if (string.endsWith(entry.getKey())) {
numPart = string.substring(0, string.length() - entry.getKey().length());
unit = entry.getKey();
break;
}
}
}
if (numPart != null) {
return sizeWithUnit(surface, numPart, unit, reference);
}
return 0;
}
/** Compute a sized value given a numeric string and a unit. */
private static double sizeWithUnit(Surface surface, String numPart, String unit, String reference) {
double value = Double.parseDouble(numPart);
return switch (unit) {
case "%" -> {
double ref;
if ("x".equals(reference)) {
ref = surface.contextWidth;
} else if ("y".equals(reference)) {
ref = surface.contextHeight;
} else {
ref = Math.hypot(surface.contextWidth, surface.contextHeight) / Math.sqrt(2);
}
yield value * ref / 100;
}
case "em" -> surface.fontSize * value;
case "ex", "ch" -> surface.fontSize * value / 2;
case "px" -> value;
default -> {
Double factor = UNITS.get(unit);
yield factor != null ? value * surface.dpi * factor : 0;
}
};
}
/** Size with no reference. */
public static double size(Surface surface, String string) {
return size(surface, string, "xy");
}
/** Parse font shorthand property. */
public static Map<String, String> parseFont(String value) {
var result = new java.util.HashMap<>(Map.of("font-family", "", "font-size", "", "font-style", "normal",
"font-variant", "normal", "font-weight", "normal", "line-height", "normal"));
var fontStyles = List.of("italic", "oblique");
var fontVariants = List.of("small-caps");
var fontWeights = List.of("bold", "bolder", "lighter", "100", "200", "300", "400", "500", "600", "700", "800",
"900");
for (String element : WHITESPACE.split(value)) {
if ("normal".equals(element))
continue;
if (!result.get("font-family").isEmpty()) {
result.put("font-family", result.get("font-family") + " " + element);
} else if (fontStyles.contains(element)) {
result.put("font-style", element);
} else if (fontVariants.contains(element)) {
result.put("font-variant", element);
} else if (fontWeights.contains(element)) {
result.put("font-weight", element);
} else {
if (result.get("font-size").isEmpty()) {
int slashIdx = element.indexOf('/');
if (slashIdx >= 0) {
result.put("font-size", element.substring(0, slashIdx));
result.put("line-height", element.substring(slashIdx + 1));
} else {
result.put("font-size", element);
}
} else {
result.put("font-family", element);
}
}
}
return result;
}
private static double parseOriginComponent(Surface surface, String value, boolean isX) {
return switch (value) {
case "center" -> isX ? surface.contextWidth / 2 : surface.contextHeight / 2;
case "left" -> 0;
case "right" -> surface.contextWidth;
case "top" -> 0;
case "bottom" -> surface.contextHeight;
default -> size(surface, value, isX ? "x" : "y");
};
}
/**
* Evaluate a CSS calc() expression, e.g. {@code calc(100% - 20px)}. Supports +,
* -, *, / and nested parentheses.
*/
private static double evalCalc(Surface surface, String expr, String reference) {
// expr starts with "calc(" (case-insensitive); find matching closing paren
int innerStart = CALC_PREFIX_LENGTH; // skip "calc("
int depth = 1, i = innerStart;
while (i < expr.length() && depth > 0) {
char c = expr.charAt(i);
if (c == '(')
depth++;
else if (c == ')')
depth--;
i++;
}
String inner = expr.substring(innerStart, i - 1).strip();
int[] pos = {0};
return calcAddSub(surface, inner, reference, pos);
}
/** Parse addition/subtraction in a calc expression (lowest precedence). */
private static double calcAddSub(Surface surface, String expr, String reference, int[] pos) {
double left = calcMulDiv(surface, expr, reference, pos);
while (pos[0] < expr.length()) {
skipCalcWhitespace(expr, pos);
if (pos[0] >= expr.length())
break;
char op = expr.charAt(pos[0]);
if (op != '+' && op != '-')
break;
pos[0]++;
double right = calcMulDiv(surface, expr, reference, pos);
if (op == '+')
left += right;
else
left -= right;
}
return left;
}
/** Parse multiplication/division in a calc expression (higher precedence). */
private static double calcMulDiv(Surface surface, String expr, String reference, int[] pos) {
double left = calcFactor(surface, expr, reference, pos);
while (pos[0] < expr.length()) {
skipCalcWhitespace(expr, pos);
if (pos[0] >= expr.length())
break;
char op = expr.charAt(pos[0]);
if (op != '*' && op != '/')
break;
pos[0]++;
double right = calcFactor(surface, expr, reference, pos);
if (op == '*')
left *= right;
else
left /= right;
}
return left;
}
/**
* Parse a calc factor: a parenthesised sub-expression, a nested calc(), or a
* CSS length/percentage value.
*/
private static double calcFactor(Surface surface, String expr, String reference, int[] pos) {
skipCalcWhitespace(expr, pos);
if (pos[0] >= expr.length())
return 0;
char first = expr.charAt(pos[0]);
// Parenthesised sub-expression or nested calc(...)
if (first == '(') {
pos[0]++; // skip '('
double val = calcAddSub(surface, expr, reference, pos);
skipCalcWhitespace(expr, pos);
if (pos[0] < expr.length() && expr.charAt(pos[0]) == ')')
pos[0]++;
return val;
}
// Nested calc() keyword
if (pos[0] + CALC_PREFIX_LENGTH <= expr.length()
&& expr.substring(pos[0], pos[0] + CALC_PREFIX_LENGTH).equalsIgnoreCase("calc(")) {
int nestedStart = pos[0];
int depth = 0;
while (pos[0] < expr.length()) {
char c = expr.charAt(pos[0]);
if (c == '(')
depth++;
else if (c == ')') {
depth--;
if (depth == 0) {
pos[0]++;
break;
}
}
pos[0]++;
}
return evalCalc(surface, expr.substring(nestedStart, pos[0]), reference);
}
// CSS value token: optional sign, digits/dot, optional exponent, optional unit
int start = pos[0];
// Optional sign
if (pos[0] < expr.length() && (first == '-' || first == '+')) {
pos[0]++;
}
// Numeric part (digits and decimal point)
while (pos[0] < expr.length() && (Character.isDigit(expr.charAt(pos[0])) || expr.charAt(pos[0]) == '.')) {
pos[0]++;
}
// Optional exponent: e/E followed by optional sign then digits
if (pos[0] < expr.length() && (expr.charAt(pos[0]) == 'e' || expr.charAt(pos[0]) == 'E')) {
int savedPos = pos[0];
pos[0]++;
if (pos[0] < expr.length() && (expr.charAt(pos[0]) == '+' || expr.charAt(pos[0]) == '-')) {
pos[0]++;
}
if (pos[0] < expr.length() && Character.isDigit(expr.charAt(pos[0]))) {
while (pos[0] < expr.length() && Character.isDigit(expr.charAt(pos[0])))
pos[0]++;
} else {
pos[0] = savedPos; // not a valid exponent — 'e' starts the unit suffix
}
}
// Optional unit suffix (letters, e.g. px, em, rem, pt, cm, mm, in, pc)
while (pos[0] < expr.length() && Character.isLetter(expr.charAt(pos[0]))) {
pos[0]++;
}
// Percentage sign
if (pos[0] < expr.length() && expr.charAt(pos[0]) == '%') {
pos[0]++;
}
String token = expr.substring(start, pos[0]).strip();
if (token.isEmpty())
return 0;
return size(surface, token, reference);
}
/** Advance past whitespace characters in a calc expression string. */
private static void skipCalcWhitespace(String s, int[] pos) {
while (pos[0] < s.length() && Character.isWhitespace(s.charAt(pos[0]))) {
pos[0]++;
}
}
// ── Numeric parsing utilities ────────────────────────────────────────
/**
* Parse a double from a string, returning {@code def} on null or parse failure.
*/
/**
* Parse a double from a string, returning the given default on
* null/empty/error.
*/
public static double parseDoubleOr(String s, double defaultValue) {
if (s == null || s.isEmpty())
return defaultValue;
try {
return Double.parseDouble(s);
} catch (NumberFormatException e) {
return defaultValue;
}
}
/** Parse a double from a string, returning 0 on null or parse failure. */
public static double parseDouble(String s) {
return parseDoubleOr(s, 0);
}
/**
* Parse a float that may be expressed as a percentage (e.g. "50%"). Returns 0
* on null/empty/parse-error.
*/
public static float parsePercent(String s) {
if (s == null || s.isEmpty())
return 0;
try {
s = s.strip();
if (s.endsWith("%")) {
return Float.parseFloat(s.substring(0, s.length() - 1)) / 100f;
}
return Float.parseFloat(s);
} catch (NumberFormatException e) {
return 0;
}
}
}