init
This commit is contained in:
16
fluent-icons-generator/README.md
Normal file
16
fluent-icons-generator/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Generator
|
||||
|
||||
This module generates codes of fluent-icons from svg.
|
||||
|
||||
Thanks for the tutorial: https://github.com/DevSrSouza/svg-to-compose
|
||||
|
||||
The source code of the tool that converts SVG to XML is from [Android Studio](https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-master-dev/sdk-common/src/main/java/com/android/ide/common/vectordrawable)
|
||||
|
||||
The source code of the tool that converts XML to Kotlin code is from [Jetpack Compose](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material/icons/generator/)
|
||||
|
||||
1. Save Icons HTML
|
||||
1. Access [Fluent UI Catalog](https://react.fluentui.dev/iframe.html?viewMode=docs&id=concepts-developer-icons-icons-catalog--page)
|
||||
2. Save page as html to `icons-catalog.html`
|
||||
3. Run `ExtractSvgFromCatalogKt`
|
||||
2. Run `ConvertToXmlKt`
|
||||
3. Run `ConvertToCodeKt`
|
||||
22
fluent-icons-generator/build.gradle.kts
Normal file
22
fluent-icons-generator/build.gradle.kts
Normal file
@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm {
|
||||
withJava()
|
||||
}
|
||||
sourceSets {
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-java:2.2.1")
|
||||
implementation("org.jsoup:jsoup:1.15.3")
|
||||
implementation("com.google.guava:guava:31.1-jre")
|
||||
implementation("com.android.tools:common:27.2.0-alpha16")
|
||||
implementation("com.android.tools:sdk-common:27.2.0-alpha16")
|
||||
implementation("com.squareup:kotlinpoet:1.12.0")
|
||||
}
|
||||
}
|
||||
val jvmTest by getting
|
||||
}
|
||||
}
|
||||
0
fluent-icons-generator/src/jvmMain/expApis.txt
Normal file
0
fluent-icons-generator/src/jvmMain/expApis.txt
Normal file
0
fluent-icons-generator/src/jvmMain/genApis.txt
Normal file
0
fluent-icons-generator/src/jvmMain/genApis.txt
Normal file
@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static java.lang.Math.atan;
|
||||
import static java.lang.Math.cos;
|
||||
import static java.lang.Math.sin;
|
||||
import static java.lang.Math.sqrt;
|
||||
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
class EllipseSolver {
|
||||
// Final results:
|
||||
private float mMajorAxis;
|
||||
private float mMinorAxis;
|
||||
private float mRotationDegree;
|
||||
private boolean mDirectionChanged;
|
||||
|
||||
/**
|
||||
* Constructs the solver with all necessary parameters, and all the output values will
|
||||
* be ready after this constructor is called.
|
||||
* <p>
|
||||
* Note that all the x y values are in absolute coordinates, such that we can apply
|
||||
* the transform directly.
|
||||
*/
|
||||
public EllipseSolver(AffineTransform totalTransform, float currentX, float currentY,
|
||||
float rx, float ry, float xAxisRotation, float largeArcFlag, float sweepFlag,
|
||||
float destX, float destY) {
|
||||
if (rx == 0 || ry == 0) {
|
||||
// From https://www.w3.org/TR/SVG11/implnote.html#ArcOutOfRangeParameters:
|
||||
// "If rx = 0 or ry = 0 then this arc is treated as a straight line segment
|
||||
// (a "lineto") joining the endpoints."
|
||||
return;
|
||||
}
|
||||
boolean largeArc = largeArcFlag != 0;
|
||||
boolean sweep = sweepFlag != 0;
|
||||
|
||||
// Compute the cx and cy first.
|
||||
Point2D.Double originalCenter = computeOriginalCenter(currentX, currentY, rx, ry,
|
||||
xAxisRotation, largeArc, sweep, destX, destY);
|
||||
|
||||
// Compute 3 points from original ellipse.
|
||||
Point2D.Double majorAxisPoint = new Point2D.Double(rx, 0);
|
||||
Point2D.Double minorAxisPoint = new Point2D.Double(0, ry);
|
||||
|
||||
majorAxisPoint = rotatePoint2D(majorAxisPoint, xAxisRotation);
|
||||
minorAxisPoint = rotatePoint2D(minorAxisPoint, xAxisRotation);
|
||||
|
||||
majorAxisPoint.x += originalCenter.x;
|
||||
majorAxisPoint.y += originalCenter.y;
|
||||
|
||||
minorAxisPoint.x += originalCenter.x;
|
||||
minorAxisPoint.y += originalCenter.y;
|
||||
|
||||
double middleRadians = Math.PI / 4; // This number can be anything between 0 and PI/2.
|
||||
double middleR = rx * ry / Math.hypot(ry * cos(middleRadians), rx * sin(middleRadians));
|
||||
|
||||
Point2D.Double middlePoint =
|
||||
new Point2D.Double(middleR * cos(middleRadians),middleR * sin(middleRadians));
|
||||
middlePoint = rotatePoint2D(middlePoint, xAxisRotation);
|
||||
middlePoint.x += originalCenter.x;
|
||||
middlePoint.y += originalCenter.y;
|
||||
|
||||
// Transform 3 points and center point into destination.
|
||||
Point2D.Double mDstMiddlePoint =
|
||||
(Point2D.Double) totalTransform.transform(middlePoint, null);
|
||||
Point2D.Double mDstMajorAxisPoint =
|
||||
(Point2D.Double) totalTransform.transform(majorAxisPoint, null);
|
||||
Point2D.Double mDstMinorAxisPoint =
|
||||
(Point2D.Double) totalTransform.transform(minorAxisPoint, null);
|
||||
Point2D dstCenter = totalTransform.transform(originalCenter, null);
|
||||
double dstCenterX = dstCenter.getX();
|
||||
double dstCenterY = dstCenter.getY();
|
||||
|
||||
// Compute the relative 3 points:
|
||||
double relativeDstMiddleX = mDstMiddlePoint.x - dstCenterX;
|
||||
double relativeDstMiddleY = mDstMiddlePoint.y - dstCenterY;
|
||||
double relativeDstMajorAxisPointX = mDstMajorAxisPoint.x - dstCenterX;
|
||||
double relativeDstMajorAxisPointY = mDstMajorAxisPoint.y - dstCenterY;
|
||||
double relativeDstMinorAxisPointX = mDstMinorAxisPoint.x - dstCenterX;
|
||||
double relativeDstMinorAxisPointY = mDstMinorAxisPoint.y - dstCenterY;
|
||||
|
||||
// Check if the direction has changed.
|
||||
mDirectionChanged = computeDirectionChange(middlePoint, majorAxisPoint, minorAxisPoint,
|
||||
mDstMiddlePoint, mDstMajorAxisPoint, mDstMinorAxisPoint);
|
||||
|
||||
// From 3 dest points, recompute the a, b and theta.
|
||||
if (computeABThetaFromControlPoints(relativeDstMiddleX, relativeDstMiddleY,
|
||||
relativeDstMajorAxisPointX, relativeDstMajorAxisPointY,
|
||||
relativeDstMinorAxisPointX, relativeDstMinorAxisPointY)) {
|
||||
getLog().log(Level.WARNING, "Early return in the ellipse transformation computation!");
|
||||
}
|
||||
}
|
||||
|
||||
private static Logger getLog() {
|
||||
return Logger.getLogger(EllipseSolver.class.getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* After a random transformation, the controls points may change its direction, left handed <->
|
||||
* right handed. In this case, we better flip the flag for the ArcTo command.
|
||||
*
|
||||
* Here, we use the cross product to figure out the direction of the 3 control points for the
|
||||
* src and dst ellipse.
|
||||
*/
|
||||
private static boolean computeDirectionChange(Point2D.Double middlePoint,
|
||||
Point2D.Double majorAxisPoint, Point2D.Double minorAxisPoint,
|
||||
Point2D.Double dstMiddlePoint, Point2D.Double dstMajorAxisPoint,
|
||||
Point2D.Double dstMinorAxisPoint) {
|
||||
// Compute both cross product, then compare the sign.
|
||||
double srcCrossProduct = getCrossProduct(middlePoint, majorAxisPoint, minorAxisPoint);
|
||||
double dstCrossProduct = getCrossProduct(dstMiddlePoint, dstMajorAxisPoint,
|
||||
dstMinorAxisPoint);
|
||||
|
||||
return srcCrossProduct * dstCrossProduct < 0;
|
||||
}
|
||||
|
||||
private static double getCrossProduct(Point2D.Double middlePoint, Point2D.Double majorAxisPoint,
|
||||
Point2D.Double minorAxisPoint) {
|
||||
double majorMinusMiddleX = majorAxisPoint.x - middlePoint.x;
|
||||
double majorMinusMiddleY = majorAxisPoint.y - middlePoint.y;
|
||||
|
||||
double minorMinusMiddleX = minorAxisPoint.x - middlePoint.x;
|
||||
double minorMinusMiddleY = minorAxisPoint.y - middlePoint.y;
|
||||
|
||||
return majorMinusMiddleX * minorMinusMiddleY - majorMinusMiddleY * minorMinusMiddleX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is an error, either due to the ellipse not being specified
|
||||
* correctly or some computation error. This error is ignorable, but the output ellipse
|
||||
* will not be correct.
|
||||
*/
|
||||
private boolean computeABThetaFromControlPoints(double relMiddleX, double relMiddleY,
|
||||
double relativeMajorAxisPointX, double relativeMajorAxisPointY,
|
||||
double relativeMinorAxisPointX, double relativeMinorAxisPointY) {
|
||||
double m11 = relMiddleX * relMiddleX;
|
||||
double m12 = relMiddleX * relMiddleY;
|
||||
double m13 = relMiddleY * relMiddleY;
|
||||
|
||||
double m21 = relativeMajorAxisPointX * relativeMajorAxisPointX;
|
||||
double m22 = relativeMajorAxisPointX * relativeMajorAxisPointY;
|
||||
double m23 = relativeMajorAxisPointY * relativeMajorAxisPointY;
|
||||
|
||||
double m31 = relativeMinorAxisPointX * relativeMinorAxisPointX;
|
||||
double m32 = relativeMinorAxisPointX * relativeMinorAxisPointY;
|
||||
double m33 = relativeMinorAxisPointY * relativeMinorAxisPointY;
|
||||
|
||||
double det = -(m13 * m22 * m31 - m12 * m23 * m31 - m13 * m21 * m32
|
||||
+ m11 * m23 * m32 + m12 * m21 * m33 - m11 * m22 * m33);
|
||||
|
||||
if (det == 0) {
|
||||
return true;
|
||||
}
|
||||
double A = (-m13 * m22 + m12 * m23 + m13 * m32 - m23 * m32 - m12 * m33 + m22 * m33)
|
||||
/ det;
|
||||
double B = (m13 * m21 - m11 * m23 - m13 * m31 + m23 * m31 + m11 * m33 - m21 * m33) / det;
|
||||
double C = (m12 * m21 - m11 * m22 - m12 * m31 + m22 * m31 + m11 * m32 - m21 * m32)
|
||||
/ (-det);
|
||||
|
||||
// Now we know A = cos(t)^2 / a^2 + sin(t)^2 / b^2
|
||||
// B = -2 cos(t) sin(t) (1/a^2 - 1/b^2)
|
||||
// C = sin(t)^2 / a^2 + cos(t)^2 / b^2
|
||||
|
||||
// Solve it, we got
|
||||
// 2*t = arctan ( B / (A - C));
|
||||
if (A - C == 0) {
|
||||
// We know that a == b now.
|
||||
mMinorAxis = (float) Math.hypot(relativeMajorAxisPointX, relativeMajorAxisPointY);
|
||||
mMajorAxis = mMinorAxis;
|
||||
mRotationDegree = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
double doubleThetaInRadians = atan(B / (A - C));
|
||||
double thetaInRadians = doubleThetaInRadians / 2;
|
||||
if (sin(doubleThetaInRadians) == 0) {
|
||||
mMinorAxis = (float) sqrt(1 / C);
|
||||
mMajorAxis = (float) sqrt(1 / A);
|
||||
mRotationDegree = 0;
|
||||
// This is a valid answer, so return false.
|
||||
return false;
|
||||
}
|
||||
|
||||
double bSqInv = (A + C + B / sin(doubleThetaInRadians)) / 2;
|
||||
double aSqInv = A + C - bSqInv;
|
||||
|
||||
if (bSqInv == 0 || aSqInv == 0) {
|
||||
return true;
|
||||
}
|
||||
mMinorAxis = (float) sqrt(1 / bSqInv);
|
||||
mMajorAxis = (float) sqrt(1 / aSqInv);
|
||||
|
||||
mRotationDegree = (float) Math.toDegrees(Math.PI / 2 + thetaInRadians);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Point2D.Double computeOriginalCenter(float x1, float y1, float rx, float ry,
|
||||
float phi, boolean largeArc, boolean sweep, float x2, float y2) {
|
||||
double cosPhi = cos(phi);
|
||||
double sinPhi = sin(phi);
|
||||
double xDelta = (x1 - x2) / 2;
|
||||
double yDelta = (y1 - y2) / 2;
|
||||
double tempX1 = cosPhi * xDelta + sinPhi * yDelta;
|
||||
double tempY1 = -sinPhi * xDelta + cosPhi * yDelta;
|
||||
|
||||
double rxSq = rx * rx;
|
||||
double rySq = ry * ry;
|
||||
double tempX1Sq = tempX1 * tempX1;
|
||||
double tempY1Sq = tempY1 * tempY1;
|
||||
|
||||
double tempCenterFactor = rxSq * rySq - rxSq * tempY1Sq - rySq * tempX1Sq;
|
||||
tempCenterFactor /= rxSq * tempY1Sq + rySq * tempX1Sq;
|
||||
if (tempCenterFactor < 0) {
|
||||
tempCenterFactor = 0;
|
||||
}
|
||||
tempCenterFactor = sqrt(tempCenterFactor);
|
||||
if (largeArc == sweep) {
|
||||
tempCenterFactor = -tempCenterFactor;
|
||||
}
|
||||
double tempCx = tempCenterFactor * rx * tempY1 / ry;
|
||||
double tempCy = -tempCenterFactor * ry * tempX1 / rx;
|
||||
|
||||
double xCenter = (x1 + x2) / 2;
|
||||
double yCenter = (y1 + y2) / 2;
|
||||
|
||||
return new Point2D.Double(cosPhi * tempCx - sinPhi * tempCy + xCenter,
|
||||
sinPhi * tempCx + cosPhi * tempCy + yCenter);
|
||||
}
|
||||
|
||||
public float getMajorAxis() {
|
||||
return mMajorAxis;
|
||||
}
|
||||
|
||||
public float getMinorAxis() {
|
||||
return mMinorAxis;
|
||||
}
|
||||
|
||||
public float getRotationDegree() {
|
||||
return mRotationDegree;
|
||||
}
|
||||
|
||||
public boolean getDirectionChanged() {
|
||||
return mDirectionChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a point by the given angle.
|
||||
*
|
||||
* @param inPoint the point to rotate
|
||||
* @param radians the rotation angle in radians
|
||||
* @return the rotated point
|
||||
*/
|
||||
private static Point2D.Double rotatePoint2D(Point2D.Double inPoint, double radians) {
|
||||
double cos = cos(radians);
|
||||
double sin = sin(radians);
|
||||
return new Point2D.Double(inPoint.x * cos - inPoint.y * sin,
|
||||
inPoint.x * sin + inPoint.y * cos);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
|
||||
/** Represents an SVG gradient stop or Android's GradientColorItem. */
|
||||
public class GradientStop {
|
||||
private String color;
|
||||
private String offset;
|
||||
private String opacity = "";
|
||||
|
||||
GradientStop(@NonNull String color, @NonNull String offset) {
|
||||
this.color = color;
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getOpacity() {
|
||||
return opacity;
|
||||
}
|
||||
|
||||
protected void setOpacity(@NonNull String opacity) {
|
||||
this.opacity = opacity;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.ide.common.vectordrawable
|
||||
|
||||
import com.android.ide.common.blame.SourcePosition
|
||||
|
||||
/**
|
||||
* Runtime exception that a resource reference incorrectly referenced from vector drawables
|
||||
* causing PNG generation to fail.
|
||||
*/
|
||||
class IllegalVectorDrawableResourceRefException(
|
||||
val value: String, val sourcePosition: SourcePosition, message: String?)
|
||||
: RuntimeException(message)
|
||||
@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.utils.XmlUtils.formatFloatValue;
|
||||
|
||||
/** Builds a string for SVG file's path data. */
|
||||
public class PathBuilder {
|
||||
private StringBuilder mPathData = new StringBuilder();
|
||||
|
||||
private static char encodeBoolean(boolean flag) {
|
||||
return flag ? '1' : '0';
|
||||
}
|
||||
|
||||
public PathBuilder absoluteMoveTo(double x, double y) {
|
||||
mPathData.append('M').append(formatFloatValue(x)).append(',').append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeMoveTo(double x, double y) {
|
||||
mPathData.append('m').append(formatFloatValue(x)).append(',').append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteLineTo(double x, double y) {
|
||||
mPathData.append('L').append(formatFloatValue(x)).append(',').append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeLineTo(double x, double y) {
|
||||
mPathData.append('l').append(formatFloatValue(x)).append(',').append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteVerticalTo(double v) {
|
||||
mPathData.append('V').append(formatFloatValue(v));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeVerticalTo(double v) {
|
||||
mPathData.append('v').append(formatFloatValue(v));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteHorizontalTo(double h) {
|
||||
mPathData.append('H').append(formatFloatValue(h));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeHorizontalTo(double h) {
|
||||
mPathData.append('h').append(formatFloatValue(h));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteCurveTo(
|
||||
double cp1x, double cp1y, double cp2x, double cp2y, double x, double y) {
|
||||
mPathData
|
||||
.append('C')
|
||||
.append(formatFloatValue(cp1x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp1y))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp2x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp2y))
|
||||
.append(',')
|
||||
.append(formatFloatValue(x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeCurveTo(
|
||||
double cp1x, double cp1y, double cp2x, double cp2y, double x, double y) {
|
||||
mPathData
|
||||
.append('c')
|
||||
.append(formatFloatValue(cp1x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp1y))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp2x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp2y))
|
||||
.append(',')
|
||||
.append(formatFloatValue(x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteSmoothCurveTo(double cp2x, double cp2y, double x, double y) {
|
||||
mPathData
|
||||
.append('S')
|
||||
.append(formatFloatValue(cp2x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp2y))
|
||||
.append(',')
|
||||
.append(formatFloatValue(x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeSmoothCurveTo(double cp2x, double cp2y, double x, double y) {
|
||||
mPathData
|
||||
.append('s')
|
||||
.append(formatFloatValue(cp2x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp2y))
|
||||
.append(',')
|
||||
.append(formatFloatValue(x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteQuadraticCurveTo(double cp1x, double cp1y, double x, double y) {
|
||||
mPathData
|
||||
.append('Q')
|
||||
.append(formatFloatValue(cp1x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp1y))
|
||||
.append(',')
|
||||
.append(formatFloatValue(x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeQuadraticCurveTo(double cp1x, double cp1y, double x, double y) {
|
||||
mPathData
|
||||
.append('q')
|
||||
.append(formatFloatValue(cp1x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(cp1y))
|
||||
.append(',')
|
||||
.append(formatFloatValue(x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteSmoothQuadraticCurveTo(double x, double y) {
|
||||
mPathData.append('T').append(formatFloatValue(x)).append(',').append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeSmoothQuadraticCurveTo(double x, double y) {
|
||||
mPathData.append('t').append(formatFloatValue(x)).append(',').append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteArcTo(
|
||||
double rx,
|
||||
double ry,
|
||||
boolean rotation,
|
||||
boolean largeArc,
|
||||
boolean sweep,
|
||||
double x,
|
||||
double y) {
|
||||
mPathData
|
||||
.append('A')
|
||||
.append(formatFloatValue(rx))
|
||||
.append(',')
|
||||
.append(formatFloatValue(ry))
|
||||
.append(',')
|
||||
.append(encodeBoolean(rotation))
|
||||
.append(',')
|
||||
.append(encodeBoolean(largeArc))
|
||||
.append(',')
|
||||
.append(encodeBoolean(sweep))
|
||||
.append(',')
|
||||
.append(formatFloatValue(x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeArcTo(
|
||||
double rx,
|
||||
double ry,
|
||||
boolean rotation,
|
||||
boolean largeArc,
|
||||
boolean sweep,
|
||||
double x,
|
||||
double y) {
|
||||
mPathData
|
||||
.append('a')
|
||||
.append(formatFloatValue(rx))
|
||||
.append(',')
|
||||
.append(formatFloatValue(ry))
|
||||
.append(',')
|
||||
.append(encodeBoolean(rotation))
|
||||
.append(',')
|
||||
.append(encodeBoolean(largeArc))
|
||||
.append(',')
|
||||
.append(encodeBoolean(sweep))
|
||||
.append(',')
|
||||
.append(formatFloatValue(x))
|
||||
.append(',')
|
||||
.append(formatFloatValue(y));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder absoluteClose() {
|
||||
mPathData.append('Z');
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathBuilder relativeClose() {
|
||||
mPathData.append('z');
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns true if the PathBuilder doesn't contain any data. */
|
||||
public boolean isEmpty() {
|
||||
return mPathData.length() == 0;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return mPathData.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility functions for parsing path information. The implementation details should be the same as
|
||||
* the PathParser in Android framework.
|
||||
*
|
||||
* <p>See https://www.w3.org/TR/SVG/paths.html#PathDataBNF for the pathData syntax.
|
||||
*/
|
||||
public class PathParser {
|
||||
private static final float[] EMPTY_FLOAT_ARRAY = new float[0];
|
||||
|
||||
private static class ExtractFloatResult {
|
||||
/** The end position of the parameter. */
|
||||
private int mEndPosition;
|
||||
/** Whether there is an explicit separator after the end position or not. */
|
||||
private boolean mExplicitSeparator;
|
||||
}
|
||||
|
||||
// Do not instantiate.
|
||||
private PathParser() {}
|
||||
|
||||
/**
|
||||
* Determines the end position of a command parameter.
|
||||
*
|
||||
* @param s the string to search
|
||||
* @param start the position to start searching
|
||||
* @param flagMode indicates Boolean flag syntax; a Boolean flag is either "0" or "1" and
|
||||
* doesn't have to be followed by a separator
|
||||
* @param result the result of the extraction
|
||||
*/
|
||||
private static void extract(
|
||||
@NonNull String s, int start, boolean flagMode, @NonNull ExtractFloatResult result) {
|
||||
boolean foundSeparator = false;
|
||||
result.mExplicitSeparator = false;
|
||||
boolean secondDot = false;
|
||||
boolean isExponential = false;
|
||||
// Looking for ' ', ',', '.' or '-' from the start.
|
||||
int currentIndex = start;
|
||||
for (; currentIndex < s.length(); currentIndex++) {
|
||||
boolean isPrevExponential = isExponential;
|
||||
isExponential = false;
|
||||
char currentChar = s.charAt(currentIndex);
|
||||
switch (currentChar) {
|
||||
case ' ':
|
||||
case ',':
|
||||
foundSeparator = true;
|
||||
result.mExplicitSeparator = true;
|
||||
break;
|
||||
case '-':
|
||||
// The negative sign following a 'e' or 'E' is not an implicit separator.
|
||||
if (currentIndex != start && !isPrevExponential) {
|
||||
foundSeparator = true;
|
||||
}
|
||||
break;
|
||||
case '.':
|
||||
if (secondDot) {
|
||||
// Second dot is an implicit separator.
|
||||
foundSeparator = true;
|
||||
} else {
|
||||
secondDot = true;
|
||||
}
|
||||
break;
|
||||
case 'e':
|
||||
case 'E':
|
||||
isExponential = true;
|
||||
break;
|
||||
}
|
||||
if (foundSeparator || flagMode && currentIndex > start) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// When there is nothing found, then we put the end position to the end of the string.
|
||||
result.mEndPosition = currentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the floats in the string this is an optimized version of parseFloat(s.split(",|\\s"));
|
||||
*
|
||||
* @param s the string containing a command and list of floats
|
||||
* @param parseMode
|
||||
* @return array of floats
|
||||
*/
|
||||
@NonNull
|
||||
private static float[] getFloats(@NonNull String s, @NonNull ParseMode parseMode) {
|
||||
char command = s.charAt(0);
|
||||
if (command == 'z' || command == 'Z') {
|
||||
return EMPTY_FLOAT_ARRAY;
|
||||
}
|
||||
try {
|
||||
boolean arcCommand = command == 'a' || command == 'A';
|
||||
float[] results = new float[s.length()];
|
||||
int count = 0;
|
||||
int startPosition = 1;
|
||||
int endPosition;
|
||||
|
||||
ExtractFloatResult result = new ExtractFloatResult();
|
||||
int totalLength = s.length();
|
||||
|
||||
// The startPosition should always be the first character of the current number, and
|
||||
// endPosition is the character after the current number.
|
||||
while (startPosition < totalLength) {
|
||||
// In ANDROID parse mode we treat flags as regular floats for compatibility with
|
||||
// old vector drawables that may have pathData not conforming to
|
||||
// https://www.w3.org/TR/SVG/paths.html#PathDataBNF. In such a case flags may be
|
||||
// represented by "1.0" or "0.0" (b/146520216).
|
||||
boolean flagMode =
|
||||
parseMode == ParseMode.SVG
|
||||
&& arcCommand
|
||||
&& (count % 7 == 3 || count % 7 == 4);
|
||||
extract(s, startPosition, flagMode, result);
|
||||
endPosition = result.mEndPosition;
|
||||
|
||||
if (startPosition < endPosition) {
|
||||
results[count++] = Float.parseFloat(s.substring(startPosition, endPosition));
|
||||
}
|
||||
|
||||
if (result.mExplicitSeparator) {
|
||||
startPosition = endPosition + 1;
|
||||
} else {
|
||||
startPosition = endPosition;
|
||||
}
|
||||
}
|
||||
return Arrays.copyOfRange(results, 0, count);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new RuntimeException("Error in parsing \"" + s + "\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addNode(@NonNull List<VdPath.Node> list, char cmd, @NonNull float[] val) {
|
||||
list.add(new VdPath.Node(cmd, val));
|
||||
}
|
||||
|
||||
private static int nextStart(@NonNull String s, int end) {
|
||||
while (end < s.length()) {
|
||||
char c = s.charAt(end);
|
||||
// Note that 'e' or 'E' are not valid path commands, but could be used for floating
|
||||
// point numbers' scientific notation. Therefore, when searching for next command, we
|
||||
// should ignore 'e' and 'E'.
|
||||
if ('A' <= c && c <= 'Z' && c != 'E' || 'a' <= c && c <= 'z' && c != 'e') {
|
||||
return end;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static VdPath.Node[] parsePath(@NonNull String value, @NonNull ParseMode mode) {
|
||||
value = value.trim();
|
||||
List<VdPath.Node> list = new ArrayList<>();
|
||||
|
||||
int start = 0;
|
||||
int end = 1;
|
||||
while (end < value.length()) {
|
||||
end = nextStart(value, end);
|
||||
String s = value.substring(start, end);
|
||||
char currentCommand = s.charAt(0);
|
||||
float[] val = getFloats(s, mode);
|
||||
|
||||
if (start == 0) {
|
||||
// For the starting command, special handling: add M 0 0 if there is none.
|
||||
// This is good for transformation.
|
||||
if (currentCommand != 'M' && currentCommand != 'm') {
|
||||
addNode(list, 'M', new float[2]);
|
||||
}
|
||||
}
|
||||
addNode(list, currentCommand, val);
|
||||
|
||||
start = end;
|
||||
end++;
|
||||
}
|
||||
if (end - start == 1 && start < value.length()) {
|
||||
addNode(list, value.charAt(start), EMPTY_FLOAT_ARRAY);
|
||||
}
|
||||
return list.toArray(new VdPath.Node[0]);
|
||||
}
|
||||
|
||||
public enum ParseMode {
|
||||
SVG,
|
||||
ANDROID
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
|
||||
/**
|
||||
* Indicates that the input vector drawable XML file included references to other Android resources.
|
||||
*/
|
||||
public class ResourcesNotSupportedException extends RuntimeException {
|
||||
private final String name;
|
||||
private final String value;
|
||||
|
||||
public ResourcesNotSupportedException(@NonNull String name, @NonNull String value) {
|
||||
super(String.format("Cannot process attribute %1$s=\"%2$s\"", name, value));
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,201 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.SdkConstants.TAG_CLIP_PATH;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_CLIP_RULE;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_MASK;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.google.common.collect.Iterables;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
* Represents a SVG group element that contains a clip-path. SvgClipPathNode's mChildren will
|
||||
* contain the actual path data of the clip-path. The path of the clip will be constructed in
|
||||
* {@link #writeXml} by concatenating mChildren's paths. mAffectedNodes contains any group or leaf
|
||||
* nodes that are clipped by the path.
|
||||
*/
|
||||
class SvgClipPathNode extends SvgGroupNode {
|
||||
private final ArrayList<SvgNode> mAffectedNodes = new ArrayList<>();
|
||||
|
||||
SvgClipPathNode(@NonNull SvgTree svgTree, @NonNull Element element, @Nullable String name) {
|
||||
super(svgTree, element, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public SvgClipPathNode deepCopy() {
|
||||
SvgClipPathNode newInstance = new SvgClipPathNode(getTree(), mDocumentElement, mName);
|
||||
newInstance.copyFrom(this);
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
protected void copyFrom(@NonNull SvgClipPathNode from) {
|
||||
super.copyFrom(from);
|
||||
for (SvgNode node : from.mAffectedNodes) {
|
||||
addAffectedNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChild(@NonNull SvgNode child) {
|
||||
// Pass the presentation map down to the children, who can override the attributes.
|
||||
mChildren.add(child);
|
||||
// The child has its own attributes map. But the parents can still fill some attributes
|
||||
// if they don't exists
|
||||
child.fillEmptyAttributes(mVdAttributesMap);
|
||||
}
|
||||
|
||||
public void addAffectedNode(@NonNull SvgNode child) {
|
||||
mAffectedNodes.add(child);
|
||||
child.fillEmptyAttributes(mVdAttributesMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flatten(@NonNull AffineTransform transform) {
|
||||
for (SvgNode n : mChildren) {
|
||||
mStackedTransform.setTransform(transform);
|
||||
mStackedTransform.concatenate(mLocalTransform);
|
||||
n.flatten(mStackedTransform);
|
||||
}
|
||||
|
||||
mStackedTransform.setTransform(transform);
|
||||
for (SvgNode n : mAffectedNodes) {
|
||||
n.flatten(mStackedTransform); // mLocalTransform does not apply to mAffectedNodes.
|
||||
}
|
||||
mStackedTransform.concatenate(mLocalTransform);
|
||||
|
||||
if (mVdAttributesMap.containsKey(Svg2Vector.SVG_STROKE_WIDTH)
|
||||
&& ((mStackedTransform.getType() & AffineTransform.TYPE_MASK_SCALE) != 0)) {
|
||||
logWarning("Scaling of the stroke width is ignored");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
super.validate();
|
||||
if (mDocumentElement.getTagName().equals(SVG_MASK) && !isWhiteFill()) {
|
||||
// A mask that is not solid white creates a transparency effect that cannot be
|
||||
// reproduced by a clip-path.
|
||||
logError("Semitransparent mask cannot be represented by a vector drawable");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isWhiteFill() {
|
||||
String fillColor = mVdAttributesMap.get("fill");
|
||||
if (fillColor == null) {
|
||||
return false;
|
||||
}
|
||||
fillColor = colorSvg2Vd(fillColor, "#000");
|
||||
if (fillColor == null) {
|
||||
return false;
|
||||
}
|
||||
return VdUtil.parseColorValue(fillColor) == 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transformIfNeeded(@NonNull AffineTransform rootTransform) {
|
||||
for (SvgNode p : Iterables.concat(mChildren, mAffectedNodes)) {
|
||||
p.transformIfNeeded(rootTransform);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeXml(@NonNull OutputStreamWriter writer, @NonNull String indent)
|
||||
throws IOException {
|
||||
writer.write(indent);
|
||||
writer.write("<group>");
|
||||
writer.write(System.lineSeparator());
|
||||
String incrementedIndent = indent + INDENT_UNIT;
|
||||
|
||||
Map<ClipRule, List<String>> clipPaths = new EnumMap<>(ClipRule.class);
|
||||
Visitor clipPathCollector = node -> {
|
||||
if (node instanceof SvgLeafNode) {
|
||||
String pathData = ((SvgLeafNode) node).getPathData();
|
||||
if (pathData != null && !pathData.isEmpty()) {
|
||||
ClipRule clipRule =
|
||||
"evenOdd".equals(node.mVdAttributesMap.get(SVG_CLIP_RULE))
|
||||
? ClipRule.EVEN_ODD
|
||||
: ClipRule.NON_ZERO;
|
||||
List<String> paths =
|
||||
clipPaths.computeIfAbsent(clipRule, key -> new ArrayList<>());
|
||||
paths.add(pathData);
|
||||
}
|
||||
}
|
||||
return VisitResult.CONTINUE;
|
||||
};
|
||||
for (SvgNode node : mChildren) {
|
||||
node.accept(clipPathCollector);
|
||||
}
|
||||
|
||||
for (Map.Entry<ClipRule, List<String>> entry : clipPaths.entrySet()) {
|
||||
ClipRule clipRule = entry.getKey();
|
||||
List<String> pathData = entry.getValue();
|
||||
writer.write(incrementedIndent);
|
||||
writer.write('<');
|
||||
writer.write(TAG_CLIP_PATH);
|
||||
writer.write(System.lineSeparator());
|
||||
writer.write(incrementedIndent);
|
||||
writer.write(INDENT_UNIT);
|
||||
writer.write(INDENT_UNIT);
|
||||
writer.write("android:pathData=\"");
|
||||
for (int i = 0; i < pathData.size(); i++) {
|
||||
String path = pathData.get(i);
|
||||
if (i > 0 && !path.startsWith("M")) {
|
||||
// Reset the current position to the origin of the coordinate system.
|
||||
writer.write("M 0,0");
|
||||
}
|
||||
writer.write(path);
|
||||
}
|
||||
writer.write('"');
|
||||
if (clipRule == ClipRule.EVEN_ODD) {
|
||||
writer.write(System.lineSeparator());
|
||||
writer.write(incrementedIndent);
|
||||
writer.write(INDENT_UNIT);
|
||||
writer.write(INDENT_UNIT);
|
||||
writer.write("android:fillType=\"evenOdd\"");
|
||||
}
|
||||
writer.write("/>");
|
||||
writer.write(System.lineSeparator());
|
||||
}
|
||||
|
||||
for (SvgNode node : mAffectedNodes) {
|
||||
node.writeXml(writer, incrementedIndent);
|
||||
}
|
||||
writer.write(indent);
|
||||
writer.write("</group>");
|
||||
writer.write(System.lineSeparator());
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates the affected nodes transformations to the clipPathNode's so it is properly
|
||||
* transformed.
|
||||
*/
|
||||
public void setClipPathNodeAttributes() {
|
||||
for (SvgNode n : mAffectedNodes) {
|
||||
mLocalTransform.concatenate(n.mLocalTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Methods for converting SVG color values to vector drawable format. */
|
||||
public class SvgColor {
|
||||
/** Color table from https://www.w3.org/TR/SVG11/types.html#ColorKeywords. */
|
||||
private static final ImmutableMap<String, String> colorMap =
|
||||
ImmutableMap.<String, String>builder()
|
||||
.put("aliceblue", "#f0f8ff")
|
||||
.put("antiquewhite", "#faebd7")
|
||||
.put("aqua", "#00ffff")
|
||||
.put("aquamarine", "#7fffd4")
|
||||
.put("azure", "#f0ffff")
|
||||
.put("beige", "#f5f5dc")
|
||||
.put("bisque", "#ffe4c4")
|
||||
.put("black", "#000000")
|
||||
.put("blanchedalmond", "#ffebcd")
|
||||
.put("blue", "#0000ff")
|
||||
.put("blueviolet", "#8a2be2")
|
||||
.put("brown", "#a52a2a")
|
||||
.put("burlywood", "#deb887")
|
||||
.put("cadetblue", "#5f9ea0")
|
||||
.put("chartreuse", "#7fff00")
|
||||
.put("chocolate", "#d2691e")
|
||||
.put("coral", "#ff7f50")
|
||||
.put("cornflowerblue", "#6495ed")
|
||||
.put("cornsilk", "#fff8dc")
|
||||
.put("crimson", "#dc143c")
|
||||
.put("cyan", "#00ffff")
|
||||
.put("darkblue", "#00008b")
|
||||
.put("darkcyan", "#008b8b")
|
||||
.put("darkgoldenrod", "#b8860b")
|
||||
.put("darkgray", "#a9a9a9")
|
||||
.put("darkgrey", "#a9a9a9")
|
||||
.put("darkgreen", "#006400")
|
||||
.put("darkkhaki", "#bdb76b")
|
||||
.put("darkmagenta", "#8b008b")
|
||||
.put("darkolivegreen", "#556b2f")
|
||||
.put("darkorange", "#ff8c00")
|
||||
.put("darkorchid", "#9932cc")
|
||||
.put("darkred", "#8b0000")
|
||||
.put("darksalmon", "#e9967a")
|
||||
.put("darkseagreen", "#8fbc8f")
|
||||
.put("darkslateblue", "#483d8b")
|
||||
.put("darkslategray", "#2f4f4f")
|
||||
.put("darkslategrey", "#2f4f4f")
|
||||
.put("darkturquoise", "#00ced1")
|
||||
.put("darkviolet", "#9400d3")
|
||||
.put("deeppink", "#ff1493")
|
||||
.put("deepskyblue", "#00bfff")
|
||||
.put("dimgray", "#696969")
|
||||
.put("dimgrey", "#696969")
|
||||
.put("dodgerblue", "#1e90ff")
|
||||
.put("firebrick", "#b22222")
|
||||
.put("floralwhite", "#fffaf0")
|
||||
.put("forestgreen", "#228b22")
|
||||
.put("fuchsia", "#ff00ff")
|
||||
.put("gainsboro", "#dcdcdc")
|
||||
.put("ghostwhite", "#f8f8ff")
|
||||
.put("gold", "#ffd700")
|
||||
.put("goldenrod", "#daa520")
|
||||
.put("gray", "#808080")
|
||||
.put("grey", "#808080")
|
||||
.put("green", "#008000")
|
||||
.put("greenyellow", "#adff2f")
|
||||
.put("honeydew", "#f0fff0")
|
||||
.put("hotpink", "#ff69b4")
|
||||
.put("indianred", "#cd5c5c")
|
||||
.put("indigo", "#4b0082")
|
||||
.put("ivory", "#fffff0")
|
||||
.put("khaki", "#f0e68c")
|
||||
.put("lavender", "#e6e6fa")
|
||||
.put("lavenderblush", "#fff0f5")
|
||||
.put("lawngreen", "#7cfc00")
|
||||
.put("lemonchiffon", "#fffacd")
|
||||
.put("lightblue", "#add8e6")
|
||||
.put("lightcoral", "#f08080")
|
||||
.put("lightcyan", "#e0ffff")
|
||||
.put("lightgoldenrodyellow", "#fafad2")
|
||||
.put("lightgray", "#d3d3d3")
|
||||
.put("lightgrey", "#d3d3d3")
|
||||
.put("lightgreen", "#90ee90")
|
||||
.put("lightpink", "#ffb6c1")
|
||||
.put("lightsalmon", "#ffa07a")
|
||||
.put("lightseagreen", "#20b2aa")
|
||||
.put("lightskyblue", "#87cefa")
|
||||
.put("lightslategray", "#778899")
|
||||
.put("lightslategrey", "#778899")
|
||||
.put("lightsteelblue", "#b0c4de")
|
||||
.put("lightyellow", "#ffffe0")
|
||||
.put("lime", "#00ff00")
|
||||
.put("limegreen", "#32cd32")
|
||||
.put("linen", "#faf0e6")
|
||||
.put("magenta", "#ff00ff")
|
||||
.put("maroon", "#800000")
|
||||
.put("mediumaquamarine", "#66cdaa")
|
||||
.put("mediumblue", "#0000cd")
|
||||
.put("mediumorchid", "#ba55d3")
|
||||
.put("mediumpurple", "#9370db")
|
||||
.put("mediumseagreen", "#3cb371")
|
||||
.put("mediumslateblue", "#7b68ee")
|
||||
.put("mediumspringgreen", "#00fa9a")
|
||||
.put("mediumturquoise", "#48d1cc")
|
||||
.put("mediumvioletred", "#c71585")
|
||||
.put("midnightblue", "#191970")
|
||||
.put("mintcream", "#f5fffa")
|
||||
.put("mistyrose", "#ffe4e1")
|
||||
.put("moccasin", "#ffe4b5")
|
||||
.put("navajowhite", "#ffdead")
|
||||
.put("navy", "#000080")
|
||||
.put("oldlace", "#fdf5e6")
|
||||
.put("olive", "#808000")
|
||||
.put("olivedrab", "#6b8e23")
|
||||
.put("orange", "#ffa500")
|
||||
.put("orangered", "#ff4500")
|
||||
.put("orchid", "#da70d6")
|
||||
.put("palegoldenrod", "#eee8aa")
|
||||
.put("palegreen", "#98fb98")
|
||||
.put("paleturquoise", "#afeeee")
|
||||
.put("palevioletred", "#db7093")
|
||||
.put("papayawhip", "#ffefd5")
|
||||
.put("peachpuff", "#ffdab9")
|
||||
.put("peru", "#cd853f")
|
||||
.put("pink", "#ffc0cb")
|
||||
.put("plum", "#dda0dd")
|
||||
.put("powderblue", "#b0e0e6")
|
||||
.put("purple", "#800080")
|
||||
.put("rebeccapurple", "#663399")
|
||||
.put("red", "#ff0000")
|
||||
.put("rosybrown", "#bc8f8f")
|
||||
.put("royalblue", "#4169e1")
|
||||
.put("saddlebrown", "#8b4513")
|
||||
.put("salmon", "#fa8072")
|
||||
.put("sandybrown", "#f4a460")
|
||||
.put("seagreen", "#2e8b57")
|
||||
.put("seashell", "#fff5ee")
|
||||
.put("sienna", "#a0522d")
|
||||
.put("silver", "#c0c0c0")
|
||||
.put("skyblue", "#87ceeb")
|
||||
.put("slateblue", "#6a5acd")
|
||||
.put("slategray", "#708090")
|
||||
.put("slategrey", "#708090")
|
||||
.put("snow", "#fffafa")
|
||||
.put("springgreen", "#00ff7f")
|
||||
.put("steelblue", "#4682b4")
|
||||
.put("tan", "#d2b48c")
|
||||
.put("teal", "#008080")
|
||||
.put("thistle", "#d8bfd8")
|
||||
.put("tomato", "#ff6347")
|
||||
.put("turquoise", "#40e0d0")
|
||||
.put("violet", "#ee82ee")
|
||||
.put("wheat", "#f5deb3")
|
||||
.put("white", "#ffffff")
|
||||
.put("whitesmoke", "#f5f5f5")
|
||||
.put("yellow", "#ffff00")
|
||||
.put("yellowgreen", "#9acd32")
|
||||
.build();
|
||||
|
||||
/** Do not instantiate. All methods are static. */
|
||||
private SvgColor() {}
|
||||
|
||||
/**
|
||||
* Converts an SVG color value to "#RRGGBB" or "#AARRGGBB" format used by vector drawables.
|
||||
* The input color value can be "none" and RGB value, e.g. "rgb(255, 0, 0)",
|
||||
* "rgba(255, 0, 0, 127)", or a color name defined in
|
||||
* https://www.w3.org/TR/SVG11/types.html#ColorKeywords.
|
||||
*
|
||||
* @param svgColorValue the SVG color value to convert
|
||||
* @return the converted value, or null if the given value cannot be interpreted as color
|
||||
* @throws IllegalArgumentException if the supplied SVG color value has invalid or unsupported
|
||||
* format
|
||||
*/
|
||||
@Nullable
|
||||
protected static String colorSvg2Vd(@NonNull String svgColorValue) {
|
||||
String color = svgColorValue.trim();
|
||||
|
||||
if (color.startsWith("#")) {
|
||||
return color;
|
||||
}
|
||||
|
||||
if ("none".equals(color)) {
|
||||
return "#00000000";
|
||||
}
|
||||
|
||||
if (color.startsWith("rgb(") && color.endsWith(")")) {
|
||||
String rgb = color.substring(4, color.length() - 1);
|
||||
String[] numbers = rgb.split(",");
|
||||
if (numbers.length != 3) {
|
||||
throw new IllegalArgumentException(svgColorValue);
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(7);
|
||||
builder.append("#");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int component = getColorComponent(numbers[i].trim(), svgColorValue);
|
||||
builder.append(String.format("%02X", component));
|
||||
}
|
||||
assert builder.length() == 7;
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
if (color.startsWith("rgba(") && color.endsWith(")")) {
|
||||
String rgb = color.substring(5, color.length() - 1);
|
||||
String[] numbers = rgb.split(",");
|
||||
if (numbers.length != 4) {
|
||||
throw new IllegalArgumentException(svgColorValue);
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(9);
|
||||
builder.append("#");
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int component = getColorComponent(numbers[(i + 3) % 4].trim(), svgColorValue);
|
||||
builder.append(String.format("%02X", component));
|
||||
}
|
||||
assert builder.length() == 9;
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
return colorMap.get(color.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
private static int getColorComponent(
|
||||
@NonNull String colorComponent, @NonNull String svgColorValue) {
|
||||
try {
|
||||
if (colorComponent.endsWith("%")) {
|
||||
float value =
|
||||
Float.parseFloat(colorComponent.substring(0, colorComponent.length() - 1));
|
||||
return clampColor(Math.round(value * 255.f / 100.f));
|
||||
}
|
||||
|
||||
return clampColor(Integer.parseInt(colorComponent));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException(svgColorValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static int clampColor(int val) {
|
||||
return Math.max(0, Math.min(255, val));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,428 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.ide.common.vectordrawable.VdUtil.parseColorValue;
|
||||
import static com.android.utils.DecimalUtils.trimInsignificantZeros;
|
||||
import static com.android.utils.XmlUtils.formatFloatValue;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.android.ide.common.vectordrawable.PathParser.ParseMode;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.NoninvertibleTransformException;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/** Represents a SVG gradient that is referenced by a SvgLeafNode. */
|
||||
class SvgGradientNode extends SvgNode {
|
||||
private static final Logger logger = Logger.getLogger(SvgGroupNode.class.getSimpleName());
|
||||
|
||||
private final ArrayList<GradientStop> myGradientStops = new ArrayList<>();
|
||||
|
||||
private SvgLeafNode mSvgLeafNode;
|
||||
|
||||
// Bounding box of mSvgLeafNode.
|
||||
private Rectangle2D boundingBox;
|
||||
|
||||
private GradientUsage mGradientUsage;
|
||||
|
||||
private static class GradientCoordResult {
|
||||
private final double mValue;
|
||||
// When the gradientUnits is set to "userSpaceOnUse", we usually use the coordinate values
|
||||
// as it is. But if the coordinate value is a percentage, we still need to multiply this
|
||||
// percentage with the viewport's bounding box, in a similar way as gradientUnits is set
|
||||
// to "objectBoundingBox".
|
||||
private final boolean mIsPercentage;
|
||||
|
||||
GradientCoordResult(double value, boolean isPercentage) {
|
||||
mValue = value;
|
||||
mIsPercentage = isPercentage;
|
||||
}
|
||||
|
||||
double getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
boolean isPercentage() {
|
||||
return mIsPercentage;
|
||||
}
|
||||
}
|
||||
|
||||
protected enum GradientUsage {
|
||||
FILL,
|
||||
STROKE
|
||||
}
|
||||
|
||||
// Maps the gradient vector's coordinate names to an int for easier array lookup.
|
||||
private static final ImmutableMap<String, Integer> vectorCoordinateMap =
|
||||
ImmutableMap.<String, Integer>builder()
|
||||
.put("x1", 0)
|
||||
.put("y1", 1)
|
||||
.put("x2", 2)
|
||||
.put("y2", 3)
|
||||
.build();
|
||||
|
||||
SvgGradientNode(@NonNull SvgTree svgTree, @NonNull Element element, @Nullable String nodeName) {
|
||||
super(svgTree, element, nodeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public SvgGradientNode deepCopy() {
|
||||
SvgGradientNode newInstance = new SvgGradientNode(getTree(), mDocumentElement, getName());
|
||||
newInstance.copyFrom(this);
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroupNode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* We do not copy mSvgLeafNode, boundingBox, or mGradientUsage because they will be set after
|
||||
* copying the SvgGradientNode. We always call deepCopy of SvgGradientNodes within a SvgLeafNode
|
||||
* and then call setSvgLeafNode for that leaf. We calculate the boundingBox and determine the
|
||||
* mGradientUsage based on the leaf node's attributes and reference to the gradient being
|
||||
* copied.
|
||||
*/
|
||||
protected void copyFrom(@NonNull SvgGradientNode from) {
|
||||
super.copyFrom(from);
|
||||
for (GradientStop g : from.myGradientStops) {
|
||||
addGradientStop(g.getColor(), g.getOffset(), g.getOpacity());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dumpNode(@NonNull String indent) {
|
||||
// Print the current node.
|
||||
logger.log(Level.FINE, indent + "current gradient is :" + getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transformIfNeeded(@NonNull AffineTransform rootTransform) {
|
||||
AffineTransform finalTransform = new AffineTransform(rootTransform);
|
||||
finalTransform.concatenate(mStackedTransform);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flatten(@NonNull AffineTransform transform) {
|
||||
mStackedTransform.setTransform(transform);
|
||||
mStackedTransform.concatenate(mLocalTransform);
|
||||
}
|
||||
|
||||
/** Parses the gradient coordinate value given as a percentage or a length. Returns a double. */
|
||||
private GradientCoordResult getGradientCoordinate(@NonNull String x, double defaultValue) {
|
||||
if (!mVdAttributesMap.containsKey(x)) {
|
||||
return new GradientCoordResult(defaultValue, false);
|
||||
}
|
||||
double val = defaultValue;
|
||||
String vdValue = mVdAttributesMap.get(x).trim();
|
||||
if (x.equals("r") && vdValue.startsWith("-")) {
|
||||
return new GradientCoordResult(defaultValue, false);
|
||||
}
|
||||
|
||||
boolean isPercentage = false;
|
||||
try {
|
||||
if (vdValue.endsWith("%")) {
|
||||
val = Double.parseDouble(vdValue.substring(0, vdValue.length() - 1)) / 100;
|
||||
isPercentage = true;
|
||||
} else {
|
||||
val = Double.parseDouble(vdValue);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
logError("Unsupported coordinate value");
|
||||
}
|
||||
return new GradientCoordResult(val, isPercentage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeXml(@NonNull OutputStreamWriter writer, @NonNull String indent)
|
||||
throws IOException {
|
||||
if (myGradientStops.isEmpty()) {
|
||||
logError("Gradient has no stop info");
|
||||
return;
|
||||
}
|
||||
|
||||
// By default, the dimensions of the gradient is the bounding box of the path.
|
||||
setBoundingBox();
|
||||
double height = boundingBox.getHeight();
|
||||
double width = boundingBox.getWidth();
|
||||
double startX = boundingBox.getX();
|
||||
double startY = boundingBox.getY();
|
||||
|
||||
String gradientUnit = mVdAttributesMap.get("gradientUnits");
|
||||
boolean isUserSpaceOnUse = "userSpaceOnUse".equals(gradientUnit);
|
||||
// If gradientUnits is specified to be "userSpaceOnUse", we use the image's dimensions.
|
||||
if (isUserSpaceOnUse) {
|
||||
startX = 0;
|
||||
startY = 0;
|
||||
height = getTree().getHeight();
|
||||
width = getTree().getWidth();
|
||||
}
|
||||
|
||||
if (width == 0 || height == 0) {
|
||||
return; // The gradient is not visible because it doesn't occupy any area.
|
||||
}
|
||||
|
||||
writer.write(indent);
|
||||
if (mGradientUsage == GradientUsage.FILL) {
|
||||
writer.write("<aapt:attr name=\"android:fillColor\">");
|
||||
} else {
|
||||
writer.write("<aapt:attr name=\"android:strokeColor\">");
|
||||
}
|
||||
writer.write(System.lineSeparator());
|
||||
writer.write(indent);
|
||||
writer.write(INDENT_UNIT);
|
||||
writer.write("<gradient ");
|
||||
|
||||
// TODO: Fix matrix transformations that include skew element and SVGs that define scale before rotate.
|
||||
// Additionally skew transformations have not been tested.
|
||||
// If there is a gradientTransform, parse and store in mLocalTransform.
|
||||
if (mVdAttributesMap.containsKey("gradientTransform")) {
|
||||
String transformValue = mVdAttributesMap.get("gradientTransform");
|
||||
parseLocalTransform(transformValue);
|
||||
if (!isUserSpaceOnUse) {
|
||||
AffineTransform tr = new AffineTransform(width, 0, 0, height, 0, 0);
|
||||
mLocalTransform.preConcatenate(tr);
|
||||
try {
|
||||
tr.invert();
|
||||
} catch (NoninvertibleTransformException e) {
|
||||
throw new Error(e); // Not going to happen because width * height != 0;
|
||||
}
|
||||
mLocalTransform.concatenate(tr);
|
||||
}
|
||||
}
|
||||
|
||||
// According to the SVG spec, the gradient transformation (mLocalTransform) always needs
|
||||
// to be applied to the gradient. However, the geometry transformation (mStackedTransform)
|
||||
// will be affecting gradient only when it is using user space because we flatten
|
||||
// everything.
|
||||
// If we are not using user space, at this moment, the bounding box already contains
|
||||
// the geometry transformation, when we apply percentage to the bounding box, we don't
|
||||
// need to multiply the geometry transformation the second time.
|
||||
if (isUserSpaceOnUse) {
|
||||
mLocalTransform.preConcatenate(mSvgLeafNode.mStackedTransform);
|
||||
}
|
||||
|
||||
// Source and target arrays to which we apply the local transform.
|
||||
double[] gradientBounds;
|
||||
double[] transformedBounds;
|
||||
|
||||
String gradientType = "linear";
|
||||
|
||||
if (mVdAttributesMap.containsKey("gradientType")) {
|
||||
gradientType = mVdAttributesMap.get("gradientType");
|
||||
}
|
||||
|
||||
if (gradientType.equals("linear")) {
|
||||
gradientBounds = new double[4];
|
||||
transformedBounds = new double[4];
|
||||
// Retrieves x1, y1, x2, y2 and calculates their coordinate in the viewport.
|
||||
// Stores the coordinates in the gradientBounds and transformedBounds arrays to apply
|
||||
// the proper transformation.
|
||||
for (Map.Entry<String, Integer> entry : vectorCoordinateMap.entrySet()) {
|
||||
// Gets the index corresponding to x1, y1, x2 and y2.
|
||||
// x1 and x2 are indexed as 0 and 2
|
||||
// y1 and y2 are indexed as 1 and 3
|
||||
String s = entry.getKey();
|
||||
int index = entry.getValue();
|
||||
|
||||
// According to SVG spec, the default coordinate value for x1, and y1 and y2 is 0.
|
||||
// The default for x2 is 1.
|
||||
double defaultValue = 0;
|
||||
if (index == 2) {
|
||||
defaultValue = 1;
|
||||
}
|
||||
GradientCoordResult result = getGradientCoordinate(s, defaultValue);
|
||||
|
||||
double coordValue = result.getValue();
|
||||
if (!isUserSpaceOnUse || result.isPercentage()) {
|
||||
if (index % 2 == 0) {
|
||||
coordValue = coordValue * width + startX;
|
||||
} else {
|
||||
coordValue = coordValue * height + startY;
|
||||
}
|
||||
}
|
||||
// In case no transforms are applied, original coordinates are also stored in
|
||||
// transformedBounds.
|
||||
gradientBounds[index] = coordValue;
|
||||
transformedBounds[index] = coordValue;
|
||||
|
||||
// We need mVdAttributesMap to contain all coordinates regardless if they are
|
||||
// specified in the SVG in order to write the default value to the VD XML.
|
||||
if (!mVdAttributesMap.containsKey(s)) {
|
||||
mVdAttributesMap.put(s, "");
|
||||
}
|
||||
}
|
||||
// transformedBounds will hold the new coordinates of the gradient.
|
||||
// This applies it to the linearGradient
|
||||
mLocalTransform.transform(gradientBounds, 0, transformedBounds, 0, 2);
|
||||
} else {
|
||||
gradientBounds = new double[2];
|
||||
transformedBounds = new double[2];
|
||||
GradientCoordResult cxResult = getGradientCoordinate("cx", .5);
|
||||
double cx = cxResult.getValue();
|
||||
if (!isUserSpaceOnUse || cxResult.isPercentage()) {
|
||||
cx = width * cx + startX;
|
||||
}
|
||||
GradientCoordResult cyResult = getGradientCoordinate("cy", .5);
|
||||
double cy = cyResult.getValue();
|
||||
if (!isUserSpaceOnUse || cyResult.isPercentage()) {
|
||||
cy = height * cy + startY;
|
||||
}
|
||||
GradientCoordResult rResult = getGradientCoordinate("r", .5);
|
||||
double r = rResult.getValue();
|
||||
if (!isUserSpaceOnUse || rResult.isPercentage()) {
|
||||
r *= Math.max(height, width);
|
||||
}
|
||||
|
||||
gradientBounds[0] = cx;
|
||||
transformedBounds[0] = cx;
|
||||
gradientBounds[1] = cy;
|
||||
transformedBounds[1] = cy;
|
||||
|
||||
// Transform radius, center point here.
|
||||
mLocalTransform.transform(gradientBounds, 0, transformedBounds, 0, 1);
|
||||
Point2D radius = new Point2D.Double(r, 0);
|
||||
Point2D transformedRadius = new Point2D.Double(r, 0);
|
||||
mLocalTransform.deltaTransform(radius, transformedRadius);
|
||||
|
||||
mVdAttributesMap.put("cx", formatFloatValue(transformedBounds[0]));
|
||||
mVdAttributesMap.put("cy", formatFloatValue(transformedBounds[1]));
|
||||
mVdAttributesMap.put("r", formatFloatValue(transformedRadius.distance(0, 0)));
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> entry : mVdAttributesMap.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String gradientAttr = Svg2Vector.gradientMap.get(key);
|
||||
String svgValue = entry.getValue().trim();
|
||||
String vdValue;
|
||||
vdValue = colorSvg2Vd(svgValue, "#000000");
|
||||
|
||||
if (vdValue == null) {
|
||||
if (vectorCoordinateMap.containsKey(key)) {
|
||||
double x = transformedBounds[vectorCoordinateMap.get(key)];
|
||||
vdValue = formatFloatValue(x);
|
||||
} else if (key.equals("spreadMethod")) {
|
||||
if (svgValue.equals("pad")) {
|
||||
vdValue = "clamp";
|
||||
} else if (svgValue.equals("reflect")) {
|
||||
vdValue = "mirror";
|
||||
} else if (svgValue.equals("repeat")) {
|
||||
vdValue = "repeat";
|
||||
} else {
|
||||
logError("Unsupported spreadMethod " + svgValue);
|
||||
vdValue = "clamp";
|
||||
}
|
||||
} else if (svgValue.endsWith("%")) {
|
||||
vdValue = formatFloatValue(getGradientCoordinate(key, 0).getValue());
|
||||
} else {
|
||||
vdValue = svgValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gradientAttr.isEmpty()) {
|
||||
writer.write(System.lineSeparator());
|
||||
writer.write(indent);
|
||||
writer.write(INDENT_UNIT);
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write(gradientAttr);
|
||||
writer.write("=\"");
|
||||
writer.write(vdValue);
|
||||
writer.write("\"");
|
||||
}
|
||||
}
|
||||
writer.write('>');
|
||||
writer.write(System.lineSeparator());
|
||||
|
||||
writeGradientStops(writer, indent + INDENT_UNIT + INDENT_UNIT);
|
||||
writer.write(indent);
|
||||
writer.write(INDENT_UNIT);
|
||||
writer.write("</gradient>");
|
||||
writer.write(System.lineSeparator());
|
||||
writer.write(indent);
|
||||
writer.write("</aapt:attr>");
|
||||
writer.write(System.lineSeparator());
|
||||
}
|
||||
|
||||
private void writeGradientStops(@NonNull OutputStreamWriter writer, @NonNull String indent)
|
||||
throws IOException {
|
||||
for (GradientStop g : myGradientStops) {
|
||||
String color = g.getColor();
|
||||
float opacity;
|
||||
try {
|
||||
opacity = Float.parseFloat(g.getOpacity());
|
||||
} catch (NumberFormatException e) {
|
||||
logWarning("Unsupported opacity value");
|
||||
opacity = 1;
|
||||
}
|
||||
int color1 = VdPath.applyAlpha(parseColorValue(color), opacity);
|
||||
color = String.format("#%08X", color1);
|
||||
|
||||
writer.write(indent);
|
||||
writer.write("<item android:offset=\"");
|
||||
writer.write(trimInsignificantZeros(g.getOffset()));
|
||||
writer.write("\"");
|
||||
writer.write(" android:color=\"");
|
||||
writer.write(color);
|
||||
writer.write("\"/>");
|
||||
writer.write(System.lineSeparator());
|
||||
|
||||
if (myGradientStops.size() == 1) {
|
||||
logWarning("Gradient has only one color stop");
|
||||
writer.write(indent);
|
||||
writer.write("<item android:offset=\"1\"");
|
||||
writer.write(" android:color=\"");
|
||||
writer.write(color);
|
||||
writer.write("\"/>");
|
||||
writer.write(System.lineSeparator());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addGradientStop(
|
||||
@NonNull String color, @NonNull String offset, @NonNull String opacity) {
|
||||
GradientStop stop = new GradientStop(color, offset);
|
||||
stop.setOpacity(opacity);
|
||||
myGradientStops.add(stop);
|
||||
}
|
||||
|
||||
public void setGradientUsage(@NonNull GradientUsage gradientUsage) {
|
||||
mGradientUsage = gradientUsage;
|
||||
}
|
||||
|
||||
public void setSvgLeafNode(@NonNull SvgLeafNode svgLeafNode) {
|
||||
mSvgLeafNode = svgLeafNode;
|
||||
}
|
||||
|
||||
private void setBoundingBox() {
|
||||
Path2D svgPath = new Path2D.Double();
|
||||
VdPath.Node[] nodes = PathParser.parsePath(mSvgLeafNode.getPathData(), ParseMode.SVG);
|
||||
VdNodeRender.createPath(nodes, svgPath);
|
||||
boundingBox = svgPath.getBounds2D();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
* Represents an SVG file's group element.
|
||||
*/
|
||||
class SvgGroupNode extends SvgNode {
|
||||
private static final Logger logger = Logger.getLogger(SvgGroupNode.class.getSimpleName());
|
||||
|
||||
protected final ArrayList<SvgNode> mChildren = new ArrayList<>();
|
||||
|
||||
SvgGroupNode(@NonNull SvgTree svgTree, @NonNull Element docNode, @Nullable String name) {
|
||||
super(svgTree, docNode, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public SvgGroupNode deepCopy() {
|
||||
SvgGroupNode newInstance = new SvgGroupNode(getTree(), mDocumentElement, getName());
|
||||
newInstance.copyFrom(this);
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
protected <T extends SvgGroupNode> void copyFrom(@NonNull T from) {
|
||||
super.copyFrom(from);
|
||||
for (SvgNode child : from.mChildren) {
|
||||
addChild(child.deepCopy());
|
||||
}
|
||||
}
|
||||
|
||||
public void addChild(@NonNull SvgNode child) {
|
||||
// Pass the presentation map down to the children, who can override the attributes.
|
||||
mChildren.add(child);
|
||||
// The child has its own attributes map. But the parents can still fill some attributes
|
||||
// if they don't exist.
|
||||
child.fillEmptyAttributes(mVdAttributesMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an existing child node with a new one.
|
||||
*
|
||||
* @param oldChild the child node to replace
|
||||
* @param newChild the node to replace the existing child node with
|
||||
*/
|
||||
public void replaceChild(@NonNull SvgNode oldChild, @NonNull SvgNode newChild) {
|
||||
int index = mChildren.indexOf(oldChild);
|
||||
if (index < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"The child being replaced doesn't belong to this group");
|
||||
}
|
||||
|
||||
mChildren.set(index, newChild);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dumpNode(@NonNull String indent) {
|
||||
// Print the current group.
|
||||
logger.log(Level.FINE, indent + "group: " + getName());
|
||||
|
||||
// Then print all the children.
|
||||
for (SvgNode node : mChildren) {
|
||||
node.dumpNode(indent + INDENT_UNIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the parent node of the input node.
|
||||
*
|
||||
* @return the parent node, or null if node is not in the tree.
|
||||
*/
|
||||
@Nullable
|
||||
public SvgGroupNode findParent(@NonNull SvgNode node) {
|
||||
for (SvgNode n : mChildren) {
|
||||
if (n == node) {
|
||||
return this;
|
||||
}
|
||||
if (n.isGroupNode()) {
|
||||
SvgGroupNode parent = ((SvgGroupNode) n).findParent(node);
|
||||
if (parent != null) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroupNode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transformIfNeeded(@NonNull AffineTransform rootTransform) {
|
||||
for (SvgNode p : mChildren) {
|
||||
p.transformIfNeeded(rootTransform);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flatten(@NonNull AffineTransform transform) {
|
||||
for (SvgNode node : mChildren) {
|
||||
mStackedTransform.setTransform(transform);
|
||||
mStackedTransform.concatenate(mLocalTransform);
|
||||
node.flatten(mStackedTransform);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
for (SvgNode node : mChildren) {
|
||||
node.validate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeXml(@NonNull OutputStreamWriter writer, @NonNull String indent)
|
||||
throws IOException {
|
||||
for (SvgNode node : mChildren) {
|
||||
node.writeXml(writer, indent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public VisitResult accept(@NonNull Visitor visitor) {
|
||||
VisitResult result = visitor.visit(this);
|
||||
if (result == VisitResult.CONTINUE) {
|
||||
for (SvgNode node : mChildren) {
|
||||
if (node.accept(visitor) == VisitResult.ABORT) {
|
||||
return VisitResult.ABORT;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result == VisitResult.SKIP_CHILDREN ? VisitResult.CONTINUE : result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void fillPresentationAttributes(@NonNull String name, @NonNull String value) {
|
||||
super.fillPresentationAttributes(name, value);
|
||||
for (SvgNode n : mChildren) {
|
||||
// Group presentation attribute should not override child.
|
||||
if (!n.mVdAttributesMap.containsKey(name)) {
|
||||
n.fillPresentationAttributes(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_FILL;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_FILL_OPACITY;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_OPACITY;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_STROKE;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_STROKE_OPACITY;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_STROKE_WIDTH;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.presentationMap;
|
||||
import static com.android.utils.XmlUtils.formatFloatValue;
|
||||
import static java.lang.Math.abs;
|
||||
import static java.lang.Math.sqrt;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.android.ide.common.vectordrawable.PathParser.ParseMode;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/** Represents an SVG file's leaf element. */
|
||||
class SvgLeafNode extends SvgNode {
|
||||
private static final Logger logger = Logger.getLogger(SvgLeafNode.class.getSimpleName());
|
||||
|
||||
@Nullable private String mPathData;
|
||||
@Nullable private SvgGradientNode mFillGradientNode;
|
||||
@Nullable private SvgGradientNode mStrokeGradientNode;
|
||||
|
||||
SvgLeafNode(@NonNull SvgTree svgTree, @NonNull Element element, @Nullable String nodeName) {
|
||||
super(svgTree, element, nodeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public SvgLeafNode deepCopy() {
|
||||
SvgLeafNode newNode = new SvgLeafNode(getTree(), mDocumentElement, getName());
|
||||
newNode.copyFrom(this);
|
||||
return newNode;
|
||||
}
|
||||
|
||||
protected void copyFrom(@NonNull SvgLeafNode from) {
|
||||
super.copyFrom(from);
|
||||
mPathData = from.mPathData;
|
||||
}
|
||||
|
||||
/** Writes attributes of this node. */
|
||||
private void writeAttributeValues(@NonNull OutputStreamWriter writer, @NonNull String indent)
|
||||
throws IOException {
|
||||
// There could be some redundant opacity information in the attributes' map,
|
||||
// like opacity vs fill-opacity / stroke-opacity.
|
||||
parsePathOpacity();
|
||||
|
||||
for (Map.Entry<String, String> entry : mVdAttributesMap.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
String attribute = presentationMap.get(name);
|
||||
if (attribute.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String svgValue = entry.getValue().trim();
|
||||
String vdValue = colorSvg2Vd(svgValue, "#000000");
|
||||
|
||||
if (vdValue == null) {
|
||||
if (svgValue.endsWith("px")) {
|
||||
vdValue = svgValue.substring(0, svgValue.length() - 2).trim();
|
||||
} else if (svgValue.startsWith("url(#") && svgValue.endsWith(")")) {
|
||||
// Copies gradient from tree.
|
||||
vdValue = svgValue.substring(5, svgValue.length() - 1);
|
||||
if (name.equals(SVG_FILL)) {
|
||||
SvgNode node = getTree().getSvgNodeFromId(vdValue);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
mFillGradientNode = (SvgGradientNode)node.deepCopy();
|
||||
mFillGradientNode.setSvgLeafNode(this);
|
||||
mFillGradientNode.setGradientUsage(SvgGradientNode.GradientUsage.FILL);
|
||||
} else if (name.equals(SVG_STROKE)) {
|
||||
SvgNode node = getTree().getSvgNodeFromId(vdValue);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
mStrokeGradientNode = (SvgGradientNode)node.deepCopy();
|
||||
mStrokeGradientNode.setSvgLeafNode(this);
|
||||
mStrokeGradientNode.setGradientUsage(SvgGradientNode.GradientUsage.STROKE);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
vdValue = svgValue;
|
||||
}
|
||||
}
|
||||
writer.write(System.lineSeparator());
|
||||
writer.write(indent);
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write(attribute);
|
||||
writer.write("=\"");
|
||||
writer.write(vdValue);
|
||||
writer.write("\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SVG path's opacity attribute into fill and stroke.
|
||||
*/
|
||||
private void parsePathOpacity() {
|
||||
double opacity = getOpacityValueFromMap(SVG_OPACITY);
|
||||
double fillOpacity = getOpacityValueFromMap(SVG_FILL_OPACITY);
|
||||
double strokeOpacity = getOpacityValueFromMap(SVG_STROKE_OPACITY);
|
||||
putOpacityValueToMap(SVG_FILL_OPACITY, fillOpacity * opacity);
|
||||
putOpacityValueToMap(SVG_STROKE_OPACITY, strokeOpacity * opacity);
|
||||
mVdAttributesMap.remove(SVG_OPACITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility function to get the opacity value as a floating point number.
|
||||
*
|
||||
* @param attributeName the name of the opacity attribute
|
||||
* @return the clamped opacity value, or 1 if not found
|
||||
*/
|
||||
private double getOpacityValueFromMap(@NonNull String attributeName) {
|
||||
// Default opacity is 1.
|
||||
double result = 1;
|
||||
String opacity = mVdAttributesMap.get(attributeName);
|
||||
if (opacity != null) {
|
||||
try {
|
||||
if (opacity.endsWith("%")) {
|
||||
result = Double.parseDouble(opacity.substring(0, opacity.length() - 1)) / 100.;
|
||||
} else {
|
||||
result = Double.parseDouble(opacity);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore here, an invalid value is replaced by the default value 1.
|
||||
}
|
||||
}
|
||||
return Math.min(Math.max(result, 0), 1);
|
||||
}
|
||||
|
||||
private void putOpacityValueToMap(@NonNull String attributeName, double opacity) {
|
||||
String attributeValue = formatFloatValue(opacity);
|
||||
if (attributeValue.equals("1")) {
|
||||
mVdAttributesMap.remove(attributeName);
|
||||
} else {
|
||||
mVdAttributesMap.put(attributeName, attributeValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dumpNode(@NonNull String indent) {
|
||||
logger.log(Level.FINE, indent + (mPathData != null ? mPathData : " null pathData ") +
|
||||
(mName != null ? mName : " null name "));
|
||||
}
|
||||
|
||||
public void setPathData(@NonNull String pathData) {
|
||||
mPathData = pathData;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPathData() {
|
||||
return mPathData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroupNode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasGradient() {
|
||||
return mFillGradientNode != null || mStrokeGradientNode != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transformIfNeeded(@NonNull AffineTransform rootTransform) {
|
||||
if (mPathData == null || mPathData.isEmpty()) {
|
||||
// Nothing to draw and transform, early return.
|
||||
return;
|
||||
}
|
||||
VdPath.Node[] nodes = PathParser.parsePath(mPathData, ParseMode.SVG);
|
||||
AffineTransform finalTransform = new AffineTransform(rootTransform);
|
||||
finalTransform.concatenate(mStackedTransform);
|
||||
boolean needsConvertRelativeMoveAfterClose = VdPath.Node.hasRelMoveAfterClose(nodes);
|
||||
if (!finalTransform.isIdentity() || needsConvertRelativeMoveAfterClose) {
|
||||
VdPath.Node.transform(finalTransform, nodes);
|
||||
}
|
||||
mPathData = VdPath.Node.nodeListToString(nodes, mSvgTree.getCoordinateFormat());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flatten(@NonNull AffineTransform transform) {
|
||||
mStackedTransform.setTransform(transform);
|
||||
mStackedTransform.concatenate(mLocalTransform);
|
||||
|
||||
if (!"non-scaling-stroke".equals(mVdAttributesMap.get("vector-effect"))
|
||||
&& (mStackedTransform.getType() & AffineTransform.TYPE_MASK_SCALE) != 0) {
|
||||
String strokeWidth = mVdAttributesMap.get(SVG_STROKE_WIDTH);
|
||||
if (strokeWidth != null) {
|
||||
try {
|
||||
// Unlike SVG, vector drawable is not capable of applying transformations
|
||||
// to stroke outline. To compensate for that we apply scaling transformation
|
||||
// to the stroke width, which produces accurate results for uniform and
|
||||
// approximate results for nonuniform scaling transformation.
|
||||
double width = Double.parseDouble(strokeWidth);
|
||||
double determinant = mStackedTransform.getDeterminant();
|
||||
if (determinant != 0) {
|
||||
width *= sqrt(abs(determinant));
|
||||
mVdAttributesMap.put(SVG_STROKE_WIDTH, formatFloatValue(width));
|
||||
}
|
||||
if ((mStackedTransform.getType() & AffineTransform.TYPE_GENERAL_SCALE) != 0) {
|
||||
logWarning("Scaling of the stroke width is approximate");
|
||||
}
|
||||
} catch (NumberFormatException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeXml(@NonNull OutputStreamWriter writer, @NonNull String indent)
|
||||
throws IOException {
|
||||
// First, decide whether or not we can skip this path, since it has no visible effect.
|
||||
if (mPathData == null || mPathData.isEmpty()) {
|
||||
return; // No path to draw.
|
||||
}
|
||||
|
||||
String fillColor = mVdAttributesMap.get(SVG_FILL);
|
||||
String strokeColor = mVdAttributesMap.get(SVG_STROKE);
|
||||
logger.log(Level.FINE, "fill color " + fillColor);
|
||||
boolean emptyFill = "none".equals(fillColor) || "#00000000".equals(fillColor);
|
||||
boolean emptyStroke = strokeColor == null || "none".equals(strokeColor);
|
||||
if (emptyFill && emptyStroke) {
|
||||
return; // Nothing to draw.
|
||||
}
|
||||
|
||||
// Second, write the color info handling the default values.
|
||||
writer.write(indent);
|
||||
writer.write("<path");
|
||||
writer.write(System.lineSeparator());
|
||||
if (fillColor == null && mFillGradientNode == null) {
|
||||
logger.log(Level.FINE, "Adding default fill color");
|
||||
writer.write(indent);
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write("android:fillColor=\"#FF000000\"");
|
||||
writer.write(System.lineSeparator());
|
||||
}
|
||||
if (!emptyStroke
|
||||
&& !mVdAttributesMap.containsKey(SVG_STROKE_WIDTH)
|
||||
&& mStrokeGradientNode == null) {
|
||||
logger.log(Level.FINE, "Adding default stroke width");
|
||||
writer.write(indent);
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write("android:strokeWidth=\"1\"");
|
||||
writer.write(System.lineSeparator());
|
||||
}
|
||||
|
||||
// Last, write the path data and all associated attributes.
|
||||
writer.write(indent);
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write("android:pathData=\"" + mPathData + "\"");
|
||||
writeAttributeValues(writer, indent);
|
||||
if (!hasGradient()) {
|
||||
writer.write('/');
|
||||
}
|
||||
writer.write('>');
|
||||
writer.write(System.lineSeparator());
|
||||
|
||||
if (mFillGradientNode != null) {
|
||||
mFillGradientNode.writeXml(writer, indent + INDENT_UNIT);
|
||||
}
|
||||
if (mStrokeGradientNode != null) {
|
||||
mStrokeGradientNode.writeXml(writer, indent + INDENT_UNIT);
|
||||
}
|
||||
if (hasGradient()) {
|
||||
writer.write(indent);
|
||||
writer.write("</path>");
|
||||
writer.write(System.lineSeparator());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_CLIP_RULE;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_FILL;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_FILL_RULE;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_STROKE;
|
||||
import static com.android.ide.common.vectordrawable.Svg2Vector.SVG_STROKE_WIDTH;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
/** Parent class for a SVG file's node, can be either group or leaf element. */
|
||||
abstract class SvgNode {
|
||||
private static final Logger logger = Logger.getLogger(SvgNode.class.getSimpleName());
|
||||
|
||||
protected static final String INDENT_UNIT = " ";
|
||||
protected static final String CONTINUATION_INDENT = INDENT_UNIT + INDENT_UNIT;
|
||||
private static final String TRANSFORM_TAG = "transform";
|
||||
|
||||
private static final String MATRIX_ATTRIBUTE = "matrix";
|
||||
private static final String TRANSLATE_ATTRIBUTE = "translate";
|
||||
private static final String ROTATE_ATTRIBUTE = "rotate";
|
||||
private static final String SCALE_ATTRIBUTE = "scale";
|
||||
private static final String SKEWX_ATTRIBUTE = "skewX";
|
||||
private static final String SKEWY_ATTRIBUTE = "skewY";
|
||||
|
||||
protected final String mName;
|
||||
// Keep a reference to the tree in order to dump the error log.
|
||||
protected final SvgTree mSvgTree;
|
||||
// Use document element to get the line number for error reporting.
|
||||
protected final Element mDocumentElement;
|
||||
|
||||
// Key is the attributes for vector drawable, and the value is the converted from SVG.
|
||||
protected final Map<String, String> mVdAttributesMap = new HashMap<>();
|
||||
// If mLocalTransform is identity, it is the same as not having any transformation.
|
||||
protected AffineTransform mLocalTransform = new AffineTransform();
|
||||
|
||||
// During the flatten() operation, we need to merge the transformation from top down.
|
||||
// This is the stacked transformation. And this will be used for the path data transform().
|
||||
protected AffineTransform mStackedTransform = new AffineTransform();
|
||||
|
||||
/** While parsing the translate() rotate() ..., update the {@code mLocalTransform}. */
|
||||
SvgNode(@NonNull SvgTree svgTree, @NonNull Element element, @Nullable String name) {
|
||||
mName = name;
|
||||
mSvgTree = svgTree;
|
||||
mDocumentElement = element;
|
||||
// Parse and generate a presentation map.
|
||||
NamedNodeMap a = element.getAttributes();
|
||||
int len = a.getLength();
|
||||
|
||||
for (int itemIndex = 0; itemIndex < len; itemIndex++) {
|
||||
Node n = a.item(itemIndex);
|
||||
String nodeName = n.getNodeName();
|
||||
String nodeValue = n.getNodeValue();
|
||||
// TODO: Handle style here. Refer to Svg2Vector::addStyleToPath().
|
||||
if (Svg2Vector.presentationMap.containsKey(nodeName)) {
|
||||
fillPresentationAttributesInternal(nodeName, nodeValue);
|
||||
}
|
||||
|
||||
if (TRANSFORM_TAG.equals(nodeName)) {
|
||||
logger.log(Level.FINE, nodeName + " " + nodeValue);
|
||||
parseLocalTransform(nodeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void parseLocalTransform(@NonNull String nodeValue) {
|
||||
// We separate the string into multiple parts and look like this:
|
||||
// "translate" "30" "rotate" "4.5e1 5e1 50"
|
||||
nodeValue = nodeValue.replaceAll(",", " ");
|
||||
String[] matrices = nodeValue.split("[()]");
|
||||
AffineTransform parsedTransform;
|
||||
for (int i = 0; i < matrices.length - 1; i += 2) {
|
||||
parsedTransform = parseOneTransform(matrices[i].trim(), matrices[i + 1].trim());
|
||||
if (parsedTransform != null) {
|
||||
mLocalTransform.concatenate(parsedTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static AffineTransform parseOneTransform(String type, String data) {
|
||||
float[] numbers = getNumbers(data);
|
||||
if (numbers == null) {
|
||||
return null;
|
||||
}
|
||||
int numLength = numbers.length;
|
||||
AffineTransform parsedTransform = new AffineTransform();
|
||||
|
||||
if (MATRIX_ATTRIBUTE.equalsIgnoreCase(type)) {
|
||||
if (numLength != 6) {
|
||||
return null;
|
||||
}
|
||||
parsedTransform.setTransform(
|
||||
numbers[0], numbers[1], numbers[2], numbers[3], numbers[4], numbers[5]);
|
||||
} else if (TRANSLATE_ATTRIBUTE.equalsIgnoreCase(type)) {
|
||||
if (numLength != 1 && numLength != 2) {
|
||||
return null;
|
||||
}
|
||||
// Default translateY is 0
|
||||
parsedTransform.translate(numbers[0], numLength == 2 ? numbers[1] : 0);
|
||||
} else if (SCALE_ATTRIBUTE.equalsIgnoreCase(type)) {
|
||||
if (numLength != 1 && numLength != 2) {
|
||||
return null;
|
||||
}
|
||||
// Default scaleY == scaleX
|
||||
parsedTransform.scale(numbers[0], numbers[numLength == 2 ? 1 : 0]);
|
||||
} else if (ROTATE_ATTRIBUTE.equalsIgnoreCase(type)) {
|
||||
if (numLength != 1 && numLength != 3) {
|
||||
return null;
|
||||
}
|
||||
parsedTransform.rotate(
|
||||
Math.toRadians(numbers[0]),
|
||||
numLength == 3 ? numbers[1] : 0,
|
||||
numLength == 3 ? numbers[2] : 0);
|
||||
} else if (SKEWX_ATTRIBUTE.equalsIgnoreCase(type)) {
|
||||
if (numLength != 1) {
|
||||
return null;
|
||||
}
|
||||
// Note that Swing is pass the shear value directly to the matrix as m01 or m10,
|
||||
// while SVG is using tan(a) in the matrix and a is in radians.
|
||||
parsedTransform.shear(Math.tan(Math.toRadians(numbers[0])), 0);
|
||||
} else if (SKEWY_ATTRIBUTE.equalsIgnoreCase(type)) {
|
||||
if (numLength != 1) {
|
||||
return null;
|
||||
}
|
||||
parsedTransform.shear(0, Math.tan(Math.toRadians(numbers[0])));
|
||||
}
|
||||
return parsedTransform;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static float[] getNumbers(String data) {
|
||||
String[] numbers = data.split("\\s+");
|
||||
int len = numbers.length;
|
||||
if (len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
float[] results = new float[len];
|
||||
for (int i = 0; i < len; i++) {
|
||||
results[i] = Float.parseFloat(numbers[i]);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected SvgTree getTree() {
|
||||
return mSvgTree;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Element getDocumentElement() {
|
||||
return mDocumentElement;
|
||||
}
|
||||
|
||||
/** Dumps the current node's debug info. */
|
||||
public abstract void dumpNode(@NonNull String indent);
|
||||
|
||||
/**
|
||||
* Writes content of the node into the VectorDrawable's XML file.
|
||||
*
|
||||
* @param writer the writer to write the group XML element to
|
||||
* @param indent whitespace used for indenting output XML
|
||||
*/
|
||||
public abstract void writeXml(@NonNull OutputStreamWriter writer, @NonNull String indent)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* Calls the {@linkplain Visitor#visit(SvgNode)} method for this node and its descendants.
|
||||
*
|
||||
* @param visitor the visitor to accept
|
||||
*/
|
||||
public VisitResult accept(@NonNull Visitor visitor) {
|
||||
return visitor.visit(this);
|
||||
}
|
||||
|
||||
/** Returns true the node is a group node. */
|
||||
public abstract boolean isGroupNode();
|
||||
|
||||
/** Transforms the current Node with the transformation matrix. */
|
||||
public abstract void transformIfNeeded(@NonNull AffineTransform finalTransform);
|
||||
|
||||
private void fillPresentationAttributesInternal(@NonNull String name, @NonNull String value) {
|
||||
if (name.equals(SVG_FILL_RULE) || name.equals(SVG_CLIP_RULE)) {
|
||||
if (value.equals("nonzero")) {
|
||||
value = "nonZero";
|
||||
} else if (value.equals("evenodd")) {
|
||||
value = "evenOdd";
|
||||
}
|
||||
}
|
||||
logger.log(Level.FINE, ">>>> PROP " + name + " = " + value);
|
||||
if (value.startsWith("url(")) {
|
||||
if (!name.equals(SVG_FILL) && !name.equals(SVG_STROKE)) {
|
||||
logError("Unsupported URL value: " + value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (name.equals(SVG_STROKE_WIDTH) && value.equals("0")) {
|
||||
mVdAttributesMap.remove(SVG_STROKE);
|
||||
}
|
||||
mVdAttributesMap.put(name, value);
|
||||
}
|
||||
|
||||
protected void fillPresentationAttributes(@NonNull String name, @NonNull String value) {
|
||||
fillPresentationAttributesInternal(name, value);
|
||||
}
|
||||
|
||||
public void fillEmptyAttributes(@NonNull Map<String, String> parentAttributesMap) {
|
||||
// Go through the parents' attributes, if the child misses any, then fill it.
|
||||
for (Map.Entry<String, String> entry : parentAttributesMap.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
if (!mVdAttributesMap.containsKey(name)) {
|
||||
mVdAttributesMap.put(name, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void flatten(@NonNull AffineTransform transform);
|
||||
|
||||
/**
|
||||
* Checks validity of the node and logs any issues associated with it. Subclasses may override.
|
||||
*/
|
||||
public void validate() {}
|
||||
|
||||
/**
|
||||
* Returns a string containing the value of the given attribute. Returns an empty string if
|
||||
* the attribute does not exist.
|
||||
*/
|
||||
public String getAttributeValue(@NonNull String attribute) {
|
||||
return mDocumentElement.getAttribute(attribute);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public abstract SvgNode deepCopy();
|
||||
|
||||
protected <T extends SvgNode> void copyFrom(@NonNull T from) {
|
||||
fillEmptyAttributes(from.mVdAttributesMap);
|
||||
mLocalTransform = (AffineTransform) from.mLocalTransform.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an SVG color value to "#RRGGBB" or "#RGB" format used by vector drawables. The input
|
||||
* color value can be "none" and RGB value, e.g. "rgb(255, 0, 0)", or a color name defined in
|
||||
* https://www.w3.org/TR/SVG11/types.html#ColorKeywords.
|
||||
*
|
||||
* @param svgColor the SVG color value to convert
|
||||
* @param errorFallbackColor the value returned if the supplied SVG color value has invalid or
|
||||
* unsupported format
|
||||
* @return the converted value, or null if the given value cannot be interpreted as color
|
||||
*/
|
||||
@Nullable
|
||||
protected String colorSvg2Vd(@NonNull String svgColor, @NonNull String errorFallbackColor) {
|
||||
try {
|
||||
return SvgColor.colorSvg2Vd(svgColor);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logError("Unsupported color format \"" + svgColor + "\"");
|
||||
return errorFallbackColor;
|
||||
}
|
||||
}
|
||||
|
||||
protected void logError(@NonNull String s) {
|
||||
mSvgTree.logError(s, mDocumentElement);
|
||||
}
|
||||
|
||||
protected void logWarning(@NonNull String s) {
|
||||
mSvgTree.logWarning(s, mDocumentElement);
|
||||
}
|
||||
|
||||
protected interface Visitor {
|
||||
/**
|
||||
* Called by the {@link SvgNode#accept(Visitor)} method for every visited node.
|
||||
*
|
||||
* @param node the node being visited
|
||||
* @return {@link VisitResult#CONTINUE} to continue visiting children,
|
||||
* {@link VisitResult#SKIP_CHILDREN} to skip children and continue visit with
|
||||
* the next sibling, {@link VisitResult#ABORT} to skip all remaining nodes
|
||||
*/
|
||||
VisitResult visit(@NonNull SvgNode node);
|
||||
}
|
||||
|
||||
protected enum VisitResult {
|
||||
CONTINUE,
|
||||
SKIP_CHILDREN,
|
||||
ABORT
|
||||
}
|
||||
|
||||
protected enum ClipRule {
|
||||
NON_ZERO,
|
||||
EVEN_ODD
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,501 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.ide.common.vectordrawable.SvgNode.CONTINUATION_INDENT;
|
||||
import static com.android.ide.common.vectordrawable.SvgNode.INDENT_UNIT;
|
||||
import static com.android.utils.PositionXmlParser.getPosition;
|
||||
import static com.android.utils.XmlUtils.formatFloatValue;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.android.utils.Pair;
|
||||
import com.android.utils.PositionXmlParser;
|
||||
import com.google.common.base.Preconditions;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
/**
|
||||
* Represents the SVG file in an internal data structure as a tree.
|
||||
*/
|
||||
class SvgTree {
|
||||
private static final Logger logger = Logger.getLogger(SvgTree.class.getSimpleName());
|
||||
|
||||
private static final String HEAD =
|
||||
"<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"";
|
||||
private static final String AAPT_BOUND = "xmlns:aapt=\"http://schemas.android.com/aapt\"";
|
||||
|
||||
public static final String SVG_WIDTH = "width";
|
||||
public static final String SVG_HEIGHT = "height";
|
||||
public static final String SVG_VIEW_BOX = "viewBox";
|
||||
|
||||
private float w = -1;
|
||||
private float h = -1;
|
||||
private final AffineTransform mRootTransform = new AffineTransform();
|
||||
private float[] viewBox;
|
||||
private float mScaleFactor = 1;
|
||||
|
||||
private SvgGroupNode mRoot;
|
||||
private String mFileName;
|
||||
|
||||
private final List<LogMessage> mLogMessages = new ArrayList<>();
|
||||
|
||||
private boolean mHasLeafNode;
|
||||
|
||||
private boolean mHasGradient;
|
||||
|
||||
/** Map of SvgNode's id to the SvgNode. */
|
||||
private final Map<String, SvgNode> mIdMap = new HashMap<>();
|
||||
|
||||
/** IDs of ignored SVG nodes. */
|
||||
private final Set<String> mIgnoredIds = new HashSet<>();
|
||||
|
||||
/** Set of SvgGroupNodes that contain use elements. */
|
||||
private final Set<SvgGroupNode> mPendingUseGroupSet = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Key is SvgNode that references a clipPath. Value is SvgGroupNode that is the parent of that
|
||||
* SvgNode.
|
||||
*/
|
||||
private final Map<SvgNode, Pair<SvgGroupNode, String>> mClipPathAffectedNodes =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Key is String that is the id of a style class. Value is set of SvgNodes referencing that
|
||||
* class.
|
||||
*/
|
||||
private final Map<String, Set<SvgNode>> mStyleAffectedNodes = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Key is String that is the id of a style class. Value is a String that contains attribute
|
||||
* information of that style class.
|
||||
*/
|
||||
private final Map<String, String> mStyleClassAttributeMap = new HashMap<>();
|
||||
|
||||
enum SvgLogLevel {
|
||||
ERROR,
|
||||
WARNING
|
||||
}
|
||||
|
||||
private static class LogMessage implements Comparable<LogMessage> {
|
||||
final SvgLogLevel level;
|
||||
final int line;
|
||||
final String message;
|
||||
|
||||
/**
|
||||
* Initializes a log message.
|
||||
*
|
||||
* @param level the severity level
|
||||
* @param line the line number of the SVG file the message applies to,
|
||||
* or zero if the message applies to the whole file
|
||||
* @param message the text of the message
|
||||
*/
|
||||
LogMessage(@NonNull SvgLogLevel level, int line, @NonNull String message) {
|
||||
this.level = level;
|
||||
this.line = line;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getFormattedMessage() {
|
||||
return level.name() + (line == 0 ? "" : " @ line " + line) + ": " + message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull LogMessage other) {
|
||||
int cmp = level.compareTo(other.level);
|
||||
if (cmp != 0) {
|
||||
return cmp;
|
||||
}
|
||||
cmp = Integer.compare(line, other.line);
|
||||
if (cmp != 0) {
|
||||
return cmp;
|
||||
}
|
||||
return message.compareTo(other.message);
|
||||
}
|
||||
}
|
||||
|
||||
public float getWidth() {
|
||||
return w;
|
||||
}
|
||||
|
||||
public float getHeight() {
|
||||
return h;
|
||||
}
|
||||
|
||||
public float getScaleFactor() {
|
||||
return mScaleFactor;
|
||||
}
|
||||
|
||||
public void setHasLeafNode(boolean hasLeafNode) {
|
||||
mHasLeafNode = hasLeafNode;
|
||||
}
|
||||
|
||||
public void setHasGradient(boolean hasGradient) {
|
||||
mHasGradient = hasGradient;
|
||||
}
|
||||
|
||||
public float[] getViewBox() {
|
||||
return viewBox;
|
||||
}
|
||||
|
||||
/** From the root, top down, pass the transformation (TODO: attributes) down the children. */
|
||||
public void flatten() {
|
||||
mRoot.flatten(new AffineTransform());
|
||||
}
|
||||
|
||||
/** Validates all nodes and logs any encountered issues. */
|
||||
public void validate() {
|
||||
mRoot.validate();
|
||||
if (mLogMessages.isEmpty() && !getHasLeafNode()) {
|
||||
logError("No vector content found", null);
|
||||
}
|
||||
}
|
||||
|
||||
public Document parse(@NonNull File f, @NonNull List<String> parseErrors) throws IOException {
|
||||
mFileName = f.getName();
|
||||
try {
|
||||
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(f));
|
||||
return PositionXmlParser.parse(inputStream, false, parseErrors);
|
||||
} catch (ParserConfigurationException e) {
|
||||
throw new Error("Internal error", e); // Should not happen unless there is a bug.
|
||||
}
|
||||
}
|
||||
|
||||
public void normalize() {
|
||||
// mRootTransform is always setup, now just need to apply the viewbox info into.
|
||||
mRootTransform.preConcatenate(new AffineTransform(1, 0, 0, 1, -viewBox[0], -viewBox[1]));
|
||||
transform(mRootTransform);
|
||||
|
||||
logger.log(Level.FINE, "matrix=" + mRootTransform);
|
||||
}
|
||||
|
||||
private void transform(@NonNull AffineTransform rootTransform) {
|
||||
mRoot.transformIfNeeded(rootTransform);
|
||||
}
|
||||
|
||||
public void dump() {
|
||||
logger.log(Level.FINE, "file: " + mFileName);
|
||||
mRoot.dumpNode("");
|
||||
}
|
||||
|
||||
public void setRoot(@NonNull SvgGroupNode root) {
|
||||
mRoot = root;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SvgGroupNode getRoot() {
|
||||
return mRoot;
|
||||
}
|
||||
|
||||
public void logError(@NonNull String s, @Nullable Node node) {
|
||||
logErrorLine(s, node, SvgLogLevel.ERROR);
|
||||
}
|
||||
|
||||
public void logWarning(@NonNull String s, @Nullable Node node) {
|
||||
logErrorLine(s, node, SvgLogLevel.WARNING);
|
||||
}
|
||||
|
||||
void logErrorLine(@NonNull String s, @Nullable Node node, @NonNull SvgLogLevel level) {
|
||||
Preconditions.checkArgument(!s.isEmpty());
|
||||
int line = node == null ? 0 : getStartLine(node);
|
||||
mLogMessages.add(new LogMessage(level, line, s));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message that combines all logged errors and warnings. If there were no
|
||||
* errors, returns an empty string.
|
||||
*/
|
||||
@NonNull
|
||||
public String getErrorMessage() {
|
||||
if (mLogMessages.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
Collections.sort(mLogMessages); // Sort by severity and line number.
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (LogMessage message : mLogMessages) {
|
||||
if (result.length() != 0) {
|
||||
result.append('\n');
|
||||
}
|
||||
result.append(message.getFormattedMessage());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/** Returns true when there is at least one valid child. */
|
||||
public boolean getHasLeafNode() {
|
||||
return mHasLeafNode;
|
||||
}
|
||||
|
||||
public boolean getHasGradient() {
|
||||
return mHasGradient;
|
||||
}
|
||||
|
||||
/** Returns the 1-based start line number of the given node. */
|
||||
public static int getStartLine(@NonNull Node node) {
|
||||
return getPosition(node).getStartLine() + 1;
|
||||
}
|
||||
|
||||
public float getViewportWidth() {
|
||||
return (viewBox == null) ? -1 : viewBox[2];
|
||||
}
|
||||
|
||||
public float getViewportHeight() { return (viewBox == null) ? -1 : viewBox[3]; }
|
||||
|
||||
private enum SizeType {
|
||||
PIXEL,
|
||||
PERCENTAGE
|
||||
}
|
||||
|
||||
public void parseDimension(@NonNull Node nNode) {
|
||||
NamedNodeMap a = nNode.getAttributes();
|
||||
int len = a.getLength();
|
||||
SizeType widthType = SizeType.PIXEL;
|
||||
SizeType heightType = SizeType.PIXEL;
|
||||
for (int i = 0; i < len; i++) {
|
||||
Node n = a.item(i);
|
||||
String name = n.getNodeName().trim();
|
||||
String value = n.getNodeValue().trim();
|
||||
int subStringSize = value.length();
|
||||
SizeType currentType = SizeType.PIXEL;
|
||||
String unit = value.substring(Math.max(value.length() - 2, 0));
|
||||
if (unit.matches("em|ex|px|in|cm|mm|pt|pc")) {
|
||||
subStringSize -= 2;
|
||||
} else if (value.endsWith("%")) {
|
||||
subStringSize -= 1;
|
||||
currentType = SizeType.PERCENTAGE;
|
||||
}
|
||||
|
||||
if (SVG_WIDTH.equals(name)) {
|
||||
w = Float.parseFloat(value.substring(0, subStringSize));
|
||||
widthType = currentType;
|
||||
} else if (SVG_HEIGHT.equals(name)) {
|
||||
h = Float.parseFloat(value.substring(0, subStringSize));
|
||||
heightType = currentType;
|
||||
} else if (SVG_VIEW_BOX.equals(name)) {
|
||||
viewBox = new float[4];
|
||||
String[] strbox = value.split(" ");
|
||||
for (int j = 0; j < viewBox.length; j++) {
|
||||
viewBox[j] = Float.parseFloat(strbox[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there is no viewbox, then set it up according to w, h.
|
||||
// From now on, viewport should be read from viewBox, and size should be from w and h.
|
||||
// w and h can be set to percentage too, in this case, set it to the viewbox size.
|
||||
if (viewBox == null && w > 0 && h > 0) {
|
||||
viewBox = new float[4];
|
||||
viewBox[2] = w;
|
||||
viewBox[3] = h;
|
||||
} else if ((w < 0 || h < 0) && viewBox != null) {
|
||||
w = viewBox[2];
|
||||
h = viewBox[3];
|
||||
}
|
||||
|
||||
if (widthType == SizeType.PERCENTAGE && w > 0) {
|
||||
w = viewBox[2] * w / 100;
|
||||
}
|
||||
if (heightType == SizeType.PERCENTAGE && h > 0) {
|
||||
h = viewBox[3] * h / 100;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an X coordinate of a width value that can be an absolute number or percentage of
|
||||
* the viewport size.
|
||||
*
|
||||
* @param value the value to parse
|
||||
* @return the parsed value
|
||||
* @throws IllegalArgumentException if the value is not a valid floating point number or
|
||||
* percentage
|
||||
*/
|
||||
public double parseXValue(@NonNull String value) {
|
||||
return parseCoordinateOrLength(value, getViewportWidth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Y coordinate of a height value that can be an absolute number or percentage of
|
||||
* the viewport size.
|
||||
*
|
||||
* @param value the value to parse
|
||||
* @return the parsed value
|
||||
* @throws IllegalArgumentException if the value is not a valid floating point number or
|
||||
* percentage
|
||||
*/
|
||||
public double parseYValue(@NonNull String value) {
|
||||
return parseCoordinateOrLength(value, getViewportHeight());
|
||||
}
|
||||
|
||||
private static double parseCoordinateOrLength(@NonNull String value, double percentageBase) {
|
||||
if (value.endsWith("%")) {
|
||||
return Double.parseDouble(value.substring(0, value.length() - 1)) / 100
|
||||
* percentageBase;
|
||||
} else {
|
||||
return Double.parseDouble(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void addIdToMap(@NonNull String id, @NonNull SvgNode svgNode) {
|
||||
mIdMap.put(id, svgNode);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SvgNode getSvgNodeFromId(@NonNull String id) {
|
||||
return mIdMap.get(id);
|
||||
}
|
||||
|
||||
public void addToPendingUseSet(@NonNull SvgGroupNode useGroup) {
|
||||
mPendingUseGroupSet.add(useGroup);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Set<SvgGroupNode> getPendingUseSet() {
|
||||
return mPendingUseGroupSet;
|
||||
}
|
||||
|
||||
public void addIgnoredId(@NonNull String id) {
|
||||
mIgnoredIds.add(id);
|
||||
}
|
||||
|
||||
public boolean isIdIgnored(@NonNull String id) {
|
||||
return mIgnoredIds.contains(id);
|
||||
}
|
||||
|
||||
public void addClipPathAffectedNode(
|
||||
@NonNull SvgNode child,
|
||||
@NonNull SvgGroupNode currentGroup,
|
||||
@NonNull String clipPathName) {
|
||||
mClipPathAffectedNodes.put(child, Pair.of(currentGroup, clipPathName));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Set<Map.Entry<SvgNode, Pair<SvgGroupNode, String>>> getClipPathAffectedNodesSet() {
|
||||
return mClipPathAffectedNodes.entrySet();
|
||||
}
|
||||
|
||||
/** Adds child to set of SvgNodes that reference the style class with id className. */
|
||||
public void addAffectedNodeToStyleClass(@NonNull String className, @NonNull SvgNode child) {
|
||||
if (mStyleAffectedNodes.containsKey(className)) {
|
||||
mStyleAffectedNodes.get(className).add(child);
|
||||
} else {
|
||||
Set<SvgNode> styleNodesSet = new HashSet<>();
|
||||
styleNodesSet.add(child);
|
||||
mStyleAffectedNodes.put(className, styleNodesSet);
|
||||
}
|
||||
}
|
||||
|
||||
public void addStyleClassToTree(@NonNull String className, @NonNull String attributes) {
|
||||
mStyleClassAttributeMap.put(className, attributes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getStyleClassAttr(@NonNull String classname) {
|
||||
return mStyleClassAttributeMap.get(classname);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Set<Map.Entry<String, Set<SvgNode>>> getStyleAffectedNodes() {
|
||||
return mStyleAffectedNodes.entrySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the parent node of the input node.
|
||||
*
|
||||
* @return the parent node, or null if node is not in the tree.
|
||||
*/
|
||||
@Nullable
|
||||
public SvgGroupNode findParent(@NonNull SvgNode node) {
|
||||
return mRoot.findParent(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link NumberFormat] of sufficient precision to use for formatting coordinate
|
||||
* values within the viewport.
|
||||
*/
|
||||
@NonNull
|
||||
public NumberFormat getCoordinateFormat() {
|
||||
float viewportWidth = getViewportWidth();
|
||||
float viewportHeight = getViewportHeight();
|
||||
return VdUtil.getCoordinateFormat(Math.max(viewportHeight, viewportWidth));
|
||||
}
|
||||
|
||||
public void writeXml(@NonNull OutputStream stream) throws IOException {
|
||||
if (mRoot == null) {
|
||||
throw new IllegalStateException("SvgTree is not fully initialized");
|
||||
}
|
||||
|
||||
OutputStreamWriter writer = new OutputStreamWriter(stream, UTF_8);
|
||||
writer.write(HEAD);
|
||||
writer.write(System.lineSeparator());
|
||||
if (getHasGradient()) {
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write(AAPT_BOUND);
|
||||
writer.write(System.lineSeparator());
|
||||
}
|
||||
float viewportWidth = getViewportWidth();
|
||||
float viewportHeight = getViewportHeight();
|
||||
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write("android:width=\"");
|
||||
writer.write(formatFloatValue(getWidth() * getScaleFactor()));
|
||||
writer.write("dp\"");
|
||||
writer.write(System.lineSeparator());
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write("android:height=\"");
|
||||
writer.write(formatFloatValue(getHeight() * getScaleFactor()));
|
||||
writer.write("dp\"");
|
||||
writer.write(System.lineSeparator());
|
||||
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write("android:viewportWidth=\"");
|
||||
writer.write(formatFloatValue(viewportWidth));
|
||||
writer.write("\"");
|
||||
writer.write(System.lineSeparator());
|
||||
writer.write(CONTINUATION_INDENT);
|
||||
writer.write("android:viewportHeight=\"");
|
||||
writer.write(formatFloatValue(viewportHeight));
|
||||
writer.write("\">");
|
||||
writer.write(System.lineSeparator());
|
||||
|
||||
normalize();
|
||||
mRoot.writeXml(writer, INDENT_UNIT);
|
||||
writer.write("</vector>");
|
||||
writer.write(System.lineSeparator());
|
||||
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
|
||||
/** Used to represent one VectorDrawable's element, can be a group or path. */
|
||||
abstract class VdElement {
|
||||
String mName;
|
||||
|
||||
boolean isClipPath;
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public abstract void draw(Graphics2D g, AffineTransform currentMatrix, float scaleX, float scaleY);
|
||||
|
||||
public abstract void parseAttributes(NamedNodeMap attributes);
|
||||
|
||||
public abstract boolean isGroup();
|
||||
|
||||
public void setClipPath(boolean isClip) {
|
||||
isClipPath = isClip;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.util.ArrayList;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
|
||||
/**
|
||||
* Used to represent one VectorDrawble's group element.
|
||||
*/
|
||||
class VdGroup extends VdElement{
|
||||
private static final Logger logger = Logger.getLogger(VdGroup.class.getSimpleName());
|
||||
|
||||
private static final String GROUP_ROTATION = "android:rotation";
|
||||
private static final String GROUP_PIVOTX = "android:pivotX";
|
||||
private static final String GROUP_PIVOTY = "android:pivotY";
|
||||
private static final String GROUP_TRANSLATEX = "android:translateX";
|
||||
private static final String GROUP_TRANSLATEY = "android:translateY";
|
||||
private static final String GROUP_SCALEX = "android:scaleX";
|
||||
private static final String GROUP_SCALEY = "android:scaleY";
|
||||
private static final String GROUP_NAME = "android:name";
|
||||
|
||||
private float mRotate = 0;
|
||||
private float mPivotX = 0;
|
||||
private float mPivotY = 0;
|
||||
private float mScaleX = 1;
|
||||
private float mScaleY = 1;
|
||||
private float mTranslateX = 0;
|
||||
private float mTranslateY = 0;
|
||||
|
||||
// Used at draw time, basically accumulative matrix from root to current group.
|
||||
private final AffineTransform mTempStackedMatrix = new AffineTransform();
|
||||
|
||||
// The current group's transformation.
|
||||
private final AffineTransform mLocalMatrix = new AffineTransform();
|
||||
|
||||
// Children can be either a {@link VdPath} or {@link VdGroup}
|
||||
private final ArrayList<VdElement> mChildren = new ArrayList<>();
|
||||
|
||||
public void add(VdElement pathOrGroup) {
|
||||
mChildren.add(pathOrGroup);
|
||||
}
|
||||
|
||||
public ArrayList<VdElement> getChildren() {
|
||||
return mChildren;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mChildren.size();
|
||||
}
|
||||
|
||||
// Src = trans * src, this is called preConcatenate() in Swing, but postConcatenate() in Android
|
||||
private static void androidPostTransform(AffineTransform src, AffineTransform trans) {
|
||||
src.preConcatenate(trans);
|
||||
}
|
||||
|
||||
private void updateLocalMatrix() {
|
||||
// The order we apply is the same as the
|
||||
// RenderNode.cpp::applyViewPropertyTransforms().
|
||||
mLocalMatrix.setToIdentity();
|
||||
|
||||
// In Android framework, the transformation is applied in
|
||||
// VectorDrawable.java VGroup::updateLocalMatrix()
|
||||
AffineTransform tempTrans = new AffineTransform();
|
||||
tempTrans.setToIdentity();
|
||||
tempTrans.translate(-mPivotX, -mPivotY);
|
||||
androidPostTransform(mLocalMatrix, tempTrans);
|
||||
|
||||
tempTrans.setToIdentity();
|
||||
tempTrans.scale(mScaleX, mScaleY);
|
||||
androidPostTransform(mLocalMatrix, tempTrans);
|
||||
|
||||
tempTrans.setToIdentity();
|
||||
tempTrans.rotate(mRotate * 3.1415926 / 180, 0, 0);
|
||||
androidPostTransform(mLocalMatrix, tempTrans);
|
||||
|
||||
tempTrans.setToIdentity();
|
||||
tempTrans.translate(mTranslateX + mPivotX, mTranslateY + mPivotY);
|
||||
androidPostTransform(mLocalMatrix, tempTrans);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Graphics2D g, AffineTransform currentMatrix, float scaleX, float scaleY) {
|
||||
// SWING default is pre-concatenate
|
||||
mTempStackedMatrix.setTransform(currentMatrix);
|
||||
mTempStackedMatrix.concatenate(mLocalMatrix);
|
||||
|
||||
for (VdElement m : mChildren) {
|
||||
m.draw(g, mTempStackedMatrix, scaleX, scaleY);
|
||||
}
|
||||
|
||||
// This only applies to the flattened SVG tree structure.
|
||||
// One clip-path applies to a single group.
|
||||
g.setClip(null);
|
||||
}
|
||||
|
||||
private void setNameValue(String name, String value) {
|
||||
if (GROUP_ROTATION.equals(name)) {
|
||||
mRotate = Float.parseFloat(value);
|
||||
} else if (GROUP_PIVOTX.equals(name)) {
|
||||
mPivotX = Float.parseFloat(value);
|
||||
} else if (GROUP_PIVOTY.equals(name)) {
|
||||
mPivotY = Float.parseFloat(value);
|
||||
} else if (GROUP_TRANSLATEX.equals(name)) {
|
||||
mTranslateX = Float.parseFloat(value);
|
||||
} else if (GROUP_TRANSLATEY.equals(name)) {
|
||||
mTranslateY = Float.parseFloat(value);
|
||||
} else if (GROUP_SCALEX.equals(name)) {
|
||||
mScaleX = Float.parseFloat(value);
|
||||
} else if (GROUP_SCALEY.equals(name)) {
|
||||
mScaleY = Float.parseFloat(value);
|
||||
} else if (GROUP_NAME.equals(name)) {
|
||||
mName = value;
|
||||
} else {
|
||||
logger.log(Level.WARNING, ">>>>>> DID NOT UNDERSTAND ! \"" + name + "\" <<<<");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseAttributes(NamedNodeMap attributes) {
|
||||
int len = attributes.getLength();
|
||||
for (int i = 0; i < len; i++) {
|
||||
String name = attributes.item(i).getNodeName();
|
||||
String value = attributes.item(i).getNodeValue();
|
||||
setNameValue(name, value);
|
||||
}
|
||||
|
||||
updateLocalMatrix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Group:" +
|
||||
" Name: " +
|
||||
mName +
|
||||
" mTranslateX: " +
|
||||
mTranslateX +
|
||||
" mTranslateY:" +
|
||||
mTranslateY +
|
||||
" mScaleX:" +
|
||||
mScaleX +
|
||||
" mScaleY:" +
|
||||
mScaleY +
|
||||
" mPivotX:" +
|
||||
mPivotX +
|
||||
" mPivotY:" +
|
||||
mPivotY +
|
||||
" mRotate:" +
|
||||
mRotate;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,330 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.SdkConstants.DOT_XML;
|
||||
import static java.awt.RenderingHints.KEY_ANTIALIASING;
|
||||
import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING;
|
||||
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
|
||||
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.android.ide.common.util.AssetUtil;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Shape;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.BufferedImageOp;
|
||||
import java.awt.image.ByteLookupTable;
|
||||
import java.awt.image.LookupOp;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import javax.swing.Icon;
|
||||
|
||||
/**
|
||||
* VdIcon wraps every vector drawable from Material Library into an icon. All of them are shown in
|
||||
* a table for developer to pick.
|
||||
*/
|
||||
public class VdIcon implements Icon, Comparable<VdIcon> {
|
||||
/** Common prefix for most of the vector icons */
|
||||
private static final String ICON_PREFIX = "ic_";
|
||||
|
||||
/** Common prefix for material icons */
|
||||
private static final String FILLED_PREFIX = "baseline_";
|
||||
|
||||
/** Common prefix for material icons */
|
||||
private static final String OUTLINE_PREFIX = "outline_";
|
||||
|
||||
/** Common prefix for material icons */
|
||||
private static final String ROUND_PREFIX = "round_";
|
||||
|
||||
/** Common prefix for material icons */
|
||||
private static final String SHARP_PREFIX = "sharp_";
|
||||
|
||||
/** Common prefix for material icons */
|
||||
private static final String TWO_TONE_PREFIX = "twotone_";
|
||||
|
||||
/** Common suffix for most of the vector icons */
|
||||
private static final String ICON_SUFFIX = "_24.xml";
|
||||
|
||||
/** Distance between the icon and the label */
|
||||
public static final int LABEL_GAP = 10;
|
||||
|
||||
private final VdTree mVdTree;
|
||||
|
||||
private final String mName;
|
||||
|
||||
private final URL mUrl;
|
||||
|
||||
private boolean mDrawCheckerBoardBackground;
|
||||
|
||||
private String mDisplayName;
|
||||
|
||||
private boolean mShowName;
|
||||
|
||||
private final Rectangle myRectangle = new Rectangle();
|
||||
|
||||
private final int mWidth;
|
||||
|
||||
private final int mHeight;
|
||||
|
||||
private final Color mBackground;
|
||||
|
||||
@SuppressWarnings({"InspectionUsingGrayColors", "UseJBColor"})
|
||||
private static final Color CHECKER_COLOR = new Color(238, 238, 238);
|
||||
|
||||
private static final byte[] COLOR_INVERSION_TABLE = new byte[256];
|
||||
static {
|
||||
for (int counter = 0; counter < 256; counter++) {
|
||||
COLOR_INVERSION_TABLE[counter] = (byte) (3 * (255 - counter) / 4);
|
||||
}
|
||||
}
|
||||
|
||||
public VdIcon(@NonNull URL url) throws IOException {
|
||||
this(url, 0, 0);
|
||||
}
|
||||
|
||||
public VdIcon(@NonNull URL url, int width, int height) throws IOException {
|
||||
mVdTree = parseVdTree(url);
|
||||
mUrl = url;
|
||||
String fileName = url.getFile();
|
||||
mName = fileName.substring(fileName.lastIndexOf('/') + 1);
|
||||
if (width != 0 && height != 0) {
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
}
|
||||
else {
|
||||
mWidth = (int)mVdTree.getPortWidth();
|
||||
mHeight = (int)mVdTree.getPortHeight();
|
||||
}
|
||||
mBackground = null;
|
||||
}
|
||||
|
||||
public VdIcon(VdIcon icon, Color background) {
|
||||
mVdTree = icon.mVdTree;
|
||||
mUrl = icon.mUrl;
|
||||
mName = icon.mName;
|
||||
mWidth = icon.mWidth;
|
||||
mHeight = icon.mHeight;
|
||||
mBackground = background;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getDisplayName() {
|
||||
if (mDisplayName == null) {
|
||||
// Turn a filename into a reasonable display name, similar to what
|
||||
// is shown on https://design.google.com/icons/ . For example, change
|
||||
// "ic_supervisor_account_black_24dp.xml" into "supervisor account"
|
||||
int begin = 0;
|
||||
if (mName.startsWith(ICON_PREFIX)) {
|
||||
begin = ICON_PREFIX.length();
|
||||
} else if (mName.startsWith(FILLED_PREFIX)) {
|
||||
begin = FILLED_PREFIX.length();
|
||||
} else if (mName.startsWith(OUTLINE_PREFIX)) {
|
||||
begin = OUTLINE_PREFIX.length();
|
||||
} else if (mName.startsWith(ROUND_PREFIX)) {
|
||||
begin = ROUND_PREFIX.length();
|
||||
} else if (mName.startsWith(SHARP_PREFIX)) {
|
||||
begin = SHARP_PREFIX.length();
|
||||
} else if (mName.startsWith(TWO_TONE_PREFIX)) {
|
||||
begin = TWO_TONE_PREFIX.length();
|
||||
}
|
||||
|
||||
int end = mName.length();
|
||||
if (mName.endsWith(ICON_SUFFIX)) {
|
||||
end -= ICON_SUFFIX.length();
|
||||
} else if (mName.endsWith(DOT_XML)) {
|
||||
end -= DOT_XML.length();
|
||||
}
|
||||
mDisplayName = mName.substring(begin, end).replace('_', ' ');
|
||||
}
|
||||
|
||||
return mDisplayName;
|
||||
}
|
||||
|
||||
public URL getURL() {
|
||||
return mUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the icon image.
|
||||
*
|
||||
* @param width the width of the image
|
||||
* @param height the height of the image
|
||||
*/
|
||||
@Nullable
|
||||
public BufferedImage renderIcon(int width, int height) {
|
||||
if (mVdTree == null) {
|
||||
return null;
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
width = Math.round(mVdTree.getBaseWidth());
|
||||
height = Math.round(mVdTree.getBaseHeight());
|
||||
}
|
||||
BufferedImage image = AssetUtil.newArgbBufferedImage(width, height);
|
||||
mVdTree.drawIntoImage(image);
|
||||
return image;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static VdTree parseVdTree(URL url) throws IOException {
|
||||
return VdParser.parse(url.openStream(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Merge this code back with GraphicsUtil in idea.
|
||||
* Paints a checkered board style background. Each grid square is {@code cellSize} pixels.
|
||||
*/
|
||||
public static void paintCheckeredBackground(Graphics g, Color backgroundColor,
|
||||
Color checkeredColor, Shape clip, int cellSize) {
|
||||
final Shape savedClip = g.getClip();
|
||||
((Graphics2D)g).clip(clip);
|
||||
|
||||
final Rectangle rect = clip.getBounds();
|
||||
g.setColor(backgroundColor);
|
||||
g.fillRect(rect.x, rect.y, rect.width, rect.height);
|
||||
g.setColor(checkeredColor);
|
||||
for (int dy = 0; dy * cellSize < rect.height; dy++) {
|
||||
for (int dx = dy % 2; dx * cellSize < rect.width; dx += 2) {
|
||||
g.fillRect(rect.x + dx * cellSize, rect.y + dy * cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
|
||||
g.setClip(savedClip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintIcon(Component c, Graphics gc, int x, int y) {
|
||||
Graphics2D g = (Graphics2D)gc;
|
||||
|
||||
// Draw the checker board first, even when the tree is empty.
|
||||
int width = c.getWidth();
|
||||
int height = c.getHeight();
|
||||
myRectangle.setBounds(0, 0, width, height);
|
||||
if (mBackground != null) {
|
||||
g.setColor(mBackground);
|
||||
g.fillRect(myRectangle.x, myRectangle.y, myRectangle.width, myRectangle.height);
|
||||
}
|
||||
else if (mDrawCheckerBoardBackground) {
|
||||
//noinspection UseJBColor
|
||||
paintCheckeredBackground(g, Color.LIGHT_GRAY, CHECKER_COLOR, myRectangle, 8);
|
||||
}
|
||||
|
||||
if (mVdTree == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show label below the icon?
|
||||
if (mShowName) {
|
||||
// Yes, so set aside space for the label
|
||||
FontMetrics fontMetrics = c.getFontMetrics(c.getFont());
|
||||
String displayName = getDisplayName();
|
||||
Rectangle2D bounds = fontMetrics.getStringBounds(displayName, g);
|
||||
|
||||
height -= (bounds.getHeight() + LABEL_GAP);
|
||||
int textX = Math.max(0, (int)(width - bounds.getWidth()) / 2);
|
||||
int textY = height + LABEL_GAP;
|
||||
|
||||
final Shape prevClip = g.getClip();
|
||||
g.clip(myRectangle);
|
||||
g.setColor(c.getForeground());
|
||||
|
||||
// Setup text antialiasing:
|
||||
g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
|
||||
//noinspection HardCodedStringLiteral
|
||||
Map map = (Map) Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints");
|
||||
if (map != null) {
|
||||
g.addRenderingHints(map);
|
||||
} else {
|
||||
g.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_LCD_HRGB);
|
||||
}
|
||||
g.drawString(displayName, textX, textY);
|
||||
g.setClip(prevClip);
|
||||
}
|
||||
|
||||
int minSize = Math.min(width, height);
|
||||
BufferedImage image = AssetUtil.newArgbBufferedImage(minSize, minSize);
|
||||
mVdTree.drawIntoImage(image);
|
||||
|
||||
image = adjustIconColor(c, image);
|
||||
|
||||
// Draw in the center of the component (we've already subtracted out the font height above if showing titles)
|
||||
Rectangle rect = new Rectangle(0, 0, width, height);
|
||||
AssetUtil.drawCenterInside(g, image, rect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the icon color when the icon is intended to be painted on top of the given
|
||||
* component. This method will convert the black icons to a light gray icon if it's being
|
||||
* painted on a component with a dark background.
|
||||
*
|
||||
* @param component the component the icon is intended to be painted on top of
|
||||
* @param image the icon image
|
||||
* @return the converted image, or the original image if the background is light
|
||||
*/
|
||||
@NonNull
|
||||
public static BufferedImage adjustIconColor(@NonNull Component component,
|
||||
@NonNull BufferedImage image) {
|
||||
Color background = component.getBackground();
|
||||
if (background != null && background.getRed() < 128) {
|
||||
ByteLookupTable table = new ByteLookupTable(0, COLOR_INVERSION_TABLE);
|
||||
BufferedImageOp invertFilter = new LookupOp(table, null);
|
||||
image = invertFilter.filter(image, null);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull VdIcon other) {
|
||||
return mName.compareTo(other.mName);
|
||||
}
|
||||
|
||||
public void enableCheckerBoardBackground(boolean enable) {
|
||||
mDrawCheckerBoardBackground = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we should show the title displayed below the image. When this is on, the icon is made
|
||||
* smaller to fit the font height.
|
||||
*/
|
||||
public void setShowName(boolean showName) {
|
||||
this.mShowName = showName;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,394 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import java.awt.geom.Path2D;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Given an array of {@link VdPath.Node}, generates a Path2D object.
|
||||
* In other words, this is the engine which converts the pathData into
|
||||
* a Path2D object, which is able to draw on Swing components.
|
||||
* The logic and math here are the same as PathParser.java in framework.
|
||||
*/
|
||||
class VdNodeRender {
|
||||
private static final Logger LOGGER = Logger.getLogger(VdNodeRender.class.getSimpleName());
|
||||
|
||||
public static void createPath(VdPath.Node[] nodes, Path2D path) {
|
||||
float[] current = new float[6];
|
||||
char lastCmd = ' ';
|
||||
for (VdPath.Node node : nodes) {
|
||||
addCommand(path, current, node.getType(), lastCmd, node.getParams());
|
||||
lastCmd = node.getType();
|
||||
}
|
||||
}
|
||||
|
||||
private static void addCommand(Path2D path, float[] current, char cmd,
|
||||
char lastCmd, float[] val) {
|
||||
int incr = 2;
|
||||
|
||||
float cx = current[0];
|
||||
float cy = current[1];
|
||||
float cpx = current[2];
|
||||
float cpy = current[3];
|
||||
float loopX = current[4];
|
||||
float loopY = current[5];
|
||||
|
||||
switch (cmd) {
|
||||
case 'z':
|
||||
case 'Z':
|
||||
path.closePath();
|
||||
cx = loopX;
|
||||
cy = loopY;
|
||||
//noinspection fallthrough
|
||||
case 'm':
|
||||
case 'M':
|
||||
case 'l':
|
||||
case 'L':
|
||||
case 't':
|
||||
case 'T':
|
||||
incr = 2;
|
||||
break;
|
||||
case 'h':
|
||||
case 'H':
|
||||
case 'v':
|
||||
case 'V':
|
||||
incr = 1;
|
||||
break;
|
||||
case 'c':
|
||||
case 'C':
|
||||
incr = 6;
|
||||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
case 'q':
|
||||
case 'Q':
|
||||
incr = 4;
|
||||
break;
|
||||
case 'a':
|
||||
case 'A':
|
||||
incr = 7;
|
||||
}
|
||||
|
||||
for (int k = 0; k < val.length; k += incr) {
|
||||
boolean reflectCtrl;
|
||||
float tempReflectedX, tempReflectedY;
|
||||
|
||||
switch (cmd) {
|
||||
case 'm':
|
||||
cx += val[k + 0];
|
||||
cy += val[k + 1];
|
||||
if (k > 0) {
|
||||
// According to the spec, if a moveto is followed by multiple
|
||||
// pairs of coordinates, the subsequent pairs are treated as
|
||||
// implicit lineto commands.
|
||||
path.lineTo(cx, cy);
|
||||
} else {
|
||||
path.moveTo(cx, cy);
|
||||
loopX = cx;
|
||||
loopY = cy;
|
||||
}
|
||||
break;
|
||||
case 'M':
|
||||
cx = val[k + 0];
|
||||
cy = val[k + 1];
|
||||
if (k > 0) {
|
||||
// According to the spec, if a moveto is followed by multiple
|
||||
// pairs of coordinates, the subsequent pairs are treated as
|
||||
// implicit lineto commands.
|
||||
path.lineTo(cx, cy);
|
||||
} else {
|
||||
path.moveTo(cx, cy);
|
||||
loopX = cx;
|
||||
loopY = cy;
|
||||
}
|
||||
break;
|
||||
case 'l':
|
||||
cx += val[k + 0];
|
||||
cy += val[k + 1];
|
||||
path.lineTo(cx, cy);
|
||||
break;
|
||||
case 'L':
|
||||
cx = val[k + 0];
|
||||
cy = val[k + 1];
|
||||
path.lineTo(cx, cy);
|
||||
break;
|
||||
case 'z':
|
||||
case 'Z':
|
||||
path.closePath();
|
||||
cx = loopX;
|
||||
cy = loopY;
|
||||
break;
|
||||
case 'h':
|
||||
cx += val[k + 0];
|
||||
path.lineTo(cx, cy);
|
||||
break;
|
||||
case 'H':
|
||||
path.lineTo(val[k + 0], cy);
|
||||
cx = val[k + 0];
|
||||
break;
|
||||
case 'v':
|
||||
cy += val[k + 0];
|
||||
path.lineTo(cx, cy);
|
||||
break;
|
||||
case 'V':
|
||||
path.lineTo(cx, val[k + 0]);
|
||||
cy = val[k + 0];
|
||||
break;
|
||||
case 'c':
|
||||
path.curveTo(cx + val[k + 0], cy + val[k + 1], cx + val[k + 2],
|
||||
cy + val[k + 3], cx + val[k + 4], cy + val[k + 5]);
|
||||
cpx = cx + val[k + 2];
|
||||
cpy = cy + val[k + 3];
|
||||
cx += val[k + 4];
|
||||
cy += val[k + 5];
|
||||
break;
|
||||
case 'C':
|
||||
path.curveTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
|
||||
val[k + 4], val[k + 5]);
|
||||
cx = val[k + 4];
|
||||
cy = val[k + 5];
|
||||
cpx = val[k + 2];
|
||||
cpy = val[k + 3];
|
||||
break;
|
||||
case 's':
|
||||
reflectCtrl = (lastCmd == 'c' || lastCmd == 's' || lastCmd == 'C' || lastCmd == 'S');
|
||||
path.curveTo(reflectCtrl ? 2 * cx - cpx : cx, reflectCtrl ? 2
|
||||
* cy - cpy : cy, cx + val[k + 0], cy + val[k + 1], cx
|
||||
+ val[k + 2], cy + val[k + 3]);
|
||||
|
||||
cpx = cx + val[k + 0];
|
||||
cpy = cy + val[k + 1];
|
||||
cx += val[k + 2];
|
||||
cy += val[k + 3];
|
||||
break;
|
||||
case 'S':
|
||||
reflectCtrl = (lastCmd == 'c' || lastCmd == 's' || lastCmd == 'C' || lastCmd == 'S');
|
||||
path.curveTo(reflectCtrl ? 2 * cx - cpx : cx, reflectCtrl ? 2
|
||||
* cy - cpy : cy, val[k + 0], val[k + 1], val[k + 2],
|
||||
val[k + 3]);
|
||||
cpx = (val[k + 0]);
|
||||
cpy = (val[k + 1]);
|
||||
cx = val[k + 2];
|
||||
cy = val[k + 3];
|
||||
break;
|
||||
case 'q':
|
||||
path.quadTo(cx + val[k + 0], cy + val[k + 1], cx + val[k + 2],
|
||||
cy + val[k + 3]);
|
||||
cpx = cx + val[k + 0];
|
||||
cpy = cy + val[k + 1];
|
||||
// Note that we have to update cpx first, since cx will be updated here.
|
||||
cx += val[k + 2];
|
||||
cy += val[k + 3];
|
||||
break;
|
||||
case 'Q':
|
||||
path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
|
||||
cx = val[k + 2];
|
||||
cy = val[k + 3];
|
||||
cpx = val[k + 0];
|
||||
cpy = val[k + 1];
|
||||
break;
|
||||
case 't':
|
||||
reflectCtrl = (lastCmd == 'q' || lastCmd == 't' || lastCmd == 'Q' || lastCmd == 'T');
|
||||
tempReflectedX = reflectCtrl ? 2 * cx - cpx : cx;
|
||||
tempReflectedY = reflectCtrl ? 2 * cy - cpy : cy;
|
||||
path.quadTo(tempReflectedX, tempReflectedY, cx + val[k + 0], cy + val[k + 1]);
|
||||
cpx = tempReflectedX;
|
||||
cpy = tempReflectedY;
|
||||
cx += val[k + 0];
|
||||
cy += val[k + 1];
|
||||
break;
|
||||
case 'T':
|
||||
reflectCtrl = (lastCmd == 'q' || lastCmd == 't' || lastCmd == 'Q' || lastCmd == 'T');
|
||||
tempReflectedX = reflectCtrl ? 2 * cx - cpx : cx;
|
||||
tempReflectedY = reflectCtrl ? 2 * cy - cpy : cy;
|
||||
path.quadTo(tempReflectedX, tempReflectedY, val[k + 0], val[k + 1]);
|
||||
cx = val[k + 0];
|
||||
cy = val[k + 1];
|
||||
cpx = tempReflectedX;
|
||||
cpy = tempReflectedY;
|
||||
break;
|
||||
case 'a':
|
||||
// (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
|
||||
drawArc(path, cx, cy, val[k + 5] + cx, val[k + 6] + cy,
|
||||
val[k + 0], val[k + 1], val[k + 2], val[k + 3] != 0,
|
||||
val[k + 4] != 0);
|
||||
cx += val[k + 5];
|
||||
cy += val[k + 6];
|
||||
cpx = cx;
|
||||
cpy = cy;
|
||||
|
||||
break;
|
||||
case 'A':
|
||||
drawArc(path, cx, cy, val[k + 5], val[k + 6], val[k + 0],
|
||||
val[k + 1], val[k + 2], val[k + 3] != 0,
|
||||
val[k + 4] != 0);
|
||||
cx = val[k + 5];
|
||||
cy = val[k + 6];
|
||||
cpx = cx;
|
||||
cpy = cy;
|
||||
break;
|
||||
|
||||
}
|
||||
lastCmd = cmd;
|
||||
}
|
||||
current[0] = cx;
|
||||
current[1] = cy;
|
||||
current[2] = cpx;
|
||||
current[3] = cpy;
|
||||
current[4] = loopX;
|
||||
current[5] = loopY;
|
||||
}
|
||||
|
||||
private static void drawArc(Path2D p, float x0, float y0, float x1,
|
||||
float y1, float a, float b, float theta, boolean isMoreThanHalf,
|
||||
boolean isPositiveArc) {
|
||||
LOGGER.log(Level.FINE, "(" + x0 + "," + y0 + ")-(" + x1 + "," + y1
|
||||
+ ") {" + a + " " + b + "}");
|
||||
/* Convert rotation angle from degrees to radians */
|
||||
double thetaD = theta * Math.PI / 180.0f;
|
||||
/* Pre-compute rotation matrix entries */
|
||||
double cosTheta = Math.cos(thetaD);
|
||||
double sinTheta = Math.sin(thetaD);
|
||||
/* Transform (x0, y0) and (x1, y1) into unit space */
|
||||
/* using (inverse) rotation, followed by (inverse) scale */
|
||||
double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
|
||||
double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
|
||||
double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
|
||||
double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
|
||||
LOGGER.log(Level.FINE, "unit space (" + x0p + "," + y0p + ")-(" + x1p
|
||||
+ "," + y1p + ")");
|
||||
/* Compute differences and averages */
|
||||
double dx = x0p - x1p;
|
||||
double dy = y0p - y1p;
|
||||
double xm = (x0p + x1p) / 2;
|
||||
double ym = (y0p + y1p) / 2;
|
||||
/* Solve for intersecting unit circles */
|
||||
double dsq = dx * dx + dy * dy;
|
||||
if (dsq == 0.0) {
|
||||
LOGGER.log(Level.FINE, " Points are coincident");
|
||||
return; /* Points are coincident */
|
||||
}
|
||||
double disc = 1.0 / dsq - 1.0 / 4.0;
|
||||
if (disc < 0.0) {
|
||||
LOGGER.log(Level.FINE, "Points are too far apart " + dsq);
|
||||
float adjust = (float) (Math.sqrt(dsq) / 1.99999);
|
||||
drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta,
|
||||
isMoreThanHalf, isPositiveArc);
|
||||
return; /* Points are too far apart */
|
||||
}
|
||||
double s = Math.sqrt(disc);
|
||||
double sdx = s * dx;
|
||||
double sdy = s * dy;
|
||||
double cx;
|
||||
double cy;
|
||||
if (isMoreThanHalf == isPositiveArc) {
|
||||
cx = xm - sdy;
|
||||
cy = ym + sdx;
|
||||
} else {
|
||||
cx = xm + sdy;
|
||||
cy = ym - sdx;
|
||||
}
|
||||
|
||||
double eta0 = Math.atan2((y0p - cy), (x0p - cx));
|
||||
LOGGER.log(Level.FINE, "eta0 = Math.atan2( " + (y0p - cy) + " , "
|
||||
+ (x0p - cx) + ") = " + Math.toDegrees(eta0));
|
||||
|
||||
double eta1 = Math.atan2((y1p - cy), (x1p - cx));
|
||||
LOGGER.log(Level.FINE, "eta1 = Math.atan2( " + (y1p - cy) + " , "
|
||||
+ (x1p - cx) + ") = " + Math.toDegrees(eta1));
|
||||
double sweep = (eta1 - eta0);
|
||||
if (isPositiveArc != (sweep >= 0)) {
|
||||
if (sweep > 0) {
|
||||
sweep -= 2 * Math.PI;
|
||||
} else {
|
||||
sweep += 2 * Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
cx *= a;
|
||||
cy *= b;
|
||||
double tcx = cx;
|
||||
cx = cx * cosTheta - cy * sinTheta;
|
||||
cy = tcx * sinTheta + cy * cosTheta;
|
||||
LOGGER.log(
|
||||
Level.FINE,
|
||||
"cx, cy, a, b, x0, y0, thetaD, eta0, sweep = " + cx + " , "
|
||||
+ cy + " , " + a + " , " + b + " , " + x0 + " , " + y0
|
||||
+ " , " + Math.toDegrees(thetaD) + " , "
|
||||
+ Math.toDegrees(eta0) + " , " + Math.toDegrees(sweep));
|
||||
|
||||
arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an arc to cubic Bezier segments and records them in p.
|
||||
*
|
||||
* @param p The target for the cubic Bezier segments
|
||||
* @param cx The x coordinate center of the ellipse
|
||||
* @param cy The y coordinate center of the ellipse
|
||||
* @param a The radius of the ellipse in the horizontal direction
|
||||
* @param b The radius of the ellipse in the vertical direction
|
||||
* @param e1x E(eta1) x coordinate of the starting point of the arc
|
||||
* @param e1y E(eta2) y coordinate of the starting point of the arc
|
||||
* @param theta The angle that the ellipse bounding rectangle makes with the horizontal plane
|
||||
* @param start The start angle of the arc on the ellipse
|
||||
* @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
|
||||
*/
|
||||
private static void arcToBezier(Path2D p, double cx, double cy, double a,
|
||||
double b, double e1x, double e1y, double theta, double start,
|
||||
double sweep) {
|
||||
// Taken from equations at:
|
||||
// http://spaceroots.org/documents/ellipse/node8.html
|
||||
// and http://www.spaceroots.org/documents/ellipse/node22.html
|
||||
// Maximum of 45 degrees per cubic Bezier segment
|
||||
int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));
|
||||
|
||||
double eta1 = start;
|
||||
double cosTheta = Math.cos(theta);
|
||||
double sinTheta = Math.sin(theta);
|
||||
double cosEta1 = Math.cos(eta1);
|
||||
double sinEta1 = Math.sin(eta1);
|
||||
double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
|
||||
double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
|
||||
|
||||
double anglePerSegment = sweep / numSegments;
|
||||
for (int i = 0; i < numSegments; i++) {
|
||||
double eta2 = eta1 + anglePerSegment;
|
||||
double sinEta2 = Math.sin(eta2);
|
||||
double cosEta2 = Math.cos(eta2);
|
||||
double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
|
||||
double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
|
||||
double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
|
||||
double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
|
||||
double tanDiff2 = Math.tan((eta2 - eta1) / 2);
|
||||
double alpha = Math.sin(eta2 - eta1)
|
||||
* (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
|
||||
double q1x = e1x + alpha * ep1x;
|
||||
double q1y = e1y + alpha * ep1y;
|
||||
double q2x = e2x - alpha * ep2x;
|
||||
double q2y = e2y - alpha * ep2y;
|
||||
|
||||
p.curveTo((float) q1x, (float) q1y, (float) q2x, (float) q2y, (float) e2x, (float) e2y);
|
||||
eta1 = eta2;
|
||||
e1x = e2x;
|
||||
e1y = e2y;
|
||||
ep1x = ep2x;
|
||||
ep1y = ep2y;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2017 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package com.android.ide.common.vectordrawable
|
||||
|
||||
import java.awt.Color
|
||||
|
||||
/**
|
||||
* Represents override information for VectorDrawable's XML file content.
|
||||
*/
|
||||
data class VdOverrideInfo(var width: Double, var height: Double, var tint: Color?, var alpha: Double,
|
||||
var autoMirrored: Boolean) {
|
||||
/** Checks if the width needs to be overridden. */
|
||||
fun needsOverrideWidth(): Boolean {
|
||||
return width > 0
|
||||
}
|
||||
|
||||
/** Checks if the height needs to be overridden. */
|
||||
fun needsOverrideHeight(): Boolean {
|
||||
return height > 0
|
||||
}
|
||||
|
||||
/** Checks if the alpha needs to be overridden. */
|
||||
fun needsOverrideAlpha(): Boolean {
|
||||
return 0 <= alpha && alpha < 1
|
||||
}
|
||||
|
||||
/** Checks if the tint needs to be overridden. */
|
||||
fun needsOverrideTint(): Boolean {
|
||||
return tintRgb() != 0
|
||||
}
|
||||
|
||||
/** Returns the RGB value of the tint. */
|
||||
fun tintRgb(): Int {
|
||||
return (tint?.rgb ?: 0) and 0xFFFFFF
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.android.utils.PositionXmlParser;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import org.w3c.dom.Document;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
* Parses a VectorDrawable's XML file, and generate an internal tree representation,
|
||||
* which can be used for drawing / previewing.
|
||||
*/
|
||||
class VdParser {
|
||||
// Note that the incoming file is an VectorDrawable's XML file, not an SVG.
|
||||
@NonNull
|
||||
public static VdTree parse(@NonNull InputStream stream, @Nullable StringBuilder vdErrorLog) {
|
||||
final VdTree tree = new VdTree();
|
||||
try {
|
||||
Document doc = PositionXmlParser.parse(stream, true);
|
||||
tree.parse(doc);
|
||||
}
|
||||
catch (ParserConfigurationException | SAXException | IOException e) {
|
||||
if (vdErrorLog != null) {
|
||||
vdErrorLog.append(e.getMessage());
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,849 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.ide.common.vectordrawable.VdUtil.parseColorValue;
|
||||
import static com.android.utils.DecimalUtils.trimInsignificantZeros;
|
||||
import static com.android.utils.XmlUtils.formatFloatValue;
|
||||
|
||||
import com.android.SdkConstants;
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.ide.common.vectordrawable.PathParser.ParseMode;
|
||||
import com.android.utils.PositionXmlParser;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.LinearGradientPaint;
|
||||
import java.awt.MultipleGradientPaint;
|
||||
import java.awt.RadialGradientPaint;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Area;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.PathIterator;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
/** Represents one path element of a vector drawable. */
|
||||
class VdPath extends VdElement {
|
||||
private static final String PATH_ID = "android:name";
|
||||
private static final String PATH_DESCRIPTION = "android:pathData";
|
||||
private static final String PATH_FILL = "android:fillColor";
|
||||
private static final String PATH_FILL_OPACITY = "android:fillAlpha";
|
||||
private static final String PATH_FILL_TYPE = "android:fillType";
|
||||
private static final String PATH_STROKE = "android:strokeColor";
|
||||
private static final String PATH_STROKE_OPACITY = "android:strokeAlpha";
|
||||
|
||||
private static final String FILL_TYPE_EVEN_ODD = "evenOdd";
|
||||
|
||||
private static final String PATH_STROKE_WIDTH = "android:strokeWidth";
|
||||
private static final String PATH_TRIM_START = "android:trimPathStart";
|
||||
private static final String PATH_TRIM_END = "android:trimPathEnd";
|
||||
private static final String PATH_TRIM_OFFSET = "android:trimPathOffset";
|
||||
private static final String PATH_STROKE_LINE_CAP = "android:strokeLineCap";
|
||||
private static final String PATH_STROKE_LINE_JOIN = "android:strokeLineJoin";
|
||||
private static final String PATH_STROKE_MITER_LIMIT = "android:strokeMiterLimit";
|
||||
|
||||
private static final String LINE_CAP_BUTT = "butt";
|
||||
private static final String LINE_CAP_ROUND = "round";
|
||||
private static final String LINE_CAP_SQUARE = "square";
|
||||
private static final String LINE_JOIN_MITER = "miter";
|
||||
private static final String LINE_JOIN_ROUND = "round";
|
||||
private static final String LINE_JOIN_BEVEL = "bevel";
|
||||
private static final float EPSILON = 1e-6f;
|
||||
private static final char INIT_TYPE = ' ';
|
||||
|
||||
private static final ImmutableMap<Character, Integer> COMMAND_STEP_MAP =
|
||||
ImmutableMap.<Character, Integer>builder()
|
||||
.put('z', 2)
|
||||
.put('Z', 2)
|
||||
.put('m', 2)
|
||||
.put('M', 2)
|
||||
.put('l', 2)
|
||||
.put('L', 2)
|
||||
.put('t', 2)
|
||||
.put('T', 2)
|
||||
.put('h', 1)
|
||||
.put('H', 1)
|
||||
.put('v', 1)
|
||||
.put('V', 1)
|
||||
.put('c', 6)
|
||||
.put('C', 6)
|
||||
.put('s', 4)
|
||||
.put('S', 4)
|
||||
.put('q', 4)
|
||||
.put('Q', 4)
|
||||
.put('a', 7)
|
||||
.put('A', 7)
|
||||
.build();
|
||||
|
||||
|
||||
private VdGradient fillGradient;
|
||||
private VdGradient strokeGradient;
|
||||
|
||||
private Node[] mNodeList;
|
||||
private int mStrokeColor;
|
||||
private int mFillColor;
|
||||
|
||||
private float mStrokeWidth;
|
||||
private int mStrokeLineCap;
|
||||
private int mStrokeLineJoin;
|
||||
private float mStrokeMiterlimit = 4;
|
||||
private float mStrokeAlpha = 1.0f;
|
||||
private float mFillAlpha = 1.0f;
|
||||
private int mFillType = PathIterator.WIND_NON_ZERO;
|
||||
// TODO: support trim path.
|
||||
private float mTrimPathStart;
|
||||
private float mTrimPathEnd = 1;
|
||||
private float mTrimPathOffset;
|
||||
|
||||
private void toPath(@NonNull Path2D path) {
|
||||
path.reset();
|
||||
if (mNodeList != null) {
|
||||
VdNodeRender.createPath(mNodeList, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents one segment of the path data, e.g. "l 0,0 1,1".
|
||||
*/
|
||||
public static class Node {
|
||||
private char mType;
|
||||
private float[] mParams;
|
||||
|
||||
public char getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public float[] getParams() {
|
||||
return mParams;
|
||||
}
|
||||
|
||||
public Node(char type, @NonNull float[] params) {
|
||||
this.mType = type;
|
||||
this.mParams = params;
|
||||
}
|
||||
|
||||
public Node(@NonNull Node n) {
|
||||
this.mType = n.mType;
|
||||
this.mParams = Arrays.copyOf(n.mParams, n.mParams.length);
|
||||
}
|
||||
|
||||
public static boolean hasRelMoveAfterClose(@NonNull Node[] nodes) {
|
||||
char preType = ' ';
|
||||
for (Node n : nodes) {
|
||||
if ((preType == 'z' || preType == 'Z') && n.mType == 'm') {
|
||||
return true;
|
||||
}
|
||||
preType = n.mType;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String nodeListToString(@NonNull Node[] nodes, @NonNull NumberFormat format) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Node node : nodes) {
|
||||
result.append(node.mType);
|
||||
int len = node.mParams.length;
|
||||
boolean implicitLineTo = false;
|
||||
char lineToType = ' ';
|
||||
if ((node.mType == 'm' || node.mType == 'M') && len > 2) {
|
||||
implicitLineTo = true;
|
||||
lineToType = node.mType == 'm' ? 'l' : 'L';
|
||||
}
|
||||
for (int j = 0; j < len; j++) {
|
||||
if (j > 0) {
|
||||
result.append(j % 2 != 0 ? "," : " ");
|
||||
}
|
||||
if (implicitLineTo && j == 2) {
|
||||
result.append(lineToType);
|
||||
}
|
||||
float param = node.mParams[j];
|
||||
if (!Float.isFinite(param)) {
|
||||
throw new IllegalArgumentException("Invalid number: " + param);
|
||||
}
|
||||
String str = trimInsignificantZeros(format.format(param));
|
||||
result.append(str);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static void transform(
|
||||
@NonNull AffineTransform totalTransform, @NonNull Node[] nodes) {
|
||||
Point2D.Float currentPoint = new Point2D.Float();
|
||||
Point2D.Float currentSegmentStartPoint = new Point2D.Float();
|
||||
char previousType = INIT_TYPE;
|
||||
for (Node n : nodes) {
|
||||
n.transform(totalTransform, currentPoint, currentSegmentStartPoint, previousType);
|
||||
previousType = n.mType;
|
||||
}
|
||||
}
|
||||
|
||||
private void transform(
|
||||
@NonNull AffineTransform totalTransform,
|
||||
@NonNull Point2D.Float currentPoint,
|
||||
@NonNull Point2D.Float currentSegmentStartPoint,
|
||||
char previousType) {
|
||||
// For horizontal and vertical lines, we have to convert to LineTo with 2 parameters.
|
||||
// And for arcTo, we also need to isolate the parameters for transformation.
|
||||
// Therefore looping will be necessary for such commands.
|
||||
//
|
||||
// Note that if the matrix is translation only, then we can save many computations.
|
||||
int paramsLen = mParams.length;
|
||||
float[] tempParams = new float[2 * paramsLen];
|
||||
// These has to be pre-transformed value. In other words, the same as it is
|
||||
// in the pathData.
|
||||
float currentX = currentPoint.x;
|
||||
float currentY = currentPoint.y;
|
||||
float currentSegmentStartX = currentSegmentStartPoint.x;
|
||||
float currentSegmentStartY = currentSegmentStartPoint.y;
|
||||
|
||||
int step = COMMAND_STEP_MAP.get(mType);
|
||||
switch (mType) {
|
||||
case 'z':
|
||||
case 'Z':
|
||||
currentX = currentSegmentStartX;
|
||||
currentY = currentSegmentStartY;
|
||||
break;
|
||||
|
||||
case 'M':
|
||||
currentSegmentStartX = mParams[0];
|
||||
currentSegmentStartY = mParams[1];
|
||||
//noinspection fallthrough
|
||||
case 'L':
|
||||
case 'T':
|
||||
case 'C':
|
||||
case 'S':
|
||||
case 'Q':
|
||||
currentX = mParams[paramsLen - 2];
|
||||
currentY = mParams[paramsLen - 1];
|
||||
|
||||
totalTransform.transform(mParams, 0, mParams, 0, paramsLen / 2);
|
||||
break;
|
||||
|
||||
case 'm':
|
||||
if (previousType == 'z' || previousType == 'Z') {
|
||||
// Replace 'm' with 'M' to work around a bug in API 21 that is triggered
|
||||
// when 'm' follows 'z'.
|
||||
mType = 'M';
|
||||
mParams[0] += currentSegmentStartX;
|
||||
mParams[1] += currentSegmentStartY;
|
||||
currentSegmentStartX = mParams[0]; // Start a new segment.
|
||||
currentSegmentStartY = mParams[1];
|
||||
for (int i = step; i < paramsLen; i += step) {
|
||||
mParams[i] += mParams[i - step];
|
||||
mParams[i + 1] += mParams[i + 1 - step];
|
||||
}
|
||||
currentX = mParams[paramsLen - 2];
|
||||
currentY = mParams[paramsLen - 1];
|
||||
|
||||
totalTransform.transform(mParams, 0, mParams, 0, paramsLen / 2);
|
||||
} else {
|
||||
int headLen = 2;
|
||||
currentX += mParams[0];
|
||||
currentY += mParams[1];
|
||||
currentSegmentStartX = currentX; // Start a new segment.
|
||||
currentSegmentStartY = currentY;
|
||||
|
||||
if (previousType == INIT_TYPE) {
|
||||
// 'm' at the start of a path is handled similar to 'M'.
|
||||
// The coordinates are transformed as absolute.
|
||||
totalTransform.transform(mParams, 0, mParams, 0, headLen / 2);
|
||||
} else if (!isTranslationOnly(totalTransform)) {
|
||||
deltaTransform(totalTransform, mParams, 0, headLen);
|
||||
}
|
||||
|
||||
for (int i = headLen; i < paramsLen; i += step) {
|
||||
currentX += mParams[i];
|
||||
currentY += mParams[i + 1];
|
||||
}
|
||||
|
||||
if (!isTranslationOnly(totalTransform)) {
|
||||
deltaTransform(totalTransform, mParams, headLen, paramsLen - headLen);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
case 't':
|
||||
case 'c':
|
||||
case 's':
|
||||
case 'q':
|
||||
for (int i = 0; i < paramsLen - step + 1; i += step) {
|
||||
currentX += mParams[i + step - 2];
|
||||
currentY += mParams[i + step - 1];
|
||||
}
|
||||
if (!isTranslationOnly(totalTransform)) {
|
||||
deltaTransform(totalTransform, mParams, 0, paramsLen);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'H':
|
||||
mType = 'L';
|
||||
for (int i = 0; i < paramsLen; i++) {
|
||||
tempParams[i * 2] = mParams[i];
|
||||
tempParams[i * 2 + 1] = currentY;
|
||||
currentX = mParams[i];
|
||||
}
|
||||
totalTransform.transform(tempParams, 0, tempParams, 0, paramsLen /*points*/);
|
||||
mParams = tempParams;
|
||||
break;
|
||||
|
||||
case 'V':
|
||||
mType = 'L';
|
||||
for (int i = 0; i < paramsLen; i++) {
|
||||
tempParams[i * 2] = currentX;
|
||||
tempParams[i * 2 + 1] = mParams[i];
|
||||
currentY = mParams[i];
|
||||
}
|
||||
totalTransform.transform(tempParams, 0, tempParams, 0, paramsLen /*points*/);
|
||||
mParams = tempParams;
|
||||
break;
|
||||
|
||||
case 'h':
|
||||
for (int i = 0; i < paramsLen; i++) {
|
||||
currentX += mParams[i];
|
||||
// tempParams may not be used but is assigned here to avoid a second loop.
|
||||
tempParams[i * 2] = mParams[i];
|
||||
tempParams[i * 2 + 1] = 0;
|
||||
}
|
||||
if (!isTranslationOnly(totalTransform)) {
|
||||
mType = 'l';
|
||||
deltaTransform(totalTransform, tempParams, 0, 2 * paramsLen);
|
||||
mParams = tempParams;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'v':
|
||||
for (int i = 0; i < paramsLen; i++) {
|
||||
// tempParams may not be used but is assigned here to avoid a second loop.
|
||||
tempParams[i * 2] = 0;
|
||||
tempParams[i * 2 + 1] = mParams[i];
|
||||
currentY += mParams[i];
|
||||
}
|
||||
|
||||
if (!isTranslationOnly(totalTransform)) {
|
||||
mType = 'l';
|
||||
deltaTransform(totalTransform, tempParams, 0, 2 * paramsLen);
|
||||
mParams = tempParams;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'A':
|
||||
for (int i = 0; i < paramsLen - step + 1; i += step) {
|
||||
// (0:rx 1:ry 2:x-axis-rotation 3:large-arc-flag 4:sweep-flag 5:x 6:y)
|
||||
// [0, 1, 2]
|
||||
if (!isTranslationOnly(totalTransform)) {
|
||||
EllipseSolver ellipseSolver = new EllipseSolver(totalTransform,
|
||||
currentX, currentY,
|
||||
mParams[i], mParams[i + 1], mParams[i + 2],
|
||||
mParams[i + 3], mParams[i + 4],
|
||||
mParams[i + 5], mParams[i + 6]);
|
||||
mParams[i] = ellipseSolver.getMajorAxis();
|
||||
mParams[i + 1] = ellipseSolver.getMinorAxis();
|
||||
mParams[i + 2] = ellipseSolver.getRotationDegree();
|
||||
if (ellipseSolver.getDirectionChanged()) {
|
||||
mParams[i + 4] = 1 - mParams[i + 4];
|
||||
}
|
||||
}
|
||||
// [5, 6]
|
||||
currentX = mParams[i + 5];
|
||||
currentY = mParams[i + 6];
|
||||
|
||||
totalTransform.transform(mParams, i + 5, mParams, i + 5, 1 /*1 point only*/);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'a':
|
||||
for (int i = 0; i < paramsLen - step + 1; i += step) {
|
||||
float oldCurrentX = currentX;
|
||||
float oldCurrentY = currentY;
|
||||
|
||||
currentX += mParams[i + 5];
|
||||
currentY += mParams[i + 6];
|
||||
if (!isTranslationOnly(totalTransform)) {
|
||||
EllipseSolver ellipseSolver = new EllipseSolver(totalTransform,
|
||||
oldCurrentX, oldCurrentY,
|
||||
mParams[i], mParams[i + 1], mParams[i + 2],
|
||||
mParams[i + 3], mParams[i + 4],
|
||||
oldCurrentX + mParams[i + 5],
|
||||
oldCurrentY + mParams[i + 6]);
|
||||
// (0:rx 1:ry 2:x-axis-rotation 3:large-arc-flag 4:sweep-flag 5:x 6:y)
|
||||
// [5, 6]
|
||||
deltaTransform(totalTransform, mParams, i + 5, 2);
|
||||
// [0, 1, 2]
|
||||
mParams[i] = ellipseSolver.getMajorAxis();
|
||||
mParams[i + 1] = ellipseSolver.getMinorAxis();
|
||||
mParams[i + 2] = ellipseSolver.getRotationDegree();
|
||||
if (ellipseSolver.getDirectionChanged()) {
|
||||
mParams[i + 4] = 1 - mParams[i + 4];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected type " + mType);
|
||||
}
|
||||
currentPoint.setLocation(currentX, currentY);
|
||||
currentSegmentStartPoint.setLocation(currentSegmentStartX, currentSegmentStartY);
|
||||
}
|
||||
|
||||
private static boolean isTranslationOnly(@NonNull AffineTransform totalTransform) {
|
||||
int type = totalTransform.getType();
|
||||
return type == AffineTransform.TYPE_IDENTITY
|
||||
|| type == AffineTransform.TYPE_TRANSLATION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies delta transform to a set of points represented by a float array.
|
||||
*
|
||||
* @param totalTransform the transform to apply
|
||||
* @param coordinates coordinates of points to apply the transform to
|
||||
* @param offset in number of floats, not points
|
||||
* @param paramsLen in number of floats, not points
|
||||
*/
|
||||
private static void deltaTransform(
|
||||
@NonNull AffineTransform totalTransform,
|
||||
@NonNull float[] coordinates,
|
||||
int offset,
|
||||
int paramsLen) {
|
||||
double[] doubleArray = new double[paramsLen];
|
||||
for (int i = 0; i < paramsLen; i++) {
|
||||
doubleArray[i] = (double) coordinates[i + offset];
|
||||
}
|
||||
|
||||
totalTransform.deltaTransform(doubleArray, 0, doubleArray, 0, paramsLen / 2);
|
||||
|
||||
for (int i = 0; i < paramsLen; i++) {
|
||||
coordinates[i + offset] = (float) doubleArray[i];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append(mType);
|
||||
int i = 0;
|
||||
for (float param : this.mParams) {
|
||||
result.append(i++ % 2 == 0 ? ' ' : ',');
|
||||
result.append(formatFloatValue(param));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private void setNameValue(@NonNull String name, @NonNull String value) {
|
||||
if (value.startsWith(SdkConstants.PREFIX_RESOURCE_REF) && PATH_FILL.equals(name)) {
|
||||
// Ignore the android resource in "android:fillColor" present in the new material icons.
|
||||
value = "#000000";
|
||||
}
|
||||
|
||||
if (value.startsWith(SdkConstants.PREFIX_RESOURCE_REF)) {
|
||||
throw new ResourcesNotSupportedException(name, value);
|
||||
}
|
||||
|
||||
if (PATH_DESCRIPTION.equals(name)) {
|
||||
mNodeList = PathParser.parsePath(value, ParseMode.ANDROID);
|
||||
} else if (PATH_ID.equals(name)) {
|
||||
mName = value;
|
||||
} else if (PATH_FILL.equals(name)) {
|
||||
mFillColor = parseColorValue(value);
|
||||
} else if (PATH_FILL_TYPE.equals(name)) {
|
||||
mFillType = parseFillType(value);
|
||||
} else if (PATH_STROKE.equals(name)) {
|
||||
mStrokeColor = parseColorValue(value);
|
||||
} else if (PATH_FILL_OPACITY.equals(name)) {
|
||||
mFillAlpha = Float.parseFloat(value);
|
||||
} else if (PATH_STROKE_OPACITY.equals(name)) {
|
||||
mStrokeAlpha = Float.parseFloat(value);
|
||||
} else if (PATH_STROKE_WIDTH.equals(name)) {
|
||||
mStrokeWidth = Float.parseFloat(value);
|
||||
} else if (PATH_TRIM_START.equals(name)) {
|
||||
mTrimPathStart = Float.parseFloat(value);
|
||||
} else if (PATH_TRIM_END.equals(name)) {
|
||||
mTrimPathEnd = Float.parseFloat(value);
|
||||
} else if (PATH_TRIM_OFFSET.equals(name)) {
|
||||
mTrimPathOffset = Float.parseFloat(value);
|
||||
} else if (PATH_STROKE_LINE_CAP.equals(name)) {
|
||||
if (LINE_CAP_BUTT.equals(value)) {
|
||||
mStrokeLineCap = 0;
|
||||
} else if (LINE_CAP_ROUND.equals(value)) {
|
||||
mStrokeLineCap = 1;
|
||||
} else if (LINE_CAP_SQUARE.equals(value)) {
|
||||
mStrokeLineCap = 2;
|
||||
}
|
||||
} else if (PATH_STROKE_LINE_JOIN.equals(name)) {
|
||||
if (LINE_JOIN_MITER.equals(value)) {
|
||||
mStrokeLineJoin = 0;
|
||||
} else if (LINE_JOIN_ROUND.equals(value)) {
|
||||
mStrokeLineJoin = 1;
|
||||
} else if (LINE_JOIN_BEVEL.equals(value)) {
|
||||
mStrokeLineJoin = 2;
|
||||
}
|
||||
} else if (PATH_STROKE_MITER_LIMIT.equals(name)) {
|
||||
mStrokeMiterlimit = Float.parseFloat(value);
|
||||
} else {
|
||||
getLogger().log(Level.WARNING, ">>>>>> DID NOT UNDERSTAND ! \"" + name + "\" <<<<");
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseFillType(@NonNull String value) {
|
||||
if (FILL_TYPE_EVEN_ODD.equalsIgnoreCase(value)) {
|
||||
return PathIterator.WIND_EVEN_ODD;
|
||||
}
|
||||
return PathIterator.WIND_NON_ZERO;
|
||||
}
|
||||
|
||||
/** Multiplies the {@code alpha} value into the alpha channel {@code color}. */
|
||||
protected static int applyAlpha(int color, float alpha) {
|
||||
int alphaBytes = (color >> 24) & 0xff;
|
||||
color &= 0x00FFFFFF;
|
||||
color |= ((int) (alphaBytes * alpha)) << 24;
|
||||
return color;
|
||||
}
|
||||
|
||||
/** Draws the current path. */
|
||||
@Override
|
||||
public void draw(
|
||||
@NonNull Graphics2D g,
|
||||
@NonNull AffineTransform currentMatrix,
|
||||
float scaleX,
|
||||
float scaleY) {
|
||||
Path2D path2d = new Path2D.Double(mFillType);
|
||||
toPath(path2d);
|
||||
|
||||
// SWing operate the matrix is using pre-concatenate by default.
|
||||
// Below is how this is handled in Android framework.
|
||||
// pathMatrix.set(groupStackedMatrix);
|
||||
// pathMatrix.postScale(scaleX, scaleY);
|
||||
g.setTransform(new AffineTransform());
|
||||
g.scale(scaleX, scaleY);
|
||||
g.transform(currentMatrix);
|
||||
|
||||
if (mFillColor != 0 && fillGradient == null) {
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
@SuppressWarnings("UseJBColor")
|
||||
Color fillColor = new Color(applyAlpha(mFillColor, mFillAlpha), true);
|
||||
g.setColor(fillColor);
|
||||
g.fill(path2d);
|
||||
}
|
||||
if (mStrokeColor != 0 && mStrokeWidth != 0 && strokeGradient == null) {
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
BasicStroke stroke = new BasicStroke(mStrokeWidth, mStrokeLineCap, mStrokeLineJoin, mStrokeMiterlimit);
|
||||
g.setStroke(stroke);
|
||||
@SuppressWarnings("UseJBColor")
|
||||
Color strokeColor = new Color(applyAlpha(mStrokeColor, mStrokeAlpha), true);
|
||||
g.setColor(strokeColor);
|
||||
g.draw(path2d);
|
||||
}
|
||||
if (isClipPath) {
|
||||
Shape clip = g.getClip();
|
||||
if (clip != null) {
|
||||
Area area = new Area(clip);
|
||||
area.add(new Area(path2d));
|
||||
g.setClip(area);
|
||||
} else {
|
||||
g.setClip(path2d);
|
||||
}
|
||||
}
|
||||
if (fillGradient != null) {
|
||||
fillGradient.drawGradient(g, path2d, true);
|
||||
}
|
||||
|
||||
if (strokeGradient != null) {
|
||||
strokeGradient.drawGradient(g, path2d, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseAttributes(@NonNull NamedNodeMap attributes) {
|
||||
for (int i = 0; i < attributes.getLength(); i++) {
|
||||
org.w3c.dom.Node attribute = attributes.item(i);
|
||||
|
||||
// See https://issuetracker.google.com/62052258 for why this check exists.
|
||||
if (Objects.equals(attribute.getNamespaceURI(), SdkConstants.TOOLS_URI)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = attribute.getNodeName();
|
||||
String value = attribute.getNodeValue();
|
||||
setNameValue(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGroup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return "Path:" +
|
||||
" Name: " + mName +
|
||||
" Node: " + Arrays.toString(mNodeList) +
|
||||
" mFillColor: " + Integer.toHexString(mFillColor) +
|
||||
" mFillAlpha:" + mFillAlpha +
|
||||
" mFillType:" + mFillType +
|
||||
" mStrokeColor:" + Integer.toHexString(mStrokeColor) +
|
||||
" mStrokeWidth:" + mStrokeWidth +
|
||||
" mStrokeAlpha:" + mStrokeAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* We parse the given node for the gradient information if it exists. If it contains a gradient,
|
||||
* depending on what type, we set the fillGradient or strokeGradient of the current VdPath to a
|
||||
* new VdGradient and add the gradient information.
|
||||
*/
|
||||
protected void addGradientIfExists(@NonNull org.w3c.dom.Node current) {
|
||||
// This should be guaranteed to be the gradient given the way we are writing the VD XMLs.
|
||||
org.w3c.dom.Node gradientNode = current.getFirstChild();
|
||||
VdGradient newGradient = new VdGradient();
|
||||
if (gradientNode != null) {
|
||||
gradientNode = gradientNode.getNextSibling();
|
||||
if (gradientNode != null) {
|
||||
// This should also be guaranteed given the way we write the VD XMLs.
|
||||
String attrValue = gradientNode.getAttributes().getNamedItem("name").getNodeValue();
|
||||
if (attrValue.equals("android:fillColor")) {
|
||||
fillGradient = newGradient;
|
||||
} else if (attrValue.equals("android:strokeColor")) {
|
||||
strokeGradient = newGradient;
|
||||
}
|
||||
gradientNode = gradientNode.getFirstChild();
|
||||
if (gradientNode != null) {
|
||||
gradientNode = gradientNode.getNextSibling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (gradientNode != null && gradientNode.getNodeName().equals("gradient")) {
|
||||
NamedNodeMap gradientAttributes = gradientNode.getAttributes();
|
||||
for (int i = 0; i < gradientAttributes.getLength(); i++) {
|
||||
String name = gradientAttributes.item(i).getNodeName();
|
||||
String value = gradientAttributes.item(i).getNodeValue();
|
||||
newGradient.setGradientValue(name, value);
|
||||
}
|
||||
|
||||
// Adding stop information to gradient.
|
||||
NodeList items = gradientNode.getChildNodes();
|
||||
for (int i = 0; i < items.getLength(); i++) {
|
||||
org.w3c.dom.Node stop = items.item(i);
|
||||
if (stop.getNodeName().equals("item")) {
|
||||
NamedNodeMap stopAttr = stop.getAttributes();
|
||||
String color = null;
|
||||
String offset = null;
|
||||
for (int j = 0; j < stopAttr.getLength(); j++) {
|
||||
org.w3c.dom.Node currentItem = stopAttr.item(j);
|
||||
if (currentItem.getNodeName().equals("android:color")) {
|
||||
color = currentItem.getNodeValue();
|
||||
if (color != null &&
|
||||
(color.charAt(0) == '@' || color.charAt(0) == '?')) {
|
||||
throw new IllegalVectorDrawableResourceRefException(
|
||||
color, PositionXmlParser.getPosition(currentItem), null);
|
||||
}
|
||||
} else if (currentItem.getNodeName().equals("android:offset")) {
|
||||
offset = currentItem.getNodeValue();
|
||||
}
|
||||
}
|
||||
if (color == null) {
|
||||
color = "#000000";
|
||||
getLogger().log(Level.WARNING, ">>>>>> No color for gradient found >>>>>>");
|
||||
}
|
||||
if (offset == null) {
|
||||
offset = "0";
|
||||
getLogger().log(Level.WARNING, ">>>>>> No offset for gradient found>>>>>>");
|
||||
}
|
||||
GradientStop gradientStop = new GradientStop(color, offset);
|
||||
newGradient.mGradientStops.add(gradientStop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains gradient information in order to draw the fill or stroke of a path with a gradient.
|
||||
*/
|
||||
class VdGradient {
|
||||
// Gradient attributes.
|
||||
private float mStartX;
|
||||
private float mStartY;
|
||||
private float mEndX;
|
||||
private float mEndY;
|
||||
private float mCenterX;
|
||||
private float mCenterY;
|
||||
private float mGradientRadius;
|
||||
private String mTileMode = "NO_CYCLE";
|
||||
private String mGradientType = "linear";
|
||||
|
||||
private final ArrayList<GradientStop> mGradientStops = new ArrayList<>();
|
||||
|
||||
VdGradient() {}
|
||||
|
||||
private void setGradientValue(@NonNull String name, @NonNull String value) {
|
||||
switch (name) {
|
||||
case "android:type":
|
||||
mGradientType = value;
|
||||
break;
|
||||
case "android:tileMode":
|
||||
mTileMode = value;
|
||||
break;
|
||||
case "android:startX":
|
||||
mStartX = Float.parseFloat(value);
|
||||
break;
|
||||
case "android:startY":
|
||||
mStartY = Float.parseFloat(value);
|
||||
break;
|
||||
case "android:endX":
|
||||
mEndX = Float.parseFloat(value);
|
||||
break;
|
||||
case "android:endY":
|
||||
mEndY = Float.parseFloat(value);
|
||||
break;
|
||||
case "android:centerX":
|
||||
mCenterX = Float.parseFloat(value);
|
||||
break;
|
||||
case "android:centerY":
|
||||
mCenterY = Float.parseFloat(value);
|
||||
break;
|
||||
case "android:gradientRadius":
|
||||
mGradientRadius = Float.parseFloat(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawGradient(@NonNull Graphics2D g, @NonNull Path2D path2d, boolean fill) {
|
||||
if (mGradientStops.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float[] mFractions = new float[mGradientStops.size()];
|
||||
Color[] mGradientColors = new Color[mGradientStops.size()];
|
||||
|
||||
for (int j = 0; j < mGradientStops.size(); j++) {
|
||||
GradientStop stop = mGradientStops.get(j);
|
||||
float fraction = Float.parseFloat(stop.getOffset());
|
||||
int colorInt = parseColorValue(stop.getColor());
|
||||
//TODO: If opacity for android gradient items becomes supported, use mOpacity to modify colors.
|
||||
@SuppressWarnings("UseJBColor")
|
||||
Color color = new Color(colorInt, true);
|
||||
|
||||
mFractions[j] = fraction;
|
||||
mGradientColors[j] = color;
|
||||
}
|
||||
|
||||
// Gradient stop fractions must be strictly increasing in Java Swing. Increment the
|
||||
// second of two equal fraction floats by a small amount to get the effect of two
|
||||
// overlapping stops. When the fraction is the 1.0, then decrement accordingly.
|
||||
// See LinearGradientPaint constructor:
|
||||
// https://docs.oracle.com/javase/7/docs/api/java/awt/LinearGradientPaint.html
|
||||
for (int i = 0; i < mGradientStops.size() - 1; i++) {
|
||||
if (mFractions[i] >= mFractions[i + 1]) {
|
||||
if (mFractions[i] + EPSILON <= 1.0f) {
|
||||
mFractions[i + 1] = mFractions[i] + EPSILON;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = mGradientStops.size() - 2; i >= 0; i--) {
|
||||
if (mFractions[i] >= mFractions[i + 1] && mFractions[i] >= 1.0f) {
|
||||
mFractions[i] = mFractions[i + 1] - EPSILON;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
// Create stroke in case the gradient applies to a stroke.
|
||||
BasicStroke stroke =
|
||||
new BasicStroke(
|
||||
mStrokeWidth, mStrokeLineCap, mStrokeLineJoin, mStrokeMiterlimit);
|
||||
|
||||
// If there is only one stop, fill should be a solid color of the one stop.
|
||||
if (mGradientStops.size() == 1) {
|
||||
g.setColor(mGradientColors[0]);
|
||||
if (!fill) {
|
||||
g.setStroke(stroke);
|
||||
}
|
||||
g.draw(path2d);
|
||||
} else {
|
||||
MultipleGradientPaint.CycleMethod tile = MultipleGradientPaint.CycleMethod.NO_CYCLE;
|
||||
if (mTileMode.equals("mirror")) {
|
||||
tile = MultipleGradientPaint.CycleMethod.REFLECT;
|
||||
} else if (mTileMode.equals("repeat")) {
|
||||
tile = MultipleGradientPaint.CycleMethod.REPEAT;
|
||||
}
|
||||
|
||||
if (mGradientType.equals("linear")) {
|
||||
LinearGradientPaint gradient =
|
||||
new LinearGradientPaint(
|
||||
mStartX,
|
||||
mStartY,
|
||||
mEndX,
|
||||
mEndY,
|
||||
mFractions,
|
||||
mGradientColors,
|
||||
tile);
|
||||
g.setPaint(gradient);
|
||||
} else if (mGradientType.equals("radial")) {
|
||||
RadialGradientPaint paint =
|
||||
new RadialGradientPaint(
|
||||
mCenterX,
|
||||
mCenterY,
|
||||
mGradientRadius,
|
||||
mFractions,
|
||||
mGradientColors,
|
||||
tile);
|
||||
g.setPaint(paint);
|
||||
} else if (mGradientType.equals("sweep")) {
|
||||
// AWT doesn't support sweep gradients but Android does.
|
||||
getLogger().log(Level.WARNING,
|
||||
">>>>>> Unable to render a sweep gradient."
|
||||
+ " Using a solid color instead. >>>>>>");
|
||||
g.setPaint(mGradientColors[0]);
|
||||
} else {
|
||||
getLogger().log(Level.WARNING,
|
||||
">>>>>> Unsupported gradient type: \"" + mGradientType + "\">>>>>>");
|
||||
}
|
||||
|
||||
if (fill) {
|
||||
g.fill(path2d);
|
||||
} else {
|
||||
g.setStroke(stroke);
|
||||
g.draw(path2d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Logger getLogger() {
|
||||
return Logger.getLogger(VdPath.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.google.common.math.DoubleMath.roundToInt;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.annotations.Nullable;
|
||||
import com.android.ide.common.util.AssetUtil;
|
||||
import com.android.utils.XmlUtils;
|
||||
import com.google.common.base.Strings;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.math.RoundingMode;
|
||||
import org.apache.xml.serialize.OutputFormat;
|
||||
import org.apache.xml.serialize.XMLSerializer;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
* Generates an image based on the VectorDrawable's XML content.
|
||||
*/
|
||||
public class VdPreview {
|
||||
private static final String ANDROID_ALPHA = "android:alpha";
|
||||
private static final String ANDROID_TINT = "android:tint";
|
||||
private static final String ANDROID_AUTO_MIRRORED = "android:autoMirrored";
|
||||
private static final String ANDROID_HEIGHT = "android:height";
|
||||
private static final String ANDROID_WIDTH = "android:width";
|
||||
private static final int MAX_PREVIEW_IMAGE_SIZE = 4096;
|
||||
private static final int MIN_PREVIEW_IMAGE_SIZE = 1;
|
||||
|
||||
/**
|
||||
* Encapsulates the information used to determine the preview image size. The reason we have
|
||||
* different ways here is that both Studio UI and build process need to use this common code
|
||||
* path to generate images for vector drawables. When {@code maxDimension} is not zero, use
|
||||
* {@code maxDimension} as the maximum dimension value while keeping the aspect ratio.
|
||||
* Otherwise, use {@code imageScale} to scale the image based on the XML's size information.
|
||||
*/
|
||||
public static class TargetSize {
|
||||
private int imageMaxDimension;
|
||||
private double imageScale;
|
||||
|
||||
private TargetSize(int maxDimension, double imageScale) {
|
||||
this.imageMaxDimension = maxDimension;
|
||||
this.imageScale = imageScale;
|
||||
}
|
||||
|
||||
public static TargetSize createFromMaxDimension(int maxDimension) {
|
||||
return new TargetSize(maxDimension, 0);
|
||||
}
|
||||
|
||||
public static TargetSize createFromScale(double imageScale) {
|
||||
return new TargetSize(0, imageScale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a format object for XML formatting.
|
||||
*/
|
||||
@NonNull
|
||||
private static OutputFormat getPrettyPrintFormat() {
|
||||
OutputFormat format = new OutputFormat();
|
||||
format.setLineWidth(120);
|
||||
format.setIndenting(true);
|
||||
format.setIndent(4);
|
||||
format.setEncoding("UTF-8");
|
||||
format.setOmitComments(true);
|
||||
format.setOmitXMLDeclaration(true);
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* The UI can override some properties of the Vector drawable. In order to override in
|
||||
* an uniform way, we re-parse the XML file and pick the appropriate attributes to override.
|
||||
*
|
||||
* @param document the parsed document of original VectorDrawable's XML file.
|
||||
* @param overrideInfo incoming override information for VectorDrawable.
|
||||
* @param errorLog log for the parsing errors and warnings.
|
||||
* @return the overridden XML, or null if exception happens or no attributes need to be
|
||||
* overridden.
|
||||
*/
|
||||
@Nullable
|
||||
public static String overrideXmlContent(
|
||||
@NonNull Document document,
|
||||
@NonNull VdOverrideInfo overrideInfo,
|
||||
@Nullable StringBuilder errorLog) {
|
||||
boolean contentChanged = false;
|
||||
Element root = document.getDocumentElement();
|
||||
|
||||
// Update attributes, note that attributes as width and height are required,
|
||||
// while others are optional.
|
||||
if (overrideInfo.needsOverrideWidth()) {
|
||||
if (setDimension(root, ANDROID_WIDTH, overrideInfo.getWidth())) {
|
||||
contentChanged = true;
|
||||
}
|
||||
}
|
||||
if (overrideInfo.needsOverrideHeight()) {
|
||||
if (setDimension(root, ANDROID_HEIGHT, overrideInfo.getHeight())) {
|
||||
contentChanged = true;
|
||||
}
|
||||
}
|
||||
if (overrideInfo.needsOverrideAlpha()) {
|
||||
String value = XmlUtils.formatFloatValue(overrideInfo.getAlpha());
|
||||
if (setAttributeValue(root, ANDROID_ALPHA, value)) {
|
||||
contentChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideInfo.needsOverrideTint()) {
|
||||
String value = String.format("#%06X", overrideInfo.tintRgb());
|
||||
if (setAttributeValue(root, ANDROID_TINT, value)) {
|
||||
contentChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (overrideInfo.getAutoMirrored()) {
|
||||
if (setAttributeValue(root, ANDROID_AUTO_MIRRORED, "true")) {
|
||||
contentChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentChanged) {
|
||||
// Pretty-print the XML string from the document.
|
||||
StringWriter stringOut = new StringWriter();
|
||||
XMLSerializer serial = new XMLSerializer(stringOut, getPrettyPrintFormat());
|
||||
try {
|
||||
serial.serialize(document);
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (errorLog != null) {
|
||||
errorLog.append("Exception while parsing XML file:\n").append(e.getMessage());
|
||||
}
|
||||
}
|
||||
return stringOut.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets value of a dimension attribute. The returned value reflects whether the value of
|
||||
* the attribute was changed or not.
|
||||
*/
|
||||
private static boolean setDimension(
|
||||
@NonNull Element element, @NonNull String attrName, double value) {
|
||||
String newValue = XmlUtils.formatFloatValue(value) + "dp";
|
||||
return setAttributeValue(element, attrName, newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets value of an attribute. The returned value reflects whether the value of the attribute
|
||||
* was changed or not.
|
||||
*/
|
||||
private static boolean setAttributeValue(
|
||||
@NonNull Element element, @NonNull String attrName, @NonNull String value) {
|
||||
String oldValue = element.getAttribute(attrName);
|
||||
element.setAttribute(attrName, value);
|
||||
return !value.equals(oldValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an image according to the VectorDrawable's content {@code xmlFileContent}.
|
||||
* At the same time, {@code vdErrorLog} captures all the errors found during parsing.
|
||||
* The size of image is determined by the {@code size}.
|
||||
*
|
||||
* @param targetSize the size of result image.
|
||||
* @param xmlFileContent VectorDrawable's XML file's content.
|
||||
* @param errorLog log for the parsing errors and warnings.
|
||||
* @return an preview image according to the VectorDrawable's XML
|
||||
*/
|
||||
@Nullable
|
||||
public static BufferedImage getPreviewFromVectorXml(@NonNull TargetSize targetSize,
|
||||
@Nullable String xmlFileContent,
|
||||
@Nullable StringBuilder errorLog) {
|
||||
if (Strings.isNullOrEmpty(xmlFileContent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(xmlFileContent.getBytes(UTF_8));
|
||||
VdTree vdTree = VdParser.parse(inputStream, errorLog);
|
||||
|
||||
return getPreviewFromVectorTree(targetSize, vdTree, errorLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* This generates an image from a vector tree.
|
||||
* The size of image is determined by the {@code size}.
|
||||
*
|
||||
* @param targetSize the size of result image.
|
||||
* @param xml The vector drawable XML document
|
||||
* @param vdErrorLog log for the errors and warnings.
|
||||
* @return an preview image according to the VectorDrawable's XML
|
||||
*/
|
||||
@NonNull
|
||||
public static BufferedImage getPreviewFromVectorDocument(@NonNull TargetSize targetSize,
|
||||
@NonNull Document xml,
|
||||
@Nullable StringBuilder vdErrorLog) {
|
||||
VdTree vdTree = new VdTree();
|
||||
vdTree.parse(xml);
|
||||
return getPreviewFromVectorTree(targetSize, vdTree, vdErrorLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an image from a vector tree. The size of image is determined by the {@code size}.
|
||||
*
|
||||
* @param targetSize the size of result image.
|
||||
* @param vdTree The vector drawable
|
||||
* @param errorLog log for the errors and warnings.
|
||||
* @return an preview image according to the VectorDrawable's XML
|
||||
*/
|
||||
@NonNull
|
||||
public static BufferedImage getPreviewFromVectorTree(@NonNull TargetSize targetSize,
|
||||
@NonNull VdTree vdTree,
|
||||
@Nullable StringBuilder errorLog) {
|
||||
// If the forceImageSize is set (>0), then we honor that.
|
||||
// Otherwise, we will ask the vector drawable for the prefer size, then apply the imageScale.
|
||||
double vdWidth = vdTree.getBaseWidth();
|
||||
double vdHeight = vdTree.getBaseHeight();
|
||||
double imageWidth;
|
||||
double imageHeight;
|
||||
int forceImageSize = targetSize.imageMaxDimension;
|
||||
double imageScale = targetSize.imageScale;
|
||||
|
||||
if (forceImageSize > 0) {
|
||||
// The goal here is to generate an image within certain size, while preserving
|
||||
// the aspect ratio as accurately as we can. If it is scaling too much to fit in,
|
||||
// we log an error.
|
||||
double maxVdSize = Math.max(vdWidth, vdHeight);
|
||||
double ratioToForceImageSize = forceImageSize / maxVdSize;
|
||||
double scaledWidth = ratioToForceImageSize * vdWidth;
|
||||
double scaledHeight = ratioToForceImageSize * vdHeight;
|
||||
imageWidth =
|
||||
limitToInterval(scaledWidth, MIN_PREVIEW_IMAGE_SIZE, MAX_PREVIEW_IMAGE_SIZE);
|
||||
imageHeight =
|
||||
limitToInterval(scaledHeight, MIN_PREVIEW_IMAGE_SIZE, MAX_PREVIEW_IMAGE_SIZE);
|
||||
if (errorLog != null && (scaledWidth != imageWidth || scaledHeight != imageHeight)) {
|
||||
errorLog.append("Invalid image size, can't fit in a square whose size is ")
|
||||
.append(forceImageSize);
|
||||
}
|
||||
} else {
|
||||
imageWidth = vdWidth * imageScale;
|
||||
imageHeight = vdHeight * imageScale;
|
||||
}
|
||||
|
||||
// Create the image according to the vector drawable's aspect ratio.
|
||||
BufferedImage image =
|
||||
AssetUtil.newArgbBufferedImage(
|
||||
roundToInt(imageWidth, RoundingMode.HALF_UP),
|
||||
roundToInt(imageHeight, RoundingMode.HALF_UP));
|
||||
vdTree.drawIntoImage(image);
|
||||
return image;
|
||||
}
|
||||
|
||||
private static double limitToInterval(double value, double begin, double end) {
|
||||
return Math.max(begin, Math.min(end, value));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.ide.common.vectordrawable;
|
||||
|
||||
import static com.android.SdkConstants.TAG_CLIP_PATH;
|
||||
import static com.android.SdkConstants.TAG_GROUP;
|
||||
import static com.android.SdkConstants.TAG_PATH;
|
||||
import static com.android.SdkConstants.TAG_VECTOR;
|
||||
import static com.android.ide.common.vectordrawable.VdUtil.parseColorValue;
|
||||
|
||||
import com.android.annotations.NonNull;
|
||||
import com.android.ide.common.util.AssetUtil;
|
||||
import java.awt.AlphaComposite;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.NoninvertibleTransformException;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
/**
|
||||
* Represents the whole VectorDrawable XML file's tree.
|
||||
*/
|
||||
class VdTree {
|
||||
private static final Pattern ATTRIBUTE_PATTERN =
|
||||
Pattern.compile("^\\s*(\\d+(\\.\\d+)*)\\s*([a-zA-Z]+)\\s*$");
|
||||
|
||||
private final VdGroup mRootGroup = new VdGroup();
|
||||
|
||||
private float mBaseWidth = 1;
|
||||
private float mBaseHeight = 1;
|
||||
private float mPortWidth = 1;
|
||||
private float mPortHeight = 1;
|
||||
private float mRootAlpha = 1;
|
||||
private int mRootTint;
|
||||
|
||||
private static final boolean DBG_PRINT_TREE = false;
|
||||
|
||||
private static final String INDENT = " ";
|
||||
|
||||
float getBaseWidth(){
|
||||
return mBaseWidth;
|
||||
}
|
||||
|
||||
float getBaseHeight(){
|
||||
return mBaseHeight;
|
||||
}
|
||||
|
||||
float getPortWidth(){
|
||||
return mPortWidth;
|
||||
}
|
||||
|
||||
float getPortHeight(){
|
||||
return mPortHeight;
|
||||
}
|
||||
|
||||
private void drawTree(Graphics2D g, int w, int h) {
|
||||
float scaleX = w / mPortWidth;
|
||||
float scaleY = h / mPortHeight;
|
||||
|
||||
AffineTransform rootMatrix = new AffineTransform(); // identity
|
||||
|
||||
mRootGroup.draw(g, rootMatrix, scaleX, scaleY);
|
||||
}
|
||||
|
||||
/** Draws the VdTree into an image. */
|
||||
@SuppressWarnings("UseJBColor") // No need to use JBColor here.
|
||||
public void drawIntoImage(@NonNull BufferedImage image) {
|
||||
Graphics2D gFinal = (Graphics2D) image.getGraphics();
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
gFinal.setColor(new Color(255, 255, 255, 0));
|
||||
gFinal.fillRect(0, 0, width, height);
|
||||
|
||||
if (mRootAlpha < 1.0) {
|
||||
// Draw into a temporary image, then draw into the result image applying alpha blending.
|
||||
BufferedImage alphaImage = AssetUtil.newArgbBufferedImage(width, height);
|
||||
Graphics2D gTemp = (Graphics2D)alphaImage.getGraphics();
|
||||
drawTree(gTemp, width, height);
|
||||
gFinal.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, mRootAlpha));
|
||||
gFinal.drawImage(alphaImage, 0, 0, null);
|
||||
gTemp.dispose();
|
||||
} else {
|
||||
drawTree(gFinal, width, height);
|
||||
}
|
||||
|
||||
if (mRootTint != 0) {
|
||||
// Apply tint.
|
||||
BufferedImage tintImage = AssetUtil.newArgbBufferedImage(width, height);
|
||||
Graphics2D gTemp = (Graphics2D)tintImage.getGraphics();
|
||||
gTemp.setPaint(new Color(mRootTint));
|
||||
gTemp.fillRect(0, 0, width, height);
|
||||
gFinal.setComposite(AlphaComposite.SrcIn);
|
||||
try {
|
||||
gFinal.drawImage(tintImage, gFinal.getTransform().createInverse(), null);
|
||||
} catch (NoninvertibleTransformException ignored) {
|
||||
}
|
||||
gTemp.dispose();
|
||||
}
|
||||
|
||||
gFinal.dispose();
|
||||
}
|
||||
|
||||
public void parse(@NonNull Document doc) {
|
||||
NodeList rootNodeList = doc.getElementsByTagName(TAG_VECTOR);
|
||||
assert rootNodeList.getLength() == 1;
|
||||
Node rootNode = rootNodeList.item(0);
|
||||
|
||||
parseRootNode(rootNode);
|
||||
parseTree(rootNode, mRootGroup);
|
||||
|
||||
if (DBG_PRINT_TREE) {
|
||||
debugPrintTree(0, mRootGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private static void parseTree(@NonNull Node currentNode, @NonNull VdGroup currentGroup) {
|
||||
NodeList childrenNodes = currentNode.getChildNodes();
|
||||
int length = childrenNodes.getLength();
|
||||
for (int i = 0; i < length; i++) {
|
||||
Node child = childrenNodes.item(i);
|
||||
if (child.getNodeType() == Node.ELEMENT_NODE) {
|
||||
String name = child.getNodeName();
|
||||
if (TAG_GROUP.equals(name)) {
|
||||
VdGroup newGroup = parseGroupAttributes(child.getAttributes());
|
||||
currentGroup.add(newGroup);
|
||||
parseTree(child, newGroup);
|
||||
} else if (TAG_PATH.equals(name)) {
|
||||
VdPath newPath = parsePathAttributes(child.getAttributes());
|
||||
newPath.addGradientIfExists(child);
|
||||
currentGroup.add(newPath);
|
||||
} else if (TAG_CLIP_PATH.equals(name)) {
|
||||
VdPath newClipPath = parsePathAttributes(child.getAttributes());
|
||||
newClipPath.setClipPath(true);
|
||||
currentGroup.add(newClipPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void debugPrintTree(int level, @NonNull VdGroup mRootGroup) {
|
||||
int len = mRootGroup.size();
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
StringBuilder prefixBuilder = new StringBuilder();
|
||||
for (int i = 0; i < level; i++) {
|
||||
prefixBuilder.append(INDENT);
|
||||
}
|
||||
String prefix = prefixBuilder.toString();
|
||||
ArrayList<VdElement> children = mRootGroup.getChildren();
|
||||
for (int i = 0; i < len; i++) {
|
||||
VdElement child = children.get(i);
|
||||
if (child.isGroup()) {
|
||||
// TODO: print group info
|
||||
debugPrintTree(level + 1, (VdGroup) child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseRootNode(@NonNull Node rootNode) {
|
||||
if (rootNode.hasAttributes()) {
|
||||
parseSize(rootNode.getAttributes());
|
||||
}
|
||||
}
|
||||
|
||||
private void parseSize(@NonNull NamedNodeMap attributes) {
|
||||
int len = attributes.getLength();
|
||||
for (int i = 0; i < len; i++) {
|
||||
String name = attributes.item(i).getNodeName();
|
||||
String value = attributes.item(i).getNodeValue();
|
||||
Matcher matcher = ATTRIBUTE_PATTERN.matcher(value);
|
||||
float size = 0;
|
||||
if (matcher.matches()) {
|
||||
size = Float.parseFloat(matcher.group(1));
|
||||
}
|
||||
|
||||
// TODO: Extract dimension units like px etc. Right now all are treated as "dp".
|
||||
if ("android:width".equals(name)) {
|
||||
mBaseWidth = size;
|
||||
} else if ("android:height".equals(name)) {
|
||||
mBaseHeight = size;
|
||||
} else if ("android:viewportWidth".equals(name)) {
|
||||
mPortWidth = Float.parseFloat(value);
|
||||
} else if ("android:viewportHeight".equals(name)) {
|
||||
mPortHeight = Float.parseFloat(value);
|
||||
} else if ("android:alpha".equals(name)) {
|
||||
mRootAlpha = Float.parseFloat(value);
|
||||
} else if ("android:tint".equals(name) && value.startsWith("#")) {
|
||||
mRootTint = parseColorValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static VdPath parsePathAttributes(@NonNull NamedNodeMap attributes) {
|
||||
VdPath vgPath = new VdPath();
|
||||
vgPath.parseAttributes(attributes);
|
||||
return vgPath;
|
||||
}
|
||||
|
||||
private static VdGroup parseGroupAttributes(@NonNull NamedNodeMap attributes) {
|
||||
VdGroup vgGroup = new VdGroup();
|
||||
vgGroup.parseAttributes(attributes);
|
||||
return vgGroup;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:JvmName("VdUtil")
|
||||
package com.android.ide.common.vectordrawable
|
||||
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
|
||||
/**
|
||||
* Returns a [NumberFormat] of sufficient precision to use for formatting coordinate
|
||||
* values given the maximum viewport dimension.
|
||||
*/
|
||||
fun getCoordinateFormat(maxViewportSize: Float): NumberFormat {
|
||||
val exponent = Math.floor(Math.log10(maxViewportSize.toDouble())).toInt()
|
||||
var fractionalDigits = 5 - exponent
|
||||
val formatBuilder = StringBuilder("#")
|
||||
if (fractionalDigits > 0) {
|
||||
// Build a string with decimal places for "#.##...", and cap at 6 digits.
|
||||
if (fractionalDigits > 6) {
|
||||
fractionalDigits = 6
|
||||
}
|
||||
formatBuilder.append('.')
|
||||
for (i in 0 until fractionalDigits) {
|
||||
formatBuilder.append('#')
|
||||
}
|
||||
}
|
||||
val fractionSeparator = DecimalFormatSymbols()
|
||||
fractionSeparator.decimalSeparator = '.'
|
||||
val format = DecimalFormat(formatBuilder.toString(), fractionSeparator)
|
||||
format.roundingMode = RoundingMode.HALF_UP
|
||||
return format
|
||||
}
|
||||
|
||||
// Workaround for https://youtrack.jetbrains.com/issue/KT-4749
|
||||
private const val ALPHA_MASK = 0xFF000000.toInt()
|
||||
|
||||
/**
|
||||
* Parses a color value in #AARRGGBB format.
|
||||
*
|
||||
* @param color the color value string
|
||||
* @return the integer color value
|
||||
*/
|
||||
fun parseColorValue(color: String): Int {
|
||||
require(color.startsWith("#")) { "Invalid color value $color" }
|
||||
|
||||
return when (color.length) {
|
||||
7 -> {
|
||||
// #RRGGBB
|
||||
Integer.parseUnsignedInt(color.substring(1), 16) or ALPHA_MASK
|
||||
}
|
||||
9 -> {
|
||||
// #AARRGGBB
|
||||
Integer.parseUnsignedInt(color.substring(1), 16)
|
||||
}
|
||||
4 -> {
|
||||
// #RGB
|
||||
val v = Integer.parseUnsignedInt(color.substring(1), 16)
|
||||
var k = (v shr 8 and 0xF) * 0x110000
|
||||
k = k or (v shr 4 and 0xF) * 0x1100
|
||||
k = k or (v and 0xF) * 0x11
|
||||
k or ALPHA_MASK
|
||||
}
|
||||
5 -> {
|
||||
// #ARGB
|
||||
val v = Integer.parseUnsignedInt(color.substring(1), 16)
|
||||
var k = (v shr 12 and 0xF) * 0x11000000
|
||||
k = k or (v shr 8 and 0xF) * 0x110000
|
||||
k = k or (v shr 4 and 0xF) * 0x1100
|
||||
k = k or (v and 0xF) * 0x11
|
||||
k or ALPHA_MASK
|
||||
}
|
||||
else -> ALPHA_MASK
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
/**
|
||||
* List of 'core' icons that will be added to the 'core' icons module, and depended on by
|
||||
* material. These icons are the set of most commonly used icons, including icons used by
|
||||
* Material components directly (such as the menu icon in an AppBar). All icons not specified
|
||||
* here will be generated to the 'extended' icons module.
|
||||
*/
|
||||
val CoreIcons = setOf(
|
||||
"Add", "Alert", "ArrowDown", "ArrowDownLeft", "ArrowDownload", "ArrowExpand", "ArrowForward", "ArrowLeft",
|
||||
"ArrowRight", "ArrowSync", "ArrowUp", "ArrowUpLeft", "ArrowUpRight", "Attach", "Backspace", "CalendarLtr",
|
||||
"CaretDown", "CaretDownRight", "CaretLeft", "CaretRight", "CaretUp", "Checkmark", "ChevronDown", "ChevronLeft",
|
||||
"ChevronRight", "ChevronUp", "Clock", "Cloud", "Copy", "Cut", "Delete", "Dismiss", "Document", "Edit",
|
||||
"ErrorCircle", "Eye", "Filter", "Flag", "Flash", "Folder", "Heart", "History", "Home", "Image", "Important", "Info",
|
||||
"Key", "Link", "Mail", "Maximize", "MoreHorizontal", "MoreVertical", "Navigation", "Open", "Options", "Pause",
|
||||
"Person", "Pin", "Play", "ReOrder", "Rename", "Save", "Search", "Send", "Settings", "Share", "Star", "Subtract",
|
||||
"Tag", "Warning", "Wrench"
|
||||
)
|
||||
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
/**
|
||||
* Represents a icon's Kotlin name, processed XML file name, theme, and XML file content.
|
||||
*
|
||||
* The [kotlinName] is typically the PascalCase equivalent of the original icon name, with the
|
||||
* caveat that icons starting with a number are prefixed with an underscore.
|
||||
*
|
||||
* @property kotlinName the name of the generated Kotlin property, for example `ZoomOutMap`.
|
||||
* @property xmlFileName the name of the processed XML file
|
||||
* @property theme the theme of this icon
|
||||
* @property fileContent the content of the source XML file that will be parsed.
|
||||
*/
|
||||
data class Icon(
|
||||
val kotlinName: String,
|
||||
val xmlFileName: String,
|
||||
val theme: IconTheme,
|
||||
val fileContent: String
|
||||
)
|
||||
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
import androidx.compose.material.icons.generator.vector.FillType
|
||||
import androidx.compose.material.icons.generator.vector.PathParser
|
||||
import androidx.compose.material.icons.generator.vector.Vector
|
||||
import androidx.compose.material.icons.generator.vector.VectorNode
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParser.END_DOCUMENT
|
||||
import org.xmlpull.v1.XmlPullParser.END_TAG
|
||||
import org.xmlpull.v1.XmlPullParser.START_TAG
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
|
||||
/**
|
||||
* Parser that converts [icon]s into [Vector]s
|
||||
*/
|
||||
class IconParser(private val icon: Icon) {
|
||||
|
||||
/**
|
||||
* @return a [Vector] representing the provided [icon].
|
||||
*/
|
||||
fun parse(): Vector {
|
||||
val parser = XmlPullParserFactory.newInstance().newPullParser().apply {
|
||||
setInput(icon.fileContent.byteInputStream(), null)
|
||||
seekToStartTag()
|
||||
}
|
||||
|
||||
check(parser.name == "vector") { "The start tag must be <vector>!" }
|
||||
|
||||
parser.next()
|
||||
|
||||
val nodes = mutableListOf<VectorNode>()
|
||||
|
||||
var currentGroup: VectorNode.Group? = null
|
||||
|
||||
while (!parser.isAtEnd()) {
|
||||
when (parser.eventType) {
|
||||
START_TAG -> {
|
||||
when (parser.name) {
|
||||
PATH -> {
|
||||
val pathData = parser.getAttributeValue(
|
||||
null,
|
||||
PATH_DATA
|
||||
)
|
||||
val fillAlpha = parser.getValueAsFloat(FILL_ALPHA)
|
||||
val strokeAlpha = parser.getValueAsFloat(STROKE_ALPHA)
|
||||
val fillType = when (parser.getAttributeValue(null, FILL_TYPE)) {
|
||||
// evenOdd and nonZero are the only supported values here, where
|
||||
// nonZero is the default if no values are defined.
|
||||
EVEN_ODD -> FillType.EvenOdd
|
||||
else -> FillType.NonZero
|
||||
}
|
||||
val path = VectorNode.Path(
|
||||
strokeAlpha = strokeAlpha ?: 1f,
|
||||
fillAlpha = fillAlpha ?: 1f,
|
||||
fillType = fillType,
|
||||
nodes = PathParser.parsePathString(pathData)
|
||||
)
|
||||
if (currentGroup != null) {
|
||||
currentGroup.paths.add(path)
|
||||
} else {
|
||||
nodes.add(path)
|
||||
}
|
||||
}
|
||||
// Material icons are simple and don't have nested groups, so this can be simple
|
||||
GROUP -> {
|
||||
val group = VectorNode.Group()
|
||||
currentGroup = group
|
||||
nodes.add(group)
|
||||
}
|
||||
CLIP_PATH -> { /* TODO: b/147418351 - parse clipping paths */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parser.next()
|
||||
}
|
||||
|
||||
return Vector(nodes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the float value for the attribute [name], or null if it couldn't be found
|
||||
*/
|
||||
private fun XmlPullParser.getValueAsFloat(name: String) =
|
||||
getAttributeValue(null, name)?.toFloatOrNull()
|
||||
|
||||
private fun XmlPullParser.seekToStartTag(): XmlPullParser {
|
||||
var type = next()
|
||||
while (type != START_TAG && type != END_DOCUMENT) {
|
||||
// Empty loop
|
||||
type = next()
|
||||
}
|
||||
if (type != START_TAG) {
|
||||
throw XmlPullParserException("No start tag found")
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private fun XmlPullParser.isAtEnd() =
|
||||
eventType == END_DOCUMENT || (depth < 1 && eventType == END_TAG)
|
||||
|
||||
// XML tag names
|
||||
private const val CLIP_PATH = "clip-path"
|
||||
private const val GROUP = "group"
|
||||
private const val PATH = "path"
|
||||
|
||||
// XML attribute names
|
||||
private const val PATH_DATA = "android:pathData"
|
||||
private const val FILL_ALPHA = "android:fillAlpha"
|
||||
private const val STROKE_ALPHA = "android:strokeAlpha"
|
||||
private const val FILL_TYPE = "android:fillType"
|
||||
|
||||
// XML attribute values
|
||||
private const val EVEN_ODD = "evenOdd"
|
||||
@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
import com.google.common.base.CaseFormat
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Processes vector drawables in [iconDirectory] into a list of icons, removing any unwanted
|
||||
* attributes (such as android: attributes that reference the theme) from the XML source.
|
||||
*
|
||||
* Drawables in [iconDirectory] should match the following structure, see download_material_icons.py
|
||||
* to update icons, using this structure.
|
||||
*
|
||||
* // Top level
|
||||
* [iconDirectory]
|
||||
* // Theme name
|
||||
* ├── filled
|
||||
* // Icon name
|
||||
* ├── menu.xml
|
||||
* └── zoom_out_map.xml
|
||||
* ├── outlined
|
||||
* ├── rounded
|
||||
* ├── twotone
|
||||
* └── sharp
|
||||
*
|
||||
* @param iconDirectory root directory containing the directory structure mentioned above
|
||||
* @param expectedApiFile location of the checked-in API file that contains the current list of
|
||||
* all icons processed and generated
|
||||
* @param generatedApiFile location of the to-be-generated API file in the build directory,
|
||||
* that we will write to and compare with [expectedApiFile]. This way the generated file can be
|
||||
* copied to overwrite the expected file, 'confirming' any API changes as a result of changing
|
||||
* icons in [iconDirectory].
|
||||
*/
|
||||
class IconProcessor(
|
||||
private val iconDirectories: List<File>,
|
||||
private val expectedApiFile: File,
|
||||
private val generatedApiFile: File,
|
||||
private val verifyApi: Boolean = true
|
||||
) {
|
||||
/**
|
||||
* @return a list of processed [Icon]s, from the given [iconDirectory].
|
||||
*/
|
||||
fun process(): List<Icon> {
|
||||
val icons = loadIcons()
|
||||
|
||||
if (verifyApi) {
|
||||
ensureIconsExistInAllThemes(icons)
|
||||
writeApiFile(icons, generatedApiFile)
|
||||
checkApi(expectedApiFile, generatedApiFile)
|
||||
}
|
||||
|
||||
return icons
|
||||
}
|
||||
|
||||
private fun loadIcons(): List<Icon> {
|
||||
val themeDirs = iconDirectories
|
||||
|
||||
return themeDirs.flatMap { dir ->
|
||||
/* val theme = dir.name.toIconTheme()
|
||||
val icons = dir.walk().filter { !it.isDirectory }.toList()
|
||||
|
||||
val transformedIcons = icons.map { file ->
|
||||
val filename = file.nameWithoutExtension
|
||||
val kotlinName = filename.toKotlinPropertyName()
|
||||
|
||||
// Prefix the icon name with a theme so we can ensure they will be unique when
|
||||
// copied to res/drawable.
|
||||
val xmlName = "${theme.themePackageName}_$filename"
|
||||
|
||||
Icon(
|
||||
kotlinName = kotlinName,
|
||||
xmlFileName = xmlName,
|
||||
theme = theme,
|
||||
fileContent = processXmlFile(file.readText())
|
||||
)
|
||||
}*/
|
||||
val icons = dir.walk().filter { !it.isDirectory }.toList()
|
||||
|
||||
val theme = when (dir.name) {
|
||||
"regular" -> IconTheme.Regular
|
||||
"filled" -> IconTheme.Filled
|
||||
else -> IconTheme.Default
|
||||
}
|
||||
|
||||
val transformedIcons = icons.map { file ->
|
||||
val filename = file.nameWithoutExtension
|
||||
val kotlinName = filename
|
||||
|
||||
// Prefix the icon name with a theme so we can ensure they will be unique when
|
||||
// copied to res/drawable.
|
||||
val xmlName = "${theme.themePackageName}_$filename"
|
||||
|
||||
println("Name: $kotlinName, Theme: $theme")
|
||||
Icon(
|
||||
kotlinName = kotlinName,
|
||||
xmlFileName = xmlName,
|
||||
theme = theme,
|
||||
fileContent = processXmlFile(file.readText())
|
||||
)
|
||||
}
|
||||
// Ensure icon names are unique when accounting for case insensitive filesystems -
|
||||
// workaround for b/216295020
|
||||
transformedIcons
|
||||
.groupBy { it.kotlinName.lowercase(Locale.ROOT) }
|
||||
.filter { it.value.size > 1 }
|
||||
.filterNot { entry ->
|
||||
entry.value.map { it.kotlinName }.containsAll(AllowedDuplicateIconNames)
|
||||
}
|
||||
.forEach { entry ->
|
||||
throw IllegalStateException(
|
||||
"""Found multiple icons with the same case-insensitive filename:
|
||||
| ${entry.value.joinToString()}. Generating icons with the same
|
||||
| case-insensitive filename will cause issues on devices without
|
||||
| a case sensitive filesystem (OSX / Windows).""".trimMargin()
|
||||
)
|
||||
}
|
||||
|
||||
transformedIcons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given [fileContent] by removing android theme attributes and values.
|
||||
*/
|
||||
private fun processXmlFile(fileContent: String): String {
|
||||
// Remove any defined tint for paths that use theme attributes
|
||||
val tintAttribute = Regex.escape("""android:tint="?attr/colorControlNormal">""")
|
||||
val tintRegex = """\n.*?$tintAttribute""".toRegex(RegexOption.MULTILINE)
|
||||
|
||||
return fileContent
|
||||
.replace(tintRegex, ">")
|
||||
// The imported icons have white as the default path color, so let's change it to be
|
||||
// black as is typical on Android.
|
||||
.replace("@android:color/white", "@android:color/black")
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that each icon in each theme is available in every other theme
|
||||
*/
|
||||
private fun ensureIconsExistInAllThemes(icons: List<Icon>) {
|
||||
val groupedIcons = icons.groupBy { it.theme }
|
||||
|
||||
check(groupedIcons.keys.containsAll(IconTheme.values().toList())) {
|
||||
"Some themes were missing from the generated icons"
|
||||
}
|
||||
|
||||
val expectedIconNames = groupedIcons.values.map { themeIcons ->
|
||||
themeIcons.map { icon -> icon.kotlinName }.sorted()
|
||||
}
|
||||
|
||||
expectedIconNames.first().let { expected ->
|
||||
expectedIconNames.forEach { actual ->
|
||||
check(actual == expected) {
|
||||
"Not all icons were found in all themes $actual $expected"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an API representation of [icons] to [file].
|
||||
*/
|
||||
private fun writeApiFile(icons: List<Icon>, file: File) {
|
||||
val apiText = icons
|
||||
.groupBy { it.theme }
|
||||
.map { (theme, themeIcons) ->
|
||||
themeIcons
|
||||
.map { icon ->
|
||||
theme.themeClassName + "." + icon.kotlinName
|
||||
}
|
||||
.sorted()
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
.sorted()
|
||||
.joinToString(separator = "\n")
|
||||
|
||||
file.writeText(apiText)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that [generatedFile] matches the checked-in API surface in [expectedFile].
|
||||
*/
|
||||
private fun checkApi(expectedFile: File, generatedFile: File) {
|
||||
check(expectedFile.exists()) {
|
||||
"API file at ${expectedFile.canonicalPath} does not exist!"
|
||||
}
|
||||
|
||||
check(expectedFile.readText() == generatedFile.readText()) {
|
||||
"""Found differences when comparing API files!
|
||||
|Please check the difference and copy over the changes if intended.
|
||||
|expected file: ${expectedFile.canonicalPath}
|
||||
|generated file: ${generatedFile.canonicalPath}
|
||||
""".trimMargin()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a snake_case name to a KotlinProperty name.
|
||||
*
|
||||
* If the first character of [this] is a digit, the resulting name will be prefixed with an `_`
|
||||
*/
|
||||
private fun String.toKotlinPropertyName(): String {
|
||||
return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, this).let { name ->
|
||||
if (name.first().isDigit()) "_$name" else name
|
||||
}
|
||||
}
|
||||
|
||||
// These icons have already shipped in a stable release, so it is too late to rename / remove one to
|
||||
// fix the clash.
|
||||
private val AllowedDuplicateIconNames = listOf("AddChart", "Addchart")
|
||||
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.asTypeName
|
||||
import com.squareup.kotlinpoet.buildCodeBlock
|
||||
import java.io.File
|
||||
import kotlin.reflect.KProperty0
|
||||
|
||||
/**
|
||||
* Generates a list named `AllIcons` that contains pairs mapping a [KProperty0] of the generated
|
||||
* icon to the name of the corresponding XML drawable. This is used so we can run tests comparing
|
||||
* the generated icon against the original source drawable.
|
||||
*
|
||||
* @property icons the list of [Icon]s to generate the manifest from
|
||||
*/
|
||||
class IconTestingManifestGenerator(private val icons: List<Icon>) {
|
||||
/**
|
||||
* Generates the list and writes it to [outputSrcDirectory].
|
||||
*/
|
||||
fun generateTo(outputSrcDirectory: File) {
|
||||
val propertyNames: MutableList<String> = mutableListOf()
|
||||
|
||||
// Split up this list by themes, otherwise we get a Method too large exception.
|
||||
// We will then generate another file that returns the result of concatenating the list
|
||||
// for each theme.
|
||||
icons
|
||||
.groupBy { it.theme }
|
||||
.map { (theme, icons) ->
|
||||
val propertyName = "${theme.themeClassName}Icons"
|
||||
propertyNames += propertyName
|
||||
theme to generateListOfIconsForTheme(propertyName, theme, icons)
|
||||
}
|
||||
.forEach { (theme, fileSpec) ->
|
||||
// KotlinPoet bans wildcard imports, and we run into class compilation errors
|
||||
// (too large a file?) if we add all the imports individually, so let's just add
|
||||
// the imports to each file manually.
|
||||
val wildcardImport =
|
||||
"import androidx.compose.material.icons.${theme.themePackageName}.*"
|
||||
|
||||
fileSpec.writeToWithCopyright(outputSrcDirectory) { fileContent ->
|
||||
fileContent.replace(
|
||||
"import androidx.compose.ui.graphics.vector.ImageVector",
|
||||
"$wildcardImport\n" +
|
||||
"import androidx.compose.ui.graphics.vector.ImageVector"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val mainGetter = FunSpec.getterBuilder()
|
||||
.addStatement("return " + propertyNames.joinToString(" + "))
|
||||
.build()
|
||||
|
||||
FileSpec.builder(PackageNames.FluentIconsPackage.packageName, "AllIcons")
|
||||
.addProperty(
|
||||
PropertySpec.builder("AllIcons", type = listOfIconsType)
|
||||
.getter(mainGetter)
|
||||
.build()
|
||||
).setIndent().build().writeToWithCopyright(outputSrcDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Kotlin file with a list containing all icons of the given [theme].
|
||||
*
|
||||
* @param propertyName the name of the top level property that we should generate the list under
|
||||
* @param theme the theme that we are generating the file for
|
||||
* @param allIcons a list containing all icons that we will filter to match [theme]
|
||||
*/
|
||||
private fun generateListOfIconsForTheme(
|
||||
propertyName: String,
|
||||
theme: IconTheme,
|
||||
allIcons: List<Icon>
|
||||
): FileSpec {
|
||||
val icons = allIcons.filter { it.theme == theme }
|
||||
|
||||
val iconStatements = icons.toStatements()
|
||||
|
||||
return FileSpec.builder(PackageNames.FluentIconsPackage.packageName, propertyName)
|
||||
.addProperty(
|
||||
PropertySpec.builder(propertyName, type = listOfIconsType)
|
||||
.initializer(
|
||||
buildCodeBlock {
|
||||
addStatement("listOf(")
|
||||
indent()
|
||||
iconStatements.forEach { add(it) }
|
||||
unindent()
|
||||
addStatement(")")
|
||||
}
|
||||
)
|
||||
.build()
|
||||
).setIndent().build()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of [CodeBlock] representing all the statements for the body of the list.
|
||||
* For example, one statement would look like `(Icons.Filled::Menu) to menu`.
|
||||
*/
|
||||
private fun List<Icon>.toStatements(): List<CodeBlock> {
|
||||
return mapIndexed { index, icon ->
|
||||
buildCodeBlock {
|
||||
val iconFunctionReference = "(%T.${icon.theme.themeClassName}::${icon.kotlinName})"
|
||||
val text = "$iconFunctionReference to \"${icon.xmlFileName}\""
|
||||
addStatement(if (index != size - 1) "$text," else text, ClassNames.Icons)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val kPropertyType =
|
||||
(KProperty0::class).asClassName().parameterizedBy(ClassNames.ImageVector)
|
||||
private val pairType = (Pair::class).asClassName().parameterizedBy(
|
||||
kPropertyType,
|
||||
(String::class).asTypeName()
|
||||
)
|
||||
private val listOfIconsType = (List::class).asClassName().parameterizedBy(pairType)
|
||||
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
/**
|
||||
* Enum representing the different themes for Material icons.
|
||||
*
|
||||
* @property themePackageName the lower case name used for package names and in xml files
|
||||
* @property themeClassName the CameCase name used for the theme objects
|
||||
*/
|
||||
enum class IconTheme(val themePackageName: String, val themeClassName: String) {
|
||||
Default("default", "Default"),
|
||||
Regular("regular", "Regular"),
|
||||
Filled("filled", "Filled")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the matching [IconTheme] from [this] [IconTheme.themePackageName].
|
||||
*/
|
||||
fun String.toIconTheme() = requireNotNull(
|
||||
IconTheme.values().find {
|
||||
it.themePackageName == this
|
||||
}
|
||||
) { "No matching theme found" }
|
||||
|
||||
/**
|
||||
* The ClassName representing this [IconTheme] object, so we can generate extension properties on
|
||||
* the object.
|
||||
*/
|
||||
val IconTheme.className get() = PackageNames.FluentIconsPackage.className("Icons", themeClassName)
|
||||
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Generates programmatic representation of all [icons] using [ImageVectorGenerator].
|
||||
*
|
||||
* @property icons the list of [Icon]s to generate Kotlin files for
|
||||
*/
|
||||
class IconWriter(private val icons: List<Icon>) {
|
||||
/**
|
||||
* Generates icons and writes them to [outputSrcDirectory], using [iconNamePredicate] to
|
||||
* filter what icons to generate for.
|
||||
*
|
||||
* @param outputSrcDirectory the directory to generate source files in
|
||||
* @param iconNamePredicate the predicate that filters what icons should be generated. If
|
||||
* false, the icon will not be parsed and generated in [outputSrcDirectory].
|
||||
*/
|
||||
fun generateTo(
|
||||
outputSrcDirectory: File,
|
||||
iconNamePredicate: (String) -> Boolean
|
||||
) {
|
||||
icons.forEach { icon ->
|
||||
if (!iconNamePredicate(icon.kotlinName)) return@forEach
|
||||
|
||||
val vector = IconParser(icon).parse()
|
||||
|
||||
val fileSpec = ImageVectorGenerator(
|
||||
icon.kotlinName,
|
||||
icon.theme,
|
||||
vector
|
||||
).createFileSpec()
|
||||
|
||||
fileSpec.writeToWithCopyright(outputSrcDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
import androidx.compose.material.icons.generator.vector.FillType
|
||||
import androidx.compose.material.icons.generator.vector.Vector
|
||||
import androidx.compose.material.icons.generator.vector.VectorNode
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.buildCodeBlock
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Generator for creating a Kotlin source file with a ImageVectror property for the given [vector],
|
||||
* with name [iconName] and theme [iconTheme].
|
||||
*
|
||||
* @param iconName the name for the generated property, which is also used for the generated file.
|
||||
* I.e if the name is `Menu`, the property will be `Menu` (inside a theme receiver object) and
|
||||
* the file will be `Menu.kt` (under the theme package name).
|
||||
* @param iconTheme the theme that this vector belongs to. Used to scope the property to the
|
||||
* correct receiver object, and also for the package name of the generated file.
|
||||
* @param vector the parsed vector to generate ImageVector.Builder commands for
|
||||
*/
|
||||
class ImageVectorGenerator(
|
||||
private val iconName: String,
|
||||
private val iconTheme: IconTheme,
|
||||
private val vector: Vector
|
||||
) {
|
||||
/**
|
||||
* @return a [FileSpec] representing a Kotlin source file containing the property for this
|
||||
* programmatic [vector] representation.
|
||||
*
|
||||
* The package name and hence file location of the generated file is:
|
||||
* [PackageNames.FluentIconsPackage] + [IconTheme.themePackageName].
|
||||
*/
|
||||
fun createFileSpec(): FileSpec {
|
||||
val iconsPackage = PackageNames.FluentIconsPackage.packageName
|
||||
val themePackage = iconTheme.themePackageName
|
||||
val combinedPackageName = "$iconsPackage.$themePackage"
|
||||
// Use a unique property name for the private backing property. This is because (as of
|
||||
// Kotlin 1.4) each property with the same name will be considered as a possible candidate
|
||||
// for resolution, regardless of the access modifier, so by using unique names we reduce
|
||||
// the size from ~6000 to 1, and speed up compilation time for these icons.
|
||||
val backingPropertyName = "_" + iconName.replaceFirstChar { it.lowercase(Locale.ROOT) }
|
||||
val backingProperty = backingProperty(name = backingPropertyName)
|
||||
return FileSpec.builder(
|
||||
packageName = combinedPackageName,
|
||||
fileName = iconName
|
||||
).addProperty(
|
||||
PropertySpec.builder(name = iconName, type = ClassNames.ImageVector)
|
||||
.receiver(iconTheme.className)
|
||||
.getter(iconGetter(backingProperty, iconName, iconTheme))
|
||||
.build()
|
||||
).addProperty(
|
||||
backingProperty
|
||||
).setIndent().build()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the body of the getter for the icon property. This getter returns the backing
|
||||
* property if it is not null, otherwise creates the icon and 'caches' it in the backing
|
||||
* property, and then returns the backing property.
|
||||
*/
|
||||
private fun iconGetter(
|
||||
backingProperty: PropertySpec,
|
||||
iconName: String,
|
||||
iconTheme: IconTheme
|
||||
): FunSpec {
|
||||
return FunSpec.getterBuilder()
|
||||
.addCode(
|
||||
buildCodeBlock {
|
||||
beginControlFlow("if (%N != null)", backingProperty)
|
||||
addStatement("return %N!!", backingProperty)
|
||||
endControlFlow()
|
||||
}
|
||||
)
|
||||
.addCode(
|
||||
buildCodeBlock {
|
||||
beginControlFlow(
|
||||
"%N = %M(name = \"%N.%N\")",
|
||||
backingProperty,
|
||||
MemberNames.FluentIcon,
|
||||
iconTheme.name,
|
||||
iconName
|
||||
)
|
||||
vector.nodes.forEach { node -> addRecursively(node) }
|
||||
endControlFlow()
|
||||
}
|
||||
)
|
||||
.addStatement("return %N!!", backingProperty)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The private backing property that is used to cache the ImageVector for a given
|
||||
* icon once created.
|
||||
*
|
||||
* @param name the name of this property
|
||||
*/
|
||||
private fun backingProperty(name: String): PropertySpec {
|
||||
val nullableImageVector = ClassNames.ImageVector.copy(nullable = true)
|
||||
return PropertySpec.builder(name = name, type = nullableImageVector)
|
||||
.mutable()
|
||||
.addModifiers(KModifier.PRIVATE)
|
||||
.initializer("null")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively adds function calls to construct the given [vectorNode] and its children.
|
||||
*/
|
||||
private fun CodeBlock.Builder.addRecursively(vectorNode: VectorNode) {
|
||||
when (vectorNode) {
|
||||
// TODO: b/147418351 - add clip-paths once they are supported
|
||||
is VectorNode.Group -> {
|
||||
beginControlFlow("%M", MemberNames.Group)
|
||||
vectorNode.paths.forEach { path ->
|
||||
addRecursively(path)
|
||||
}
|
||||
endControlFlow()
|
||||
}
|
||||
is VectorNode.Path -> {
|
||||
addPath(vectorNode) {
|
||||
vectorNode.nodes.forEach { pathNode ->
|
||||
addStatement(pathNode.asFunctionCall())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function call to create the given [path], with [pathBody] containing the commands for
|
||||
* the path.
|
||||
*/
|
||||
private fun CodeBlock.Builder.addPath(
|
||||
path: VectorNode.Path,
|
||||
pathBody: CodeBlock.Builder.() -> Unit
|
||||
) {
|
||||
// Only set the fill type if it is EvenOdd - otherwise it will just be the default.
|
||||
val setFillType = path.fillType == FillType.EvenOdd
|
||||
|
||||
val parameterList = with(path) {
|
||||
listOfNotNull(
|
||||
"fillAlpha = ${fillAlpha}f".takeIf { fillAlpha != 1f },
|
||||
"strokeAlpha = ${strokeAlpha}f".takeIf { strokeAlpha != 1f },
|
||||
"pathFillType = %M".takeIf { setFillType }
|
||||
)
|
||||
}
|
||||
|
||||
val parameters = if (parameterList.isNotEmpty()) {
|
||||
parameterList.joinToString(prefix = "(", postfix = ")")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if (setFillType) {
|
||||
beginControlFlow("%M$parameters", MemberNames.FluentPath, MemberNames.EvenOdd)
|
||||
} else {
|
||||
beginControlFlow("%M$parameters", MemberNames.FluentPath)
|
||||
}
|
||||
pathBody()
|
||||
endControlFlow()
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Writes the given [FileSpec] to [directory], appending a copyright notice to the beginning.
|
||||
* This is needed as this functionality isn't supported in KotlinPoet natively, and is not
|
||||
* intended to be supported. https://github.com/square/kotlinpoet/pull/514#issuecomment-441397363
|
||||
*
|
||||
* @param directory directory to write this [FileSpec] to
|
||||
* @param textTransform optional transformation to apply to the source file before writing to disk
|
||||
*/
|
||||
fun FileSpec.writeToWithCopyright(directory: File, textTransform: ((String) -> String)? = null) {
|
||||
var outputDirectory = directory
|
||||
|
||||
if (packageName.isNotEmpty()) {
|
||||
for (packageComponent in packageName.split('.').dropLastWhile { it.isEmpty() }) {
|
||||
outputDirectory = outputDirectory.resolve(packageComponent)
|
||||
}
|
||||
}
|
||||
|
||||
Files.createDirectories(outputDirectory.toPath())
|
||||
|
||||
val file = outputDirectory.resolve("$name.kt")
|
||||
|
||||
// Write this FileSpec to a StringBuilder, so we can process the text before writing to file.
|
||||
val fileContent = StringBuilder().run {
|
||||
writeTo(this)
|
||||
toString()
|
||||
}
|
||||
|
||||
val transformedText = textTransform?.invoke(fileContent) ?: fileContent
|
||||
|
||||
file.writeText(copyright + "\n\n" + transformedText)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the indent for this [FileSpec] to match that of our code style.
|
||||
*/
|
||||
fun FileSpec.Builder.setIndent() = indent(Indent)
|
||||
|
||||
// Code style indent is 4 spaces, compared to KotlinPoet's default of 2
|
||||
private val Indent = " ".repeat(4)
|
||||
|
||||
/**
|
||||
* AOSP copyright notice. Given that we generate this code every build, it is never checked in,
|
||||
* so we should update the copyright with the current year every time we write to disk.
|
||||
*/
|
||||
/* private val copyright
|
||||
get() = """
|
||||
/*
|
||||
* Copyright $currentYear The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
""".trimIndent() */
|
||||
|
||||
private val copyright
|
||||
get() = ""
|
||||
|
||||
private val currentYear: String get() = SimpleDateFormat("yyyy").format(Date())
|
||||
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.MemberName
|
||||
|
||||
/**
|
||||
* Package names used for icon generation.
|
||||
*/
|
||||
enum class PackageNames(val packageName: String) {
|
||||
FluentIconsPackage("com.konyaco.fluent.icons"),
|
||||
GraphicsPackage("androidx.compose.ui.graphics"),
|
||||
VectorPackage(GraphicsPackage.packageName + ".vector")
|
||||
}
|
||||
|
||||
/**
|
||||
* [ClassName]s used for icon generation.
|
||||
*/
|
||||
object ClassNames {
|
||||
val Icons = PackageNames.FluentIconsPackage.className("Icons")
|
||||
val ImageVector = PackageNames.VectorPackage.className("ImageVector")
|
||||
val PathFillType = PackageNames.GraphicsPackage.className("PathFillType", "Companion")
|
||||
}
|
||||
|
||||
/**
|
||||
* [MemberName]s used for icon generation.
|
||||
*/
|
||||
object MemberNames {
|
||||
val FluentIcon = MemberName(PackageNames.FluentIconsPackage.packageName, "fluentIcon")
|
||||
val FluentPath = MemberName(PackageNames.FluentIconsPackage.packageName, "fluentPath")
|
||||
|
||||
val EvenOdd = MemberName(ClassNames.PathFillType, "EvenOdd")
|
||||
val Group = MemberName(PackageNames.VectorPackage.packageName, "group")
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the [ClassName] of the given [classNames] inside this package.
|
||||
*/
|
||||
fun PackageNames.className(vararg classNames: String) = ClassName(this.packageName, *classNames)
|
||||
@ -0,0 +1,31 @@
|
||||
# Material Iconography
|
||||
|
||||
## Modules / components
|
||||
Material iconography is split across these modules:
|
||||
|
||||
1. The `generator` module, in `generator/` - this module processes and generates Kotlin source files as part of the build step of the other modules. This module is not shipped as an artifact, and caches its outputs based on the input icons (found in `generator/raw-icons`).
|
||||
2. `material-icons-core` , in `core/` - this module contains _core_ icons, the set of most-commonly-used icons used by applications, including the icons that are required by Material components themselves, such as the menu icon. This module is fairly small and is depended on by `material`.
|
||||
3. `material-icons-extended`, in `extended/` - this module contains every icon that is not in `material-icons-core`, and has a transitive `api` dependency on `material-icons-core`, so depending on this module will provide every single Material icon (over 5000 at the time of writing). Due to the excessive size of this module, this module should ***NOT*** be included as a direct dependency of any other library, and should only be used if Proguard / R8 is enabled.
|
||||
4. `material-icons-extended-$theme`, in `extended/` - these modules each contain a specific theme from material-icons-extended, to facilitate compiling the icon soure files more quickly in parallel
|
||||
|
||||
## Icon Generation
|
||||
|
||||
Generation is split into a few distinct steps:
|
||||
|
||||
1. Icons are downloaded (manually) using the Google Fonts API, using the script in the `generator` module. This downloads vector drawables for every single Material icon to the `raw-icons` folder.
|
||||
2. During compilation of the core and extended modules, these icons are processed to remove theme attributes that we cannot generate code for, checked to ensure that all icons exist in all themes, and then an API tracking file similar to API files in other modules is generated. This API file tracks what icons we have processed / will generate code, and the build will fail at this point if there are differences between the checked in API file and the generated API file.
|
||||
3. Once these icons are processed, we then parse each file, create a Vector-like representation, and convert this to `VectorAssetBuilder` commands that during run time will create a matching source code representation of this XML file. We then write this generated Kotlin file to the output directory, where it will be compiled as part of the `core` / `extended` module's source code, as if it was manually written and checked in. Each XML file creates a corresponding Kotlin file, containing a `by lazy` property representing that icon. For more information on using the generated icons, see `androidx.compose.material.icons.Icons`.
|
||||
|
||||
## Adding new icons
|
||||
To add new icons, simply use the icon downloading script at `generator/download_material_icons.py`, run any Gradle command that will trigger compilation of the icon modules (such as `./gradlew buildOnServer`), and follow the message in the build failure asking to confirm API changes by updating the API tracking file.
|
||||
|
||||
## Icon Testing
|
||||
Similar to how we generate Kotlin source for each icon, we also generate a 'testing manifest' that contains a list of all the source drawables, matched to their generated code representations. This allows us to run screenshot comparison tests (`IconComparisonTest`) that compare each pixel of the generated and source drawables, to ensure we generated the correct code, and that any changes in parsing logic that causes inconsistencies with our generation logic is caught in CI.
|
||||
|
||||
## Useful files
|
||||
|
||||
- `generator/download_material_icons.py` - script to download icons from Google Fonts API
|
||||
- `IconGenerationTask` - base Gradle task for generating icons / associated testing resources as part of the build. See subclasses for specific task logic.
|
||||
- `IconProcessor` - processes raw XML files in `generator/raw-icons`, creates a list of all icons that we will generate source for and ensures the API surface of these icons has not changed. (We do not run Metalava (or lint) on the extended module due to the extreme size of the module (5000+ source files) - running Metalava here would take hours.)
|
||||
- `IconParser` - simple XML parser that parses the processed XML files into Vector representations.
|
||||
- `VectorAssetGenerator` - converts icons parsed by `IconParser` into Kotlin source files that represent the icon.
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator.vector
|
||||
|
||||
/**
|
||||
* Determines the winding rule that decides how the interior of a [VectorNode.Path] is calculated.
|
||||
*
|
||||
* This maps to [android.graphics.Path.FillType] used in the framework, and can be defined in XML
|
||||
* via `android:fillType`.
|
||||
*/
|
||||
enum class FillType {
|
||||
NonZero,
|
||||
EvenOdd
|
||||
}
|
||||
@ -0,0 +1,453 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator.vector
|
||||
|
||||
/**
|
||||
* Class representing a singular path command in a vector.
|
||||
*
|
||||
* @property isCurve whether this command is a curve command
|
||||
* @property isQuad whether this command is a quad command
|
||||
*/
|
||||
/* ktlint-disable max-line-length */
|
||||
sealed class PathNode(val isCurve: Boolean = false, val isQuad: Boolean = false) {
|
||||
/**
|
||||
* Maps a [PathNode] to a string representing an invocation of the corresponding PathBuilder
|
||||
* function to add this node to the builder.
|
||||
*/
|
||||
abstract fun asFunctionCall(): String
|
||||
|
||||
// RelativeClose and Close are considered the same internally, so we represent both with Close
|
||||
// for simplicity and to make equals comparisons robust.
|
||||
object Close : PathNode() {
|
||||
override fun asFunctionCall() = "close()"
|
||||
}
|
||||
|
||||
data class RelativeMoveTo(val x: Float, val y: Float) : PathNode() {
|
||||
override fun asFunctionCall() = "moveToRelative(${x}f, ${y}f)"
|
||||
}
|
||||
data class MoveTo(val x: Float, val y: Float) : PathNode() {
|
||||
override fun asFunctionCall() = "moveTo(${x}f, ${y}f)"
|
||||
}
|
||||
|
||||
data class RelativeLineTo(val x: Float, val y: Float) : PathNode() {
|
||||
override fun asFunctionCall() = "lineToRelative(${x}f, ${y}f)"
|
||||
}
|
||||
data class LineTo(val x: Float, val y: Float) : PathNode() {
|
||||
override fun asFunctionCall() = "lineTo(${x}f, ${y}f)"
|
||||
}
|
||||
|
||||
data class RelativeHorizontalTo(val x: Float) : PathNode() {
|
||||
override fun asFunctionCall() = "horizontalLineToRelative(${x}f)"
|
||||
}
|
||||
data class HorizontalTo(val x: Float) : PathNode() {
|
||||
override fun asFunctionCall() = "horizontalLineTo(${x}f)"
|
||||
}
|
||||
|
||||
data class RelativeVerticalTo(val y: Float) : PathNode() {
|
||||
override fun asFunctionCall() = "verticalLineToRelative(${y}f)"
|
||||
}
|
||||
data class VerticalTo(val y: Float) : PathNode() {
|
||||
override fun asFunctionCall() = "verticalLineTo(${y}f)"
|
||||
}
|
||||
|
||||
data class RelativeCurveTo(
|
||||
val dx1: Float,
|
||||
val dy1: Float,
|
||||
val dx2: Float,
|
||||
val dy2: Float,
|
||||
val dx3: Float,
|
||||
val dy3: Float
|
||||
) : PathNode(isCurve = true) {
|
||||
override fun asFunctionCall() = "curveToRelative(${dx1}f, ${dy1}f, ${dx2}f, ${dy2}f, ${dx3}f, ${dy3}f)"
|
||||
}
|
||||
|
||||
data class CurveTo(
|
||||
val x1: Float,
|
||||
val y1: Float,
|
||||
val x2: Float,
|
||||
val y2: Float,
|
||||
val x3: Float,
|
||||
val y3: Float
|
||||
) : PathNode(isCurve = true) {
|
||||
override fun asFunctionCall() = "curveTo(${x1}f, ${y1}f, ${x2}f, ${y2}f, ${x3}f, ${y3}f)"
|
||||
}
|
||||
|
||||
data class RelativeReflectiveCurveTo(
|
||||
val x1: Float,
|
||||
val y1: Float,
|
||||
val x2: Float,
|
||||
val y2: Float
|
||||
) : PathNode(isCurve = true) {
|
||||
override fun asFunctionCall() = "reflectiveCurveToRelative(${x1}f, ${y1}f, ${x2}f, ${y2}f)"
|
||||
}
|
||||
|
||||
data class ReflectiveCurveTo(
|
||||
val x1: Float,
|
||||
val y1: Float,
|
||||
val x2: Float,
|
||||
val y2: Float
|
||||
) : PathNode(isCurve = true) {
|
||||
override fun asFunctionCall() = "reflectiveCurveTo(${x1}f, ${y1}f, ${x2}f, ${y2}f)"
|
||||
}
|
||||
|
||||
data class RelativeQuadTo(
|
||||
val x1: Float,
|
||||
val y1: Float,
|
||||
val x2: Float,
|
||||
val y2: Float
|
||||
) : PathNode(isQuad = true) {
|
||||
override fun asFunctionCall() = "quadToRelative(${x1}f, ${y1}f, ${x2}f, ${y2}f)"
|
||||
}
|
||||
|
||||
data class QuadTo(
|
||||
val x1: Float,
|
||||
val y1: Float,
|
||||
val x2: Float,
|
||||
val y2: Float
|
||||
) : PathNode(isQuad = true) {
|
||||
override fun asFunctionCall() = "quadTo(${x1}f, ${y1}f, ${x2}f, ${y2}f)"
|
||||
}
|
||||
|
||||
data class RelativeReflectiveQuadTo(
|
||||
val x: Float,
|
||||
val y: Float
|
||||
) : PathNode(isQuad = true) {
|
||||
override fun asFunctionCall() = "reflectiveQuadToRelative(${x}f, ${y}f)"
|
||||
}
|
||||
|
||||
data class ReflectiveQuadTo(
|
||||
val x: Float,
|
||||
val y: Float
|
||||
) : PathNode(isQuad = true) {
|
||||
override fun asFunctionCall() = "reflectiveQuadTo(${x}f, ${y}f)"
|
||||
}
|
||||
|
||||
data class RelativeArcTo(
|
||||
val horizontalEllipseRadius: Float,
|
||||
val verticalEllipseRadius: Float,
|
||||
val theta: Float,
|
||||
val isMoreThanHalf: Boolean,
|
||||
val isPositiveArc: Boolean,
|
||||
val arcStartDx: Float,
|
||||
val arcStartDy: Float
|
||||
) : PathNode() {
|
||||
override fun asFunctionCall() = "arcToRelative(${horizontalEllipseRadius}f, ${verticalEllipseRadius}f, ${theta}f, $isMoreThanHalf, $isPositiveArc, ${arcStartDx}f, ${arcStartDy}f)"
|
||||
}
|
||||
|
||||
data class ArcTo(
|
||||
val horizontalEllipseRadius: Float,
|
||||
val verticalEllipseRadius: Float,
|
||||
val theta: Float,
|
||||
val isMoreThanHalf: Boolean,
|
||||
val isPositiveArc: Boolean,
|
||||
val arcStartX: Float,
|
||||
val arcStartY: Float
|
||||
) : PathNode() {
|
||||
override fun asFunctionCall() = "arcTo(${horizontalEllipseRadius}f, ${verticalEllipseRadius}f, ${theta}f, $isMoreThanHalf, $isPositiveArc, ${arcStartX}f, ${arcStartY}f)"
|
||||
}
|
||||
}
|
||||
/* ktlint-enable max-line-length */
|
||||
|
||||
/**
|
||||
* Return the corresponding [PathNode] for the given character key if it exists.
|
||||
* If the key is unknown then [IllegalArgumentException] is thrown
|
||||
* @return [PathNode] that matches the key
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
internal fun Char.toPathNodes(args: FloatArray): List<PathNode> = when (this) {
|
||||
RelativeCloseKey, CloseKey -> listOf(
|
||||
PathNode.Close
|
||||
)
|
||||
RelativeMoveToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_MOVE_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeMoveTo(
|
||||
x = array[0],
|
||||
y = array[1]
|
||||
)
|
||||
}
|
||||
|
||||
MoveToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_MOVE_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.MoveTo(
|
||||
x = array[0],
|
||||
y = array[1]
|
||||
)
|
||||
}
|
||||
|
||||
RelativeLineToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_LINE_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeLineTo(
|
||||
x = array[0],
|
||||
y = array[1]
|
||||
)
|
||||
}
|
||||
|
||||
LineToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_LINE_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.LineTo(
|
||||
x = array[0],
|
||||
y = array[1]
|
||||
)
|
||||
}
|
||||
|
||||
RelativeHorizontalToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_HORIZONTAL_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeHorizontalTo(
|
||||
x = array[0]
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_HORIZONTAL_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.HorizontalTo(x = array[0])
|
||||
}
|
||||
|
||||
RelativeVerticalToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_VERTICAL_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeVerticalTo(y = array[0])
|
||||
}
|
||||
|
||||
VerticalToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_VERTICAL_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.VerticalTo(y = array[0])
|
||||
}
|
||||
|
||||
RelativeCurveToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_CURVE_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeCurveTo(
|
||||
dx1 = array[0],
|
||||
dy1 = array[1],
|
||||
dx2 = array[2],
|
||||
dy2 = array[3],
|
||||
dx3 = array[4],
|
||||
dy3 = array[5]
|
||||
)
|
||||
}
|
||||
|
||||
CurveToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_CURVE_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.CurveTo(
|
||||
x1 = array[0],
|
||||
y1 = array[1],
|
||||
x2 = array[2],
|
||||
y2 = array[3],
|
||||
x3 = array[4],
|
||||
y3 = array[5]
|
||||
)
|
||||
}
|
||||
|
||||
RelativeReflectiveCurveToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_REFLECTIVE_CURVE_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeReflectiveCurveTo(
|
||||
x1 = array[0],
|
||||
y1 = array[1],
|
||||
x2 = array[2],
|
||||
y2 = array[3]
|
||||
)
|
||||
}
|
||||
|
||||
ReflectiveCurveToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_REFLECTIVE_CURVE_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.ReflectiveCurveTo(
|
||||
x1 = array[0],
|
||||
y1 = array[1],
|
||||
x2 = array[2],
|
||||
y2 = array[3]
|
||||
)
|
||||
}
|
||||
|
||||
RelativeQuadToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_QUAD_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeQuadTo(
|
||||
x1 = array[0],
|
||||
y1 = array[1],
|
||||
x2 = array[2],
|
||||
y2 = array[3]
|
||||
)
|
||||
}
|
||||
|
||||
QuadToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_QUAD_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.QuadTo(
|
||||
x1 = array[0],
|
||||
y1 = array[1],
|
||||
x2 = array[2],
|
||||
y2 = array[3]
|
||||
)
|
||||
}
|
||||
|
||||
RelativeReflectiveQuadToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_REFLECTIVE_QUAD_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeReflectiveQuadTo(
|
||||
x = array[0],
|
||||
y = array[1]
|
||||
)
|
||||
}
|
||||
|
||||
ReflectiveQuadToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_REFLECTIVE_QUAD_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.ReflectiveQuadTo(
|
||||
x = array[0],
|
||||
y = array[1]
|
||||
)
|
||||
}
|
||||
|
||||
RelativeArcToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_ARC_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.RelativeArcTo(
|
||||
horizontalEllipseRadius = array[0],
|
||||
verticalEllipseRadius = array[1],
|
||||
theta = array[2],
|
||||
isMoreThanHalf = array[3].compareTo(0.0f) != 0,
|
||||
isPositiveArc = array[4].compareTo(0.0f) != 0,
|
||||
arcStartDx = array[5],
|
||||
arcStartDy = array[6]
|
||||
)
|
||||
}
|
||||
|
||||
ArcToKey ->
|
||||
pathNodesFromArgs(
|
||||
args,
|
||||
NUM_ARC_TO_ARGS
|
||||
) { array ->
|
||||
PathNode.ArcTo(
|
||||
horizontalEllipseRadius = array[0],
|
||||
verticalEllipseRadius = array[1],
|
||||
theta = array[2],
|
||||
isMoreThanHalf = array[3].compareTo(0.0f) != 0,
|
||||
isPositiveArc = array[4].compareTo(0.0f) != 0,
|
||||
arcStartX = array[5],
|
||||
arcStartY = array[6]
|
||||
)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown command for: $this")
|
||||
}
|
||||
|
||||
private inline fun pathNodesFromArgs(
|
||||
args: FloatArray,
|
||||
numArgs: Int,
|
||||
nodeFor: (subArray: FloatArray) -> PathNode
|
||||
): List<PathNode> {
|
||||
return (0..args.size - numArgs step numArgs).map { index ->
|
||||
val subArray = args.slice(index until index + numArgs).toFloatArray()
|
||||
val node = nodeFor(subArray)
|
||||
when {
|
||||
// According to the spec, if a MoveTo is followed by multiple pairs of coordinates,
|
||||
// the subsequent pairs are treated as implicit corresponding LineTo commands.
|
||||
node is PathNode.MoveTo && index > 0 -> PathNode.LineTo(
|
||||
subArray[0],
|
||||
subArray[1]
|
||||
)
|
||||
node is PathNode.RelativeMoveTo && index > 0 ->
|
||||
PathNode.RelativeLineTo(
|
||||
subArray[0],
|
||||
subArray[1]
|
||||
)
|
||||
else -> node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constants used by [Char.toPathNodes] for creating [PathNode]s from parsed paths.
|
||||
*/
|
||||
private const val RelativeCloseKey = 'z'
|
||||
private const val CloseKey = 'Z'
|
||||
private const val RelativeMoveToKey = 'm'
|
||||
private const val MoveToKey = 'M'
|
||||
private const val RelativeLineToKey = 'l'
|
||||
private const val LineToKey = 'L'
|
||||
private const val RelativeHorizontalToKey = 'h'
|
||||
private const val HorizontalToKey = 'H'
|
||||
private const val RelativeVerticalToKey = 'v'
|
||||
private const val VerticalToKey = 'V'
|
||||
private const val RelativeCurveToKey = 'c'
|
||||
private const val CurveToKey = 'C'
|
||||
private const val RelativeReflectiveCurveToKey = 's'
|
||||
private const val ReflectiveCurveToKey = 'S'
|
||||
private const val RelativeQuadToKey = 'q'
|
||||
private const val QuadToKey = 'Q'
|
||||
private const val RelativeReflectiveQuadToKey = 't'
|
||||
private const val ReflectiveQuadToKey = 'T'
|
||||
private const val RelativeArcToKey = 'a'
|
||||
private const val ArcToKey = 'A'
|
||||
|
||||
/**
|
||||
* Constants for the number of expected arguments for a given node. If the number of received
|
||||
* arguments is a multiple of these, the excess will be converted into additional path nodes.
|
||||
*/
|
||||
private const val NUM_MOVE_TO_ARGS = 2
|
||||
private const val NUM_LINE_TO_ARGS = 2
|
||||
private const val NUM_HORIZONTAL_TO_ARGS = 1
|
||||
private const val NUM_VERTICAL_TO_ARGS = 1
|
||||
private const val NUM_CURVE_TO_ARGS = 6
|
||||
private const val NUM_REFLECTIVE_CURVE_TO_ARGS = 4
|
||||
private const val NUM_QUAD_TO_ARGS = 4
|
||||
private const val NUM_REFLECTIVE_QUAD_TO_ARGS = 2
|
||||
private const val NUM_ARC_TO_ARGS = 7
|
||||
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator.vector
|
||||
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Trimmed down copy of PathParser that doesn't handle interacting with Paths, and only is
|
||||
* responsible for parsing path strings.
|
||||
*/
|
||||
object PathParser {
|
||||
/**
|
||||
* Parses the path string to create a collection of PathNode instances with their corresponding
|
||||
* arguments
|
||||
* throws an IllegalArgumentException or NumberFormatException if the parameters are invalid
|
||||
*/
|
||||
fun parsePathString(pathData: String): List<PathNode> {
|
||||
val nodes = mutableListOf<PathNode>()
|
||||
|
||||
fun addNode(cmd: Char, args: FloatArray) {
|
||||
nodes.addAll(cmd.toPathNodes(args))
|
||||
}
|
||||
|
||||
var start = 0
|
||||
var end = 1
|
||||
while (end < pathData.length) {
|
||||
end = nextStart(pathData, end)
|
||||
val s = pathData.substring(start, end).trim { it <= ' ' }
|
||||
if (s.isNotEmpty()) {
|
||||
val args = getFloats(s)
|
||||
addNode(s[0], args)
|
||||
}
|
||||
|
||||
start = end
|
||||
end++
|
||||
}
|
||||
if (end - start == 1 && start < pathData.length) {
|
||||
addNode(pathData[start], FloatArray(0))
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
private fun nextStart(s: String, end: Int): Int {
|
||||
var index = end
|
||||
var c: Char
|
||||
|
||||
while (index < s.length) {
|
||||
c = s[index]
|
||||
// Note that 'e' or 'E' are not valid path commands, but could be
|
||||
// used for floating point numbers' scientific notation.
|
||||
// Therefore, when searching for next command, we should ignore 'e'
|
||||
// and 'E'.
|
||||
if (((c - 'A') * (c - 'Z') <= 0 || (c - 'a') * (c - 'z') <= 0) &&
|
||||
c != 'e' && c != 'E'
|
||||
) {
|
||||
return index
|
||||
}
|
||||
index++
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
@Throws(NumberFormatException::class)
|
||||
private fun getFloats(s: String): FloatArray {
|
||||
if (s[0] == 'z' || s[0] == 'Z') {
|
||||
return FloatArray(0)
|
||||
}
|
||||
val results = FloatArray(s.length)
|
||||
var count = 0
|
||||
var startPosition = 1
|
||||
var endPosition: Int
|
||||
|
||||
val result =
|
||||
ExtractFloatResult()
|
||||
val totalLength = s.length
|
||||
|
||||
// The startPosition should always be the first character of the
|
||||
// current number, and endPosition is the character after the current
|
||||
// number.
|
||||
while (startPosition < totalLength) {
|
||||
extract(s, startPosition, result)
|
||||
endPosition = result.endPosition
|
||||
|
||||
if (startPosition < endPosition) {
|
||||
results[count++] = java.lang.Float.parseFloat(
|
||||
s.substring(startPosition, endPosition)
|
||||
)
|
||||
}
|
||||
|
||||
startPosition = if (result.endWithNegativeOrDot) {
|
||||
// Keep the '-' or '.' sign with next number.
|
||||
endPosition
|
||||
} else {
|
||||
endPosition + 1
|
||||
}
|
||||
}
|
||||
return copyOfRange(results, 0, count)
|
||||
}
|
||||
|
||||
private fun copyOfRange(original: FloatArray, start: Int, end: Int): FloatArray {
|
||||
if (start > end) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
val originalLength = original.size
|
||||
if (start < 0 || start > originalLength) {
|
||||
throw ArrayIndexOutOfBoundsException()
|
||||
}
|
||||
val resultLength = end - start
|
||||
val copyLength = min(resultLength, originalLength - start)
|
||||
val result = FloatArray(resultLength)
|
||||
original.copyInto(result, 0, start, start + copyLength)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun extract(s: String, start: Int, result: ExtractFloatResult) {
|
||||
// Now looking for ' ', ',', '.' or '-' from the start.
|
||||
var currentIndex = start
|
||||
var foundSeparator = false
|
||||
result.endWithNegativeOrDot = false
|
||||
var secondDot = false
|
||||
var isExponential = false
|
||||
while (currentIndex < s.length) {
|
||||
val isPrevExponential = isExponential
|
||||
isExponential = false
|
||||
when (s[currentIndex]) {
|
||||
' ', ',' -> foundSeparator = true
|
||||
'-' ->
|
||||
// The negative sign following a 'e' or 'E' is not a separator.
|
||||
if (currentIndex != start && !isPrevExponential) {
|
||||
foundSeparator = true
|
||||
result.endWithNegativeOrDot = true
|
||||
}
|
||||
'.' ->
|
||||
if (!secondDot) {
|
||||
secondDot = true
|
||||
} else {
|
||||
// This is the second dot, and it is considered as a separator.
|
||||
foundSeparator = true
|
||||
result.endWithNegativeOrDot = true
|
||||
}
|
||||
'e', 'E' -> isExponential = true
|
||||
}
|
||||
if (foundSeparator) {
|
||||
break
|
||||
}
|
||||
currentIndex++
|
||||
}
|
||||
// When there is nothing found, then we put the end position to the end
|
||||
// of the string.
|
||||
result.endPosition = currentIndex
|
||||
}
|
||||
|
||||
private data class ExtractFloatResult(
|
||||
// We need to return the position of the next separator and whether the
|
||||
// next float starts with a '-' or a '.'.
|
||||
var endPosition: Int = 0,
|
||||
var endWithNegativeOrDot: Boolean = false
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator.vector
|
||||
|
||||
/**
|
||||
* Simplified representation of a vector, with root [nodes].
|
||||
*
|
||||
* [nodes] may either be a singleton list of the root group, or a list of root paths / groups if
|
||||
* there are multiple top level declaration.
|
||||
*/
|
||||
class Vector(val nodes: List<VectorNode>)
|
||||
|
||||
/**
|
||||
* Simplified vector node representation, as the total set of properties we need to care about
|
||||
* for Material icons is very limited.
|
||||
*/
|
||||
sealed class VectorNode {
|
||||
class Group(val paths: MutableList<Path> = mutableListOf()) : VectorNode()
|
||||
class Path(
|
||||
val strokeAlpha: Float,
|
||||
val fillAlpha: Float,
|
||||
val fillType: FillType,
|
||||
val nodes: List<PathNode>
|
||||
) : VectorNode()
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package generator
|
||||
|
||||
import androidx.compose.material.icons.generator.CoreIcons
|
||||
import androidx.compose.material.icons.generator.IconProcessor
|
||||
import androidx.compose.material.icons.generator.IconWriter
|
||||
|
||||
fun main() {
|
||||
val icons = IconProcessor(
|
||||
listOf(baseDir.resolve("icon_output_xml/filled"), baseDir.resolve("icon_output_xml/regular")),
|
||||
baseDir.resolve("expApis.txt"),
|
||||
baseDir.resolve("genApis.txt"),
|
||||
false
|
||||
).process()
|
||||
val outDir = baseDir.resolve("icon_output_kt_core").also {
|
||||
it.mkdirs()
|
||||
}
|
||||
IconWriter(icons).generateTo(outDir) { it in CoreIcons }
|
||||
|
||||
val extendedDir = baseDir.resolve("icon_output_kt_extended").also {
|
||||
it.mkdirs()
|
||||
}
|
||||
IconWriter(icons).generateTo(extendedDir) { it !in CoreIcons }
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package generator
|
||||
|
||||
import com.android.ide.common.vectordrawable.Svg2Vector
|
||||
|
||||
fun main() {
|
||||
val outDir = baseDir.resolve("icon_output_xml").also { it.mkdirs() }
|
||||
val filled = outDir.resolve("filled").also { it.mkdirs() }
|
||||
val regular = outDir.resolve("regular").also { it.mkdirs() }
|
||||
|
||||
val error = mutableListOf<String>()
|
||||
|
||||
baseDir.resolve("icon_output").listFiles()!!.forEach { file ->
|
||||
val filename = file.nameWithoutExtension
|
||||
val theme = when {
|
||||
filename.endsWith("Regular") -> "regular"
|
||||
filename.endsWith("Filled") -> "filled"
|
||||
else -> error("Unknown theme")
|
||||
}
|
||||
val outputDir = when (theme) {
|
||||
"regular" -> regular
|
||||
"filled" -> filled
|
||||
else -> regular
|
||||
}
|
||||
|
||||
val size =
|
||||
filename.substring(filename.length - theme.length - 2, filename.length - theme.length)
|
||||
val iconName = filename.substringBeforeLast(size)
|
||||
|
||||
println(iconName)
|
||||
val outputFile = outputDir.resolve("$iconName.xml")
|
||||
outputFile.outputStream().use { stream ->
|
||||
Svg2Vector.parseSvgToXml(file, stream)
|
||||
}
|
||||
if (outputFile.length() == 0L) {
|
||||
error.add(filename)
|
||||
System.err.println("Error: $filename")
|
||||
outputFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
error.forEach {
|
||||
println("Error: $it")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package generator
|
||||
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
fun main() {
|
||||
val html = baseDir.resolve("icons-catalog.html").readText()
|
||||
val document = Jsoup.parse(html)
|
||||
val outputDir = baseDir.resolve("icon_output").also {
|
||||
it.mkdirs()
|
||||
}
|
||||
document.select(".sbdocs-content > div:last-child > div > div").asSequence().map {
|
||||
val name = it.select("code")[0]!!.text()
|
||||
val svg = it.select("svg")[0].toString()
|
||||
name to svg
|
||||
}.forEach { (name, svg) ->
|
||||
println(svg)
|
||||
val converted = svg.replace(Regex("""\sclass="[\s\S]+?""""), "")
|
||||
.replace(Regex("""\saria-hidden="true""""), "")
|
||||
.replace(" fill=\"currentColor\"", "")
|
||||
.replace("viewbox", "viewBox")
|
||||
|
||||
println(converted)
|
||||
outputDir.resolve("$name.svg").writeText(converted)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package generator
|
||||
|
||||
import java.io.File
|
||||
|
||||
val baseDir = File("fluent-icons-generator")
|
||||
243
fluent-icons-generator/tasks/IconGenerationTask.kt
Normal file
243
fluent-icons-generator/tasks/IconGenerationTask.kt
Normal file
@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator.tasks
|
||||
|
||||
import androidx.compose.material.icons.generator.Icon
|
||||
import androidx.compose.material.icons.generator.IconProcessor
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.CacheableTask
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputDirectory
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.Optional
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Base [org.gradle.api.Task] for tasks relating to icon generation.
|
||||
*/
|
||||
@CacheableTask
|
||||
abstract class IconGenerationTask : DefaultTask() {
|
||||
|
||||
/**
|
||||
* Directory containing raw drawables. These icons will be processed to generate programmatic
|
||||
* representations.
|
||||
*/
|
||||
@PathSensitive(PathSensitivity.RELATIVE)
|
||||
@InputDirectory
|
||||
val allIconsDirectory =
|
||||
project.rootProject.project(GeneratorProject).projectDir.resolve("raw-icons")
|
||||
|
||||
/**
|
||||
* Specific theme to generate icons for, or null to generate all
|
||||
*/
|
||||
@Optional
|
||||
@Input
|
||||
var themeName: String? = null
|
||||
|
||||
/**
|
||||
* Specific icon directories to use in this task
|
||||
*/
|
||||
@Internal
|
||||
fun getIconDirectories(): List<File> {
|
||||
val themeName = themeName
|
||||
if (themeName != null) {
|
||||
return listOf(allIconsDirectory.resolve(themeName))
|
||||
} else {
|
||||
return allIconsDirectory.listFiles()!!.filter { it.isDirectory }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checked-in API file for the generator module, where we will track all the generated icons
|
||||
*/
|
||||
@PathSensitive(PathSensitivity.NONE)
|
||||
@InputFile
|
||||
val expectedApiFile =
|
||||
project.rootProject.project(GeneratorProject).projectDir.resolve("api/icons.txt")
|
||||
|
||||
/**
|
||||
* Root build directory for this task, where outputs will be placed into.
|
||||
*/
|
||||
@OutputDirectory
|
||||
lateinit var buildDirectory: File
|
||||
|
||||
/**
|
||||
* Generated API file that will be placed in the build directory. This can be copied manually
|
||||
* to [expectedApiFile] to confirm that API changes were intended.
|
||||
*/
|
||||
@get:OutputFile
|
||||
val generatedApiFile: File
|
||||
get() = buildDirectory.resolve("api/icons.txt")
|
||||
|
||||
/**
|
||||
* @return a list of all processed [Icon]s from [getIconDirectories].
|
||||
*/
|
||||
fun loadIcons(): List<Icon> {
|
||||
// material-icons-core loads and verifies all of the icons from all of the themes:
|
||||
// both that all icons are present in all themes, and also that no icons have been removed.
|
||||
// So, when we're loading just one theme, we don't need to verify it
|
||||
val verifyApi = themeName == null
|
||||
return IconProcessor(
|
||||
getIconDirectories(),
|
||||
expectedApiFile,
|
||||
generatedApiFile,
|
||||
verifyApi
|
||||
).process()
|
||||
}
|
||||
|
||||
@get:OutputDirectory
|
||||
val generatedSrcMainDirectory: File
|
||||
get() = buildDirectory.resolve(GeneratedSrcMain)
|
||||
|
||||
@get:OutputDirectory
|
||||
val generatedSrcAndroidTestDirectory: File
|
||||
get() = buildDirectory.resolve(GeneratedSrcAndroidTest)
|
||||
|
||||
@get:OutputDirectory
|
||||
val generatedResourceDirectory: File
|
||||
get() = buildDirectory.resolve(GeneratedResource)
|
||||
|
||||
/**
|
||||
* The action for this task
|
||||
*/
|
||||
@TaskAction
|
||||
abstract fun run()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Registers the core [project]. The core project contains only the icons defined in
|
||||
* [androidx.compose.material.icons.generator.CoreIcons], and no tests.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun registerCoreIconProject(
|
||||
project: Project,
|
||||
libraryExtension: LibraryExtension,
|
||||
isMpp: Boolean
|
||||
) {
|
||||
if (isMpp) {
|
||||
CoreIconGenerationTask.register(project, null)
|
||||
} else {
|
||||
libraryExtension.libraryVariants.all { variant ->
|
||||
CoreIconGenerationTask.register(project, variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the extended [project]. The core project contains all icons except for the
|
||||
* icons defined in [androidx.compose.material.icons.generator.CoreIcons], as well as a
|
||||
* bitmap comparison test for every icon in both the core and extended project.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun registerExtendedIconThemeProject(
|
||||
project: Project,
|
||||
libraryExtension: LibraryExtension,
|
||||
isMpp: Boolean
|
||||
) {
|
||||
if (isMpp) {
|
||||
ExtendedIconGenerationTask.register(project, null)
|
||||
} else {
|
||||
libraryExtension.libraryVariants.all { variant ->
|
||||
ExtendedIconGenerationTask.register(project, variant)
|
||||
}
|
||||
}
|
||||
|
||||
// b/175401659 - disable lint as it takes a long time, and most errors should
|
||||
// be caught by lint on material-icons-core anyway
|
||||
project.afterEvaluate {
|
||||
project.tasks.named("lintAnalyzeDebug") { t ->
|
||||
t.enabled = false
|
||||
}
|
||||
project.tasks.named("lintDebug") { t ->
|
||||
t.enabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun registerExtendedIconMainProject(
|
||||
project: Project,
|
||||
libraryExtension: LibraryExtension
|
||||
) {
|
||||
libraryExtension.testVariants.all { variant ->
|
||||
IconTestingGenerationTask.register(project, variant)
|
||||
}
|
||||
}
|
||||
|
||||
const val GeneratedSrcMain = "src/commonMain/kotlin"
|
||||
|
||||
const val GeneratedSrcAndroidTest = "src/androidAndroidTest/kotlin"
|
||||
|
||||
const val GeneratedResource = "generatedIcons/res"
|
||||
}
|
||||
}
|
||||
|
||||
// Path to the generator project
|
||||
private const val GeneratorProject = ":compose:material:material:icons:generator"
|
||||
|
||||
/**
|
||||
* Registers a new [T] in [this], and sets [IconGenerationTask.buildDirectory] depending on
|
||||
* [variant].
|
||||
*
|
||||
* @param variant the [com.android.build.gradle.api.BaseVariant] to associate this task with, or
|
||||
* `null` if this task does not change between variants.
|
||||
* @return a [Pair] of the created [TaskProvider] of [T] of [IconGenerationTask], and the [File]
|
||||
* for the directory that files will be generated to
|
||||
*/
|
||||
@Suppress("DEPRECATION") // BaseVariant
|
||||
fun <T : IconGenerationTask> Project.registerGenerationTask(
|
||||
taskName: String,
|
||||
taskClass: Class<T>,
|
||||
variant: com.android.build.gradle.api.BaseVariant? = null
|
||||
): Pair<TaskProvider<T>, File> {
|
||||
val variantName = variant?.name ?: "allVariants"
|
||||
|
||||
val themeName = if (project.name.contains("material-icons-extended-")) {
|
||||
project.name.replace("material-icons-extended-", "")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val buildDirectory = project.buildDir.resolve("generatedIcons/$variantName")
|
||||
|
||||
return tasks.register("$taskName${variantName.capitalize(Locale.getDefault())}", taskClass) {
|
||||
it.themeName = themeName
|
||||
it.buildDirectory = buildDirectory
|
||||
} to buildDirectory
|
||||
}
|
||||
|
||||
fun Project.getMultiplatformSourceSet(name: String): KotlinSourceSet {
|
||||
val sourceSet = project.multiplatformExtension!!.sourceSets.find { it.name == name }
|
||||
return requireNotNull(sourceSet) {
|
||||
"No source sets found matching $name"
|
||||
}
|
||||
}
|
||||
|
||||
private val Project.multiplatformExtension
|
||||
get() = extensions.findByType(KotlinMultiplatformExtension::class.java)
|
||||
164
fluent-icons-generator/tasks/IconSourceTasks.kt
Normal file
164
fluent-icons-generator/tasks/IconSourceTasks.kt
Normal file
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator.tasks
|
||||
|
||||
import androidx.compose.material.icons.generator.CoreIcons
|
||||
import androidx.compose.material.icons.generator.IconWriter
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.CacheableTask
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
import org.gradle.api.tasks.bundling.Jar
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Task responsible for converting core icons from xml to a programmatic representation.
|
||||
*/
|
||||
@CacheableTask
|
||||
open class CoreIconGenerationTask : IconGenerationTask() {
|
||||
override fun run() =
|
||||
IconWriter(loadIcons()).generateTo(generatedSrcMainDirectory) { it in CoreIcons }
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Registers [CoreIconGenerationTask] in [project].
|
||||
*/
|
||||
@Suppress("DEPRECATION") // BaseVariant
|
||||
fun register(project: Project, variant: com.android.build.gradle.api.BaseVariant? = null) {
|
||||
val (task, buildDirectory) = project.registerGenerationTask(
|
||||
"generateCoreIcons",
|
||||
CoreIconGenerationTask::class.java,
|
||||
variant
|
||||
)
|
||||
// Multiplatform
|
||||
if (variant == null) {
|
||||
registerIconGenerationTask(project, task, buildDirectory)
|
||||
}
|
||||
// AGP
|
||||
else variant.registerIconGenerationTask(project, task, buildDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task responsible for converting extended icons from xml to a programmatic representation.
|
||||
*/
|
||||
@CacheableTask
|
||||
open class ExtendedIconGenerationTask : IconGenerationTask() {
|
||||
override fun run() =
|
||||
IconWriter(loadIcons()).generateTo(generatedSrcMainDirectory) { it !in CoreIcons }
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Registers [ExtendedIconGenerationTask] in [project]. (for use with mpp)
|
||||
*/
|
||||
@Suppress("DEPRECATION") // BaseVariant
|
||||
fun register(project: Project, variant: com.android.build.gradle.api.BaseVariant? = null) {
|
||||
val (task, buildDirectory) = project.registerGenerationTask(
|
||||
"generateExtendedIcons",
|
||||
ExtendedIconGenerationTask::class.java,
|
||||
variant
|
||||
)
|
||||
// Multiplatform
|
||||
if (variant == null) {
|
||||
registerIconGenerationTask(project, task, buildDirectory)
|
||||
}
|
||||
// AGP
|
||||
else variant.registerIconGenerationTask(project, task, buildDirectory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the icon generation task just for source jar generation, and not for
|
||||
* compilation. This is temporarily needed since we manually parallelize compilation in
|
||||
* material-icons-extended for the AGP build. When we remove that parallelization code,
|
||||
* we can remove this too.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Suppress("DEPRECATION") // BaseVariant
|
||||
fun registerSourceJarOnly(
|
||||
project: Project,
|
||||
variant: com.android.build.gradle.api.BaseVariant
|
||||
) {
|
||||
// Setup the source jar task if this is the release variant
|
||||
if (variant.name == "release") {
|
||||
val (task, buildDirectory) = project.registerGenerationTask(
|
||||
"generateExtendedIcons",
|
||||
ExtendedIconGenerationTask::class.java,
|
||||
variant
|
||||
)
|
||||
val generatedSrcMainDirectory = buildDirectory.resolve(GeneratedSrcMain)
|
||||
project.addToSourceJar(generatedSrcMainDirectory, task)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to register [task] that outputs to [buildDirectory] as the Kotlin source generating
|
||||
* task for [project].
|
||||
*/
|
||||
private fun registerIconGenerationTask(
|
||||
project: Project,
|
||||
task: TaskProvider<*>,
|
||||
buildDirectory: File
|
||||
) {
|
||||
val sourceSet = project.getMultiplatformSourceSet(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
|
||||
val generatedSrcMainDirectory = buildDirectory.resolve(IconGenerationTask.GeneratedSrcMain)
|
||||
sourceSet.kotlin.srcDir(project.files(generatedSrcMainDirectory).builtBy(task))
|
||||
// add it to the multiplatform sources as well.
|
||||
project.tasks.named("multiplatformSourceJar", Jar::class.java).configure {
|
||||
it.from(task.map { generatedSrcMainDirectory })
|
||||
}
|
||||
project.addToSourceJar(generatedSrcMainDirectory, task)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to register [task] as the java source generating task that outputs to [buildDirectory].
|
||||
*/
|
||||
@Suppress("DEPRECATION") // BaseVariant
|
||||
private fun com.android.build.gradle.api.BaseVariant.registerIconGenerationTask(
|
||||
project: Project,
|
||||
task: TaskProvider<*>,
|
||||
buildDirectory: File
|
||||
) {
|
||||
val generatedSrcMainDirectory = buildDirectory.resolve(IconGenerationTask.GeneratedSrcMain)
|
||||
registerJavaGeneratingTask(task, generatedSrcMainDirectory)
|
||||
// Setup the source jar task if this is the release variant
|
||||
if (name == "release") {
|
||||
project.addToSourceJar(generatedSrcMainDirectory, task)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the contents of [buildDirectory] to the source jar generated for this [Project] by [task]
|
||||
*/
|
||||
// TODO: b/191485164 remove when AGP lets us get generated sources from a TestedExtension or
|
||||
// similar, then we can just add generated sources in SourceJarTaskHelper for all projects,
|
||||
// instead of needing one-off support here.
|
||||
private fun Project.addToSourceJar(buildDirectory: File, task: TaskProvider<*>) {
|
||||
afterEvaluate {
|
||||
val sourceJar = tasks.named("sourceJarRelease", Jar::class.java)
|
||||
sourceJar.configure {
|
||||
// Generating source jars requires the generation task to run first. This shouldn't
|
||||
// be needed for the MPP build because we use builtBy to set up the dependency
|
||||
// (https://github.com/gradle/gradle/issues/17250) but the path is different for AGP,
|
||||
// so we will still need this for the AGP build.
|
||||
it.dependsOn(task)
|
||||
it.from(buildDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
77
fluent-icons-generator/tasks/IconTestingGenerationTask.kt
Normal file
77
fluent-icons-generator/tasks/IconTestingGenerationTask.kt
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material.icons.generator.tasks
|
||||
|
||||
import androidx.compose.material.icons.generator.IconTestingManifestGenerator
|
||||
import java.io.File
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.CacheableTask
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
|
||||
/**
|
||||
* Task responsible for generating files related to testing.
|
||||
*
|
||||
* - Generates a list of all icons mapped to the drawable ID used in testing, so we can bitmap
|
||||
* compare the programmatic icon with the original source drawable.
|
||||
*
|
||||
* - Flattens all the source drawables into a drawable folder that will be used in comparison tests.
|
||||
*/
|
||||
@CacheableTask
|
||||
open class IconTestingGenerationTask : IconGenerationTask() {
|
||||
/**
|
||||
* Directory to generate the flattened drawables used for testing to.
|
||||
*/
|
||||
@get:OutputDirectory
|
||||
val drawableDirectory: File
|
||||
get() = generatedResourceDirectory.resolve("drawable")
|
||||
|
||||
override fun run() {
|
||||
// Copy all drawables to the drawable directory
|
||||
loadIcons().forEach { icon ->
|
||||
drawableDirectory.resolve("${icon.xmlFileName}.xml").apply {
|
||||
createNewFile()
|
||||
writeText(icon.fileContent)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the testing manifest to the androidTest directory
|
||||
IconTestingManifestGenerator(loadIcons()).generateTo(generatedSrcAndroidTestDirectory)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Registers [IconTestingGenerationTask] in [project] for [variant].
|
||||
*/
|
||||
@Suppress("DEPRECATION") // BaseVariant
|
||||
fun register(project: Project, variant: com.android.build.gradle.api.BaseVariant) {
|
||||
val (task, buildDirectory) = project.registerGenerationTask(
|
||||
"generateTestFiles",
|
||||
IconTestingGenerationTask::class.java,
|
||||
variant
|
||||
)
|
||||
|
||||
val generatedResourceDirectory = buildDirectory.resolve(GeneratedResource)
|
||||
|
||||
variant.registerGeneratedResFolders(
|
||||
project.files(generatedResourceDirectory).builtBy(task)
|
||||
)
|
||||
|
||||
val generatedSrcAndroidTestDirectory = buildDirectory.resolve(GeneratedSrcAndroidTest)
|
||||
variant.registerJavaGeneratingTask(task, generatedSrcAndroidTestDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user