update emoji data

This commit is contained in:
tateisu 2021-02-15 16:32:16 +09:00
parent 6b4d38b8d5
commit 853d023d0a
56 changed files with 24974 additions and 59572 deletions

35
_Emoji/build.gradle Normal file
View File

@ -0,0 +1,35 @@
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.4.30'
}
group 'jp.juggler'
version '0.0.1'
repositories {
mavenCentral()
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'src/lib')
implementation "com.google.guava:guava:28.1-jre"
implementation "org.jetbrains.kotlin:kotlin-stdlib"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
def ktor_version="1.5.0"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-cio:$ktor_version"
implementation "io.ktor:ktor-client-features:$ktor_version"
implementation "io.ktor:ktor-client-encoding:$ktor_version"
// StringEscapeUtils.unescapeHtml4
implementation "org.apache.commons:commons-text:1.9"
// HTML5パーサ
implementation "org.jsoup:jsoup:1.13.1"
}
test {
useJUnitPlatform()
}

File diff suppressed because it is too large Load Diff

View File

@ -7,9 +7,10 @@
rm -fr emojione
git clone -b v2.2.7 git@github.com:emojione/emojione.git emojione
# Gargron's fork of emoji-mart (master branch)
rm -fr emoji-mart
git clone git@github.com:Gargron/emoji-mart.git emoji-mart
# 2021/02 不要になった
## Gargron's fork of emoji-mart (master branch)
#rm -fr emoji-mart
#git clone git@github.com:Gargron/emoji-mart.git emoji-mart
rm -fr emoji-data
git clone git@github.com:iamcal/emoji-data.git emoji-data
@ -36,7 +37,8 @@ mkdir assets drawable-nodpi
rm -f assets/* drawable-nodpi/* category-pretty.json
* ビルド
perl makeJavaCode.pl 2>error.log
####perl makeJavaCode.pl 2>error.log
2021/02 からkotlinのコードに変えた
* 出力

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2011 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.annotations;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Denotes that a parameter, field or method return value can never be null.
* <p/>
* This is a marker annotation and it has no specific attributes.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({METHOD,PARAMETER,LOCAL_VARIABLE,FIELD})
public @interface NonNull {
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2011 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.annotations;
import static java.lang.annotation.ElementType.PACKAGE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Denotes that all parameters, fields or methods within a class or method by
* default can not be null. This can be overridden by adding specific
* {@link com.android.annotations.Nullable} annotations on fields, parameters or
* methods that should not use the default.
* <p/>
* NOTE: Eclipse does not yet handle defaults well (in particular, if
* you add this on a class which implements Comparable, then it will insist
* that your compare method is changing the nullness of the compare parameter,
* so you'll need to add @Nullable on it, which also is not right (since
* the method should have implied @NonNull and you do not need to check
* the parameter.). For now, it's best to individually annotate methods,
* parameters and fields.
* <p/>
* This is a marker annotation and it has no specific attributes.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({PACKAGE, TYPE})
@interface NonNullByDefault {
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2011 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.annotations;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Denotes that a parameter, field or method return value can be null.
* <b>Note</b>: this is the default assumption for most Java APIs and the
* default assumption made by most static code checking tools, so usually you
* don't need to use this annotation; its primary use is to override a default
* wider annotation like {@link NonNullByDefault}.
* <p/>
* When decorating a method call parameter, this denotes the parameter can
* legitimately be null and the method will gracefully deal with it. Typically
* used on optional parameters.
* <p/>
* When decorating a method, this denotes the method might legitimately return
* null.
* <p/>
* This is a marker annotation and it has no specific attributes.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target({METHOD, PARAMETER, LOCAL_VARIABLE, FIELD})
public @interface Nullable {
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2013 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.annotations.concurrency;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the target class to which this annotation is applied
* is immutable.
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Immutable {
}

View File

@ -0,0 +1,194 @@
/*
* 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.blame;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.concurrency.Immutable;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.util.List;
@Immutable
public final class Message {
@NonNull
private final Kind mKind;
@NonNull
private final String mText;
@NonNull
private final List<SourceFilePosition> mSourceFilePositions;
@NonNull
private final String mRawMessage;
/**
* Create a new message, which has a {@link Kind}, a String which will be shown to the user and
* at least one {@link SourceFilePosition}.
*
* @param kind the message type.
* @param text the text of the message.
* @param sourceFilePosition the first source file position the message .
* @param sourceFilePositions any additional source file positions, may be empty.
*/
public Message(@NonNull Kind kind,
@NonNull String text,
@NonNull SourceFilePosition sourceFilePosition,
@NonNull SourceFilePosition... sourceFilePositions) {
mKind = kind;
mText = text;
mRawMessage = text;
mSourceFilePositions = ImmutableList.<SourceFilePosition>builder()
.add(sourceFilePosition).add(sourceFilePositions).build();
}
/**
* Create a new message, which has a {@link Kind}, a String which will be shown to the user and
* at least one {@link SourceFilePosition}.
* <p>
* It also has a rawMessage, to store the original string for cases when the message is
* constructed by parsing the output from another tool.
*
* @param kind the message kind.
* @param text a human-readable string explaining the issue.
* @param rawMessage the original text of the message, usually from an external tool.
* @param sourceFilePosition the first source file position.
* @param sourceFilePositions any additional source file positions, may be empty.
*/
public Message(@NonNull Kind kind,
@NonNull String text,
@NonNull String rawMessage,
@NonNull SourceFilePosition sourceFilePosition,
@NonNull SourceFilePosition... sourceFilePositions) {
mKind = kind;
mText = text;
mRawMessage = rawMessage;
mSourceFilePositions = ImmutableList.<SourceFilePosition>builder()
.add(sourceFilePosition).add(sourceFilePositions).build();
}
public Message(@NonNull Kind kind,
@NonNull String text,
@NonNull String rawMessage,
@NonNull ImmutableList<SourceFilePosition> positions) {
mKind = kind;
mText = text;
mRawMessage = rawMessage;
if (positions.isEmpty()) {
mSourceFilePositions = ImmutableList.of(SourceFilePosition.UNKNOWN);
} else {
mSourceFilePositions = positions;
}
}
@NonNull
public Kind getKind() {
return mKind;
}
@NonNull
public String getText() {
return mText;
}
/**
* Returns a list of source positions. Will always contain at least one item.
*/
@NonNull
public List<SourceFilePosition> getSourceFilePositions() {
return mSourceFilePositions;
}
@NonNull
public String getRawMessage() {
return mRawMessage;
}
@Nullable
public String getSourcePath() {
File file = mSourceFilePositions.get(0).getFile().getSourceFile();
if (file == null) {
return null;
}
return file.getAbsolutePath();
}
/**
* Returns a legacy 1-based line number.
*/
@Deprecated
public int getLineNumber() {
return mSourceFilePositions.get(0).getPosition().getStartLine() + 1;
}
/**
* @return a legacy 1-based column number.
*/
@Deprecated
public int getColumn() {
return mSourceFilePositions.get(0).getPosition().getStartColumn() + 1;
}
public enum Kind {
ERROR, WARNING, INFO, STATISTICS, UNKNOWN, SIMPLE;
public static Kind findIgnoringCase(String s, Kind defaultKind) {
for (Kind kind : values()) {
if (kind.toString().equalsIgnoreCase(s)) {
return kind;
}
}
return defaultKind;
}
@Nullable
public static Kind findIgnoringCase(String s) {
return findIgnoringCase(s, null);
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Message)) {
return false;
}
Message that = (Message) o;
return Objects.equal(mKind, that.mKind) &&
Objects.equal(mText, that.mText) &&
Objects.equal(mSourceFilePositions, that.mSourceFilePositions);
}
@Override
public int hashCode() {
return Objects.hashCode(mKind, mText, mSourceFilePositions);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("kind", mKind).add("text", mText).add("sources",
mSourceFilePositions).toString();
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.blame;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.concurrency.Immutable;
import com.google.common.base.Objects;
import java.io.File;
/**
* Represents a source file.
*/
@Immutable
public final class SourceFile {
public static final SourceFile UNKNOWN = new SourceFile();
@Nullable
private final File mSourceFile;
/**
* A human readable description
*
* Usually the file name is OK for the short output, but for the manifest merger,
* where all of the files will be named AndroidManifest.xml the variant name is more useful.
*/
@Nullable
private final String mDescription;
@SuppressWarnings("NullableProblems")
public SourceFile(
@NonNull File sourceFile,
@NonNull String description) {
mSourceFile = sourceFile;
mDescription = description;
}
public SourceFile(
@SuppressWarnings("NullableProblems") @NonNull File sourceFile) {
mSourceFile = sourceFile;
mDescription = null;
}
public SourceFile(
@SuppressWarnings("NullableProblems") @NonNull String description) {
mSourceFile = null;
mDescription = description;
}
private SourceFile() {
mSourceFile = null;
mDescription = null;
}
@Nullable
public File getSourceFile() {
return mSourceFile;
}
@Nullable
public String getDescription() {
return mDescription;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SourceFile)) {
return false;
}
SourceFile other = (SourceFile) obj;
return Objects.equal(mDescription, other.mDescription) &&
Objects.equal(mSourceFile, other.mSourceFile);
}
@Override
public int hashCode() {
return Objects.hashCode(mSourceFile, mDescription);
}
@Override
public String toString() {
return print(false /* shortFormat */);
}
public String print(boolean shortFormat) {
if (mSourceFile == null) {
if (mDescription == null) {
return "Unknown source file";
}
return mDescription;
}
String fileName = mSourceFile.getName();
String fileDisplayName = shortFormat ? fileName : mSourceFile.getAbsolutePath();
if (mDescription == null || mDescription.equals(fileName)) {
return fileDisplayName;
} else {
return String.format("[%1$s] %2$s", mDescription, fileDisplayName);
}
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.blame;
import com.android.annotations.NonNull;
import com.android.annotations.concurrency.Immutable;
import com.google.common.base.Objects;
import java.io.File;
@Immutable
public final class SourceFilePosition {
public static final com.android.ide.common.blame.SourceFilePosition UNKNOWN =
new SourceFilePosition(SourceFile.UNKNOWN, SourcePosition.UNKNOWN);
@NonNull
private final SourceFile mSourceFile;
@NonNull
private final SourcePosition mSourcePosition;
public SourceFilePosition(@NonNull SourceFile sourceFile,
@NonNull SourcePosition sourcePosition) {
mSourceFile = sourceFile;
mSourcePosition = sourcePosition;
}
public SourceFilePosition(@NonNull File file,
@NonNull SourcePosition sourcePosition) {
this(new SourceFile(file), sourcePosition);
}
@NonNull
public SourcePosition getPosition() {
return mSourcePosition;
}
@NonNull
public SourceFile getFile() {
return mSourceFile;
}
@Override
public String toString() {
return print(false);
}
public String print(boolean shortFormat) {
if (mSourcePosition.equals(SourcePosition.UNKNOWN)) {
return mSourceFile.print(shortFormat);
} else {
return mSourceFile.print(shortFormat) + ':' + mSourcePosition.toString();
}
}
@Override
public int hashCode() {
return Objects.hashCode(mSourceFile, mSourcePosition);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SourceFilePosition)) {
return false;
}
SourceFilePosition other = (SourceFilePosition) obj;
return Objects.equal(mSourceFile, other.mSourceFile) &&
Objects.equal(mSourcePosition, other.mSourcePosition);
}
}

View File

@ -0,0 +1,156 @@
/*
* 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.blame;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.concurrency.Immutable;
import com.google.common.base.Objects;
/**
* An immutable position in a text file, used in errors to point the user to an issue.
*
* Positions that are unknown are represented by -1.
*/
@Immutable
public final class SourcePosition {
public static final SourcePosition UNKNOWN = new SourcePosition();
private final int mStartLine, mStartColumn, mStartOffset, mEndLine, mEndColumn, mEndOffset;
public SourcePosition(int startLine, int startColumn, int startOffset,
int endLine, int endColumn, int endOffset) {
mStartLine = startLine;
mStartColumn = startColumn;
mStartOffset = startOffset;
mEndLine = endLine;
mEndColumn = endColumn;
mEndOffset = endOffset;
}
public SourcePosition(int lineNumber, int column, int offset) {
mStartLine = mEndLine = lineNumber;
mStartColumn = mEndColumn = column;
mStartOffset = mEndOffset = offset;
}
private SourcePosition() {
mStartLine = mStartColumn = mStartOffset = mEndLine = mEndColumn = mEndOffset = -1;
}
protected SourcePosition(SourcePosition copy) {
mStartLine = copy.getStartLine();
mStartColumn = copy.getStartColumn();
mStartOffset = copy.getStartOffset();
mEndLine = copy.getEndLine();
mEndColumn = copy.getEndColumn();
mEndOffset = copy.getEndOffset();
}
/**
* Outputs positions as human-readable formatted strings.
*
* e.g.
* <pre>84
* 84-86
* 84:5
* 84:5-28
* 85:5-86:47</pre>
*
* @return a human readable position.
*/
@Override
public String toString() {
if (mStartLine == -1) {
return "?";
}
StringBuilder sB = new StringBuilder(15);
sB.append(mStartLine + 1); // Humans think that the first line is line 1.
if (mStartColumn != -1) {
sB.append(':');
sB.append(mStartColumn + 1);
}
if (mEndLine != -1) {
if (mEndLine == mStartLine) {
if (mEndColumn != -1 && mEndColumn != mStartColumn) {
sB.append('-');
sB.append(mEndColumn + 1);
}
} else {
sB.append('-');
sB.append(mEndLine + 1);
if (mEndColumn != -1) {
sB.append(':');
sB.append(mEndColumn + 1);
}
}
}
return sB.toString();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SourcePosition)) {
return false;
}
SourcePosition other = (SourcePosition) obj;
return other.mStartLine == mStartLine &&
other.mStartColumn == mStartColumn &&
other.mStartOffset == mStartOffset &&
other.mEndLine == mEndLine &&
other.mEndColumn == mEndColumn &&
other.mEndOffset == mEndOffset;
}
@Override
public int hashCode() {
return Objects
.hashCode(mStartLine, mStartColumn, mStartOffset, mEndLine, mEndColumn, mEndOffset);
}
public int getStartLine() {
return mStartLine;
}
public int getStartColumn() {
return mStartColumn;
}
public int getStartOffset() {
return mStartOffset;
}
public int getEndLine() {
return mEndLine;
}
public int getEndColumn() {
return mEndColumn;
}
public int getEndOffset() {
return mEndOffset;
}
}

View File

@ -0,0 +1,95 @@
/*
* 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;
/**
* Build a string for Svg file's path data.
*/
class PathBuilder {
private StringBuilder mPathData = new StringBuilder();
private String booleanToString(boolean flag) {
return flag ? "1" : "0";
}
public PathBuilder absoluteMoveTo(float x, float y) {
mPathData.append("M"+ x + "," + y);
return this;
}
public PathBuilder relativeMoveTo(float x, float y) {
mPathData.append("m"+ x + "," + y);
return this;
}
public PathBuilder absoluteLineTo(float x, float y) {
mPathData.append("L"+ x + "," + y);
return this;
}
public PathBuilder relativeLineTo(float x, float y) {
mPathData.append("l"+ x + "," + y);
return this;
}
public PathBuilder absoluteVerticalTo(float v) {
mPathData.append("V"+ v);
return this;
}
public PathBuilder relativeVerticalTo(float v) {
mPathData.append("v"+ v);
return this;
}
public PathBuilder absoluteHorizontalTo(float h) {
mPathData.append("H"+ h);
return this;
}
public PathBuilder relativeHorizontalTo(float h) {
mPathData.append("h"+ h);
return this;
}
public PathBuilder absoluteArcTo(float rx, float ry, boolean rotation,
boolean largeArc, boolean sweep, float x, float y) {
mPathData.append("A"+ rx + "," + ry + "," + booleanToString(rotation) + "," +
booleanToString(largeArc) + "," + booleanToString(sweep) + "," + x + "," + y );
return this;
}
public PathBuilder relativeArcTo(float rx, float ry, boolean rotation,
boolean largeArc, boolean sweep, float x, float y) {
mPathData.append("a"+ rx + "," + ry + "," + booleanToString(rotation) + "," +
booleanToString(largeArc) + "," + booleanToString(sweep) + "," + x + "," + y );
return this;
}
public PathBuilder absoluteClose() {
mPathData.append("Z");
return this;
}
public PathBuilder relativeClose() {
mPathData.append("z");
return this;
}
public String toString() {
return mPathData.toString();
}
}

View File

@ -0,0 +1,658 @@
/*
* 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.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.*;
import java.util.HashSet;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Converts SVG to VectorDrawable's XML
*/
public class Svg2Vector {
private static Logger logger = Logger.getLogger(Svg2Vector.class.getSimpleName());
public static final String SVG_POLYGON = "polygon";
public static final String SVG_RECT = "rect";
public static final String SVG_CIRCLE = "circle";
public static final String SVG_LINE = "line";
public static final String SVG_PATH = "path";
public static final String SVG_GROUP = "g";
public static final String SVG_TRANSFORM = "transform";
public static final String SVG_WIDTH = "width";
public static final String SVG_HEIGHT = "height";
public static final String SVG_VIEW_BOX = "viewBox";
public static final String SVG_STYLE = "style";
public static final String SVG_DISPLAY = "display";
public static final String SVG_D = "d";
public static final String SVG_STROKE_COLOR = "stroke";
public static final String SVG_STROKE_OPACITY = "stroke-opacity";
public static final String SVG_STROKE_LINEJOINE = "stroke-linejoin";
public static final String SVG_STROKE_LINECAP = "stroke-linecap";
public static final String SVG_STROKE_WIDTH = "stroke-width";
public static final String SVG_FILL_COLOR = "fill";
public static final String SVG_FILL_OPACITY = "fill-opacity";
public static final String SVG_OPACITY = "opacity";
public static final String SVG_CLIP = "clip";
public static final String SVG_POINTS = "points";
public static final ImmutableMap<String, String> presentationMap =
ImmutableMap.<String, String>builder()
.put(SVG_STROKE_COLOR, "android:strokeColor")
.put(SVG_STROKE_OPACITY, "android:strokeAlpha")
.put(SVG_STROKE_LINEJOINE, "android:strokeLinejoin")
.put(SVG_STROKE_LINECAP, "android:strokeLinecap")
.put(SVG_STROKE_WIDTH, "android:strokeWidth")
.put(SVG_FILL_COLOR, "android:fillColor")
.put(SVG_FILL_OPACITY, "android:fillAlpha")
.put(SVG_CLIP, "android:clip").put(SVG_OPACITY, "android:fillAlpha")
.build();
// List all the Svg nodes that we don't support. Categorized by the types.
private static final HashSet<String> unsupportedSvgNodes = Sets.newHashSet(
// Animation elements
"animate", "animateColor", "animateMotion", "animateTransform", "mpath", "set",
// Container elements
"a", "defs", "glyph", "marker", "mask", "missing-glyph", "pattern", "switch", "symbol",
// Filter primitive elements
"feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix",
"feDiffuseLighting", "feDisplacementMap", "feFlood", "feFuncA", "feFuncB", "feFuncG",
"feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology",
"feOffset", "feSpecularLighting", "feTile", "feTurbulence",
// Font elements
"font", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri",
"hkern", "vkern",
// Gradient elements
"linearGradient", "radialGradient", "stop",
// Graphics elements
"ellipse", "polyline", "text", "use",
// Light source elements
"feDistantLight", "fePointLight", "feSpotLight",
// Structural elements
"defs", "symbol", "use",
// Text content elements
"altGlyph", "altGlyphDef", "altGlyphItem", "glyph", "glyphRef", "textPath", "text", "tref",
"tspan",
// Text content child elements
"altGlyph", "textPath", "tref", "tspan",
// Uncategorized elements
"clipPath", "color-profile", "cursor", "filter", "foreignObject", "script", "view");
@NonNull
public static SvgTree parse(File f) throws Exception {
SvgTree svgTree = new SvgTree();
Document doc = svgTree.parse(f);
NodeList nSvgNode;
// Parse svg elements
nSvgNode = doc.getElementsByTagName("svg");
if (nSvgNode.getLength() != 1) {
throw new IllegalStateException("Not a proper SVG file");
}
Node rootNode = nSvgNode.item(0);
for (int i = 0; i < nSvgNode.getLength(); i++) {
Node nNode = nSvgNode.item(i);
if (nNode.getNodeType() == Node.ELEMENT_NODE) {
parseDimension(svgTree, nNode);
}
}
if (svgTree.viewBox == null) {
svgTree.logErrorLine("Missing \"viewBox\" in <svg> element", rootNode, SvgTree.SvgLogLevel.ERROR);
return svgTree;
}
if ((svgTree.w == 0 || svgTree.h == 0) && svgTree.viewBox[2] > 0 && svgTree.viewBox[3] > 0) {
svgTree.w = svgTree.viewBox[2];
svgTree.h = svgTree.viewBox[3];
}
// Parse transformation information.
// TODO: Properly handle transformation in the group level. In the "use" case, we treat
// it as global for now.
NodeList nUseTags;
svgTree.matrix = new float[6];
svgTree.matrix[0] = 1;
svgTree.matrix[3] = 1;
nUseTags = doc.getElementsByTagName("use");
for (int temp = 0; temp < nUseTags.getLength(); temp++) {
Node nNode = nUseTags.item(temp);
if (nNode.getNodeType() == Node.ELEMENT_NODE) {
parseTransformation(svgTree, nNode);
}
}
SvgGroupNode root = new SvgGroupNode(svgTree, rootNode, "root");
svgTree.setRoot(root);
// Parse all the group and path node recursively.
traverseSVGAndExtract(svgTree, root, rootNode);
svgTree.dump(root);
return svgTree;
}
private static void traverseSVGAndExtract(SvgTree svgTree, SvgGroupNode currentGroup, Node item) {
// Recursively traverse all the group and path nodes
NodeList allChildren = item.getChildNodes();
for (int i = 0; i < allChildren.getLength(); i++) {
Node currentNode = allChildren.item(i);
String nodeName = currentNode.getNodeName();
if (SVG_PATH.equals(nodeName) ||
SVG_RECT.equals(nodeName) ||
SVG_CIRCLE.equals(nodeName) ||
SVG_POLYGON.equals(nodeName) ||
SVG_LINE.equals(nodeName)) {
SvgLeafNode child = new SvgLeafNode(svgTree, currentNode, nodeName + i);
extractAllItemsAs(svgTree, child, currentNode);
currentGroup.addChild(child);
} else if (SVG_GROUP.equals(nodeName)) {
SvgGroupNode childGroup = new SvgGroupNode(svgTree, currentNode, "child" + i);
currentGroup.addChild(childGroup);
traverseSVGAndExtract(svgTree, childGroup, currentNode);
} else {
// For other fancy tags, like <refs>, they can contain children too.
// Report the unsupported nodes.
if (unsupportedSvgNodes.contains(nodeName)) {
svgTree.logErrorLine("<" + nodeName + "> is not supported", currentNode,
SvgTree.SvgLogLevel.ERROR);
}
traverseSVGAndExtract(svgTree, currentGroup, currentNode);
}
}
}
private static void parseTransformation(SvgTree avg, Node nNode) {
NamedNodeMap a = nNode.getAttributes();
int len = a.getLength();
for (int i = 0; i < len; i++) {
Node n = a.item(i);
String name = n.getNodeName();
String value = n.getNodeValue();
if (SVG_TRANSFORM.equals(name)) {
if (value.startsWith("matrix(")) {
value = value.substring("matrix(".length(), value.length() - 1);
String[] sp = value.split(" ");
for (int j = 0; j < sp.length; j++) {
avg.matrix[j] = Float.parseFloat(sp[j]);
}
}
} else if (name.equals("y")) {
Float.parseFloat(value);
} else if (name.equals("x")) {
Float.parseFloat(value);
}
}
}
private static void parseDimension(SvgTree avg, Node nNode) {
NamedNodeMap a = nNode.getAttributes();
int len = a.getLength();
for (int i = 0; i < len; i++) {
Node n = a.item(i);
String name = n.getNodeName();
String value = n.getNodeValue();
int subStringSize = value.length();
if (subStringSize > 2) {
if (value.endsWith("px")) {
subStringSize = subStringSize - 2;
}
}
if (SVG_WIDTH.equals(name)) {
avg.w = Float.parseFloat(value.substring(0, subStringSize));
} else if (SVG_HEIGHT.equals(name)) {
avg.h = Float.parseFloat(value.substring(0, subStringSize));
} else if (SVG_VIEW_BOX.equals(name)) {
avg.viewBox = new float[4];
String[] strbox = value.split(" ");
for (int j = 0; j < avg.viewBox.length; j++) {
avg.viewBox[j] = Float.parseFloat(strbox[j]);
}
}
}
if (avg.viewBox == null && avg.w != 0 && avg.h != 0) {
avg.viewBox = new float[4];
avg.viewBox[2] = avg.w;
avg.viewBox[3] = avg.h;
}
}
// Read the content from currentItem, and fill into "child"
private static void extractAllItemsAs(SvgTree avg, SvgLeafNode child, Node currentItem) {
Node currentGroup = currentItem.getParentNode();
boolean hasNodeAttr = false;
String styleContent = "";
boolean nothingToDisplay = false;
while (currentGroup != null && currentGroup.getNodeName().equals("g")) {
// Parse the group's attributes.
logger.log(Level.FINE, "Printing current parent");
printlnCommon(currentGroup);
NamedNodeMap attr = currentGroup.getAttributes();
Node nodeAttr = attr.getNamedItem(SVG_STYLE);
// Search for the "display:none", if existed, then skip this item.
if (nodeAttr != null) {
styleContent += nodeAttr.getTextContent() + ";";
logger.log(Level.FINE, "styleContent is :" + styleContent + "at number group ");
if (styleContent.contains("display:none")) {
logger.log(Level.FINE, "Found none style, skip the whole group");
nothingToDisplay = true;
break;
} else {
hasNodeAttr = true;
}
}
Node displayAttr = attr.getNamedItem(SVG_DISPLAY);
if (displayAttr != null && "none".equals(displayAttr.getNodeValue())) {
logger.log(Level.FINE, "Found display:none style, skip the whole group");
nothingToDisplay = true;
break;
}
currentGroup = currentGroup.getParentNode();
}
if (nothingToDisplay) {
// Skip this current whole item.
return;
}
logger.log(Level.FINE, "Print current item");
printlnCommon(currentItem);
if (hasNodeAttr && styleContent != null) {
addStyleToPath(child, styleContent);
}
Node currentGroupNode = currentItem;
if (SVG_PATH.equals(currentGroupNode.getNodeName())) {
extractPathItem(avg, child, currentGroupNode);
}
if (SVG_RECT.equals(currentGroupNode.getNodeName())) {
extractRectItem(avg, child, currentGroupNode);
}
if (SVG_CIRCLE.equals(currentGroupNode.getNodeName())) {
extractCircleItem(avg, child, currentGroupNode);
}
if (SVG_POLYGON.equals(currentGroupNode.getNodeName())) {
extractPolyItem(avg, child, currentGroupNode);
}
if (SVG_LINE.equals(currentGroupNode.getNodeName())) {
extractLineItem(avg, child, currentGroupNode);
}
}
private static void printlnCommon(Node n) {
logger.log(Level.FINE, " nodeName=\"" + n.getNodeName() + "\"");
String val = n.getNamespaceURI();
if (val != null) {
logger.log(Level.FINE, " uri=\"" + val + "\"");
}
val = n.getPrefix();
if (val != null) {
logger.log(Level.FINE, " pre=\"" + val + "\"");
}
val = n.getLocalName();
if (val != null) {
logger.log(Level.FINE, " local=\"" + val + "\"");
}
val = n.getNodeValue();
if (val != null) {
logger.log(Level.FINE, " nodeValue=");
if (val.trim().equals("")) {
// Whitespace
logger.log(Level.FINE, "[WS]");
} else {
logger.log(Level.FINE, "\"" + n.getNodeValue() + "\"");
}
}
}
/**
* Convert polygon element into a path.
*/
private static void extractPolyItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
logger.log(Level.FINE, "Rect found" + currentGroupNode.getTextContent());
if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
NamedNodeMap a = currentGroupNode.getAttributes();
int len = a.getLength();
for (int itemIndex = 0; itemIndex < len; itemIndex++) {
Node n = a.item(itemIndex);
String name = n.getNodeName();
String value = n.getNodeValue();
if (name.equals(SVG_STYLE)) {
addStyleToPath(child, value);
} else if (presentationMap.containsKey(name)) {
child.fillPresentationAttributes(name, value);
} else if (name.equals(SVG_POINTS)) {
PathBuilder builder = new PathBuilder();
String[] split = value.split("[\\s,]+");
float baseX = Float.parseFloat(split[0]);
float baseY = Float.parseFloat(split[1]);
builder.absoluteMoveTo(baseX, baseY);
for (int j = 2; j < split.length; j += 2) {
float x = Float.parseFloat(split[j]);
float y = Float.parseFloat(split[j + 1]);
builder.relativeLineTo(x - baseX, y - baseY);
baseX = x;
baseY = y;
}
builder.relativeClose();
child.setPathData(builder.toString());
}
}
}
}
/**
* Convert rectangle element into a path.
*/
private static void extractRectItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
logger.log(Level.FINE, "Rect found" + currentGroupNode.getTextContent());
if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
float x = 0;
float y = 0;
float width = Float.NaN;
float height = Float.NaN;
NamedNodeMap a = currentGroupNode.getAttributes();
int len = a.getLength();
boolean pureTransparent = false;
for (int j = 0; j < len; j++) {
Node n = a.item(j);
String name = n.getNodeName();
String value = n.getNodeValue();
if (name.equals(SVG_STYLE)) {
addStyleToPath(child, value);
if (value.contains("opacity:0;")) {
pureTransparent = true;
}
} else if (presentationMap.containsKey(name)) {
child.fillPresentationAttributes(name, value);
} else if (name.equals("clip-path") && value.startsWith("url(#SVGID_")) {
} else if (name.equals("x")) {
x = Float.parseFloat(value);
} else if (name.equals("y")) {
y = Float.parseFloat(value);
} else if (name.equals("width")) {
width = Float.parseFloat(value);
} else if (name.equals("height")) {
height = Float.parseFloat(value);
} else if (name.equals("style")) {
}
}
if (!pureTransparent && avg != null && !Float.isNaN(x) && !Float.isNaN(y)
&& !Float.isNaN(width)
&& !Float.isNaN(height)) {
// "M x, y h width v height h -width z"
PathBuilder builder = new PathBuilder();
builder.absoluteMoveTo(x, y);
builder.relativeHorizontalTo(width);
builder.relativeVerticalTo(height);
builder.relativeHorizontalTo(-width);
builder.relativeClose();
child.setPathData(builder.toString());
}
}
}
/**
* Convert circle element into a path.
*/
private static void extractCircleItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
logger.log(Level.FINE, "circle found" + currentGroupNode.getTextContent());
if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
float cx = 0;
float cy = 0;
float radius = 0;
NamedNodeMap a = currentGroupNode.getAttributes();
int len = a.getLength();
boolean pureTransparent = false;
for (int j = 0; j < len; j++) {
Node n = a.item(j);
String name = n.getNodeName();
String value = n.getNodeValue();
if (name.equals(SVG_STYLE)) {
addStyleToPath(child, value);
if (value.contains("opacity:0;")) {
pureTransparent = true;
}
} else if (presentationMap.containsKey(name)) {
child.fillPresentationAttributes(name, value);
} else if (name.equals("clip-path") && value.startsWith("url(#SVGID_")) {
} else if (name.equals("cx")) {
cx = Float.parseFloat(value);
} else if (name.equals("cy")) {
cy = Float.parseFloat(value);
} else if (name.equals("r")) {
radius = Float.parseFloat(value);
}
}
if (!pureTransparent && avg != null && !Float.isNaN(cx) && !Float.isNaN(cy)) {
// "M cx cy m -r, 0 a r,r 0 1,1 (r * 2),0 a r,r 0 1,1 -(r * 2),0"
PathBuilder builder = new PathBuilder();
builder.absoluteMoveTo(cx, cy);
builder.relativeMoveTo(-radius, 0);
builder.relativeArcTo(radius, radius, false, true, true, 2 * radius, 0);
builder.relativeArcTo(radius, radius, false, true, true, -2 * radius, 0);
child.setPathData(builder.toString());
}
}
}
/**
* Convert line element into a path.
*/
private static void extractLineItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
logger.log(Level.FINE, "line found" + currentGroupNode.getTextContent());
if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
float x1 = 0;
float y1 = 0;
float x2 = 0;
float y2 = 0;
NamedNodeMap a = currentGroupNode.getAttributes();
int len = a.getLength();
boolean pureTransparent = false;
for (int j = 0; j < len; j++) {
Node n = a.item(j);
String name = n.getNodeName();
String value = n.getNodeValue();
if (name.equals(SVG_STYLE)) {
addStyleToPath(child, value);
if (value.contains("opacity:0;")) {
pureTransparent = true;
}
} else if (presentationMap.containsKey(name)) {
child.fillPresentationAttributes(name, value);
} else if (name.equals("clip-path") && value.startsWith("url(#SVGID_")) {
// TODO: Handle clip path here.
} else if (name.equals("x1")) {
x1 = Float.parseFloat(value);
} else if (name.equals("y1")) {
y1 = Float.parseFloat(value);
} else if (name.equals("x2")) {
x2 = Float.parseFloat(value);
} else if (name.equals("y2")) {
y2 = Float.parseFloat(value);
}
}
if (!pureTransparent && avg != null && !Float.isNaN(x1) && !Float.isNaN(y1)
&& !Float.isNaN(x2) && !Float.isNaN(y2)) {
// "M x1, y1 L x2, y2"
PathBuilder builder = new PathBuilder();
builder.absoluteMoveTo(x1, y1);
builder.absoluteLineTo(x2, y2);
child.setPathData(builder.toString());
}
}
}
private static void extractPathItem(SvgTree avg, SvgLeafNode child, Node currentGroupNode) {
logger.log(Level.FINE, "Path found " + currentGroupNode.getTextContent());
if (currentGroupNode.getNodeType() == Node.ELEMENT_NODE) {
Element eElement = (Element)currentGroupNode;
NamedNodeMap a = currentGroupNode.getAttributes();
int len = a.getLength();
for (int j = 0; j < len; j++) {
Node n = a.item(j);
String name = n.getNodeName();
String value = n.getNodeValue();
if (name.equals(SVG_STYLE)) {
addStyleToPath(child, value);
} else if (presentationMap.containsKey(name)) {
child.fillPresentationAttributes(name, value);
} else if (name.equals(SVG_D)) {
String pathData = value.replaceAll("(\\d)-", "$1,-");
child.setPathData(pathData);
}
}
}
}
private static void addStyleToPath(SvgLeafNode path, String value) {
logger.log(Level.FINE, "Style found is " + value);
if (value != null) {
String[] parts = value.split(";");
for (int k = parts.length - 1; k >= 0; k--) {
String subStyle = parts[k];
String[] nameValue = subStyle.split(":");
if (nameValue.length == 2 && nameValue[0] != null && nameValue[1] != null) {
if (presentationMap.containsKey(nameValue[0])) {
path.fillPresentationAttributes(nameValue[0], nameValue[1]);
} else if (nameValue[0].equals(SVG_OPACITY)) {
// TODO: This is hacky, since we don't have a group level
// android:opacity. This only works when the path didn't overlap.
path.fillPresentationAttributes(SVG_FILL_OPACITY, nameValue[1]);
}
}
}
}
}
private static final String head = "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n";
private static String getSizeString(float w, float h, float scaleFactor) {
String size = " android:width=\"" + (int) (w * scaleFactor) + "dp\"\n" +
" android:height=\"" + (int) (h * scaleFactor) + "dp\"\n";
return size;
}
public static void writeFile(OutputStream outStream, SvgTree svgTree) throws IOException {
OutputStreamWriter fw = new OutputStreamWriter(outStream);
fw.write(head);
float finalWidth = svgTree.w;
float finalHeight = svgTree.h;
fw.write(getSizeString(finalWidth, finalHeight, svgTree.mScaleFactor));
fw.write(" android:viewportWidth=\"" + svgTree.w + "\"\n");
fw.write(" android:viewportHeight=\"" + svgTree.h + "\">\n");
svgTree.normalize();
// TODO: this has to happen in the tree mode!!!
writeXML(svgTree, fw);
fw.write("</vector>\n");
fw.close();
}
private static void writeXML(SvgTree svgTree, OutputStreamWriter fw) throws IOException {
svgTree.getRoot().writeXML(fw);
}
/**
* Convert a SVG file into VectorDrawable's XML content, if no error is found.
*
* @param inputSVG the input SVG file
* @param outStream the converted VectorDrawable's content. This can be
* empty if there is any error found during parsing
* @return the error messages, which contain things like all the tags
* VectorDrawble don't support or exception message.
*/
public static String parseSvgToXml(File inputSVG, OutputStream outStream) {
// Write all the error message during parsing into SvgTree. and return here as getErrorLog().
// We will also log the exceptions here.
String errorLog = null;
try {
SvgTree svgTree = parse(inputSVG);
errorLog = svgTree.getErrorLog();
// When there was anything in the input SVG file that we can't
// convert to VectorDrawable, we logged them as errors.
// After we logged all the errors, we skipped the XML file generation.
if (svgTree.canConvertToVectorDrawable()) {
writeFile(outStream, svgTree);
}
} catch (Exception e) {
errorLog = "EXCEPTION in parsing " + inputSVG.getName() + ":\n" + e.getMessage();
}
return errorLog;
}
}

View File

@ -0,0 +1,72 @@
/*
* 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 org.w3c.dom.Node;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represent a SVG file's group element.
*/
class SvgGroupNode extends SvgNode {
private static Logger logger = Logger.getLogger(SvgGroupNode.class.getSimpleName());
private static final String INDENT_LEVEL = " ";
private ArrayList<SvgNode> mChildren = new ArrayList<SvgNode>();
public SvgGroupNode(SvgTree svgTree, Node docNode, String name) {
super(svgTree, docNode, name);
}
public void addChild(SvgNode child) {
mChildren.add(child);
}
@Override
public void dumpNode(String indent) {
// Print the current group.
logger.log(Level.FINE, indent + "current group is :" + getName());
// Then print all the children.
for (SvgNode node : mChildren) {
node.dumpNode(indent + INDENT_LEVEL);
}
}
@Override
public boolean isGroupNode() {
return true;
}
@Override
public void transform(float a, float b, float c, float d, float e, float f) {
for (SvgNode p : mChildren) {
p.transform(a, b, c, d, e, f);
}
}
@Override
public void writeXML(OutputStreamWriter writer) throws IOException {
for (SvgNode node : mChildren) {
node.writeXML(writer);
}
}
}

View File

@ -0,0 +1,177 @@
/*
* 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.Nullable;
import com.google.common.collect.ImmutableMap;
import org.w3c.dom.Node;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represent a SVG file's leave element.
*/
class SvgLeafNode extends SvgNode {
private static Logger logger = Logger.getLogger(SvgLeafNode.class.getSimpleName());
private String mPathData;
// Key is the attributes for vector drawable, and the value is the converted from SVG.
private HashMap<String, String> mVdAttributesMap = new HashMap<String, String>();
public SvgLeafNode(SvgTree svgTree, Node node, String nodeName) {
super(svgTree, node, nodeName);
}
private String getAttributeValues(ImmutableMap<String, String> presentationMap) {
StringBuilder sb = new StringBuilder("/>\n");
for (String key : mVdAttributesMap.keySet()) {
String vectorDrawableAttr = presentationMap.get(key);
String svgValue = mVdAttributesMap.get(key);
String vdValue = svgValue.trim();
// There are several cases we need to convert from SVG format to
// VectorDrawable format. Like "none", "3px" or "rgb(255, 0, 0)"
if ("none".equals(vdValue)) {
vdValue = "#00000000";
} else if (vdValue.endsWith("px")){
vdValue = vdValue.substring(0, vdValue.length() - 2);
} else if (vdValue.startsWith("rgb")) {
vdValue = vdValue.substring(3, vdValue.length());
vdValue = convertRGBToHex(vdValue);
if (vdValue == null) {
getTree().logErrorLine("Unsupported Color format " + vdValue, getDocumentNode(),
SvgTree.SvgLogLevel.ERROR);
}
}
String attr = "\n " + vectorDrawableAttr + "=\"" +
vdValue + "\"";
sb.insert(0, attr);
}
return sb.toString();
}
public static int clamp(int val, int min, int max) {
return Math.max(min, Math.min(max, val));
}
/**
* SVG allows using rgb(int, int, int) or rgb(float%, float%, float%) to
* represent a color, but Android doesn't. Therefore, we need to convert
* them into #RRGGBB format.
* @param svgValue in either "(int, int, int)" or "(float%, float%, float%)"
* @return #RRGGBB in hex format, or null, if an error is found.
*/
@Nullable
private String convertRGBToHex(String svgValue) {
// We don't support color keyword yet.
// http://www.w3.org/TR/SVG11/types.html#ColorKeywords
String result = null;
String functionValue = svgValue.trim();
functionValue = svgValue.substring(1, functionValue.length() - 1);
// After we cut the "(", ")", we can deal with the numbers.
String[] numbers = functionValue.split(",");
if (numbers.length != 3) {
return null;
}
int[] color = new int[3];
for (int i = 0; i < 3; i ++) {
String number = numbers[i];
number = number.trim();
if (number.endsWith("%")) {
float value = Float.parseFloat(number.substring(0, number.length() - 1));
color[i] = clamp((int)(value * 255.0f / 100.0f), 0, 255);
} else {
int value = Integer.parseInt(number);
color[i] = clamp(value, 0, 255);
}
}
StringBuilder builder = new StringBuilder();
builder.append("#");
for (int i = 0; i < 3; i ++) {
builder.append(String.format("%02X", color[i]));
}
result = builder.toString();
assert result.length() == 7;
return result;
}
@Override
public void dumpNode(String indent) {
logger.log(Level.FINE, indent + (mPathData != null ? mPathData : " null pathData ") +
(mName != null ? mName : " null name "));
}
public void setPathData(String pathData) {
mPathData = pathData;
}
@Override
public boolean isGroupNode() {
return false;
}
@Override
public void transform(float a, float b, float c, float d, float e, float f) {
if ("none".equals(mVdAttributesMap.get("fill")) || (mPathData == null)) {
// Nothing to draw and transform, early return.
return;
}
// TODO: We need to just apply the transformation to group.
VdPath.Node[] n = VdParser.parsePath(mPathData);
if (!(a == 1 && d == 1 && b == 0 && c == 0 && e == 0 && f == 0)) {
VdPath.Node.transform(a, b, c, d, e, f, n);
}
mPathData = VdPath.Node.NodeListToString(n);
}
@Override
public void writeXML(OutputStreamWriter writer) throws IOException {
String fillColor = mVdAttributesMap.get(Svg2Vector.SVG_FILL_COLOR);
String strokeColor = mVdAttributesMap.get(Svg2Vector.SVG_STROKE_COLOR);
logger.log(Level.FINE, "fill color " + fillColor);
boolean emptyFill = fillColor != null && ("none".equals(fillColor) || "#0000000".equals(fillColor));
boolean emptyStroke = strokeColor == null || "none".equals(strokeColor);
boolean emptyPath = mPathData == null;
boolean nothingToDraw = emptyPath || emptyFill && emptyStroke;
if (nothingToDraw) {
return;
}
writer.write(" <path\n");
if (!mVdAttributesMap.containsKey(Svg2Vector.SVG_FILL_COLOR)) {
logger.log(Level.FINE, "ADDING FILL SVG_FILL_COLOR");
writer.write(" android:fillColor=\"#FF000000\"\n");
}
writer.write(" android:pathData=\"" + mPathData + "\"");
writer.write(getAttributeValues(Svg2Vector.presentationMap));
}
public void fillPresentationAttributes(String name, String value) {
logger.log(Level.FINE, ">>>> PROP " + name + " = " + value);
if (value.startsWith("url(")) {
getTree().logErrorLine("Unsupported URL value: " + value, getDocumentNode(),
SvgTree.SvgLogLevel.ERROR);
return;
}
mVdAttributesMap.put(name, value);
}
}

View File

@ -0,0 +1,72 @@
/*
* 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 org.w3c.dom.Node;
import java.io.IOException;
import java.io.OutputStreamWriter;
/**
* Parent class for a SVG file's node, can be either group or leave element.
*/
abstract class SvgNode {
protected String mName;
// Keep a reference to the tree in order to dump the error log.
private SvgTree mSvgTree;
// Use document node to get the line number for error reporting.
private Node mDocumentNode;
public SvgNode(SvgTree svgTree, Node node, String name) {
mName = name;
mSvgTree = svgTree;
mDocumentNode = node;
}
protected SvgTree getTree() {
return mSvgTree;
}
public String getName() {
return mName;
}
public Node getDocumentNode() {
return mDocumentNode;
}
/**
* dump the current node's debug info.
*/
public abstract void dumpNode(String indent);
/**
* Write the Node content into the VectorDrawable's XML file.
*/
public abstract void writeXML(OutputStreamWriter writer) throws IOException;
/**
* @return true the node is a group node.
*/
public abstract boolean isGroupNode();
/**
* Transform the current Node with the transformation matrix.
*/
public abstract void transform(float a, float b, float c, float d, float e, float f);
}

View File

@ -0,0 +1,129 @@
/*
* 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.ide.common.blame.SourcePosition;
import com.android.utils.PositionXmlParser;
import com.google.common.base.Strings;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represent the SVG file in an internal data structure as a tree.
*/
public class SvgTree {
private static Logger logger = Logger.getLogger(SvgTree.class.getSimpleName());
public float w;
public float h;
public float[] matrix;
public float[] viewBox;
public float mScaleFactor = 1;
private SvgGroupNode mRoot;
private String mFileName;
private ArrayList<String> mErrorLines = new ArrayList<String>();
public enum SvgLogLevel {
ERROR,
WARNING
}
public Document parse(File f) throws Exception {
mFileName = f.getName();
Document doc = PositionXmlParser.parse(new FileInputStream(f), false);
return doc;
}
public void normalize() {
if (matrix != null) {
transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
}
if (viewBox != null && (viewBox[0] != 0 || viewBox[1] != 0)) {
transform(1, 0, 0, 1, -viewBox[0], -viewBox[1]);
}
logger.log(Level.FINE, "matrix=" + Arrays.toString(matrix));
}
private void transform(float a, float b, float c, float d, float e, float f) {
mRoot.transform(a, b, c, d, e, f);
}
public void dump(SvgGroupNode root) {
logger.log(Level.FINE, "current file is :" + mFileName);
root.dumpNode("");
}
public void setRoot(SvgGroupNode root) {
mRoot = root;
}
@Nullable
public SvgGroupNode getRoot() {
return mRoot;
}
public void logErrorLine(String s, Node node, SvgLogLevel level) {
if (!Strings.isNullOrEmpty(s)) {
if (node != null) {
SourcePosition position = getPosition(node);
mErrorLines.add(level.name() + "@ line " + (position.getStartLine() + 1) +
" " + s + "\n");
} else {
mErrorLines.add(s);
}
}
}
/**
* @return Error log. Empty string if there are no errors.
*/
@NonNull
public String getErrorLog() {
StringBuilder errorBuilder = new StringBuilder();
if (!mErrorLines.isEmpty()) {
errorBuilder.append("In " + mFileName + ":\n");
}
for (String log : mErrorLines) {
errorBuilder.append(log);
}
return errorBuilder.toString();
}
/**
* @return true when there is no error found when parsing the SVG file.
*/
public boolean canConvertToVectorDrawable() {
return mErrorLines.isEmpty();
}
private SourcePosition getPosition(Node node) {
return PositionXmlParser.getPosition(node);
}
}

View File

@ -0,0 +1,28 @@
/*
* 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;
/**
* Used to represent one VectorDrawble's element, can be a group or path.
*/
abstract class VdElement {
String mName;
public String getName() {
return mName;
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.util.ArrayList;
/**
* Used to represent one VectorDrawble's group element.
* TODO: Add group transformation here.
*/
class VdGroup extends VdElement{
public VdGroup() {
mName = this.toString(); // to ensure paths have unique names
}
// Children can be either a {@link VdPath} or {@link VdGroup}
private ArrayList<VdElement> mChildren = new ArrayList<VdElement>();
public void add(VdElement pathOrGroup) {
mChildren.add(pathOrGroup);
}
public ArrayList<VdElement> getChildren() {
return mChildren;
}
public int size() {
return mChildren.size();
}
}

View File

@ -0,0 +1,82 @@
/*
* 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 javax.swing.*;
import java.awt.*;
import java.net.URL;
/**
* VdIcon wrap 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> {
private VdTree mVdTree;
private final String mName;
private final URL mUrl;
public VdIcon(URL url) {
setDynamicIcon(url);
mUrl = url;
String fileName = url.getFile();
mName = fileName.substring(fileName.lastIndexOf("/") + 1);
}
public String getName() {
return mName;
}
public URL getURL() {
return mUrl;
}
public void setDynamicIcon(URL url) {
final VdParser p = new VdParser();
try {
mVdTree = p.parse(url.openStream(), null);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
// // We knew all the icons from Material library are square shape.
// int minSize = Math.min(c.getWidth(), c.getHeight());
// final BufferedImage image = AssetUtil.newArgbBufferedImage(minSize, minSize);
// mVdTree.drawIntoImage(image);
//
// // Draw in the center of the component.
// Rectangle rect = new Rectangle(0, 0, c.getWidth(), c.getHeight());
// AssetUtil.drawCenterInside((Graphics2D)g, image, rect);
}
@Override
public int getIconWidth() {
return (int) (mVdTree != null ? mVdTree.mPortWidth : 0);
}
@Override
public int getIconHeight() {
return (int) (mVdTree != null ? mVdTree.mPortHeight : 0);
}
@Override
public int compareTo(VdIcon other) {
return mName.compareTo(other.mName);
}
}

View File

@ -0,0 +1,389 @@
/*
* 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 VdPath.Node, generate a Path2D object.
* In another word, 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 Logger logger = Logger.getLogger(VdNodeRender.class
.getSimpleName());
public static void creatPath(VdPath.Node[] node, Path2D path) {
float[] current = new float[6];
char lastCmd = ' ';
for (int i = 0; i < node.length; i++) {
addCommand(path, current, node[i].type, lastCmd,node[i].params);
lastCmd = node[i].type;
}
}
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;
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 = false;
float tempReflectedX, tempReflectedY;
switch (cmd) {
case 'm':
cx += val[k + 0];
cy += val[k + 1];
path.moveTo(cx, cy);
loopX = cx;
loopY = cy;
break;
case 'M':
cx = val[k + 0];
cy = val[k + 1];
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 = Math.abs((int) Math.ceil(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;
}
}
}

View File

@ -0,0 +1,74 @@
/*
* 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;
/**
* Used to represent info to override the VectorDrawble's XML file content.
*/
public class VdOverrideInfo {
private int mWidth;
private int mHeight;
private int mOpacity;
private boolean mAutoMirrored;
public VdOverrideInfo(int width, int height, int opacity, boolean autoMirrored) {
mWidth = width;
mHeight = height;
mOpacity = opacity;
mAutoMirrored = autoMirrored;
}
public int getWidth() {
return mWidth;
}
public void setWidth(int width) {
mWidth = width;
}
public int getOpacity() {
return mOpacity;
}
public void setOpacity(int opacity) {
mOpacity = opacity;
}
public int getHeight() {
return mHeight;
}
public void setHeight(int height) {
mHeight = height;
}
boolean needsOverrideWidth() {
return getWidth() > 0;
}
boolean needsOverrideHeight() {
return getHeight() > 0;
}
boolean needsOverrideOpacity() {
return getOpacity() < 100 && getOpacity() >= 0;
}
boolean needsOverrideAutoMirrored() {
return mAutoMirrored;
}
}

View File

@ -0,0 +1,543 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.common.vectordrawable;
import com.android.SdkConstants;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
/**
* Parse a VectorDrawble's XML file, and generate an internal tree representation,
* which can be used for drawing / previewing.
*/
class VdParser {
private static Logger logger = Logger.getLogger(VdParser.class.getSimpleName());
private static final String PATH_SHIFT_X = "shift-x";
private static final String PATH_SHIFT_Y = "shift-y";
private static final String SHAPE_VECTOR = "vector";
private static final String SHAPE_PATH = "path";
private static final String SHAPE_GROUP = "group";
private static final String PATH_ID = "android:name";
private static final String PATH_DESCRIPTION = "android:pathData";
private static final String PATH_FILL = "android:fillColor";
private static final String PATH_FILL_OPACTIY = "android:fillAlpha";
private static final String PATH_STROKE = "android:strokeColor";
private static final String PATH_STROKE_OPACTIY = "android:strokeAlpha";
private static final String PATH_STROKE_WIDTH = "android:strokeWidth";
private static final String PATH_ROTATION = "android:rotation";
private static final String PATH_ROTATION_X = "android:pivotX";
private static final String PATH_ROTATION_Y = "android:pivotY";
private static final String PATH_TRIM_START = "android:trimPathStart";
private static final String PATH_TRIM_END = "android:trimPathEnd";
private static final String PATH_TRIM_OFFSET = "android:trimPathOffset";
private static final String PATH_STROKE_LINECAP = "android:strokeLinecap";
private static final String PATH_STROKE_LINEJOIN = "android:strokeLinejoin";
private static final String PATH_STROKE_MITERLIMIT = "android:strokeMiterlimit";
private static final String PATH_CLIP = "android:clipToPath";
private static final String LINECAP_BUTT = "butt";
private static final String LINECAP_ROUND = "round";
private static final String LINECAP_SQUARE = "square";
private static final String LINEJOIN_MITER = "miter";
private static final String LINEJOIN_ROUND = "round";
private static final String LINEJOIN_BEVEL = "bevel";
interface ElemParser {
void parse(VdTree path, Attributes attributes);
}
ElemParser mParseSize = new ElemParser() {
@Override
public void parse(VdTree tree, Attributes attributes) {
parseSize(tree, attributes);
}
};
ElemParser mParsePath = new ElemParser() {
@Override
public void parse(VdTree tree, Attributes attributes) {
VdPath p = parsePathAttributes(attributes);
tree.add(p);
}
};
ElemParser mParseGroup = new ElemParser() {
@Override
public void parse(VdTree tree, Attributes attributes) {
VdGroup g = parseGroupAttributes(attributes);
tree.add(g);
}
};
HashMap<String, ElemParser> tagSwitch = new HashMap<String, ElemParser>();
{
tagSwitch.put(SHAPE_VECTOR, mParseSize);
tagSwitch.put(SHAPE_PATH, mParsePath);
tagSwitch.put(SHAPE_GROUP, mParseGroup);
// TODO: add <g> tag and start to build the tree.
}
// Note that the incoming file is the VectorDrawable's XML file, not the SVG.
// TODO: Use Document to parse and make sure no big performance difference.
public VdTree parse(InputStream is, StringBuilder vdErrorLog) {
try {
final VdTree tree = new VdTree();
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
XMLReader xr = sp.getXMLReader();
xr.setContentHandler(new ContentHandler() {
String space = " ";
@Override
public void setDocumentLocator(Locator locator) {
}
@Override
public void startDocument() throws SAXException {
}
@Override
public void endDocument() throws SAXException {
}
@Override
public void startPrefixMapping(String s, String s2) throws SAXException {
}
@Override
public void endPrefixMapping(String s) throws SAXException {
}
@Override
public void startElement(String s, String s2, String s3, Attributes attributes)
throws SAXException {
String name = s3;
if (tagSwitch.containsKey(name)) {
tagSwitch.get(name).parse(tree, attributes);
}
space += " ";
}
@Override
public void endElement(String s, String s2, String s3) throws SAXException {
space = space.substring(1);
}
@Override
public void characters(char[] chars, int i, int i2) throws SAXException {
}
@Override
public void ignorableWhitespace(char[] chars, int i, int i2) throws SAXException {
}
@Override
public void processingInstruction(String s, String s2) throws SAXException {
}
@Override
public void skippedEntity(String s) throws SAXException {
}
});
xr.parse(new InputSource(is));
tree.parseFinish();
return tree;
} catch (Exception e) {
vdErrorLog.append("Exception while parsing XML file:\n" + e.getMessage());
return null;
}
}
public VdParser() {
}
private static int nextStart(String s, int end) {
char c;
while (end < s.length()) {
c = s.charAt(end);
// Note that 'e' or 'E' are not valid path commands, but could be
// used for floating point numbers' scientific notation.
// Therefore, when searching for next command, we should ignore 'e'
// and 'E'.
if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
&& c != 'e' && c != 'E') {
return end;
}
end++;
}
return end;
}
public static VdPath.Node[] parsePath(String value) {
int start = 0;
int end = 1;
ArrayList<VdPath.Node> list = new ArrayList<VdPath.Node>();
while (end < value.length()) {
end = nextStart(value, end);
String s = value.substring(start, end);
float[] val = getFloats(s);
addNode(list, s.charAt(0), val);
start = end;
end++;
}
if ((end - start) == 1 && start < value.length()) {
addNode(list, value.charAt(start), new float[0]);
}
return list.toArray(new VdPath.Node[list.size()]);
}
private static class ExtractFloatResult {
// We need to return the position of the next separator and whether the
// next float starts with a '-' or a '.'.
int mEndPosition;
boolean mEndWithNegOrDot;
}
/**
* Copies elements from {@code original} into a new array, from indexes start (inclusive) to
* end (exclusive). The original order of elements is preserved.
* If {@code end} is greater than {@code original.length}, the result is padded
* with the value {@code 0.0f}.
*
* @param original the original array
* @param start the start index, inclusive
* @param end the end index, exclusive
* @return the new array
* @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length}
* @throws IllegalArgumentException if {@code start > end}
* @throws NullPointerException if {@code original == null}
*/
private static float[] copyOfRange(float[] original, int start, int end) {
if (start > end) {
throw new IllegalArgumentException();
}
int originalLength = original.length;
if (start < 0 || start > originalLength) {
throw new ArrayIndexOutOfBoundsException();
}
int resultLength = end - start;
int copyLength = Math.min(resultLength, originalLength - start);
float[] result = new float[resultLength];
System.arraycopy(original, start, result, 0, copyLength);
return result;
}
/**
* Calculate the position of the next comma or space or negative sign
* @param s the string to search
* @param start the position to start searching
* @param result the result of the extraction, including the position of the
* the starting position of next number, whether it is ending with a '-'.
*/
private static void extract(String s, int start, ExtractFloatResult result) {
// Now looking for ' ', ',', '.' or '-' from the start.
int currentIndex = start;
boolean foundSeparator = false;
result.mEndWithNegOrDot = false;
boolean secondDot = false;
boolean isExponential = false;
for (; currentIndex < s.length(); currentIndex++) {
boolean isPrevExponential = isExponential;
isExponential = false;
char currentChar = s.charAt(currentIndex);
switch (currentChar) {
case ' ':
case ',':
foundSeparator = true;
break;
case '-':
// The negative sign following a 'e' or 'E' is not a separator.
if (currentIndex != start && !isPrevExponential) {
foundSeparator = true;
result.mEndWithNegOrDot = true;
}
break;
case '.':
if (!secondDot) {
secondDot = true;
} else {
// This is the second dot, and it is considered as a separator.
foundSeparator = true;
result.mEndWithNegOrDot = true;
}
break;
case 'e':
case 'E':
isExponential = true;
break;
}
if (foundSeparator) {
break;
}
}
// When there is nothing found, then we put the end position to the end
// of the string.
result.mEndPosition = currentIndex;
}
/**
* parse the floats in the string this is an optimized version of parseFloat(s.split(",|\\s"));
*
* @param s the string containing a command and list of floats
* @return array of floats
*/
private static float[] getFloats(String s) {
if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
return new float[0];
}
try {
float[] results = new float[s.length()];
int count = 0;
int startPosition = 1;
int endPosition = 0;
ExtractFloatResult result = new ExtractFloatResult();
int totalLength = s.length();
// The startPosition should always be the first character of the
// current number, and endPosition is the character after the current
// number.
while (startPosition < totalLength) {
extract(s, startPosition, result);
endPosition = result.mEndPosition;
if (startPosition < endPosition) {
results[count++] = Float.parseFloat(
s.substring(startPosition, endPosition));
}
if (result.mEndWithNegOrDot) {
// Keep the '-' or '.' sign with next number.
startPosition = endPosition;
} else {
startPosition = endPosition + 1;
}
}
return copyOfRange(results, 0, count);
} catch (NumberFormatException e) {
throw new RuntimeException("error in parsing \"" + s + "\"", e);
}
}
// End of copy from PathParser.java
////////////////////////////////////////////////////////////////
private static void addNode(ArrayList<VdPath.Node> list, char cmd, float[] val) {
list.add(new VdPath.Node(cmd, val));
}
public VdTree parse(URL r, StringBuilder vdErrorLog) throws Exception {
return parse(r.openStream(), vdErrorLog);
}
private void parseSize(VdTree vdTree, Attributes attributes) {
Pattern pattern = Pattern.compile("^\\s*(\\d+(\\.\\d+)*)\\s*([a-zA-Z]+)\\s*$");
HashMap<String, Integer> m = new HashMap<String, Integer>();
m.put(SdkConstants.UNIT_PX, 1);
m.put(SdkConstants.UNIT_DIP, 1);
m.put(SdkConstants.UNIT_DP, 1);
m.put(SdkConstants.UNIT_SP, 1);
m.put(SdkConstants.UNIT_PT, 1);
m.put(SdkConstants.UNIT_IN, 1);
m.put(SdkConstants.UNIT_MM, 1);
int len = attributes.getLength();
for (int i = 0; i < len; i++) {
String name = attributes.getQName(i);
String value = attributes.getValue(i);
Matcher matcher = pattern.matcher(value);
float size = 0;
if (matcher.matches()) {
float v = Float.parseFloat(matcher.group(1));
String unit = matcher.group(3).toLowerCase(Locale.getDefault());
size = v;
}
// -- Extract dimension units.
if ("android:width".equals(name)) {
vdTree.mBaseWidth = size;
} else if ("android:height".equals(name)) {
vdTree.mBaseHeight = size;
} else if ("android:viewportWidth".equals(name)) {
vdTree.mPortWidth = Float.parseFloat(value);
} else if ("android:viewportHeight".equals(name)) {
vdTree.mPortHeight = Float.parseFloat(value);
} else if ("android:alpha".equals(name)) {
vdTree.mRootAlpha = Float.parseFloat(value);
} else {
continue;
}
}
}
private VdPath parsePathAttributes(Attributes attributes) {
int len = attributes.getLength();
VdPath vgPath = new VdPath();
for (int i = 0; i < len; i++) {
String name = attributes.getQName(i);
String value = attributes.getValue(i);
logger.log(Level.FINE, "name " + name + "value " + value);
setNameValue(vgPath, name, value);
}
return vgPath;
}
private VdGroup parseGroupAttributes(Attributes attributes) {
int len = attributes.getLength();
VdGroup vgGroup = new VdGroup();
for (int i = 0; i < len; i++) {
String name = attributes.getQName(i);
String value = attributes.getValue(i);
logger.log(Level.FINE, "name " + name + "value " + value);
}
return vgGroup;
}
public void setNameValue(VdPath vgPath, String name, String value) {
if (PATH_DESCRIPTION.equals(name)) {
vgPath.mNode = parsePath(value);
} else if (PATH_ID.equals(name)) {
vgPath.mName = value;
} else if (PATH_FILL.equals(name)) {
vgPath.mFillColor = calculateColor(value);
if (!Float.isNaN(vgPath.mFillOpacity)) {
vgPath.mFillColor &= 0x00FFFFFF;
vgPath.mFillColor |= ((int) (0xFF * vgPath.mFillOpacity)) << 24;
}
} else if (PATH_STROKE.equals(name)) {
vgPath.mStrokeColor = calculateColor(value);
if (!Float.isNaN(vgPath.mStrokeOpacity)) {
vgPath.mStrokeColor &= 0x00FFFFFF;
vgPath.mStrokeColor |= ((int) (0xFF * vgPath.mStrokeOpacity)) << 24;
}
} else if (PATH_FILL_OPACTIY.equals(name)) {
vgPath.mFillOpacity = Float.parseFloat(value);
vgPath.mFillColor &= 0x00FFFFFF;
vgPath.mFillColor |= ((int) (0xFF * vgPath.mFillOpacity)) << 24;
} else if (PATH_STROKE_OPACTIY.equals(name)) {
vgPath.mStrokeOpacity = Float.parseFloat(value);
vgPath.mStrokeColor &= 0x00FFFFFF;
vgPath.mStrokeColor |= ((int) (0xFF * vgPath.mStrokeOpacity)) << 24;
} else if (PATH_STROKE_WIDTH.equals(name)) {
vgPath.mStrokeWidth = Float.parseFloat(value);
} else if (PATH_ROTATION.equals(name)) {
vgPath.mRotate = Float.parseFloat(value);
} else if (PATH_SHIFT_X.equals(name)) {
vgPath.mShiftX = Float.parseFloat(value);
} else if (PATH_SHIFT_Y.equals(name)) {
vgPath.mShiftY = Float.parseFloat(value);
} else if (PATH_ROTATION_Y.equals(name)) {
vgPath.mRotateY = Float.parseFloat(value);
} else if (PATH_ROTATION_X.equals(name)) {
vgPath.mRotateX = Float.parseFloat(value);
} else if (PATH_CLIP.equals(name)) {
vgPath.mClip = Boolean.parseBoolean(value);
} else if (PATH_TRIM_START.equals(name)) {
vgPath.mTrimPathStart = Float.parseFloat(value);
} else if (PATH_TRIM_END.equals(name)) {
vgPath.mTrimPathEnd = Float.parseFloat(value);
} else if (PATH_TRIM_OFFSET.equals(name)) {
vgPath.mTrimPathOffset = Float.parseFloat(value);
} else if (PATH_STROKE_LINECAP.equals(name)) {
if (LINECAP_BUTT.equals(value)) {
vgPath.mStrokeLineCap = 0;
} else if (LINECAP_ROUND.equals(value)) {
vgPath.mStrokeLineCap = 1;
} else if (LINECAP_SQUARE.equals(value)) {
vgPath.mStrokeLineCap = 2;
}
} else if (PATH_STROKE_LINEJOIN.equals(name)) {
if (LINEJOIN_MITER.equals(value)) {
vgPath.mStrokeLineJoin = 0;
} else if (LINEJOIN_ROUND.equals(value)) {
vgPath.mStrokeLineJoin = 1;
} else if (LINEJOIN_BEVEL.equals(value)) {
vgPath.mStrokeLineJoin = 2;
}
} else if (PATH_STROKE_MITERLIMIT.equals(name)) {
vgPath.mStrokeMiterlimit = Float.parseFloat(value);
} else {
logger.log(Level.FINE, ">>>>>> DID NOT UNDERSTAND ! \"" + name + "\" <<<<");
}
}
private int calculateColor(String value) {
int len = value.length();
int ret;
int k = 0;
switch (len) {
case 7: // #RRGGBB
ret = (int) Long.parseLong(value.substring(1), 16);
ret |= 0xFF000000;
break;
case 9: // #AARRGGBB
ret = (int) Long.parseLong(value.substring(1), 16);
break;
case 4: // #RGB
ret = (int) Long.parseLong(value.substring(1), 16);
k |= ((ret >> 8) & 0xF) * 0x110000;
k |= ((ret >> 4) & 0xF) * 0x1100;
k |= ((ret) & 0xF) * 0x11;
ret = k | 0xFF000000;
break;
case 5: // #ARGB
ret = (int) Long.parseLong(value.substring(1), 16);
k |= ((ret >> 16) & 0xF) * 0x11000000;
k |= ((ret >> 8) & 0xF) * 0x110000;
k |= ((ret >> 4) & 0xF) * 0x1100;
k |= ((ret) & 0xF) * 0x11;
break;
default:
return 0xFF000000;
}
logger.log(Level.FINE, "color = " + value + " = " + Integer.toHexString(ret));
return ret;
}
}

View File

@ -0,0 +1,266 @@
/*
* 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.Arrays;
/**
* Used to represent one VectorDrawble's path element.
*/
class VdPath extends VdElement{
Node[] mNode = null;
int mStrokeColor = 0;
int mFillColor = 0;
float mStrokeWidth = 0;
float mRotate = 0;
float mShiftX = 0;
float mShiftY = 0;
float mRotateX = 0;
float mRotateY = 0;
float trimPathStart = 0;
float trimPathEnd = 1;
float trimPathOffset = 0;
int mStrokeLineCap = -1;
int mStrokeLineJoin = -1;
float mStrokeMiterlimit = -1;
boolean mClip = false;
float mStrokeOpacity = Float.NaN;
float mFillOpacity = Float.NaN;
float mTrimPathStart = 0;
float mTrimPathEnd = 1;
float mTrimPathOffset = 0;
public void toPath(Path2D path) {
path.reset();
if (mNode != null) {
VdNodeRender.creatPath(mNode, path);
}
}
public static class Node {
char type;
float[] params;
public Node(char type, float[] params) {
this.type = type;
this.params = params;
}
public Node(Node n) {
this.type = n.type;
this.params = Arrays.copyOf(n.params, n.params.length);
}
public static String NodeListToString(Node[] nodes) {
String s = "";
for (int i = 0; i < nodes.length; i++) {
Node n = nodes[i];
s += n.type;
int len = n.params.length;
for (int j = 0; j < len; j++) {
if (j > 0) {
s += ((j & 1) == 1) ? "," : " ";
}
// To avoid trailing zeros like 17.0, use this trick
float value = n.params[j];
if (value == (long) value) {
s += String.valueOf((long) value);
} else {
s += String.valueOf(value);
}
}
}
return s;
}
public static void transform(float a,
float b,
float c,
float d,
float e,
float f,
Node[] nodes) {
float[] pre = new float[2];
for (int i = 0; i < nodes.length; i++) {
nodes[i].transform(a, b, c, d, e, f, pre);
}
}
public void transform(float a,
float b,
float c,
float d,
float e,
float f,
float[] pre) {
int incr = 0;
float[] tempParams;
float[] origParams;
switch (type) {
case 'z':
case 'Z':
return;
case 'M':
case 'L':
case 'T':
incr = 2;
pre[0] = params[params.length - 2];
pre[1] = params[params.length - 1];
for (int i = 0; i < params.length; i += incr) {
matrix(a, b, c, d, e, f, i, i + 1);
}
break;
case 'm':
case 'l':
case 't':
incr = 2;
pre[0] += params[params.length - 2];
pre[1] += params[params.length - 1];
for (int i = 0; i < params.length; i += incr) {
matrix(a, b, c, d, 0, 0, i, i + 1);
}
break;
case 'h':
type = 'l';
pre[0] += params[params.length - 1];
tempParams = new float[params.length * 2];
origParams = params;
params = tempParams;
for (int i = 0; i < params.length; i += 2) {
params[i] = origParams[i / 2];
params[i + 1] = 0;
matrix(a, b, c, d, 0, 0, i, i + 1);
}
break;
case 'H':
type = 'L';
pre[0] = params[params.length - 1];
tempParams = new float[params.length * 2];
origParams = params;
params = tempParams;
for (int i = 0; i < params.length; i += 2) {
params[i] = origParams[i / 2];
params[i + 1] = pre[1];
matrix(a, b, c, d, e, f, i, i + 1);
}
break;
case 'v':
pre[1] += params[params.length - 1];
type = 'l';
tempParams = new float[params.length * 2];
origParams = params;
params = tempParams;
for (int i = 0; i < params.length; i += 2) {
params[i] = 0;
params[i + 1] = origParams[i / 2];
matrix(a, b, c, d, 0, 0, i, i + 1);
}
break;
case 'V':
type = 'L';
pre[1] = params[params.length - 1];
tempParams = new float[params.length * 2];
origParams = params;
params = tempParams;
for (int i = 0; i < params.length; i += 2) {
params[i] = pre[0];
params[i + 1] = origParams[i / 2];
matrix(a, b, c, d, e, f, i, i + 1);
}
break;
case 'C':
case 'S':
case 'Q':
pre[0] = params[params.length - 2];
pre[1] = params[params.length - 1];
for (int i = 0; i < params.length; i += 2) {
matrix(a, b, c, d, e, f, i, i + 1);
}
break;
case 's':
case 'q':
case 'c':
pre[0] += params[params.length - 2];
pre[1] += params[params.length - 1];
for (int i = 0; i < params.length; i += 2) {
matrix(a, b, c, d, 0, 0, i, i + 1);
}
break;
case 'a':
incr = 7;
pre[0] += params[params.length - 2];
pre[1] += params[params.length - 1];
for (int i = 0; i < params.length; i += incr) {
matrix(a, b, c, d, 0, 0, i, i + 1);
double ang = Math.toRadians(params[i + 2]);
params[i + 2] = (float) Math.toDegrees(ang + Math.atan2(b, d));
matrix(a, b, c, d, 0, 0, i + 5, i + 6);
}
break;
case 'A':
incr = 7;
pre[0] = params[params.length - 2];
pre[1] = params[params.length - 1];
for (int i = 0; i < params.length; i += incr) {
matrix(a, b, c, d, e, f, i, i + 1);
double ang = Math.toRadians(params[i + 2]);
params[i + 2] = (float) Math.toDegrees(ang + Math.atan2(b, d));
matrix(a, b, c, d, e, f, i + 5, i + 6);
}
break;
}
}
void matrix(float a,
float b,
float c,
float d,
float e,
float f,
int offx,
int offy) {
float inx = (offx < 0) ? 1 : params[offx];
float iny = (offy < 0) ? 1 : params[offy];
float x = inx * a + iny * c + e;
float y = inx * b + iny * d + f;
if (offx >= 0) {
params[offx] = x;
}
if (offy >= 0) {
params[offy] = y;
}
}
}
public VdPath() {
mName = this.toString(); // to ensure paths have unique names
}
/**
* TODO: support rotation attribute for stroke width
*/
public void transform(float a, float b, float c, float d, float e, float f) {
mStrokeWidth *= Math.hypot(a + b, c + d);
Node.transform(a, b, c, d, e, f, mNode);
}
}

View File

@ -0,0 +1,289 @@
/*
* 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.sun.org.apache.xml.internal.serialize.OutputFormat;
//import com.sun.org.apache.xml.internal.serialize.XMLSerializer;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.io.IOException;
import java.io.StringWriter;
/**
* Generate a Image based on the VectorDrawable's XML content.
*
* <p>This class also contains a main method, which can be used to preview a vector drawable file.
*/
public class VdPreview {
private static final String ANDROID_ALPHA = "android:alpha";
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";
public static final int MAX_PREVIEW_IMAGE_SIZE = 4096;
public static final int MIN_PREVIEW_IMAGE_SIZE = 1;
/**
* This 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 vectordrawable.
* When mUseWidth is true, use {@code mImageMaxDimension} as the maximum
* dimension value while keeping the aspect ratio.
* Otherwise, use {@code mImageScale} to scale the image based on the XML's size information.
*/
public static class TargetSize {
private boolean mUseWidth;
private int mImageMaxDimension;
private float mImageScale;
private TargetSize(boolean useWidth, int imageWidth, float imageScale) {
mUseWidth = useWidth;
mImageMaxDimension = imageWidth;
mImageScale = imageScale;
}
public static TargetSize createSizeFromWidth(int imageWidth) {
return new TargetSize(true, imageWidth, 0.0f);
}
public static TargetSize createSizeFromScale(float imageScale) {
return new TargetSize(false, 0, imageScale);
}
}
/**
* Since we allow overriding the vector drawable's size, we also need to keep
* the original size and aspect ratio.
*/
public static class SourceSize {
public int getHeight() {
return mSourceHeight;
}
public int getWidth() {
return mSourceWidth;
}
private int mSourceWidth;
private int mSourceHeight;
}
// /**
// * @return 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);
// return format;
// }
/**
* Get the vector drawable's original size.
*/
public static SourceSize getVdOriginalSize(@NonNull Document document) {
Element root = document.getDocumentElement();
SourceSize srcSize = new SourceSize();
// Update attributes, note that attributes as width and height are required,
// while others are optional.
NamedNodeMap attr = root.getAttributes();
Node nodeAttr = attr.getNamedItem(ANDROID_WIDTH);
assert nodeAttr != null;
srcSize.mSourceWidth = parseDimension(0, nodeAttr, false);
nodeAttr = attr.getNamedItem(ANDROID_HEIGHT);
assert nodeAttr != null;
srcSize.mSourceHeight = parseDimension(0, nodeAttr, false);
return srcSize;
}
// /**
// * 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 info incoming override information for VectorDrawable.
// * @param errorLog log for the parsing errors and warnings.
// * param srcSize as an output, store the original size of the VectorDrawable
// * @return the overridden XML file in one string. If exception happens
// * or no attributes needs to be overriden, return null.
// */
// @Nullable
// public static String overrideXmlContent(@NonNull Document document,
// @NonNull VdOverrideInfo info,
// @Nullable StringBuilder errorLog) {
// boolean isXmlFileContentChanged = false;
// Element root = document.getDocumentElement();
//
// // Update attributes, note that attributes as width and height are required,
// // while others are optional.
// NamedNodeMap attr = root.getAttributes();
// if (info.needsOverrideWidth()) {
// Node nodeAttr = attr.getNamedItem(ANDROID_WIDTH);
// int overrideValue = info.getWidth();
// int originalValue = parseDimension(overrideValue, nodeAttr, true);
// if (originalValue != overrideValue) {
// isXmlFileContentChanged = true;
// }
// }
// if (info.needsOverrideHeight()) {
// Node nodeAttr = attr.getNamedItem(ANDROID_HEIGHT);
// int overrideValue = info.getHeight();
// int originalValue = parseDimension(overrideValue, nodeAttr, true);
// if (originalValue != overrideValue) {
// isXmlFileContentChanged = true;
// }
// }
// if (info.needsOverrideOpacity()) {
// Node nodeAttr = attr.getNamedItem(ANDROID_ALPHA);
// String opacityValue = String.format("%.2f", info.getOpacity() / 100.0f);
// if (nodeAttr != null) {
// nodeAttr.setTextContent(opacityValue);
// }
// else {
// root.setAttribute(ANDROID_ALPHA, opacityValue);
// }
// isXmlFileContentChanged = true;
// }
// // When auto mirror is set to true, then we always need to set it.
// // Because SVG has no such attribute at all.
// if (info.needsOverrideAutoMirrored()) {
// Node nodeAttr = attr.getNamedItem(ANDROID_AUTO_MIRRORED);
// if (nodeAttr != null) {
// nodeAttr.setTextContent("true");
// }
// else {
// root.setAttribute(ANDROID_AUTO_MIRRORED, "true");
// }
// isXmlFileContentChanged = true;
// }
//
// if (isXmlFileContentChanged) {
// // Prettify 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();
// } else {
// return null;
// }
// }
/**
* Query the dimension info and override it if needed.
*
* @param overrideValue the dimension value to override with.
* @param nodeAttr the node who contains dimension info.
* @param override if true then override the dimension.
* @return the original dimension value.
*/
private static int parseDimension(int overrideValue, Node nodeAttr, boolean override) {
assert nodeAttr != null;
String content = nodeAttr.getTextContent();
assert content.endsWith("dp");
int originalValue = Integer.parseInt(content.substring(0, content.length() - 2));
if (override) {
nodeAttr.setTextContent(overrideValue + "dp");
}
return originalValue;
}
// /**
// * This generates an image according to the VectorDrawable's content {@code xmlFileContent}.
// * At the same time, {@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 vdErrorLog 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 vdErrorLog) {
// if (xmlFileContent == null || xmlFileContent.isEmpty()) {
// return null;
// }
// VdParser p = new VdParser();
// VdTree vdTree;
//
// InputStream inputStream = new ByteArrayInputStream(
// xmlFileContent.getBytes(Charsets.UTF_8));
// vdTree = p.parse(inputStream, vdErrorLog);
// if (vdTree == null) {
// return null;
// }
//
// // If the forceImageSize is set (>0), then we honor that.
// // Otherwise, we will ask the vectorDrawable for the prefer size, then apply the imageScale.
// float vdWidth = vdTree.getBaseWidth();
// float vdHeight = vdTree.getBaseHeight();
// float imageWidth;
// float imageHeight;
// int forceImageSize = targetSize.mImageMaxDimension;
// float imageScale = targetSize.mImageScale;
//
// if (forceImageSize > 0) {
// // The goal here is to generate an image within certain size, while keeping the
// // aspect ration as much as we can.
// // If it is scaling too much to fit in, we log an error.
// float maxVdSize = Math.max(vdWidth, vdHeight);
// float ratioToForceImageSize = forceImageSize / maxVdSize;
// float scaledWidth = ratioToForceImageSize * vdWidth;
// float scaledHeight = ratioToForceImageSize * vdHeight;
// imageWidth = Math.max(MIN_PREVIEW_IMAGE_SIZE, Math.min(MAX_PREVIEW_IMAGE_SIZE, scaledWidth));
// imageHeight = Math.max(MIN_PREVIEW_IMAGE_SIZE, Math.min(MAX_PREVIEW_IMAGE_SIZE, scaledHeight));
// if (scaledWidth != imageWidth || scaledHeight != imageHeight) {
// vdErrorLog.append("Invalid image size, can't fit in a square whose size is" + forceImageSize);
// }
// } else {
// imageWidth = vdWidth * imageScale;
// imageHeight = vdHeight * imageScale;
// }
//
// // Create the image according to the vectorDrawable's aspect ratio.
//
// BufferedImage image = AssetUtil.newArgbBufferedImage((int)imageWidth, (int)imageHeight);
// vdTree.drawIntoImage(image);
// return image;
// }
public static void main(String[] args) {
System.out.println("Hello from sdk-common-lib.");
}
}

View File

@ -0,0 +1,151 @@
/*
* 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.*;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Used to represent the whole VectorDrawable XML file's tree.
*/
class VdTree {
private static Logger logger = Logger.getLogger(VdTree.class.getSimpleName());
VdGroup mCurrentGroup = new VdGroup();
ArrayList<VdElement> mChildren;
float mBaseWidth = 1;
float mBaseHeight = 1;
float mPortWidth = 1;
float mPortHeight = 1;
float mRootAlpha = 1;
/**
* Ensure there is at least one animation for every path in group (linking
* them by names) Build the "current" path based on the first group
*/
void parseFinish() {
mChildren = mCurrentGroup.getChildren();
}
void add(VdElement pathOrGroup) {
mCurrentGroup.add(pathOrGroup);
}
float getBaseWidth(){
return mBaseWidth;
}
float getBaseHeight(){
return mBaseHeight;
}
private void drawInternal(Graphics g, int w, int h) {
float scaleX = w / mPortWidth;
float scaleY = h / mPortHeight;
float minScale = Math.min(scaleX, scaleY);
if (mChildren == null) {
logger.log(Level.FINE, "no pathes");
return;
}
((Graphics2D) g).scale(scaleX, scaleY);
Rectangle bounds = null;
for (int i = 0; i < mChildren.size(); i++) {
// TODO: do things differently when it is a path or group!!
VdPath path = (VdPath) mChildren.get(i);
logger.log(Level.FINE, "mCurrentPaths[" + i + "]=" + path.getName() +
Integer.toHexString(path.mFillColor));
if (mChildren.get(i) != null) {
Rectangle r = drawPath(path, g, w, h, minScale);
if (bounds == null) {
bounds = r;
} else {
bounds.add(r);
}
}
}
logger.log(Level.FINE, "Rectangle " + bounds);
logger.log(Level.FINE, "Port " + mPortWidth + "," + mPortHeight);
double right = mPortWidth - bounds.getMaxX();
double bot = mPortHeight - bounds.getMaxY();
logger.log(Level.FINE, "x " + bounds.getMinX() + ", " + right);
logger.log(Level.FINE, "y " + bounds.getMinY() + ", " + bot);
}
private Rectangle drawPath(VdPath path, Graphics canvas, int w, int h, float scale) {
Path2D path2d = new Path2D.Double();
Graphics2D g = (Graphics2D) canvas;
path.toPath(path2d);
// TODO: Use AffineTransform to apply group's transformation info.
double theta = Math.toRadians(path.mRotate);
g.rotate(theta, path.mRotateX, path.mRotateY);
if (path.mClip) {
logger.log(Level.FINE, "CLIP");
g.setColor(Color.RED);
g.fill(path2d);
}
if (path.mFillColor != 0) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(new Color(path.mFillColor, true));
g.fill(path2d);
}
if (path.mStrokeColor != 0) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setStroke(new BasicStroke(path.mStrokeWidth));
g.setColor(new Color(path.mStrokeColor, true));
g.draw(path2d);
}
g.rotate(-theta, path.mRotateX, path.mRotateY);
return path2d.getBounds();
}
// /**
// * Draw the VdTree into an image.
// * If the root alpha is less than 1.0, then draw into a temporary image,
// * then draw into the result image applying alpha blending.
// */
// public void drawIntoImage(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);
//
// float rootAlpha = mRootAlpha;
// if (rootAlpha < 1.0) {
// BufferedImage alphaImage = AssetUtil.newArgbBufferedImage(width, height);
// Graphics2D gTemp = (Graphics2D)alphaImage.getGraphics();
// drawInternal(gTemp, width, height);
// gFinal.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, rootAlpha));
// gFinal.drawImage(alphaImage, 0, 0, null);
// gTemp.dispose();
// } else {
// drawInternal(gFinal, width, height);
// }
// gFinal.dispose();
// }
}

View File

@ -0,0 +1,136 @@
/*
* Copyright (C) 2006 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.utils;
import java.lang.reflect.Array;
// XXX these should be changed to reflect the actual memory allocator we use.
// it looks like right now objects want to be powers of 2 minus 8
// and the array size eats another 4 bytes
/**
* ArrayUtils contains some methods that you can call to find out
* the most efficient increments by which to grow arrays.
*/
/* package */ class ArrayUtils
{
private static final Object[] EMPTY = new Object[0];
private static final int CACHE_SIZE = 73;
private static Object[] sCache = new Object[CACHE_SIZE];
private ArrayUtils() { /* cannot be instantiated */ }
public static int idealByteArraySize(int need) {
for (int i = 4; i < 32; i++)
if (need <= (1 << i) - 12)
return (1 << i) - 12;
return need;
}
public static int idealBooleanArraySize(int need) {
return idealByteArraySize(need);
}
public static int idealShortArraySize(int need) {
return idealByteArraySize(need * 2) / 2;
}
public static int idealCharArraySize(int need) {
return idealByteArraySize(need * 2) / 2;
}
public static int idealIntArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
public static int idealFloatArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
public static int idealObjectArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
public static int idealLongArraySize(int need) {
return idealByteArraySize(need * 8) / 8;
}
/**
* Checks if the beginnings of two byte arrays are equal.
*
* @param array1 the first byte array
* @param array2 the second byte array
* @param length the number of bytes to check
* @return true if they're equal, false otherwise
*/
public static boolean equals(byte[] array1, byte[] array2, int length) {
if (array1 == array2) {
return true;
}
if (array1 == null || array2 == null || array1.length < length || array2.length < length) {
return false;
}
for (int i = 0; i < length; i++) {
if (array1[i] != array2[i]) {
return false;
}
}
return true;
}
/**
* Returns an empty array of the specified type. The intent is that
* it will return the same empty array every time to avoid reallocation,
* although this is not guaranteed.
*/
@SuppressWarnings("unchecked")
public static <T> T[] emptyArray(Class<T> kind) {
if (kind == Object.class) {
return (T[]) EMPTY;
}
int bucket = ((System.identityHashCode(kind) / 8) & 0x7FFFFFFF) % CACHE_SIZE;
Object cache = sCache[bucket];
if (cache == null || cache.getClass().getComponentType() != kind) {
cache = Array.newInstance(kind, 0);
sCache[bucket] = cache;
// Log.e("cache", "new empty " + kind.getName() + " at " + bucket);
}
return (T[]) cache;
}
/**
* Checks that value is present as at least one of the elements of the array.
* @param array the array to check in
* @param value the value to check for
* @return true if the value is present in the array
*/
public static <T> boolean contains(T[] array, T value) {
for (T element : array) {
if (element == null) {
if (value == null) return true;
} else {
if (value != null && element.equals(value)) return true;
}
}
return false;
}
}

View File

@ -0,0 +1,193 @@
/*
* Copyright (C) 2012 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.utils;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.io.Closeables;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class GrabProcessOutput {
public enum Wait {
/**
* Doesn't wait for the exec to complete.
* This still monitors the output but does not wait for the process to finish.
* In this mode the process return code is unknown and always 0.
*/
ASYNC,
/**
* This waits for the process to finish.
* In this mode, {@link GrabProcessOutput#grabProcessOutput} returns the
* error code from the process.
* In some rare cases and depending on the OS, the process might not have
* finished dumping data into stdout/stderr.
* <p/>
* Use this when you don't particularly care for the output but instead
* care for the return code of the executed process.
*/
WAIT_FOR_PROCESS,
/**
* This waits for the process to finish <em>and</em> for the stdout/stderr
* threads to complete.
* In this mode, {@link GrabProcessOutput#grabProcessOutput} returns the
* error code from the process.
* <p/>
* Use this one when capturing all the output from the process is important.
*/
WAIT_FOR_READERS,
}
public interface IProcessOutput {
/**
* Processes an stdout message line.
* @param line The stdout message line. Null when the reader reached the end of stdout.
*/
void out(@Nullable String line);
/**
* Processes an stderr message line.
* @param line The stderr message line. Null when the reader reached the end of stderr.
*/
void err(@Nullable String line);
}
/**
* Get the stderr/stdout outputs of a process and return when the process is done.
* Both <b>must</b> be read or the process will block on windows.
*
* @param process The process to get the output from.
* @param output Optional object to capture stdout/stderr.
* Note that on Windows capturing the output is not optional. If output is null
* the stdout/stderr will be captured and discarded.
* @param waitMode Whether to wait for the process and/or the readers to finish.
* @return the process return code.
* @throws InterruptedException if {@link Process#waitFor()} was interrupted.
*/
public static int grabProcessOutput(
@NonNull final Process process,
Wait waitMode,
@Nullable final IProcessOutput output) throws InterruptedException {
// read the lines as they come. if null is returned, it's
// because the process finished
Thread threadErr = new Thread("stderr") {
@Override
public void run() {
// create a buffer to read the stderr output
InputStream is = process.getErrorStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader errReader = new BufferedReader(isr);
try {
while (true) {
String line = errReader.readLine();
if (output != null) {
output.err(line);
}
if (line == null) {
break;
}
}
} catch (IOException e) {
// do nothing.
} finally {
try {
Closeables.close(is, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
try {
Closeables.close(isr, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
try {
Closeables.close(errReader, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
}
}
};
Thread threadOut = new Thread("stdout") {
@Override
public void run() {
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader outReader = new BufferedReader(isr);
try {
while (true) {
String line = outReader.readLine();
if (output != null) {
output.out(line);
}
if (line == null) {
break;
}
}
} catch (IOException e) {
// do nothing.
} finally {
try {
Closeables.close(is, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
try {
Closeables.close(isr, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
try {
Closeables.close(outReader, true /* swallowIOException */);
} catch (IOException e) {
// cannot happen
}
}
}
};
threadErr.start();
threadOut.start();
if (waitMode == Wait.ASYNC) {
return 0;
}
// it looks like on windows process#waitFor() can return
// before the thread have filled the arrays, so we wait for both threads and the
// process itself.
if (waitMode == Wait.WAIT_FOR_READERS) {
try {
threadErr.join();
} catch (InterruptedException e) {
}
try {
threadOut.join();
} catch (InterruptedException e) {
}
}
// get the return code from the process
return process.waitFor();
}
}

View File

@ -0,0 +1,338 @@
/*
* Copyright (C) 2013 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.utils;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.net.URL;
public class HtmlBuilder {
@NonNull private final StringBuilder mStringBuilder;
private String mTableDataExtra;
public HtmlBuilder(@NonNull StringBuilder stringBuilder) {
mStringBuilder = stringBuilder;
}
public HtmlBuilder() {
mStringBuilder = new StringBuilder(100);
}
public HtmlBuilder openHtmlBody() {
addHtml("<html><body>");
return this;
}
public HtmlBuilder closeHtmlBody() {
addHtml("</body></html>");
return this;
}
public HtmlBuilder addHtml(@NonNull String html) {
mStringBuilder.append(html);
return this;
}
public HtmlBuilder addNbsp() {
mStringBuilder.append("&nbsp;");
return this;
}
public HtmlBuilder addNbsps(int count) {
for (int i = 0; i < count; i++) {
addNbsp();
}
return this;
}
public HtmlBuilder newline() {
mStringBuilder.append("<BR/>");
return this;
}
public HtmlBuilder newlineIfNecessary() {
if (!SdkUtils.endsWith(mStringBuilder, "<BR/>")) {
mStringBuilder.append("<BR/>");
}
return this;
}
public HtmlBuilder addLink(@Nullable String textBefore,
@NonNull String linkText,
@Nullable String textAfter,
@NonNull String url) {
if (textBefore != null) {
add(textBefore);
}
addLink(linkText, url);
if (textAfter != null) {
add(textAfter);
}
return this;
}
public HtmlBuilder addLink(@NonNull String text, @NonNull String url) {
int begin = 0;
int length = text.length();
for (; begin < length; begin++) {
char c = text.charAt(begin);
if (Character.isWhitespace(c)) {
mStringBuilder.append(c);
} else {
break;
}
}
mStringBuilder.append("<A HREF=\"");
mStringBuilder.append(url);
mStringBuilder.append("\">");
XmlUtils.appendXmlTextValue(mStringBuilder, text.trim());
mStringBuilder.append("</A>");
int end = length - 1;
for (; end > begin; end--) {
char c = text.charAt(begin);
if (Character.isWhitespace(c)) {
mStringBuilder.append(c);
}
}
return this;
}
public HtmlBuilder add(@NonNull String text) {
XmlUtils.appendXmlTextValue(mStringBuilder, text);
return this;
}
@NonNull
public String getHtml() {
return mStringBuilder.toString();
}
public HtmlBuilder beginBold() {
mStringBuilder.append("<B>");
return this;
}
public HtmlBuilder endBold() {
mStringBuilder.append("</B>");
return this;
}
public HtmlBuilder addBold(String text) {
beginBold();
add(text);
endBold();
return this;
}
public HtmlBuilder beginItalic() {
mStringBuilder.append("<I>");
return this;
}
public HtmlBuilder endItalic() {
mStringBuilder.append("</I>");
return this;
}
public HtmlBuilder addItalic(String text) {
beginItalic();
add(text);
endItalic();
return this;
}
public HtmlBuilder beginDiv() {
return beginDiv(null);
}
public HtmlBuilder beginDiv(@Nullable String cssStyle) {
mStringBuilder.append("<div");
if (cssStyle != null) {
mStringBuilder.append(" style=\"");
mStringBuilder.append(cssStyle);
mStringBuilder.append("\"");
}
mStringBuilder.append('>');
return this;
}
public HtmlBuilder endDiv() {
mStringBuilder.append("</div>");
return this;
}
public HtmlBuilder addHeading(@NonNull String text, @NonNull String fontColor) {
mStringBuilder.append("<font style=\"font-weight:bold; color:").append(fontColor)
.append(";\">");
add(text);
mStringBuilder.append("</font>");
return this;
}
/**
* The JEditorPane HTML renderer creates really ugly bulleted lists; the
* size is hardcoded to use a giant heavy bullet. So, use a definition
* list instead.
*/
private static final boolean USE_DD_LISTS = true;
public HtmlBuilder beginList() {
if (USE_DD_LISTS) {
mStringBuilder.append("<DL>");
} else {
mStringBuilder.append("<UL>");
}
return this;
}
public HtmlBuilder endList() {
if (USE_DD_LISTS) {
mStringBuilder.append("</DL>");
} else {
mStringBuilder.append("</UL>");
}
return this;
}
public HtmlBuilder listItem() {
if (USE_DD_LISTS) {
mStringBuilder.append("<DD>");
mStringBuilder.append("-&NBSP;");
} else {
mStringBuilder.append("<LI>");
}
return this;
}
public HtmlBuilder addImage(URL url, @Nullable String altText) {
String link = "";
try {
link = url.toURI().toURL().toExternalForm();
}
catch (Throwable t) {
// pass
}
mStringBuilder.append("<img src='");
mStringBuilder.append(link);
mStringBuilder.append("'");
if (altText != null) {
mStringBuilder.append(" alt=\"");
mStringBuilder.append(altText);
mStringBuilder.append("\"");
}
mStringBuilder.append(" />");
return this;
}
public HtmlBuilder addIcon(@Nullable String src) {
if (src != null) {
mStringBuilder.append("<img src='");
mStringBuilder.append(src);
mStringBuilder.append("' width=16 height=16 border=0 />");
}
return this;
}
public HtmlBuilder beginTable(@Nullable String tdExtra) {
mStringBuilder.append("<table>");
mTableDataExtra = tdExtra;
return this;
}
public HtmlBuilder beginTable() {
return beginTable(null);
}
public HtmlBuilder endTable() {
mStringBuilder.append("</table>");
return this;
}
public HtmlBuilder beginTableRow() {
mStringBuilder.append("<tr>");
return this;
}
public HtmlBuilder endTableRow() {
mStringBuilder.append("</tr>");
return this;
}
public HtmlBuilder addTableRow(boolean isHeader, String... columns) {
if (columns == null || columns.length == 0) {
return this;
}
String tag = "t" + (isHeader ? 'h' : 'd');
beginTableRow();
for (String c : columns) {
mStringBuilder.append('<');
mStringBuilder.append(tag);
if (mTableDataExtra != null) {
mStringBuilder.append(' ');
mStringBuilder.append(mTableDataExtra);
}
mStringBuilder.append('>');
mStringBuilder.append(c);
mStringBuilder.append("</");
mStringBuilder.append(tag);
mStringBuilder.append('>');
}
endTableRow();
return this;
}
public HtmlBuilder addTableRow(String... columns) {
return addTableRow(false, columns);
}
@NonNull
public StringBuilder getStringBuilder() {
return mStringBuilder;
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2012 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.utils;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.util.Formatter;
/**
* Interface used to display warnings/errors while parsing the SDK content.
* <p/>
* There are a few default implementations available:
* <ul>
* <li> {@link NullLogger} is an implementation that does <em>nothing</em> with the log.
* Useful for limited cases where you need to call a class that requires a non-null logging
* yet the calling code does not have any mean of reporting logs itself. It can be
* acceptable for use as a temporary implementation but most of the time that means the caller
* code needs to be reworked to take a logger object from its own caller.
* </li>
* <li> {@link StdLogger} is an implementation that dumps the log to {@link System#out} or
* {@link System#err}. This is useful for unit tests or code that does not have any GUI.
* GUI based apps based should not use it and should provide a better way to report to the user.
* </li>
* </ul>
*/
public interface ILogger {
/**
* Prints an error message.
*
* @param t is an optional {@link Throwable} or {@link Exception}. If non-null, its
* message will be printed out.
* @param msgFormat is an optional error format. If non-null, it will be printed
* using a {@link Formatter} with the provided arguments.
* @param args provides the arguments for errorFormat.
*/
void error(@Nullable Throwable t, @Nullable String msgFormat, Object... args);
/**
* Prints a warning message.
*
* @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
* @param args provides the arguments for warningFormat.
*/
void warning(@NonNull String msgFormat, Object... args);
/**
* Prints an information message.
*
* @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
* @param args provides the arguments for msgFormat.
*/
void info(@NonNull String msgFormat, Object... args);
/**
* Prints a verbose message.
*
* @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
* @param args provides the arguments for msgFormat.
*/
void verbose(@NonNull String msgFormat, Object... args);
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2013 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.utils;
import com.android.annotations.NonNull;
import java.io.IOException;
/**
* Interface to read a line from the {@link System#in} input stream.
* <p/>
* The interface also implements {@link ILogger} since code that needs to ask for
* a command-line input will most likely also want to use {@link ILogger#info(String, Object...)}
* to print information such as an input prompt.
*/
public interface IReaderLogger extends ILogger {
/**
* Reads a line from {@link System#in}.
* <p/>
* This call is blocking and should only be called from command-line enabled applications.
*
* @param inputBuffer A non-null buffer where to place the input.
* @return The number of bytes read into the buffer.
* @throws IOException as returned by {code System.in.read()}.
*/
int readLine(@NonNull byte[] inputBuffer) throws IOException;
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2012 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.utils;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
/**
* Dummy implementation of an {@link ILogger}.
* <p/>
* Use {@link #getLogger()} to get a default instance of this {@link NullLogger}.
*/
public class NullLogger implements ILogger {
private static final ILogger sThis = new NullLogger();
public static ILogger getLogger() {
return sThis;
}
@Override
public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) {
// ignore
}
@Override
public void warning(@NonNull String warningFormat, Object... args) {
// ignore
}
@Override
public void info(@NonNull String msgFormat, Object... args) {
// ignore
}
@Override
public void verbose(@NonNull String msgFormat, Object... args) {
// ignore
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright (C) 2010 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.utils;
/**
* A Pair class is simply a 2-tuple for use in this package. We might want to
* think about adding something like this to a more central utility place, or
* replace it by a common tuple class if one exists, or even rewrite the layout
* classes using this Pair by a more dedicated data structure (so we don't have
* to pass around generic signatures as is currently done, though at least the
* construction is helped a bit by the {@link #of} factory method.
*
* @param <S> The type of the first value
* @param <T> The type of the second value
*/
public class Pair<S,T> {
private final S mFirst;
private final T mSecond;
// Use {@link Pair#of} factory instead since it infers generic types
private Pair(S first, T second) {
this.mFirst = first;
this.mSecond = second;
}
/**
* Return the first item in the pair
*
* @return the first item in the pair
*/
public S getFirst() {
return mFirst;
}
/**
* Return the second item in the pair
*
* @return the second item in the pair
*/
public T getSecond() {
return mSecond;
}
/**
* Constructs a new pair of the given two objects, inferring generic types.
*
* @param first the first item to store in the pair
* @param second the second item to store in the pair
* @param <S> the type of the first item
* @param <T> the type of the second item
* @return a new pair wrapping the two items
*/
public static <S,T> Pair<S,T> of(S first, T second) {
return new Pair<S,T>(first,second);
}
@Override
public String toString() {
return "Pair [first=" + mFirst + ", second=" + mSecond + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
return result;
}
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Pair other = (Pair) obj;
if (mFirst == null) {
if (other.mFirst != null)
return false;
} else if (!mFirst.equals(other.mFirst))
return false;
if (mSecond == null) {
if (other.mSecond != null)
return false;
} else if (!mSecond.equals(other.mSecond))
return false;
return true;
}
}

View File

@ -0,0 +1,790 @@
/*
* Copyright (C) 2011 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.utils;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourcePosition;
import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.DefaultHandler2;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
/**
* A simple DOM XML parser which can retrieve exact beginning and end offsets
* (and line and column numbers) for element nodes as well as attribute nodes.
*/
public class PositionXmlParser {
private static final String UTF_8 = "UTF-8"; //$NON-NLS-1$
private static final String UTF_16 = "UTF_16"; //$NON-NLS-1$
private static final String UTF_16LE = "UTF_16LE"; //$NON-NLS-1$
private static final String CONTENT_KEY = "contents"; //$NON-NLS-1$
private static final String POS_KEY = "offsets"; //$NON-NLS-1$
private static final String NAMESPACE_PREFIX_FEATURE =
"http://xml.org/sax/features/namespace-prefixes"; //$NON-NLS-1$
private static final String NAMESPACE_FEATURE =
"http://xml.org/sax/features/namespaces"; //$NON-NLS-1$
private static final String PROVIDE_XMLNS_URIS =
"http://xml.org/sax/features/xmlns-uris"; //$NON-NLS-1$
/** See http://www.w3.org/TR/REC-xml/#NT-EncodingDecl */
private static final Pattern ENCODING_PATTERN =
Pattern.compile("encoding=['\"](\\S*)['\"]"); //$NON-NLS-1$
private static final String LOAD_EXTERNAL_DTD =
"http://apache.org/xml/features/nonvalidating/load-external-dtd";; //$NON-NLS-1$
/**
* Parses the XML content from the given input stream.
*
* @param input the input stream containing the XML to be parsed
* @param checkDtd whether or not download the DTD and validate it
* @return the corresponding document
* @throws ParserConfigurationException if a SAX parser is not available
* @throws SAXException if the document contains a parsing error
* @throws IOException if something is seriously wrong. This should not
* happen since the input source is known to be constructed from
* a string.
*/
@NonNull
public static Document parse(@NonNull InputStream input, boolean checkDtd)
throws ParserConfigurationException, SAXException, IOException {
// Read in all the data
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
while (true) {
int r = input.read(buf);
if (r == -1) {
break;
}
out.write(buf, 0, r);
}
input.close();
return parse(out.toByteArray(), checkDtd);
}
/**
* @see PositionXmlParser#parse(InputStream, boolean)
*/
@NonNull
public static Document parse(@NonNull InputStream input)
throws IOException, SAXException, ParserConfigurationException {
return parse(input, true);
}
/**
* @see PositionXmlParser#parse(byte[], boolean)
*/
@NonNull
public static Document parse(@NonNull byte[] data)
throws ParserConfigurationException, SAXException, IOException {
return parse(data, true);
}
/**
* Parses the XML content from the given byte array
*
* @param data the raw XML data (with unknown encoding)
* @param checkDtd whether or not download the DTD and validate it
* @return the corresponding document
* @throws ParserConfigurationException if a SAX parser is not available
* @throws SAXException if the document contains a parsing error
* @throws IOException if something is seriously wrong. This should not
* happen since the input source is known to be constructed from
* a string.
*/
@NonNull
public static Document parse(@NonNull byte[] data, boolean checkDtd)
throws ParserConfigurationException, SAXException, IOException {
String xml = getXmlString(data);
xml = XmlUtils.stripBom(xml);
return parse(xml, new InputSource(new StringReader(xml)), true, checkDtd);
}
/**
* Parses the given XML content.
*
* @param xml the XML string to be parsed. This must be in the correct
* encoding already.
* @return the corresponding document
* @throws ParserConfigurationException if a SAX parser is not available
* @throws SAXException if the document contains a parsing error
* @throws IOException if something is seriously wrong. This should not
* happen since the input source is known to be constructed from
* a string.
*/
@NonNull
public static Document parse(@NonNull String xml)
throws ParserConfigurationException, SAXException, IOException {
xml = XmlUtils.stripBom(xml);
return parse(xml, new InputSource(new StringReader(xml)), true, true);
}
@NonNull
private static Document parse(@NonNull String xml, @NonNull InputSource input, boolean checkBom,
boolean checkDtd)
throws ParserConfigurationException, SAXException, IOException {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
if (checkDtd) {
factory.setFeature(NAMESPACE_FEATURE, true);
factory.setFeature(NAMESPACE_PREFIX_FEATURE, true);
factory.setFeature(PROVIDE_XMLNS_URIS, true);
} else {
factory.setFeature(LOAD_EXTERNAL_DTD, false);
}
SAXParser parser = factory.newSAXParser();
DomBuilder handler = new DomBuilder(xml);
XMLReader xmlReader = parser.getXMLReader();
xmlReader.setProperty(
"http://xml.org/sax/properties/lexical-handler",
handler
);
parser.parse(input, handler);
return handler.getDocument();
} catch (SAXException e) {
if (checkBom && e.getMessage().contains("Content is not allowed in prolog")) {
// Byte order mark in the string? Skip it. There are many markers
// (see http://en.wikipedia.org/wiki/Byte_order_mark) so here we'll
// just skip those up to the XML prolog beginning character, <
xml = xml.replaceFirst("^([\\W]+)<","<"); //$NON-NLS-1$ //$NON-NLS-2$
return parse(xml, new InputSource(new StringReader(xml)), false, checkDtd);
}
throw e;
}
}
/**
* Returns the String corresponding to the given byte array of XML data
* (with unknown encoding). This method attempts to guess the encoding based
* on the XML prologue.
* @param data the XML data to be decoded into a string
* @return a string corresponding to the XML data
*/
@NonNull
public static String getXmlString(@NonNull byte[] data) {
return getXmlString(data, UTF_8);
}
/**
* Returns the String corresponding to the given byte array of XML data
* (with unknown encoding). This method attempts to guess the encoding based
* on the XML prologue.
* @param data the XML data to be decoded into a string
* @param defaultCharset the default charset to use if not specified by an encoding prologue
* attribute or a byte order mark
* @return a string corresponding to the XML data
*/
@NonNull
public static String getXmlString(@NonNull byte[] data, @NonNull String defaultCharset) {
int offset = 0;
String charset = null;
// Look for the byte order mark, to see if we need to remove bytes from
// the input stream (and to determine whether files are big endian or little endian) etc
// for files which do not specify the encoding.
// See http://unicode.org/faq/utf_bom.html#BOM for more.
if (data.length > 4) {
if (data[0] == (byte)0xef && data[1] == (byte)0xbb && data[2] == (byte)0xbf) {
// UTF-8
defaultCharset = charset = UTF_8;
offset += 3;
} else if (data[0] == (byte)0xfe && data[1] == (byte)0xff) {
// UTF-16, big-endian
defaultCharset = charset = UTF_16;
offset += 2;
} else if (data[0] == (byte)0x0 && data[1] == (byte)0x0
&& data[2] == (byte)0xfe && data[3] == (byte)0xff) {
// UTF-32, big-endian
defaultCharset = charset = "UTF_32"; //$NON-NLS-1$
offset += 4;
} else if (data[0] == (byte)0xff && data[1] == (byte)0xfe
&& data[2] == (byte)0x0 && data[3] == (byte)0x0) {
// UTF-32, little-endian. We must check for this *before* looking for
// UTF_16LE since UTF_32LE has the same prefix!
defaultCharset = charset = "UTF_32LE"; //$NON-NLS-1$
offset += 4;
} else if (data[0] == (byte)0xff && data[1] == (byte)0xfe) {
// UTF-16, little-endian
defaultCharset = charset = UTF_16LE;
offset += 2;
}
}
int length = data.length - offset;
// Guess encoding by searching for an encoding= entry in the first line.
// The prologue, and the encoding names, will always be in ASCII - which means
// we don't need to worry about strange character encodings for the prologue characters.
// However, one wrinkle is that the whole file may be encoded in something like UTF-16
// where there are two bytes per character, so we can't just look for
// ['e','n','c','o','d','i','n','g'] etc in the byte array since there could be
// multiple bytes for each character. However, since again the prologue is in ASCII,
// we can just drop the zeroes.
boolean seenOddZero = false;
boolean seenEvenZero = false;
int prologueStart = -1;
for (int lineEnd = offset; lineEnd < data.length; lineEnd++) {
if (data[lineEnd] == 0) {
if ((lineEnd - offset) % 2 == 0) {
seenEvenZero = true;
} else {
seenOddZero = true;
}
} else if (data[lineEnd] == '\n' || data[lineEnd] == '\r') {
break;
} else if (data[lineEnd] == '<') {
prologueStart = lineEnd;
} else if (data[lineEnd] == '>') {
// End of prologue. Quick check to see if this is a utf-8 file since that's
// common
for (int i = lineEnd - 4; i >= 0; i--) {
if ((data[i] == 'u' || data[i] == 'U')
&& (data[i + 1] == 't' || data[i + 1] == 'T')
&& (data[i + 2] == 'f' || data[i + 2] == 'F')
&& (data[i + 3] == '-' || data[i + 3] == '_')
&& (data[i + 4] == '8')
) {
charset = UTF_8;
break;
}
}
if (charset == null) {
StringBuilder sb = new StringBuilder();
for (int i = prologueStart; i <= lineEnd; i++) {
if (data[i] != 0) {
sb.append((char) data[i]);
}
}
String prologue = sb.toString();
int encodingIndex = prologue.indexOf("encoding"); //$NON-NLS-1$
if (encodingIndex != -1) {
Matcher matcher = ENCODING_PATTERN.matcher(prologue);
if (matcher.find(encodingIndex)) {
charset = matcher.group(1);
}
}
}
break;
}
}
// No prologue on the first line, and no byte order mark: Assume UTF-8/16
if (charset == null) {
charset = seenOddZero ? UTF_16LE : seenEvenZero ? UTF_16 : defaultCharset;
}
String xml = null;
try {
xml = new String(data, offset, length, charset);
} catch (UnsupportedEncodingException e) {
try {
if (charset != defaultCharset) {
xml = new String(data, offset, length, defaultCharset);
}
} catch (UnsupportedEncodingException u) {
// Just use the default encoding below
}
}
if (xml == null) {
xml = new String(data, offset, length);
}
return xml;
}
/**
* Returns the position for the given node. This is the start position. The
* end position can be obtained via {@link Position#getEnd()}.
*
* @param node the node to look up position for
* @return the position, or null if the node type is not supported for
* position info
*/
@NonNull
public static SourcePosition getPosition(@NonNull Node node) {
return getPosition(node, -1, -1);
}
/**
* Returns the position for the given node. This is the start position. The
* end position can be obtained via {@link Position#getEnd()}. A specific
* range within the node can be specified with the {@code start} and
* {@code end} parameters.
*
* @param node the node to look up position for
* @param start the relative offset within the node range to use as the
* starting position, inclusive, or -1 to not limit the range
* @param end the relative offset within the node range to use as the ending
* position, or -1 to not limit the range
* @return the position, or null if the node type is not supported for
* position info
*/
@NonNull
public static SourcePosition getPosition(@NonNull Node node, int start, int end) {
Position p = getPositionHelper(node, start, end);
return p == null ? SourcePosition.UNKNOWN : p.toSourcePosition();
}
@Nullable
private static Position getPositionHelper(@NonNull Node node, int start, int end) {
// Look up the position information stored while parsing for the given node.
// Note however that we only store position information for elements (because
// there is no SAX callback for individual attributes).
// Therefore, this method special cases this:
// -- First, it looks at the owner element and uses its position
// information as a first approximation.
// -- Second, it uses that, as well as the original XML text, to search
// within the node range for an exact text match on the attribute name
// and if found uses that as the exact node offsets instead.
if (node instanceof Attr) {
Attr attr = (Attr) node;
Position pos = (Position) attr.getOwnerElement().getUserData(POS_KEY);
if (pos != null) {
int startOffset = pos.getOffset();
int endOffset = pos.getEnd().getOffset();
if (start != -1) {
startOffset += start;
if (end != -1) {
endOffset = startOffset + (end - start);
}
}
// Find attribute in the text
String contents = (String) node.getOwnerDocument().getUserData(CONTENT_KEY);
if (contents == null) {
return null;
}
// Locate the name=value attribute in the source text
// Fast string check first for the common occurrence
String name = attr.getName();
Pattern pattern = Pattern.compile(attr.getPrefix() != null
? String.format("(%1$s\\s*=\\s*[\"'].*?[\"'])", name) //$NON-NLS-1$
: String.format("[^:](%1$s\\s*=\\s*[\"'].*?[\"'])", name));//$NON-NLS-1$
Matcher matcher = pattern.matcher(contents);
if (matcher.find(startOffset) && matcher.start(1) <= endOffset) {
int index = matcher.start(1);
// Adjust the line and column to this new offset
int line = pos.getLine();
int column = pos.getColumn();
for (int offset = pos.getOffset(); offset < index; offset++) {
char t = contents.charAt(offset);
if (t == '\n') {
line++;
column = 0;
} else {
column++;
}
}
Position attributePosition = new Position(line, column, index);
// Also set end range for retrieval in getLocation
attributePosition.setEnd(
new Position(line, column + matcher.end(1) - index, matcher.end(1)));
return attributePosition;
} else {
// No regexp match either: just fall back to element position
return pos;
}
}
} else if (node instanceof Text) {
// Position of parent element, if any
Position pos = null;
if (node.getPreviousSibling() != null) {
pos = (Position) node.getPreviousSibling().getUserData(POS_KEY);
}
if (pos == null) {
pos = (Position) node.getParentNode().getUserData(POS_KEY);
}
if (pos != null) {
// Attempt to point forward to the actual text node
int startOffset = pos.getOffset();
int endOffset = pos.getEnd().getOffset();
int line = pos.getLine();
int column = pos.getColumn();
// Find attribute in the text
String contents = (String) node.getOwnerDocument().getUserData(CONTENT_KEY);
if (contents == null || contents.length() < endOffset) {
return null;
}
boolean inAttribute = false;
for (int offset = startOffset; offset <= endOffset; offset++) {
char c = contents.charAt(offset);
if (c == '>' && !inAttribute) {
// Found the end of the element open tag: this is where the
// text begins.
// Skip >
offset++;
column++;
String text = node.getNodeValue();
int textIndex = 0;
int textLength = text.length();
int newLine = line;
int newColumn = column;
if (start != -1) {
textLength = Math.min(textLength, start);
for (; textIndex < textLength; textIndex++) {
char t = text.charAt(textIndex);
if (t == '\n') {
newLine++;
newColumn = 0;
} else {
newColumn++;
}
}
} else {
// Skip text whitespace prefix, if the text node contains
// non-whitespace characters
for (; textIndex < textLength; textIndex++) {
char t = text.charAt(textIndex);
if (t == '\n') {
newLine++;
newColumn = 0;
} else if (!Character.isWhitespace(t)) {
break;
} else {
newColumn++;
}
}
}
if (textIndex == text.length()) {
textIndex = 0; // Whitespace node
} else {
line = newLine;
column = newColumn;
}
Position attributePosition = new Position(line, column, offset + textIndex);
// Also set end range for retrieval in getLocation
if (end != -1) {
attributePosition.setEnd(new Position(line, column, offset + end));
} else {
attributePosition.setEnd(
new Position(line, column, offset + textLength));
}
return attributePosition;
} else if (c == '"') {
inAttribute = !inAttribute;
} else if (c == '\n') {
line++;
column = -1; // pre-subtract column added below
}
column++;
}
return pos;
}
}
return (Position) node.getUserData(POS_KEY);
}
/**
* SAX parser handler which incrementally builds up a DOM document as we go
* along, and updates position information along the way. Position
* information is attached to the DOM nodes by setting user data with the
* {@link #POS_KEY} key.
*/
private static final class DomBuilder extends DefaultHandler2 {
private final String mXml;
private final Document mDocument;
private Locator mLocator;
private int mCurrentLine = 0;
private int mCurrentOffset;
private int mCurrentColumn;
private final List<Element> mStack = new ArrayList<Element>();
private final StringBuilder mPendingText = new StringBuilder();
private DomBuilder(String xml) throws ParserConfigurationException {
mXml = xml;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
DocumentBuilder docBuilder = factory.newDocumentBuilder();
mDocument = docBuilder.newDocument();
mDocument.setUserData(CONTENT_KEY, xml, null);
}
/** Returns the document parsed by the handler */
Document getDocument() {
return mDocument;
}
@Override
public void setDocumentLocator(Locator locator) {
this.mLocator = locator;
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
try {
flushText();
Element element = mDocument.createElementNS(uri, qName);
for (int i = 0; i < attributes.getLength(); i++) {
if (attributes.getURI(i) != null && !attributes.getURI(i).isEmpty()) {
Attr attr = mDocument.createAttributeNS(attributes.getURI(i),
attributes.getQName(i));
attr.setValue(attributes.getValue(i));
element.setAttributeNodeNS(attr);
assert attr.getOwnerElement() == element;
} else {
Attr attr = mDocument.createAttribute(attributes.getQName(i));
attr.setValue(attributes.getValue(i));
element.setAttributeNode(attr);
assert attr.getOwnerElement() == element;
}
}
Position pos = getCurrentPosition();
// The starting position reported to us by SAX is really the END of the
// open tag in an element, when all the attributes have been processed.
// We have to scan backwards to find the real beginning. We'll do that
// by scanning backwards.
// -1: Make sure that when we have <foo></foo> we don't consider </foo>
// the beginning since pos.offset will typically point to the first character
// AFTER the element open tag, which could be a closing tag or a child open
// tag
element.setUserData(POS_KEY, findOpeningTag(pos), null);
mStack.add(element);
} catch (Exception t) {
throw new SAXException(t);
}
}
@Override
public void endElement(String uri, String localName, String qName) {
flushText();
Element element = mStack.remove(mStack.size() - 1);
Position pos = (Position) element.getUserData(POS_KEY);
assert pos != null;
pos.setEnd(getCurrentPosition());
addNodeToParent(element);
}
@Override
public void comment(char[] chars, int start, int length) throws SAXException {
flushText();
String comment = new String(chars, start, length);
Comment domComment = mDocument.createComment(comment);
// current position is the closing comment tag.
Position currentPosition = getCurrentPosition();
Position startPosition = findOpeningTag(currentPosition);
startPosition.setEnd(currentPosition);
domComment.setUserData(POS_KEY, startPosition, null);
addNodeToParent(domComment);
}
/**
* Adds a node to the current parent element being visited, or to the document if there is
* no parent in context.
* @param nodeToAdd xml node to add.
*/
private void addNodeToParent(Node nodeToAdd) {
if (mStack.isEmpty()){
mDocument.appendChild(nodeToAdd);
} else {
Element parent = mStack.get(mStack.size() - 1);
parent.appendChild(nodeToAdd);
}
}
/**
* Find opening tags from the current position.
* < cannot appear in attribute values or anywhere else within
* an element open tag, so we know the first occurrence is the real
* element start
* For comments, it is not legal to put < in a comment, however we are not
* validating so we will return an invalid column in that case.
* @param startingPosition the position to walk backwards until < is reached.
* @return the opening tag position or startPosition if cannot be found.
*/
private Position findOpeningTag(Position startingPosition) {
for (int offset = startingPosition.getOffset() - 1; offset >= 0; offset--) {
char c = mXml.charAt(offset);
if (c == '<') {
// Adjust line position
int line = startingPosition.getLine();
for (int i = offset, n = startingPosition.getOffset(); i < n; i++) {
if (mXml.charAt(i) == '\n') {
line--;
}
}
// Compute new column position
int column = 0;
for (int i = offset - 1; i >= 0; i--, column++) {
if (mXml.charAt(i) == '\n') {
break;
}
}
return new Position(line, column, offset);
}
}
// we did not find it, approximate.
return startingPosition;
}
/**
* Returns a position holder for the current position. The most
* important part of this function is to incrementally compute the
* offset as well, by counting forwards until it reaches the new line
* number and column position of the XML parser, counting characters as
* it goes along.
*/
private Position getCurrentPosition() {
int line = mLocator.getLineNumber() - 1;
int column = mLocator.getColumnNumber() - 1;
// Compute offset incrementally now that we have the new line and column
// numbers
int xmlLength = mXml.length();
while (mCurrentLine < line && mCurrentOffset < xmlLength) {
char c = mXml.charAt(mCurrentOffset);
if (c == '\r' && mCurrentOffset < xmlLength - 1) {
if (mXml.charAt(mCurrentOffset + 1) != '\n') {
mCurrentLine++;
mCurrentColumn = 0;
}
} else if (c == '\n') {
mCurrentLine++;
mCurrentColumn = 0;
} else {
mCurrentColumn++;
}
mCurrentOffset++;
}
mCurrentOffset += column - mCurrentColumn;
if (mCurrentOffset >= xmlLength) {
// The parser sometimes passes wrong column numbers at the
// end of the file: Ensure that the offset remains valid.
mCurrentOffset = xmlLength;
}
mCurrentColumn = column;
return new Position(mCurrentLine, mCurrentColumn, mCurrentOffset);
}
@Override
public void characters(char c[], int start, int length) throws SAXException {
mPendingText.append(c, start, length);
}
private void flushText() {
if (mPendingText.length() > 0 && !mStack.isEmpty()) {
Element element = mStack.get(mStack.size() - 1);
Node textNode = mDocument.createTextNode(mPendingText.toString());
element.appendChild(textNode);
mPendingText.setLength(0);
}
}
}
private static class Position {
/** The line number (0-based where the first line is line 0) */
private final int mLine;
private final int mColumn;
private final int mOffset;
private Position mEnd;
/**
* Creates a new {@link Position}
*
* @param line the 0-based line number, or -1 if unknown
* @param column the 0-based column number, or -1 if unknown
* @param offset the offset, or -1 if unknown
*/
public Position(int line, int column, int offset) {
this.mLine = line;
this.mColumn = column;
this.mOffset = offset;
}
public int getLine() {
return mLine;
}
public int getOffset() {
return mOffset;
}
public int getColumn() {
return mColumn;
}
public Position getEnd() {
return mEnd;
}
public void setEnd(@NonNull Position end) {
mEnd = end;
}
public SourcePosition toSourcePosition() {
int endLine = mLine, endColumn = mColumn, endOffset = mOffset;
if (mEnd != null) {
endLine = mEnd.getLine();
endColumn = mEnd.getColumn();
endOffset = mEnd.getOffset();
}
return new SourcePosition(mLine, mColumn, mOffset, endLine, endColumn, endOffset);
}
}
private PositionXmlParser() { }
}

View File

@ -0,0 +1,511 @@
/*
* Copyright (C) 2012 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.utils;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.base.CaseFormat;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.List;
import static com.android.SdkConstants.DOT_WEBP;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.DOT_GIF;
import static com.android.SdkConstants.DOT_9PNG;
import static com.android.SdkConstants.DOT_JPEG;
import static com.android.SdkConstants.DOT_JPG;
import static com.android.SdkConstants.DOT_BMP;
/** Miscellaneous utilities used by the Android SDK tools */
public class SdkUtils {
/**
* Returns true if the given string ends with the given suffix, using a
* case-insensitive comparison.
*
* @param string the full string to be checked
* @param suffix the suffix to be checked for
* @return true if the string case-insensitively ends with the given suffix
*/
public static boolean endsWithIgnoreCase(@NonNull String string, @NonNull String suffix) {
return string.regionMatches(true /* ignoreCase */, string.length() - suffix.length(),
suffix, 0, suffix.length());
}
/**
* Returns true if the given sequence ends with the given suffix (case
* sensitive).
*
* @param sequence the character sequence to be checked
* @param suffix the suffix to look for
* @return true if the given sequence ends with the given suffix
*/
public static boolean endsWith(@NonNull CharSequence sequence, @NonNull CharSequence suffix) {
return endsWith(sequence, sequence.length(), suffix);
}
/**
* Returns true if the given sequence ends at the given offset with the given suffix (case
* sensitive)
*
* @param sequence the character sequence to be checked
* @param endOffset the offset at which the sequence is considered to end
* @param suffix the suffix to look for
* @return true if the given sequence ends with the given suffix
*/
public static boolean endsWith(@NonNull CharSequence sequence, int endOffset,
@NonNull CharSequence suffix) {
if (endOffset < suffix.length()) {
return false;
}
for (int i = endOffset - 1, j = suffix.length() - 1; j >= 0; i--, j--) {
if (sequence.charAt(i) != suffix.charAt(j)) {
return false;
}
}
return true;
}
/**
* Returns true if the given string starts with the given prefix, using a
* case-insensitive comparison.
*
* @param string the full string to be checked
* @param prefix the prefix to be checked for
* @return true if the string case-insensitively starts with the given prefix
*/
public static boolean startsWithIgnoreCase(@NonNull String string, @NonNull String prefix) {
return string.regionMatches(true /* ignoreCase */, 0, prefix, 0, prefix.length());
}
/**
* Returns true if the given string starts at the given offset with the
* given prefix, case insensitively.
*
* @param string the full string to be checked
* @param offset the offset in the string to start looking
* @param prefix the prefix to be checked for
* @return true if the string case-insensitively starts at the given offset
* with the given prefix
*/
public static boolean startsWith(@NonNull String string, int offset, @NonNull String prefix) {
return string.regionMatches(true /* ignoreCase */, offset, prefix, 0, prefix.length());
}
/**
* Strips the whitespace from the given string
*
* @param string the string to be cleaned up
* @return the string, without whitespace
*/
public static String stripWhitespace(@NonNull String string) {
StringBuilder sb = new StringBuilder(string.length());
for (int i = 0, n = string.length(); i < n; i++) {
char c = string.charAt(i);
if (!Character.isWhitespace(c)) {
sb.append(c);
}
}
return sb.toString();
}
/**
* Returns true if the given string has an upper case character.
*
* @param s the string to check
* @return true if it contains uppercase characters
*/
public static boolean hasUpperCaseCharacter(@NonNull String s) {
for (int i = 0; i < s.length(); i++) {
if (Character.isUpperCase(s.charAt(i))) {
return true;
}
}
return false;
}
/** For use by {@link #getLineSeparator()} */
private static String sLineSeparator;
/**
* Returns the default line separator to use.
* <p>
* NOTE: If you have an associated IDocument (Eclipse), it is better to call
* TextUtilities#getDefaultLineDelimiter(IDocument) since that will
* allow (for example) editing a \r\n-delimited document on a \n-delimited
* platform and keep a consistent usage of delimiters in the file.
*
* @return the delimiter string to use
*/
@NonNull
public static String getLineSeparator() {
if (sLineSeparator == null) {
// This is guaranteed to exist:
sLineSeparator = System.getProperty("line.separator"); //$NON-NLS-1$
}
return sLineSeparator;
}
/**
* Wraps the given text at the given line width, with an optional hanging
* indent.
*
* @param text the text to be wrapped
* @param lineWidth the number of characters to wrap the text to
* @param hangingIndent the hanging indent (to be used for the second and
* subsequent lines in each paragraph, or null if not known
* @return the string, wrapped
*/
@NonNull
public static String wrap(
@NonNull String text,
int lineWidth,
@Nullable String hangingIndent) {
if (hangingIndent == null) {
hangingIndent = "";
}
int explanationLength = text.length();
StringBuilder sb = new StringBuilder(explanationLength * 2);
int index = 0;
while (index < explanationLength) {
int lineEnd = text.indexOf('\n', index);
int next;
if (lineEnd != -1 && (lineEnd - index) < lineWidth) {
next = lineEnd + 1;
} else {
// Line is longer than available width; grab as much as we can
lineEnd = Math.min(index + lineWidth, explanationLength);
if (lineEnd - index < lineWidth) {
next = explanationLength;
} else {
// then back up to the last space
int lastSpace = text.lastIndexOf(' ', lineEnd);
if (lastSpace > index) {
lineEnd = lastSpace;
next = lastSpace + 1;
} else {
// No space anywhere on the line: it contains something wider than
// can fit (like a long URL) so just hard break it
next = lineEnd + 1;
}
}
}
if (sb.length() > 0) {
sb.append(hangingIndent);
} else {
lineWidth -= hangingIndent.length();
}
sb.append(text.substring(index, lineEnd));
sb.append('\n');
index = next;
}
return sb.toString();
}
/**
* Returns the given localized string as an int. For example, in the
* US locale, "1,000", will return 1000. In the French locale, "1.000" will return
* 1000. It will return 0 for empty strings.
* <p>
* To parse a string without catching parser exceptions, call
* {@link #parseLocalizedInt(String, int)} instead, passing the
* default value to be returned if the format is invalid.
*
* @param string the string to be parsed
* @return the integer value
* @throws ParseException if the format is not correct
*/
public static int parseLocalizedInt(@NonNull String string) throws ParseException {
if (string.isEmpty()) {
return 0;
}
return NumberFormat.getIntegerInstance().parse(string).intValue();
}
/**
* Returns the given localized string as an int. For example, in the
* US locale, "1,000", will return 1000. In the French locale, "1.000" will return
* 1000. If the format is invalid, returns the supplied default value instead.
*
* @param string the string to be parsed
* @param defaultValue the value to be returned if there is a parsing error
* @return the integer value
*/
public static int parseLocalizedInt(@NonNull String string, int defaultValue) {
try {
return parseLocalizedInt(string);
} catch (ParseException e) {
return defaultValue;
}
}
/**
* Returns the given localized string as a double. For example, in the
* US locale, "3.14", will return 3.14. In the French locale, "3,14" will return
* 3.14. It will return 0 for empty strings.
* <p>
* To parse a string without catching parser exceptions, call
* {@link #parseLocalizedDouble(String, double)} instead, passing the
* default value to be returned if the format is invalid.
*
* @param string the string to be parsed
* @return the double value
* @throws ParseException if the format is not correct
*/
public static double parseLocalizedDouble(@NonNull String string) throws ParseException {
if (string.isEmpty()) {
return 0.0;
}
return NumberFormat.getNumberInstance().parse(string).doubleValue();
}
/**
* Returns the given localized string as a double. For example, in the
* US locale, "3.14", will return 3.14. In the French locale, "3,14" will return
* 3.14. If the format is invalid, returns the supplied default value instead.
*
* @param string the string to be parsed
* @param defaultValue the value to be returned if there is a parsing error
* @return the double value
*/
public static double parseLocalizedDouble(@NonNull String string, double defaultValue) {
try {
return parseLocalizedDouble(string);
} catch (ParseException e) {
return defaultValue;
}
}
/**
* Returns the corresponding {@link File} for the given file:// url
*
* @param url the URL string, e.g. file://foo/bar
* @return the corresponding {@link File} (which may or may not exist)
* @throws MalformedURLException if the URL string is malformed or is not a file: URL
*/
@NonNull
public static File urlToFile(@NonNull String url) throws MalformedURLException {
return urlToFile(new URL(url));
}
@NonNull
public static File urlToFile(@NonNull URL url) throws MalformedURLException {
try {
return new File(url.toURI());
}
catch (IllegalArgumentException e) {
MalformedURLException ex = new MalformedURLException(e.getLocalizedMessage());
ex.initCause(e);
throw ex;
}
catch (URISyntaxException e) {
return new File(url.getPath());
}
}
/**
* Returns the corresponding URL string for the given {@link File}
*
* @param file the file to look up the URL for
* @return the corresponding URL
* @throws MalformedURLException in very unexpected cases
*/
public static String fileToUrlString(@NonNull File file) throws MalformedURLException {
return fileToUrl(file).toExternalForm();
}
/**
* Returns the corresponding URL for the given {@link File}
*
* @param file the file to look up the URL for
* @return the corresponding URL
* @throws MalformedURLException in very unexpected cases
*/
public static URL fileToUrl(@NonNull File file) throws MalformedURLException {
return file.toURI().toURL();
}
/** Prefix in comments which mark the source locations for merge results */
public static final String FILENAME_PREFIX = "From: ";
/**
* Creates the path comment XML string. Note that it does not escape characters
* such as &amp; and &lt;; those are expected to be escaped by the caller (for
* example, handled by a call to {@link org.w3c.dom.Document#createComment(String)})
*
*
* @param file the file to create a path comment for
* @param includePadding whether to include padding. The final comment recognized by
* error recognizers expect padding between the {@code <!--} and
* the start marker (From:); you can disable padding if the caller
* already is in a context where the padding has been added.
* @return the corresponding XML contents of the string
*/
public static String createPathComment(@NonNull File file, boolean includePadding)
throws MalformedURLException {
String url = fileToUrlString(file);
int dashes = url.indexOf("--");
if (dashes != -1) { // Not allowed inside XML comments - for SGML compatibility. Sigh.
url = url.replace("--", "%2D%2D");
}
if (includePadding) {
return ' ' + FILENAME_PREFIX + url + ' ';
} else {
return FILENAME_PREFIX + url;
}
}
/**
* Copies the given XML file to the given new path. It also inserts a comment at
* the end of the file which points to the original source location. This is intended
* for use with error parsers which can rewrite for example AAPT error messages in
* say layout or manifest files, which occur in the merged (copied) output, and present
* it as an error pointing to one of the user's original source files.
*/
public static void copyXmlWithSourceReference(@NonNull File from, @NonNull File to)
throws IOException {
copyXmlWithComment(from, to, createPathComment(from, true));
}
/** Copies a given XML file, and appends a given comment to the end */
private static void copyXmlWithComment(@NonNull File from, @NonNull File to,
@Nullable String comment) throws IOException {
assert endsWithIgnoreCase(from.getPath(), DOT_XML) : from;
int successfulOps = 0;
InputStream in = new FileInputStream(from);
try {
FileOutputStream out = new FileOutputStream(to, false);
try {
ByteStreams.copy(in, out);
successfulOps++;
if (comment != null) {
String commentText = "<!--" + XmlUtils.toXmlTextValue(comment) + "-->";
byte[] suffix = commentText.getBytes(Charsets.UTF_8);
out.write(suffix);
}
} finally {
Closeables.close(out, successfulOps < 1);
successfulOps++;
}
} finally {
Closeables.close(in, successfulOps < 2);
}
}
/**
* Translates an XML name (e.g. xml-name) into a Java / C++ constant name (e.g. XML_NAME)
* @param xmlName the hyphen separated lower case xml name.
* @return the equivalent constant name.
*/
public static String xmlNameToConstantName(String xmlName) {
return CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, xmlName);
}
/**
* Translates a camel case name (e.g. xmlName) into a Java / C++ constant name (e.g. XML_NAME)
* @param camelCaseName the camel case name.
* @return the equivalent constant name.
*/
public static String camelCaseToConstantName(String camelCaseName) {
return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, camelCaseName);
}
/**
* Translates a Java / C++ constant name (e.g. XML_NAME) into camel case name (e.g. xmlName)
* @param constantName the constant name.
* @return the equivalent camel case name.
*/
public static String constantNameToCamelCase(String constantName) {
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, constantName);
}
/**
* Translates a Java / C++ constant name (e.g. XML_NAME) into a XML case name (e.g. xml-name)
* @param constantName the constant name.
* @return the equivalent XML name.
*/
public static String constantNameToXmlName(String constantName) {
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, constantName);
}
/**
* Get the R field name from a resource name, since
* AAPT will flatten the namespace, turning dots, dashes and colons into _
*
* @param resourceName the name to convert
* @return the corresponding R field name
*/
@NonNull
public static String getResourceFieldName(@NonNull String resourceName) {
// AAPT will flatten the namespace, turning dots, dashes and colons into _
for (int i = 0, n = resourceName.length(); i < n; i++) {
char c = resourceName.charAt(i);
if (c == '.' || c == ':' || c == '-') {
return resourceName.replace('.', '_').replace('-', '_').replace(':', '_');
}
}
return resourceName;
}
public static final List<String> IMAGE_EXTENSIONS = ImmutableList.of(
DOT_PNG, DOT_9PNG, DOT_GIF, DOT_JPEG, DOT_JPG, DOT_BMP, DOT_WEBP);
/**
* Returns true if the given file path points to an image file recognized by
* Android. See http://developer.android.com/guide/appendix/media-formats.html
* for details.
*
* @param path the filename to be tested
* @return true if the file represents an image file
*/
public static boolean hasImageExtension(String path) {
for (String ext: IMAGE_EXTENSIONS) {
if (endsWithIgnoreCase(path, ext)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,238 @@
/*
* Copyright (C) 2006 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.utils;
/**
* SparseIntArrays map integers to integers. Unlike a normal array of integers,
* there can be gaps in the indices. It is intended to be more efficient
* than using a HashMap to map Integers to Integers.
*/
public class SparseIntArray {
/**
* Creates a new SparseIntArray containing no mappings.
*/
public SparseIntArray() {
this(10);
}
/**
* Creates a new SparseIntArray containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings.
*/
public SparseIntArray(int initialCapacity) {
initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
mKeys = new int[initialCapacity];
mValues = new int[initialCapacity];
mSize = 0;
}
/**
* Gets the int mapped from the specified key, or <code>0</code>
* if no such mapping has been made.
*/
public int get(int key) {
return get(key, 0);
}
/**
* Gets the int mapped from the specified key, or the specified value
* if no such mapping has been made.
*/
public int get(int key, int valueIfKeyNotFound) {
int i = binarySearch(mKeys, 0, mSize, key);
if (i < 0) {
return valueIfKeyNotFound;
} else {
return mValues[i];
}
}
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = binarySearch(mKeys, 0, mSize, key);
if (i >= 0) {
removeAt(i);
}
}
/**
* Removes the mapping at the given index.
*/
public void removeAt(int index) {
System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
mSize--;
}
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, int value) {
int i = binarySearch(mKeys, 0, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;
if (mSize >= mKeys.length) {
int n = ArrayUtils.idealIntArraySize(mSize + 1);
int[] nkeys = new int[n];
int[] nvalues = new int[n];
// Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n);
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
mKeys = nkeys;
mValues = nvalues;
}
if (mSize - i != 0) {
// Log.e("SparseIntArray", "move " + (mSize - i));
System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
}
mKeys[i] = key;
mValues[i] = value;
mSize++;
}
}
/**
* Returns the number of key-value mappings that this SparseIntArray
* currently stores.
*/
public int size() {
return mSize;
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseIntArray stores.
*/
public int keyAt(int index) {
return mKeys[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the value from the <code>index</code>th key-value mapping that this
* SparseIntArray stores.
*/
public int valueAt(int index) {
return mValues[index];
}
/**
* Returns the index for which {@link #keyAt} would return the
* specified key, or a negative number if the specified
* key is not mapped.
*/
public int indexOfKey(int key) {
return binarySearch(mKeys, 0, mSize, key);
}
/**
* Returns an index for which {@link #valueAt} would return the
* specified key, or a negative number if no keys map to the
* specified value.
* Beware that this is a linear search, unlike lookups by key,
* and that multiple keys can map to the same value and this will
* find only one of them.
*/
public int indexOfValue(int value) {
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
/**
* Removes all key-value mappings from this SparseIntArray.
*/
public void clear() {
mSize = 0;
}
/**
* Puts a key/value pair into the array, optimizing for the case where
* the key is greater than all existing keys in the array.
*/
public void append(int key, int value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
int pos = mSize;
if (pos >= mKeys.length) {
int n = ArrayUtils.idealIntArraySize(pos + 1);
int[] nkeys = new int[n];
int[] nvalues = new int[n];
// Log.e("SparseIntArray", "grow " + mKeys.length + " to " + n);
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
mKeys = nkeys;
mValues = nvalues;
}
mKeys[pos] = key;
mValues[pos] = value;
mSize = pos + 1;
}
private static int binarySearch(int[] a, int start, int len, int key) {
int high = start + len, low = start - 1, guess;
while (high - low > 1) {
guess = (high + low) / 2;
if (a[guess] < key)
low = guess;
else
high = guess;
}
if (high == start + len)
return ~(start + len);
else if (a[high] == key)
return high;
else
return ~high;
}
private int[] mKeys;
private int[] mValues;
private int mSize;
}

View File

@ -0,0 +1,178 @@
/*
* Copyright (C) 2012 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.utils;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.io.PrintStream;
import java.util.Formatter;
/**
* An implementation of {@link ILogger} that prints to {@link System#out} and {@link System#err}.
* <p/>
*
*/
public class StdLogger implements ILogger {
private final Level mLevel;
public enum Level {
VERBOSE(0),
INFO(1),
WARNING(2),
ERROR(3);
private final int mLevel;
Level(int level) {
mLevel = level;
}
}
/**
* Creates the {@link StdLogger} with a given log {@link Level}.
* @param level the log Level.
*/
public StdLogger(@NonNull Level level) {
if (level == null) {
throw new IllegalArgumentException("level cannot be null");
}
mLevel = level;
}
/**
* Returns the logger's log {@link Level}.
* @return the log level.
*/
public Level getLevel() {
return mLevel;
}
/**
* Prints an error message.
* <p/>
* The message will be tagged with "Error" on the output so the caller does not
* need to put such a prefix in the format string.
* <p/>
* The output is done on {@link System#err}.
* <p/>
* This is always displayed, independent of the logging {@link Level}.
*
* @param t is an optional {@link Throwable} or {@link Exception}. If non-null, it's
* message will be printed out.
* @param errorFormat is an optional error format. If non-null, it will be printed
* using a {@link Formatter} with the provided arguments.
* @param args provides the arguments for errorFormat.
*/
@Override
public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) {
if (errorFormat != null) {
String msg = String.format("Error: " + errorFormat, args);
printMessage(msg, System.err);
}
if (t != null) {
System.err.println(String.format("Error: %1$s", t.getMessage()));
}
}
/**
* Prints a warning message.
* <p/>
* The message will be tagged with "Warning" on the output so the caller does not
* need to put such a prefix in the format string.
* <p/>
* The output is done on {@link System#out}.
* <p/>
* This is displayed only if the logging {@link Level} is {@link Level#WARNING} or higher.
*
* @param warningFormat is a string format to be used with a {@link Formatter}. Cannot be null.
* @param args provides the arguments for warningFormat.
*/
@Override
public void warning(@NonNull String warningFormat, Object... args) {
if (mLevel.mLevel > Level.WARNING.mLevel) {
return;
}
String msg = String.format("Warning: " + warningFormat, args);
printMessage(msg, System.out);
}
/**
* Prints an info message.
* <p/>
* The output is done on {@link System#out}.
* <p/>
* This is displayed only if the logging {@link Level} is {@link Level#INFO} or higher.
*
* @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
* @param args provides the arguments for msgFormat.
*/
@Override
public void info(@NonNull String msgFormat, Object... args) {
if (mLevel.mLevel > Level.INFO.mLevel) {
return;
}
String msg = String.format(msgFormat, args);
printMessage(msg, System.out);
}
/**
* Prints a verbose message.
* <p/>
* The output is done on {@link System#out}.
* <p/>
* This is displayed only if the logging {@link Level} is {@link Level#VERBOSE} or higher.
*
* @param msgFormat is a string format to be used with a {@link Formatter}. Cannot be null.
* @param args provides the arguments for msgFormat.
*/
@Override
public void verbose(@NonNull String msgFormat, Object... args) {
if (mLevel.mLevel > Level.VERBOSE.mLevel) {
return;
}
String msg = String.format(msgFormat, args);
printMessage(msg, System.out);
}
private void printMessage(String msg, PrintStream stream) {
if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS &&
!msg.endsWith("\r\n") &&
msg.endsWith("\n")) {
// remove last \n so that println can use \r\n as needed.
msg = msg.substring(0, msg.length() - 1);
}
stream.print(msg);
if (!msg.endsWith("\n")) {
stream.println();
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2013 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.utils;
import com.android.annotations.NonNull;
import java.util.Locale;
/**
*/
public class StringHelper {
@NonNull
public static String capitalize(@NonNull String string) {
StringBuilder sb = new StringBuilder();
sb.append(string.substring(0, 1).toUpperCase(Locale.US)).append(string.substring(1));
return sb.toString();
}
}

View File

@ -0,0 +1,586 @@
/*
* Copyright (C) 2012 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.utils;
import static com.android.SdkConstants.AMP_ENTITY;
import static com.android.SdkConstants.ANDROID_NS_NAME;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.APOS_ENTITY;
import static com.android.SdkConstants.APP_PREFIX;
import static com.android.SdkConstants.GT_ENTITY;
import static com.android.SdkConstants.LT_ENTITY;
import static com.android.SdkConstants.QUOT_ENTITY;
import static com.android.SdkConstants.XMLNS;
import static com.android.SdkConstants.XMLNS_PREFIX;
import static com.android.SdkConstants.XMLNS_URI;
import static com.google.common.base.Charsets.UTF_16BE;
import static com.google.common.base.Charsets.UTF_16LE;
import static com.google.common.base.Charsets.UTF_8;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.io.Files;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.HashSet;
import java.util.Locale;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/** XML Utilities */
public class XmlUtils {
public static final String XML_COMMENT_BEGIN = "<!--"; //$NON-NLS-1$
public static final String XML_COMMENT_END = "-->"; //$NON-NLS-1$
public static final String XML_PROLOG =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$
/**
* Separator for xml namespace and localname
*/
public static final char NS_SEPARATOR = ':'; //$NON-NLS-1$
/**
* Returns the namespace prefix matching the requested namespace URI.
* If no such declaration is found, returns the default "android" prefix for
* the Android URI, and "app" for other URI's. By default the app namespace
* will be created. If this is not desirable, call
* {@link #lookupNamespacePrefix(Node, String, boolean)} instead.
*
* @param node The current node. Must not be null.
* @param nsUri The namespace URI of which the prefix is to be found,
* e.g. {@link SdkConstants#ANDROID_URI}
* @return The first prefix declared or the default "android" prefix
* (or "app" for non-Android URIs)
*/
@NonNull
public static String lookupNamespacePrefix(@NonNull Node node, @NonNull String nsUri) {
String defaultPrefix = ANDROID_URI.equals(nsUri) ? ANDROID_NS_NAME : APP_PREFIX;
return lookupNamespacePrefix(node, nsUri, defaultPrefix, true /*create*/);
}
/**
* Returns the namespace prefix matching the requested namespace URI. If no
* such declaration is found, returns the default "android" prefix for the
* Android URI, and "app" for other URI's.
*
* @param node The current node. Must not be null.
* @param nsUri The namespace URI of which the prefix is to be found, e.g.
* {@link SdkConstants#ANDROID_URI}
* @param create whether the namespace declaration should be created, if
* necessary
* @return The first prefix declared or the default "android" prefix (or
* "app" for non-Android URIs)
*/
@NonNull
public static String lookupNamespacePrefix(@NonNull Node node, @NonNull String nsUri,
boolean create) {
String defaultPrefix = ANDROID_URI.equals(nsUri) ? ANDROID_NS_NAME : APP_PREFIX;
return lookupNamespacePrefix(node, nsUri, defaultPrefix, create);
}
/**
* Returns the namespace prefix matching the requested namespace URI. If no
* such declaration is found, returns the default "android" prefix.
*
* @param node The current node. Must not be null.
* @param nsUri The namespace URI of which the prefix is to be found, e.g.
* {@link SdkConstants#ANDROID_URI}
* @param defaultPrefix The default prefix (root) to use if the namespace is
* not found. If null, do not create a new namespace if this URI
* is not defined for the document.
* @param create whether the namespace declaration should be created, if
* necessary
* @return The first prefix declared or the provided prefix (possibly with a
* number appended to avoid conflicts with existing prefixes.
*/
public static String lookupNamespacePrefix(
@Nullable Node node, @Nullable String nsUri, @Nullable String defaultPrefix,
boolean create) {
// Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java
// The following code emulates this simple call:
// String prefix = node.lookupPrefix(NS_RESOURCES);
// if the requested URI is null, it denotes an attribute with no namespace.
if (nsUri == null) {
return null;
}
// per XML specification, the "xmlns" URI is reserved
if (XMLNS_URI.equals(nsUri)) {
return XMLNS;
}
HashSet<String> visited = new HashSet<String>();
Document doc = node == null ? null : node.getOwnerDocument();
// Ask the document about it. This method may not be implemented by the Document.
String nsPrefix = null;
try {
nsPrefix = doc != null ? doc.lookupPrefix(nsUri) : null;
if (nsPrefix != null) {
return nsPrefix;
}
} catch (Throwable t) {
// ignore
}
// If that failed, try to look it up manually.
// This also gathers prefixed in use in the case we want to generate a new one below.
for (; node != null && node.getNodeType() == Node.ELEMENT_NODE;
node = node.getParentNode()) {
NamedNodeMap attrs = node.getAttributes();
for (int n = attrs.getLength() - 1; n >= 0; --n) {
Node attr = attrs.item(n);
if (XMLNS.equals(attr.getPrefix())) {
String uri = attr.getNodeValue();
nsPrefix = attr.getLocalName();
// Is this the URI we are looking for? If yes, we found its prefix.
if (nsUri.equals(uri)) {
return nsPrefix;
}
visited.add(nsPrefix);
}
}
}
// Failed the find a prefix. Generate a new sensible default prefix, unless
// defaultPrefix was null in which case the caller does not want the document
// modified.
if (defaultPrefix == null) {
return null;
}
//
// We need to make sure the prefix is not one that was declared in the scope
// visited above. Pick a unique prefix from the provided default prefix.
String prefix = defaultPrefix;
String base = prefix;
for (int i = 1; visited.contains(prefix); i++) {
prefix = base + Integer.toString(i);
}
// Also create & define this prefix/URI in the XML document as an attribute in the
// first element of the document.
if (doc != null) {
node = doc.getFirstChild();
while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
node = node.getNextSibling();
}
if (node != null && create) {
// This doesn't work:
//Attr attr = doc.createAttributeNS(XMLNS_URI, prefix);
//attr.setPrefix(XMLNS);
//
// Xerces throws
//org.w3c.dom.DOMException: NAMESPACE_ERR: An attempt is made to create or
// change an object in a way which is incorrect with regard to namespaces.
//
// Instead pass in the concatenated prefix. (This is covered by
// the UiElementNodeTest#testCreateNameSpace() test.)
Attr attr = doc.createAttributeNS(XMLNS_URI, XMLNS_PREFIX + prefix);
attr.setValue(nsUri);
node.getAttributes().setNamedItemNS(attr);
}
}
return prefix;
}
/**
* Converts the given attribute value to an XML-attribute-safe value, meaning that
* single and double quotes are replaced with their corresponding XML entities.
*
* @param attrValue the value to be escaped
* @return the escaped value
*/
@NonNull
public static String toXmlAttributeValue(@NonNull String attrValue) {
for (int i = 0, n = attrValue.length(); i < n; i++) {
char c = attrValue.charAt(i);
if (c == '"' || c == '\'' || c == '<' || c == '&') {
StringBuilder sb = new StringBuilder(2 * attrValue.length());
appendXmlAttributeValue(sb, attrValue);
return sb.toString();
}
}
return attrValue;
}
/**
* Converts the given XML-attribute-safe value to a java string
*
* @param escapedAttrValue the escaped value
* @return the unescaped value
*/
@NonNull
public static String fromXmlAttributeValue(@NonNull String escapedAttrValue) {
String workingString = escapedAttrValue.replace(QUOT_ENTITY, "\"");
workingString = workingString.replace(LT_ENTITY, "<");
workingString = workingString.replace(APOS_ENTITY, "'");
workingString = workingString.replace(AMP_ENTITY, "&");
workingString = workingString.replace(GT_ENTITY, ">");
return workingString;
}
/**
* Converts the given attribute value to an XML-text-safe value, meaning that
* less than and ampersand characters are escaped.
*
* @param textValue the text value to be escaped
* @return the escaped value
*/
@NonNull
public static String toXmlTextValue(@NonNull String textValue) {
for (int i = 0, n = textValue.length(); i < n; i++) {
char c = textValue.charAt(i);
if (c == '<' || c == '&') {
StringBuilder sb = new StringBuilder(2 * textValue.length());
appendXmlTextValue(sb, textValue);
return sb.toString();
}
}
return textValue;
}
/**
* Appends text to the given {@link StringBuilder} and escapes it as required for a
* DOM attribute node.
*
* @param sb the string builder
* @param attrValue the attribute value to be appended and escaped
*/
public static void appendXmlAttributeValue(@NonNull StringBuilder sb,
@NonNull String attrValue) {
int n = attrValue.length();
// &, ", ' and < are illegal in attributes; see http://www.w3.org/TR/REC-xml/#NT-AttValue
// (' legal in a " string and " is legal in a ' string but here we'll stay on the safe
// side)
for (int i = 0; i < n; i++) {
char c = attrValue.charAt(i);
if (c == '"') {
sb.append(QUOT_ENTITY);
} else if (c == '<') {
sb.append(LT_ENTITY);
} else if (c == '\'') {
sb.append(APOS_ENTITY);
} else if (c == '&') {
sb.append(AMP_ENTITY);
} else {
sb.append(c);
}
}
}
/**
* Appends text to the given {@link StringBuilder} and escapes it as required for a
* DOM text node.
*
* @param sb the string builder
* @param textValue the text value to be appended and escaped
*/
public static void appendXmlTextValue(@NonNull StringBuilder sb, @NonNull String textValue) {
for (int i = 0, n = textValue.length(); i < n; i++) {
char c = textValue.charAt(i);
if (c == '<') {
sb.append(LT_ENTITY);
} else if (c == '&') {
sb.append(AMP_ENTITY);
} else {
sb.append(c);
}
}
}
/**
* Returns true if the given node has one or more element children
*
* @param node the node to test for element children
* @return true if the node has one or more element children
*/
public static boolean hasElementChildren(@NonNull Node node) {
NodeList children = node.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
return true;
}
}
return false;
}
/**
* Returns a character reader for the given file, which must be a UTF encoded file.
* <p>
* The reader does not need to be closed by the caller (because the file is read in
* full in one shot and the resulting array is then wrapped in a byte array input stream,
* which does not need to be closed.)
*/
public static Reader getUtfReader(@NonNull File file) throws IOException {
byte[] bytes = Files.toByteArray(file);
int length = bytes.length;
if (length == 0) {
return new StringReader("");
}
switch (bytes[0]) {
case (byte)0xEF: {
if (length >= 3
&& bytes[1] == (byte)0xBB
&& bytes[2] == (byte)0xBF) {
// UTF-8 BOM: EF BB BF: Skip it
return new InputStreamReader(new ByteArrayInputStream(bytes, 3, length - 3),
UTF_8);
}
break;
}
case (byte)0xFE: {
if (length >= 2
&& bytes[1] == (byte)0xFF) {
// UTF-16 Big Endian BOM: FE FF
return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2),
UTF_16BE);
}
break;
}
case (byte)0xFF: {
if (length >= 2
&& bytes[1] == (byte)0xFE) {
if (length >= 4
&& bytes[2] == (byte)0x00
&& bytes[3] == (byte)0x00) {
// UTF-32 Little Endian BOM: FF FE 00 00
return new InputStreamReader(new ByteArrayInputStream(bytes, 4,
length - 4), "UTF-32LE");
}
// UTF-16 Little Endian BOM: FF FE
return new InputStreamReader(new ByteArrayInputStream(bytes, 2, length - 2),
UTF_16LE);
}
break;
}
case (byte)0x00: {
if (length >= 4
&& bytes[0] == (byte)0x00
&& bytes[1] == (byte)0x00
&& bytes[2] == (byte)0xFE
&& bytes[3] == (byte)0xFF) {
// UTF-32 Big Endian BOM: 00 00 FE FF
return new InputStreamReader(new ByteArrayInputStream(bytes, 4, length - 4),
"UTF-32BE");
}
break;
}
}
// No byte order mark: Assume UTF-8 (where the BOM is optional).
return new InputStreamReader(new ByteArrayInputStream(bytes), UTF_8);
}
/**
* Parses the given XML string as a DOM document, using the JDK parser. The parser does not
* validate, and is optionally namespace aware.
*
* @param xml the XML content to be parsed (must be well formed)
* @param namespaceAware whether the parser is namespace aware
* @return the DOM document
*/
@NonNull
public static Document parseDocument(@NonNull String xml, boolean namespaceAware)
throws ParserConfigurationException, IOException, SAXException {
xml = stripBom(xml);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
InputSource is = new InputSource(new StringReader(xml));
factory.setNamespaceAware(namespaceAware);
factory.setValidating(false);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(is);
}
/**
* Parses the given UTF file as a DOM document, using the JDK parser. The parser does not
* validate, and is optionally namespace aware.
*
* @param file the UTF encoded file to parse
* @param namespaceAware whether the parser is namespace aware
* @return the DOM document
*/
@NonNull
public static Document parseUtfXmlFile(@NonNull File file, boolean namespaceAware)
throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Reader reader = getUtfReader(file);
try {
InputSource is = new InputSource(reader);
factory.setNamespaceAware(namespaceAware);
factory.setValidating(false);
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(is);
} finally {
reader.close();
}
}
/** Strips out a leading UTF byte order mark, if present */
@NonNull
public static String stripBom(@NonNull String xml) {
if (!xml.isEmpty() && xml.charAt(0) == '\uFEFF') {
return xml.substring(1);
}
return xml;
}
/**
* Parses the given XML string as a DOM document, using the JDK parser. The parser does not
* validate, and is optionally namespace aware. Any parsing errors are silently ignored.
*
* @param xml the XML content to be parsed (must be well formed)
* @param namespaceAware whether the parser is namespace aware
* @return the DOM document, or null
*/
@Nullable
public static Document parseDocumentSilently(@NonNull String xml, boolean namespaceAware) {
try {
return parseDocument(xml, namespaceAware);
} catch (Exception e) {
// pass
// This method is deliberately silent; will return null
}
return null;
}
/**
* Dump an XML tree to string. This does not perform any pretty printing.
* To perform pretty printing, use {@code XmlPrettyPrinter.prettyPrint(node)} in
* {@code sdk-common}.
*/
public static String toXml(Node node) {
StringBuilder sb = new StringBuilder(1000);
append(sb, node, 0);
return sb.toString();
}
/** Dump node to string without indentation adjustments */
private static void append(
@NonNull StringBuilder sb,
@NonNull Node node,
int indent) {
short nodeType = node.getNodeType();
switch (nodeType) {
case Node.DOCUMENT_NODE:
case Node.DOCUMENT_FRAGMENT_NODE: {
sb.append(XML_PROLOG);
NodeList children = node.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
append(sb, children.item(i), indent);
}
break;
}
case Node.COMMENT_NODE:
sb.append(XML_COMMENT_BEGIN);
sb.append(node.getNodeValue());
sb.append(XML_COMMENT_END);
break;
case Node.TEXT_NODE: {
sb.append(toXmlTextValue(node.getNodeValue()));
break;
}
case Node.CDATA_SECTION_NODE: {
sb.append("<![CDATA["); //$NON-NLS-1$
sb.append(node.getNodeValue());
sb.append("]]>"); //$NON-NLS-1$
break;
}
case Node.ELEMENT_NODE: {
sb.append('<');
Element element = (Element) node;
sb.append(element.getTagName());
NamedNodeMap attributes = element.getAttributes();
NodeList children = element.getChildNodes();
int childCount = children.getLength();
int attributeCount = attributes.getLength();
if (attributeCount > 0) {
for (int i = 0; i < attributeCount; i++) {
Node attribute = attributes.item(i);
sb.append(' ');
sb.append(attribute.getNodeName());
sb.append('=').append('"');
sb.append(toXmlAttributeValue(attribute.getNodeValue()));
sb.append('"');
}
}
if (childCount == 0) {
sb.append('/');
}
sb.append('>');
if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
Node child = children.item(i);
append(sb, child, indent + 1);
}
sb.append('<').append('/');
sb.append(element.getTagName());
sb.append('>');
}
break;
}
default:
throw new UnsupportedOperationException(
"Unsupported node type " + nodeType + ": not yet implemented");
}
}
/**
* Format the given floating value into an XML string, omitting decimals if
* 0
*
* @param value the value to be formatted
* @return the corresponding XML string for the value
*/
public static String formatFloatAttribute(double value) {
if (value != (int) value) {
// Run String.format without a locale, because we don't want locale-specific
// conversions here like separating the decimal part with a comma instead of a dot!
return String.format((Locale) null, "%.2f", value); //$NON-NLS-1$
} else {
return Integer.toString((int) value);
}
}
}

View File

@ -0,0 +1,46 @@
package jp.juggler.subwaytooter.emoji
import java.io.PrintWriter
class JavaCodeWriter(private val writer: PrintWriter) {
private var linesInFunction = 0
private var functionsCount = 0
fun addCode(code: String) {
// open new function
if (linesInFunction == 0) {
++functionsCount
writer.println("\n\tprivate static void init$functionsCount(){")
}
// write code
writer.print("\t\t")
writer.println(code)
// close function
if (++linesInFunction > 100) {
writer.println("\t}")
linesInFunction = 0
}
}
fun closeFunction() {
if (linesInFunction > 0) {
writer.println("\t}")
linesInFunction = 0
}
}
fun writeDefinition(s: String) {
writer.println("\t$s")
writer.println("")
}
fun writeInitializer() {
writer.println("\tstatic void initAll(){")
for (i in 1..functionsCount) {
writer.println("\t\tinit$i();")
}
writer.println("\t}")
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
package jp.juggler.subwaytooter.emoji
@Suppress("ClassName")
object log {
fun d(msg: String) = println("D/ $msg")
fun w(msg: String) = println("W/ $msg")
fun e(msg: String) = println("E/ $msg")
}

View File

@ -0,0 +1,868 @@
package jp.juggler.subwaytooter.emoji
import com.android.ide.common.vectordrawable.Svg2Vector
import io.ktor.client.*
import io.ktor.client.features.*
import kotlinx.coroutines.runBlocking
import java.io.*
import kotlin.math.max
import java.io.FileInputStream
import java.lang.StringBuilder
//pngフォルダにある画像ファイルを参照する
//emoji-data/emoji.json を参照する
//
//以下のjavaコードを生成する
//- UTF-16文字列 => 画像リソースID のマップ。同一のIDに複数のUTF-16文字列が振られることがある。
//- shortcode => 画像リソースID のマップ。同一のIDに複数のshortcodeが振られることがある。
//- shortcode中の区切り文字はハイフンもアンダーバーもありうる。出力データではアンダーバーに寄せる
//- アプリはshortcodeの探索時にキー文字列の区切り文字をアンダーバーに正規化すること
const val pathCwebp = "C:/cygwin64/bin/cwebp.exe"
private const val hexChars = "0123456789abcdef"
private val reHex = """([0-9A-Fa-f]+)""".toRegex()
class Category(
val nameLower: String,
val enumId: String,
val url: String?
) {
//
override fun equals(other: Any?) = enumId == (other as? Category)?.enumId
override fun hashCode(): Int = enumId.hashCode()
// ショートコード登場順序がある
val shortcodes = ArrayList<String>()
}
// my $utf8 = Encode::find_encoding("utf8");
// my $utf16 = Encode::find_encoding("UTF-16BE");
var utf16_max_length = 0
// list of codepoints
class CodepointList(val list: ArrayList<Int>) {
override fun equals(other: Any?): Boolean = list == (other as?CodepointList)?.list
override fun hashCode(): Int = list.hashCode()
// make string "uuuu-uuuu-uuuu-uuuu"
override fun toString() = StringBuilder(list.size * 5).also {
list.forEachIndexed { i, v ->
if (i > 0) it.append('-')
it.append(String.format("%04x", v))
}
}.toString()
// make raw string
fun toRawString() = StringBuilder(list.size + 10).also { sb ->
for (cp in list) {
sb.appendCodePoint(cp)
}
}.toString()
fun makeUtf16(): String {
// java の文字列にする
// UTF-16にエンコード
val bytesUtf16 = toRawString().toByteArray(Charsets.UTF_16BE)
// UTF-16の符号単位数
val length = bytesUtf16.size / 2
if (length > utf16_max_length) {
utf16_max_length = length
}
val sb = StringBuilder()
for (i in bytesUtf16.indices step 2) {
val v0 = bytesUtf16[i].toInt()
val v1 = bytesUtf16[i + 1].toInt()
sb.append("\\u")
sb.append(hexChars[v0.and(255).shr(4)])
sb.append(hexChars[v0.and(15)])
sb.append(hexChars[v1.and(255).shr(4)])
sb.append(hexChars[v1.and(15)])
}
return sb.toString()
}
}
// hh-hh-hh-hh => arrayOf("hh","hh","hh","hh")
fun String.parseCodePoints() = ArrayList<Int>()
.also { dst ->
reHex.findAll(this).forEach { mr ->
dst.add(mr.groupValues[1].toInt(16))
}
}.notEmpty()?.let { CodepointList(it) }
// ショートコードの名前を正規化する
fun String.parseShortName() = toLowerCase().replace("-", "_")
private val reColonHead = """\A:""".toRegex()
private val reColonTail = """:\z""".toRegex()
private val reZWJ = """-(?:200d|fe0f)""".toRegex(RegexOption.IGNORE_CASE)
// 前後の::を取り除いてショートネーム正規化
fun String.parseAlphaCode() =
this.replace(reColonHead, "").replace(reColonTail, "").parseShortName()
// xxxx-xxxx-xxxx から ZWJのコードを除去する
fun String.removeZWJ() = replace(reZWJ, "")
class EmojiVariant(val dir: String) {
val used = ArrayList<String>()
}
val emojiVariants = arrayOf(
EmojiVariant("img-twitter-64"),
EmojiVariant("img-google-64"),
EmojiVariant("img-apple-64"),
EmojiVariant("img-apple-160"),
EmojiVariant("img-facebook-64"),
EmojiVariant("img-messenger-64"),
)
val emojiDataCodepointsVendors = arrayOf("docomo", "au", "softbank", "google")
// returns path of emoji
fun findEmojiImage(image: String): String? {
for (variant in emojiVariants) {
val path = "emoji-data/${variant.dir}/$image"
if (File(path).isFile) {
if (variant.used.size < 5) variant.used.add(image)
return path
}
}
return null
}
fun copyFile(dst: File, src: File) {
try {
FileInputStream(src).use { streamIn ->
FileOutputStream(dst).use { streamOut ->
streamOut.write(streamIn.readAllBytes())
}
}
} catch (ex: Throwable) {
dst.delete()
throw IOException("copyFile failed. src=$src dst=$dst", ex)
}
}
fun svgToVectorDrawable(dst: File, src: File) {
val tmp = ByteArrayOutputStream()
// Write all the error message during parsing into SvgTree. and return here as getErrorLog().
// We will also log the exceptions here.
try {
val svgTree = Svg2Vector.parse(src)
svgTree.mScaleFactor = 24 / max(svgTree.w, svgTree.h)
if (svgTree.canConvertToVectorDrawable()) {
Svg2Vector.writeFile(tmp, svgTree)
}
val errorLog = svgTree.errorLog
if (errorLog.isNotEmpty()) println("$src $errorLog")
FileOutputStream(dst).use { outStream ->
outStream.write(tmp.toByteArray())
}
} catch (e: Exception) {
println("svgToVectorDrawable: ${e.message} ${src.canonicalPath}")
}
}
class Resource(
val res_name: String,
val unified: CodepointList,
var isToneVariation: Boolean = false
) {
val codepointMap = HashMap<String, CodepointList>()
val shortnames = HashSet<String>()
val categories = HashSet<Category>()
fun addCodePoints(listCode: Iterable<CodepointList>) {
listCode.forEach { codepointMap[it.toString()] = it }
}
fun addShortnames(listName: Iterable<String>) {
shortnames.addAll(listName)
}
}
class SkinToneModifier(val code: String, val suffixList: Array<String>)
class App {
companion object {
private val reSvgFile = """(.+?)\.svg\z""".toRegex(RegexOption.IGNORE_CASE)
private val reExtPng = """\.png\z""".toRegex()
private val skin_tone_modifier = arrayOf(
SkinToneModifier("1F3FB", arrayOf("_tone1", "_light_skin_tone")),
SkinToneModifier("1F3FC", arrayOf("_tone2", "_medium_light_skin_tone")),
SkinToneModifier("1F3FD", arrayOf("_tone3", "_medium_skin_tone")),
SkinToneModifier("1F3FE", arrayOf("_tone4", "_medium_dark_skin_tone")),
SkinToneModifier("1F3FF", arrayOf("_tone5", "_dark_skin_tone")),
)
private val categoryNameMapping = HashMap<String, Category>().apply {
fun a(nameLower: String, enumId: String, url: String?) {
put(nameLower, Category(nameLower, enumId, url))
}
a("smileys & people", "CATEGORY_PEOPLE", "https://emojipedia.org/people/")
a("animals & nature", "CATEGORY_NATURE", "https://emojipedia.org/nature/")
a("food & drink", "CATEGORY_FOODS", "https://emojipedia.org/food-drink/")
a("activities", "CATEGORY_ACTIVITY", "https://emojipedia.org/activity/")
a("travel & places", "CATEGORY_PLACES", "https://emojipedia.org/travel-places/")
a("objects", "CATEGORY_OBJECTS", "https://emojipedia.org/objects/")
a("symbols", "CATEGORY_SYMBOLS", "https://emojipedia.org/symbols/")
a("flags", "CATEGORY_FLAGS", "https://emojipedia.org/flags/")
a("other", "CATEGORY_OTHER", null)
}
}
// resName => Resource
private val resNameMap = HashMap<String, Resource>()
// map: code => resName => Resource
private val codeMap = HashMap<String, HashMap<String, Resource>>()
// map shortname => resName => Resource
private val nameMap = HashMap<String, HashMap<String, Resource>>()
private val pngConverts = ArrayList<Pair<File, File>>()
private val svgConverts = ArrayList<Pair<File, File>>() // dst,src
private val mastodonSvg = ArrayList<String>()
private val twemojiSvg = ArrayList<String>()
private val overrideSvg = ArrayList<String>()
private val overridePng = ArrayList<String>()
private val emojiDataPng = ArrayList<String>()
// val shortname2unified = HashMap<String, String>()
private fun mayCopySvg(dst: File, src: File): Boolean {
if (!src.isFile) return false
if (!dst.exists()) copyFile(dst, src)
return true
}
private fun mayCopyWebp(dst: File, src: File): Boolean {
if (!src.isFile) return false
if (!dst.exists()) pngConverts.add(Pair(dst, src))
return true
}
// returns resName
private fun getEmojiResId(image: String): String {
// 小文字で拡張子なし
val imageLc = image.toLowerCase().replace(reExtPng, "")
// 画像リソースの名前
val resName = "emj_$imageLc".replace("-", "_")
// 出力先ファイル名
val dstWebp = File("drawable-nodpi/$resName.webp")
val dstSvg = File("assets/$resName.svg")
// using override SVG?
var src = "override/$imageLc.svg"
if (mayCopySvg(dstSvg, File(src))) {
overrideSvg.add(src)
return resName
}
// using override PNG?
src = "override/$imageLc.png"
if (mayCopyWebp(dstWebp, File(src))) {
overridePng.add(src)
return resName
}
// using svg from mastodon folder?
src = "mastodon/public/emoji/$imageLc.svg"
if (mayCopySvg(dstSvg, File(src))) {
mastodonSvg.add(src)
return resName
}
// using svg from twemoji?
src = "twemoji/assets/svg/$imageLc.svg"
if (mayCopySvg(dstSvg, File(src))) {
twemojiSvg.add(src)
return resName
}
// using emoji-data PNG?
src = findEmojiImage(image)
?: error("emoji-data has no emoji for $image")
if (mayCopyWebp(dstWebp, File(src))) {
emojiDataPng.add(src)
return resName
}
error("missing emoji: $imageLc")
}
// returns resName
private fun getEmojiResIdOld(image: String): String {
// コードポイントに合う画像ファイルがあるか調べる
val imageFile = File("emojione/assets/png/$image.png")
if (!imageFile.isFile) {
error("getEmojiResIdOld: missing. imageFile=${imageFile.path}")
}
// 画像リソースの名前
val resName = "emj_${image.toLowerCase()}".replace("-", "_")
val dstPathWebp = "drawable-nodpi/$resName.webp"
mayCopyWebp(File(dstPathWebp), imageFile)
return resName
}
private fun registerResource(
unified: CodepointList,
image: String,
list_code: Iterable<CodepointList>,
list_name: Iterable<String>,
has_tone: Boolean = false,
isToneVariation: Boolean = false
) {
val resName = getEmojiResId(image)
val self = resNameMap.prepare(resName) {
Resource(
res_name = resName,
unified = unified,
isToneVariation = isToneVariation
)
}
if (self.unified != unified) {
error("unified not match. res_name=$resName, unified = ")
}
self.addCodePoints(list_code)
self.addShortnames(list_name)
}
private fun registerResourceEmojione(
unified: CodepointList,
image: String,
list_code: Iterable<CodepointList>,
list_name: Iterable<String>,
) {
val resName = getEmojiResIdOld(image)
val self = resNameMap.prepare(resName) {
Resource(
res_name = resName,
unified = unified,
)
}
if (self.unified != unified) {
error("unified not match. res_name=$resName")
}
self.addCodePoints(list_code)
self.addShortnames(list_name)
}
private fun copyImages() {
log.d("count mastodonSvg=${mastodonSvg.size}")
log.d("count twemojiSvg =${twemojiSvg.size}")
log.d("count overrideSvg =${overrideSvg.size}")
log.d("count overridePng =${overridePng.size}")
log.d("count emojiDataPng =${emojiDataPng.size}")
log.d("converting svg... ${svgConverts.size}")
svgConverts.forEach { pair ->
val (dst, src) = pair
svgToVectorDrawable(dst, src)
}
log.d("converting png... ${pngConverts.size}")
pngConverts.forEach { pair ->
val (dst, src) = pair
val pb = ProcessBuilder(pathCwebp, src.path, "-quiet", "-o", dst.path)
val rv = pb.start().waitFor()
if (rv != 0) error("cwebp failed. dst=$dst src=$src")
}
}
private fun updateCodeMap() {
codeMap.clear()
resNameMap.values.forEach { res_info ->
res_info.codepointMap.keys.forEach { codeKey ->
codeMap.prepare(codeKey) { HashMap() }[res_info.res_name] = res_info
codeMap.prepare(codeKey.removeZWJ()) { HashMap() }[res_info.res_name] = res_info
}
}
}
private fun updateNameMap() {
nameMap.clear()
resNameMap.values.forEach { res_info ->
res_info.shortnames.forEach { name ->
nameMap.prepare(name) { HashMap() }[res_info.res_name] = res_info
}
}
}
private fun readEmojiData() {
File("./emoji-data/emoji.json")
.readAllBytes()
.decodeUtf8()
.decodeJsonArray()
.objectList()
.forEach { emoji ->
// short_name のリスト
val shortnames = ArrayList<String>().also{ dst->
emoji.string("short_name")?.parseShortName()?.addTo(dst)
emoji.stringArrayList("short_names")?.forEach {
it.parseShortName().addTo(dst)
}
}.notEmpty() ?: error("emojiData ${emoji.string("unified")} has no shortName")
// 絵文字のコードポイント一覧
val codepoints = ArrayList<CodepointList>().also{ dst->
emoji.string("unified")?.parseCodePoints()?.addTo(dst)
emoji.stringArrayList("variations")?.forEach {
it.parseCodePoints()?.addTo(dst)
}
for (k in emojiDataCodepointsVendors) {
emoji.string(k)?.parseCodePoints()?.addTo(dst)
}
}.notEmpty() ?: error("emojiData ${emoji.string("unified")} has no codeponts")
val shortName = shortnames.first()
registerResource(
unified = emoji.string("unified")!!.parseCodePoints()!!,
image = emoji.string("image").notEmpty()!!,
list_code = codepoints,
list_name = shortnames,
has_tone = emoji["skin_variations"] != null
)
// スキントーン
emoji.jsonObject("skin_variations")?.let { skinVariations ->
skin_tone_modifier.forEach { mod ->
val data = skinVariations.jsonObject(mod.code)
if (data == null) {
log.w("$shortName : missing skin tone ${mod.code}")
} else {
mod.suffixList.forEach { suffix ->
registerResource(
unified = data.string("unified")!!.parseCodePoints()!!,
image = data.string("image").notEmpty()!!,
list_code = listOf(data.string("unified").notEmpty()!!.parseCodePoints()!!),
list_name = shortnames.map { it + suffix },
isToneVariation = true,
)
}
}
}
}
}
}
// twemojiのsvgファイルを直接読む
private fun readTwemoji() {
File("twemoji/assets/svg").listFiles()?.forEach { file ->
val name = file.name
val code = reSvgFile.find(name)?.groupValues?.elementAtOrNull(1)?.toLowerCase()
if (code == null || codeMap.containsKey(code)) return@forEach
val unified = code.parseCodePoints()!!
log.d("twemoji $unified")
registerResource(
// FIXME: add shortName
unified = unified,
image = code,
list_code = listOf(unified),
list_name = emptyList(),
)
}
}
private fun readOldEmojione() {
val root = File("./old-emojione.json")
.readAllBytes()
.decodeUtf8()
.decodeJsonObject()
val oldNames = HashMap<String, JsonObject>()
val lostCodes = HashMap<String, String>()
for ( (code,item) in root.entries) {
if( item !is JsonObject) continue
item["_code"] = code
// 名前を集めておく
val names = ArrayList<String>().also { item["names"] = it }
item.string("alpha code")?.parseAlphaCode()?.notEmpty()?.addTo(names)
item.string("aliases")?.split("|")?.forEach {
it.parseAlphaCode().notEmpty()?.addTo(names)
}
names.forEach { oldNames[it] = item }
if( names.isEmpty()) error("readOldEmojione: missing name for code $code")
// コードを確認する
val code2 = code.removeZWJ()
val rh = codeMap[code2]
if (rh != null) {
for (res_info in rh.values) {
for (c in arrayOf(code, code2)) {
res_info.codepointMap[c] = c.parseCodePoints()!!
}
names.forEach { it.addTo(res_info.shortnames) }
}
continue
} else {
// 該当するコードがないので、emojioneの画像を持ってくる
lostCodes[code] = names.joinToString(",")
registerResourceEmojione(
unified = code.parseCodePoints()!!,
image = code,
list_code = listOf(code.parseCodePoints()!!),
list_name = names
)
}
}
updateCodeMap()
updateNameMap()
val lost_names = HashMap<String, String>()
for ((name, item) in oldNames) {
if (!nameMap.containsKey(name))
lost_names[name] = item.string("_code")!!
}
for (code in lostCodes.keys.sorted()) {
log.w("old-emojione: load old emojione code $code ${lostCodes[code]}")
}
for (name in lost_names.keys.sorted()) {
log.w("old-emojione: lost name $name ${lost_names[name]}")
}
}
suspend fun run(){
HttpClient {
install(HttpTimeout) {
val t = 30000L
requestTimeoutMillis = t
connectTimeoutMillis = t
socketTimeoutMillis = t
}
}.use { client ->
// emoji_data のデータを読む
readEmojiData()
emojiVariants.forEach { variant ->
if (variant.used.size > 0)
log.d("variant: ${variant.dir} ${variant.used.joinToString(",")}")
}
// twemojiにはemoji_dataより多くの絵文字が登録されている
updateCodeMap()
updateNameMap()
readTwemoji()
// 古いemojioneのデータを読む
updateCodeMap()
updateNameMap()
readOldEmojione()
// 画像データのコピー
copyImages()
// 重複チェック
val fix_code = ArrayList<Pair<String, String>>()
val fix_name = ArrayList<Pair<String, String>>()
val fix_category = ArrayList<Pair<String, String>>()
val reComment = """#.*""".toRegex()
val fixFile = "./fix_code.txt"
File(fixFile).forEachLine { lno, rawLine ->
val line = rawLine
.replace(reComment, "")
.trim()
val mr = """\A(\w+)\s*(\w+)\s*(.*)""".toRegex().find(line)
if (mr != null) {
val type = mr.groupValues[1]
val key = mr.groupValues[2]
val data = """([\w+-]+)""".toRegex().findAll(mr.groupValues[3]).map { it.groupValues[1] }.toList()
if (data.size != 1) return@forEachLine
when (type) {
"code" -> Pair(key, data.first()).addTo(fix_code)
"name" -> Pair(key, data.first()).addTo(fix_name)
"category" -> Pair(key, data.first()).addTo(fix_category)
else -> error("$fixFile $lno : bad fix_data type=$type")
}
}
}
updateCodeMap()
updateNameMap()
// あるUnicodeが指す絵文字画像を1種類だけにする
for ((code, selected_res_name) in fix_code) {
val rh = codeMap[code]
if (rh == null) {
log.w("fix_code: code_map[$code] is null")
continue
}
var found = false
for ((res_name, res_info) in rh.entries.sortedBy { it.key }) {
if (res_name == selected_res_name) {
found = true
} else {
log.w("fix_code: remove $code from $res_name")
res_info.codepointMap.remove(code)
}
}
if (!found) log.w("fix_code: missing relation for $code and $selected_res_name")
}
updateCodeMap()
updateNameMap()
// あるshortcodeが指す絵文字画像を1種類だけにする
for ((shortcode, selected_res_name) in fix_name) {
val rh = nameMap[shortcode]
if (rh == null) {
val resInfo = resNameMap[ selected_res_name]
if(resInfo==null){
log.w("fix_name: missing both of shortcode=$shortcode,resName=$selected_res_name")
}else{
// ないなら追加する
resInfo.shortnames.add(shortcode)
if( shortcode.indexOf("_skin_tone") != -1){
resInfo.isToneVariation = true
}
}
}else{
var found = false
for ((res_name, res_info) in rh.entries.sortedBy { it.key }) {
if (res_name == selected_res_name) {
found = true
} else {
log.w("fix_name: remove $shortcode from $res_name")
res_info.shortnames.remove(shortcode)
}
}
if (!found) log.w("fix_name: missing relation for $shortcode and $selected_res_name")
}
}
updateCodeMap()
updateNameMap()
// 絵文字のショートネームを外部から拾ってくる
for( url in arrayOf("https://unicode.org/Public/emoji/13.1/emoji-sequences.txt",
"https://unicode.org/Public/emoji/13.1/emoji-zwj-sequences.txt")
){
client.cachedGetString(url,mapOf())
.split("""[\x0d\x0a]""".toRegex())
.forEach { rawLine->
val line = rawLine.replace(reComment,"").trim()
if(line.isEmpty()) return@forEach
val cols = line.split(";",limit = 3).map{it.trim()}
if( cols.size == 3 ){
val(codeSpec,_,descriptionSpec) = cols
if(codeSpec.indexOf("..")!=-1) return@forEach
val shortname = descriptionSpec.toLowerCase()
.replace("medium-light skin tone","medium_light_skin_tone")
.replace("medium skin tone","medium_skin_tone")
.replace("medium-dark skin tone","medium_dark_skin_tone")
.replace("light skin tone","light_skin_tone")
.replace("dark skin tone","dark_skin_tone")
.replace("""[^\w\d]+""".toRegex(),"_")
val codePoints = codeSpec.parseCodePoints()
val resInfo = codeMap[codePoints.toString()]?.values?.firstOrNull()
if( resInfo==null){
log.w("missing resource for codepoint=${codePoints.toString()} shortname=$shortname")
}else if( resInfo.shortnames.isEmpty()){
resInfo.shortnames.add(shortname)
if( shortname.indexOf("_skin_tone") != -1){
resInfo.isToneVariation = true
}
}
}
}
}
updateCodeMap()
updateNameMap()
val nameChars = HashSet<Char>()
var nameConflict = false
for ((name, rh) in nameMap.entries) {
name.forEach { nameChars.add(it) }
val resList = rh.values
if (resList.size != 1) {
log.w("name $name has multiple resource. ${resList.map { it.res_name }.joinToString(",")}")
nameConflict = true
}
}
log.w("nameChars: [${nameChars.sorted().joinToString("")}]")
if (nameConflict) log.e("please fix name=>resource conflicts.")
for ((code, rh) in codeMap.entries.sortedBy { it.key }) {
val resList = rh.values
if (resList.size != 1) {
log.w("code $code ${
resList.joinToString(",") {
it.res_name
}
} # / ${
resList.joinToString(" / ") {
"${it.unified} ${it.unified.toRawString()}"
}
}")
}
}
categoryNameMapping.values.forEach { category ->
if( category.url !=null){
val root = client.cachedGetString(category.url, mapOf()).parseHtml(category.url)
val list = root.getElementsByClass("emoji-list").first()
list.getElementsByTag("li").forEach { node ->
val shortName = node.getElementsByTag("a")!!.attr("href")
.replace("/", "").parseShortName()
val span = node.getElementsByClass("emoji").text()
.toCodePointList().joinToString("-") { String.format("%04x", it) }
val resInfo = codeMap[span]?.values?.first()
?: nameMap[shortName]?.values?.firstOrNull()
if (resInfo == null) {
log.e("missing ${category.enumId} $shortName $span ")
} else {
resInfo.categories.add(category)
category.shortcodes.add(resInfo.shortnames.first())
}
}
}
}
for( (enumId,shortcode) in fix_category) {
val category = categoryNameMapping.values.find { it.enumId == enumId }
if (category == null) {
log.w("fix_category: missing category $enumId")
continue
}
var found = false
nameMap[shortcode.parseShortName()]?.values?.forEach { resInfo->
resInfo.categories.add(category)
category.shortcodes.add(resInfo.shortnames.first())
found=true
}
if(!found){
// 見つからない場合のみ、画像リソース名でもカテゴリを指定できるようにする
resNameMap[shortcode]?.let{ resInfo->
if(resInfo.categories.isEmpty()){
resInfo.categories.add(category)
if(resInfo.shortnames.isEmpty()){
resInfo.shortnames.add( resInfo.res_name)
}
category.shortcodes.add( resInfo.shortnames.first())
}
}
}
}
for ((res_name, res_info) in resNameMap.entries) {
if (res_info.isToneVariation) continue
val shortnames = res_info.shortnames.sorted().joinToString(",")
if (shortnames.isEmpty()) {
log.w("missing shortnames for res_name=$res_name")
}
}
val missing = ArrayList<String>()
for ((_, res_info) in resNameMap.entries) {
if (res_info.isToneVariation) continue
val shortnames = res_info.shortnames.sorted().joinToString(",")
if (shortnames.isNotEmpty() && res_info.categories.isEmpty()) {
missing.add(shortnames)
}
}
missing.sorted().forEach {
log.w("missing category: $it")
}
// JSONコードを出力する
val out_file = "EmojiData202102.java"
PrintWriter(
OutputStreamWriter(
BufferedOutputStream(FileOutputStream(File(out_file))),
Charsets.UTF_8
)
).use { stream ->
val jcw = JavaCodeWriter(stream)
// 画像リソースIDとUnidoceシーケンスの関連付けを出力する
for ((res_name, res_info) in resNameMap.entries.sortedBy { it.key }) {
for ((_, codepoints) in res_info.codepointMap.entries.sortedBy { it.key }) {
val javaChars = codepoints.makeUtf16()
if (File("assets/$res_name.svg").isFile) {
jcw.addCode("code(\"$javaChars\", \"$res_name.svg\");")
} else {
jcw.addCode("code(\"$javaChars\", R.drawable.$res_name);")
}
}
}
// 画像リソースIDとshortcodeの関連付けを出力する
// 投稿時にshortcodeをユニコードに変換するため、shortcodeとUTF-16シーケンスの関連付けを出力する
for ((name, rh) in nameMap.entries.sortedBy { it.key }) {
val resInfo = rh.values.first()
val utf16Unified = resInfo.unified.makeUtf16()
jcw.addCode("name(\"$name\", \"$utf16Unified\");")
}
categoryNameMapping.values.forEach { category ->
for( code in category.shortcodes) {
jcw.addCode("category( ${category.enumId}, \"$code\");")
}
}
jcw.closeFunction()
jcw.writeDefinition("public static final int utf16_max_length=$utf16_max_length;")
jcw.writeInitializer()
}
log.d("wrote $out_file")
// shortname => unicode
JsonArray()
.also { dst ->
for ((name, rh) in nameMap.entries.sortedBy { it.key }) {
val resInfo = rh.values.first()
dst.add(jsonObject("shortcode" to name, "unicode" to resInfo.unified))
}
}
.toString(2)
.encodeUtf8()
.saveTo(File("shortcode-emoji-data-and-old-emojione2.json"))
}
}
}
fun main(args: Array<String>) =runBlocking{
App().run()
}

View File

@ -0,0 +1,241 @@
package jp.juggler.subwaytooter.emoji
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import org.jsoup.Jsoup
import java.io.*
import java.nio.charset.Charset
import java.security.MessageDigest
import java.util.*
fun String.isTruth() = when {
this == "" -> false
this == "0" -> false
this.startsWith("f", ignoreCase = true) -> false
this.startsWith("t", ignoreCase = true) -> true
this == "on" -> true
else -> true
}
// split CharSequence to Unicode codepoints
fun CharSequence.eachCodePoint(block: (Int) -> Unit) {
val end = length
var i = 0
while (i < end) {
val c1 = get(i++)
if (Character.isHighSurrogate(c1) && i < length) {
val c2 = get(i)
if (Character.isLowSurrogate(c2)) {
i++
block(Character.toCodePoint(c1, c2))
continue
}
}
block(c1.toInt())
}
}
// split CharSequence to Unicode codepoints
fun CharSequence.toCodePointList() = ArrayList<Int>().also{ dst->
val end = length
var i = 0
while (i < end) {
val c1 = get(i++)
if (Character.isHighSurrogate(c1) && i < length) {
val c2 = get(i)
if (Character.isLowSurrogate(c2)) {
i++
dst.add(Character.toCodePoint(c1, c2))
continue
}
}
dst.add(c1.toInt())
}
}
// split codepoint to UTF-8 bytes
fun codePointToUtf8(cp: Int, block: (Int) -> Unit) {
// incorrect codepoint
if (cp < 0 || cp > 0x10FFFF) codePointToUtf8('?'.toInt(), block)
if (cp >= 128) {
if (cp >= 2048) {
if (cp >= 65536) {
block(0xF0.or(cp.shr(18)))
block(0x80.or(cp.shr(12).and(0x3f)))
} else {
block(0xE0.or(cp.shr(12)))
}
block(0x80.or(cp.shr(6).and(0x3f)))
} else {
block(0xC0.or(cp.shr(6)))
}
block(0x80.or(cp.and(0x3f)))
} else {
block(cp)
}
}
private const val hexString = "0123456789ABCDEF"
private val encodePercentSkipChars by lazy {
HashSet<Int>().apply {
('0'..'9').forEach { add(it.toInt()) }
('A'..'Z').forEach { add(it.toInt()) }
('a'..'z').forEach { add(it.toInt()) }
add('-'.toInt())
add('_'.toInt())
add('.'.toInt())
}
}
fun String.encodePercent(): String =
StringBuilder(length).also { sb ->
eachCodePoint { cp ->
if (encodePercentSkipChars.contains(cp)) {
sb.append(cp.toChar())
} else {
codePointToUtf8(cp) { b ->
sb.append('%')
.append(hexString[b shr 4])
.append(hexString[b and 15])
}
}
}
}.toString()
// same as x?.let{ dst.add(it) }
fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this)
fun <T> T.addTo(dst: HashSet<T>) = dst.add(this)
fun <E : List<*>> E?.notEmpty(): E? =
if (this?.isNotEmpty() == true) this else null
fun <E : Map<*, *>> E?.notEmpty(): E? =
if (this?.isNotEmpty() == true) this else null
fun <T : CharSequence> T?.notEmpty(): T? =
if (this?.isNotEmpty() == true) this else null
fun ByteArray.digestSha256() =
MessageDigest.getInstance("SHA-256")?.let {
it.update(this@digestSha256)
it.digest()
}!!
fun ByteArray.encodeBase64UrlSafe(): String {
val bytes = Base64.getUrlEncoder().encode(this)
return StringBuilder(bytes.size).apply {
for (b in bytes) {
val c = b.toChar()
if (c != '=') append(c)
}
}.toString()
}
fun ByteArray.decodeUtf8() = toString(Charsets.UTF_8)
fun String.encodeUtf8() = toByteArray(Charsets.UTF_8)
inline fun <reified T> Any?.castOrThrow(name:String,block: T.() -> Unit){
if (this !is T) error("type mismatch. $name is ${T::class.qualifiedName}")
block()
}
// 型推論できる文脈だと型名を書かずにすむ
@Suppress("unused")
inline fun <reified T : Any> Any?.cast(): T? = this as? T
@Suppress("unused")
inline fun <reified T : Any> Any.castNotNull(): T = this as T
fun <T : Comparable<T>> minComparable(a: T, b: T): T = if (a <= b) a else b
fun <T : Comparable<T>> maxComparable(a: T, b: T): T = if (a >= b) a else b
fun <T : Any> MutableCollection<T>.removeFirst(check: (T) -> Boolean): T? {
val it = iterator()
while (it.hasNext()) {
val item = it.next()
if (check(item)) {
it.remove()
return item
}
}
return null
}
fun File.readAllBytes() =
FileInputStream(this).use { it.readBytes() }
fun File.save(data: ByteArray) {
val tmpFile = File("$absolutePath.tmp")
FileOutputStream(tmpFile).use { it.write(data) }
this.delete()
if (!tmpFile.renameTo(this)) error("$this: rename failed.")
}
fun ByteArray.saveTo(file: File) = file.save(this)
fun File.forEachLine(charset: Charset = Charsets.UTF_8, block:(Int, String)->Unit)=
BufferedReader(InputStreamReader(FileInputStream(this),charset)).use { reader ->
var lno = 0
reader.forEachLine {
block(++lno, it)
}
lno
}
inline fun <K,V> HashMap<K,V>.prepare(key:K,creator:()->V):V{
var value = get(key)
if( value == null) {
value = creator()
put(key,value)
}
return value!!
}
private val reFileNameBadChars = """[\\/:*?"<>|-]+""".toRegex()
private val cacheDir by lazy{ File("./cache").apply { mkdirs() }}
fun clearCache(){
cacheDir.list()?.forEach { name->
File(cacheDir,name).takeIf { it.isFile }?.delete()
}
}
private val cacheExpire by lazy{ 8 * 3600000L }
suspend fun HttpClient.cachedGetBytes(url: String, headers: Map<String, String>): ByteArray {
val fName = reFileNameBadChars.replace(url, "-")
val cacheFile = File(cacheDir, fName)
if (System.currentTimeMillis() - cacheFile.lastModified() <= cacheExpire) {
println("GET(cached) $url")
return cacheFile.readAllBytes()
}
println("GET $url")
get<HttpResponse>(url) {
headers.entries.forEach {
header(it.key, it.value)
}
}.let { res ->
return when (res.status) {
HttpStatusCode.OK ->
res.receive<ByteArray>().also { it.saveTo(cacheFile) }
else -> {
cacheFile.delete()
error("get failed. $url ${res.status}")
}
}
}
}
suspend fun HttpClient.cachedGetString(url: String, headers: Map<String, String>): String =
cachedGetBytes(url,headers).decodeUtf8()
fun String.parseHtml(baseUri: String) =
Jsoup.parse(this, baseUri)

View File

@ -185,7 +185,13 @@ class EmojiPicker(
R.string.emoji_category_flags
)
)
page_list.add(
EmojiPickerPage(
true,
EmojiMap.CATEGORY_OTHER,
R.string.emoji_category_others
)
)
this.viewRoot = activity.layoutInflater.inflate(R.layout.dlg_picker_emoji, null, false)
this.pager = viewRoot.findViewById(R.id.pager)
this.pager_strip = viewRoot.findViewById(R.id.pager_strip)

View File

@ -530,6 +530,7 @@
<string name="emoji_category_objects">Objects</string>
<string name="emoji_category_symbols">Symbols</string>
<string name="emoji_category_flags">Flags</string>
<string name="emoji_category_others">Others</string>
<string name="emoji_category_custom">Custom</string>
<string name="emoji_category_recent">Recent</string>
<string name="open_picker_emoji">Emoji picker…</string>

File diff suppressed because it is too large Load Diff