Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.util;
     18 
     19 import com.android.ide.common.rendering.api.LayoutLog;
     20 import com.android.layoutlib.bridge.Bridge;
     21 import com.android.layoutlib.bridge.impl.DelegateManager;
     22 import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
     23 
     24 import android.annotation.NonNull;
     25 import android.graphics.Path_Delegate;
     26 
     27 import java.util.ArrayList;
     28 import java.util.Arrays;
     29 import java.util.logging.Level;
     30 import java.util.logging.Logger;
     31 
     32 /**
     33  * Delegate that provides implementation for native methods in {@link android.util.PathParser}
     34  * <p/>
     35  * Through the layoutlib_create tool, selected methods of PathParser have been replaced by calls to
     36  * methods of the same name in this delegate class.
     37  *
     38  * Most of the code has been taken from the implementation in
     39  * {@code tools/base/sdk-common/src/main/java/com/android/ide/common/vectordrawable/PathParser.java}
     40  * revision be6fe89a3b686db5a75e7e692a148699973957f3
     41  */
     42 public class PathParser_Delegate {
     43 
     44     private static final Logger LOGGER = Logger.getLogger("PathParser");
     45 
     46     // ---- Builder delegate manager ----
     47     private static final DelegateManager<PathParser_Delegate> sManager =
     48             new DelegateManager<PathParser_Delegate>(PathParser_Delegate.class);
     49 
     50     // ---- delegate data ----
     51     @NonNull
     52     private PathDataNode[] mPathDataNodes;
     53 
     54     public static PathParser_Delegate getDelegate(long nativePtr) {
     55         return sManager.getDelegate(nativePtr);
     56     }
     57 
     58     private PathParser_Delegate(@NonNull PathDataNode[] nodes) {
     59         mPathDataNodes = nodes;
     60     }
     61 
     62     public PathDataNode[] getPathDataNodes() {
     63         return mPathDataNodes;
     64     }
     65 
     66     @LayoutlibDelegate
     67     /*package*/ static void nParseStringForPath(long pathPtr, @NonNull String pathString, int
     68             stringLength) {
     69         Path_Delegate path_delegate = Path_Delegate.getDelegate(pathPtr);
     70         if (path_delegate == null) {
     71             return;
     72         }
     73         assert pathString.length() == stringLength;
     74         PathDataNode.nodesToPath(createNodesFromPathData(pathString), path_delegate);
     75     }
     76 
     77     @LayoutlibDelegate
     78     /*package*/ static void nCreatePathFromPathData(long outPathPtr, long pathData) {
     79         Path_Delegate path_delegate = Path_Delegate.getDelegate(outPathPtr);
     80         PathParser_Delegate source = sManager.getDelegate(outPathPtr);
     81         if (source == null || path_delegate == null) {
     82             return;
     83         }
     84         PathDataNode.nodesToPath(source.mPathDataNodes, path_delegate);
     85     }
     86 
     87     @LayoutlibDelegate
     88     /*package*/ static long nCreateEmptyPathData() {
     89         PathParser_Delegate newDelegate = new PathParser_Delegate(new PathDataNode[0]);
     90         return sManager.addNewDelegate(newDelegate);
     91     }
     92 
     93     @LayoutlibDelegate
     94     /*package*/ static long nCreatePathData(long nativePtr) {
     95         PathParser_Delegate source = sManager.getDelegate(nativePtr);
     96         if (source == null) {
     97             return 0;
     98         }
     99         PathParser_Delegate dest = new PathParser_Delegate(deepCopyNodes(source.mPathDataNodes));
    100         return sManager.addNewDelegate(dest);
    101     }
    102 
    103     @LayoutlibDelegate
    104     /*package*/ static long nCreatePathDataFromString(@NonNull String pathString,
    105             int stringLength) {
    106         assert pathString.length() == stringLength : "Inconsistent path string length.";
    107         PathDataNode[] nodes = createNodesFromPathData(pathString);
    108         PathParser_Delegate delegate = new PathParser_Delegate(nodes);
    109         return sManager.addNewDelegate(delegate);
    110 
    111     }
    112 
    113     @LayoutlibDelegate
    114     /*package*/ static boolean nInterpolatePathData(long outDataPtr, long fromDataPtr,
    115             long toDataPtr, float fraction) {
    116         PathParser_Delegate out = sManager.getDelegate(outDataPtr);
    117         PathParser_Delegate from = sManager.getDelegate(fromDataPtr);
    118         PathParser_Delegate to = sManager.getDelegate(toDataPtr);
    119         if (out == null || from == null || to == null) {
    120             return false;
    121         }
    122         int length = from.mPathDataNodes.length;
    123         if (length != to.mPathDataNodes.length) {
    124             Bridge.getLog().error(LayoutLog.TAG_BROKEN,
    125                     "Cannot interpolate path data with different lengths (from " + length + " to " +
    126                             to.mPathDataNodes.length + ").", null);
    127             return false;
    128         }
    129         if (out.mPathDataNodes.length != length) {
    130             out.mPathDataNodes = new PathDataNode[length];
    131         }
    132         for (int i = 0; i < length; i++) {
    133             if (out.mPathDataNodes[i] == null) {
    134                 out.mPathDataNodes[i] = new PathDataNode(from.mPathDataNodes[i]);
    135             }
    136             out.mPathDataNodes[i].interpolatePathDataNode(from.mPathDataNodes[i],
    137                         to.mPathDataNodes[i], fraction);
    138         }
    139         return true;
    140     }
    141 
    142     @LayoutlibDelegate
    143     /*package*/ static void nFinalize(long nativePtr) {
    144         sManager.removeJavaReferenceFor(nativePtr);
    145     }
    146 
    147     @LayoutlibDelegate
    148     /*package*/ static boolean nCanMorph(long fromDataPtr, long toDataPtr) {
    149         PathParser_Delegate fromPath = PathParser_Delegate.getDelegate(fromDataPtr);
    150         PathParser_Delegate toPath = PathParser_Delegate.getDelegate(toDataPtr);
    151         if (fromPath == null || toPath == null || fromPath.getPathDataNodes() == null || toPath
    152                 .getPathDataNodes() == null) {
    153             return true;
    154         }
    155         return PathParser_Delegate.canMorph(fromPath.getPathDataNodes(), toPath.getPathDataNodes());
    156     }
    157 
    158     @LayoutlibDelegate
    159     /*package*/ static void nSetPathData(long outDataPtr, long fromDataPtr) {
    160         PathParser_Delegate out = sManager.getDelegate(outDataPtr);
    161         PathParser_Delegate from = sManager.getDelegate(fromDataPtr);
    162         if (from == null || out == null) {
    163             return;
    164         }
    165         out.mPathDataNodes = deepCopyNodes(from.mPathDataNodes);
    166     }
    167 
    168     /**
    169      * @param pathData The string representing a path, the same as "d" string in svg file.
    170      *
    171      * @return an array of the PathDataNode.
    172      */
    173     @NonNull
    174     public static PathDataNode[] createNodesFromPathData(@NonNull String pathData) {
    175         int start = 0;
    176         int end = 1;
    177 
    178         ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
    179         while (end < pathData.length()) {
    180             end = nextStart(pathData, end);
    181             String s = pathData.substring(start, end).trim();
    182             if (s.length() > 0) {
    183                 float[] val = getFloats(s);
    184                 addNode(list, s.charAt(0), val);
    185             }
    186 
    187             start = end;
    188             end++;
    189         }
    190         if ((end - start) == 1 && start < pathData.length()) {
    191             addNode(list, pathData.charAt(start), new float[0]);
    192         }
    193         return list.toArray(new PathDataNode[list.size()]);
    194     }
    195 
    196     /**
    197      * @param source The array of PathDataNode to be duplicated.
    198      *
    199      * @return a deep copy of the <code>source</code>.
    200      */
    201     @NonNull
    202     public static PathDataNode[] deepCopyNodes(@NonNull PathDataNode[] source) {
    203         PathDataNode[] copy = new PathDataNode[source.length];
    204         for (int i = 0; i < source.length; i++) {
    205             copy[i] = new PathDataNode(source[i]);
    206         }
    207         return copy;
    208     }
    209 
    210     /**
    211      * @param nodesFrom The source path represented in an array of PathDataNode
    212      * @param nodesTo The target path represented in an array of PathDataNode
    213      * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
    214      */
    215     public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) {
    216         if (nodesFrom == null || nodesTo == null) {
    217             return false;
    218         }
    219 
    220         if (nodesFrom.length != nodesTo.length) {
    221             return false;
    222         }
    223 
    224         for (int i = 0; i < nodesFrom.length; i ++) {
    225             if (nodesFrom[i].mType != nodesTo[i].mType
    226                     || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) {
    227                 return false;
    228             }
    229         }
    230         return true;
    231     }
    232 
    233     /**
    234      * Update the target's data to match the source.
    235      * Before calling this, make sure canMorph(target, source) is true.
    236      *
    237      * @param target The target path represented in an array of PathDataNode
    238      * @param source The source path represented in an array of PathDataNode
    239      */
    240     public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
    241         for (int i = 0; i < source.length; i ++) {
    242             target[i].mType = source[i].mType;
    243             for (int j = 0; j < source[i].mParams.length; j ++) {
    244                 target[i].mParams[j] = source[i].mParams[j];
    245             }
    246         }
    247     }
    248 
    249     private static int nextStart(@NonNull String s, int end) {
    250         char c;
    251 
    252         while (end < s.length()) {
    253             c = s.charAt(end);
    254             // Note that 'e' or 'E' are not valid path commands, but could be
    255             // used for floating point numbers' scientific notation.
    256             // Therefore, when searching for next command, we should ignore 'e'
    257             // and 'E'.
    258             if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
    259                     && c != 'e' && c != 'E') {
    260                 return end;
    261             }
    262             end++;
    263         }
    264         return end;
    265     }
    266 
    267     /**
    268      * Calculate the position of the next comma or space or negative sign
    269      *
    270      * @param s the string to search
    271      * @param start the position to start searching
    272      * @param result the result of the extraction, including the position of the the starting
    273      * position of next number, whether it is ending with a '-'.
    274      */
    275     private static void extract(@NonNull String s, int start, @NonNull ExtractFloatResult result) {
    276         // Now looking for ' ', ',', '.' or '-' from the start.
    277         int currentIndex = start;
    278         boolean foundSeparator = false;
    279         result.mEndWithNegOrDot = false;
    280         boolean secondDot = false;
    281         boolean isExponential = false;
    282         for (; currentIndex < s.length(); currentIndex++) {
    283             boolean isPrevExponential = isExponential;
    284             isExponential = false;
    285             char currentChar = s.charAt(currentIndex);
    286             switch (currentChar) {
    287                 case ' ':
    288                 case ',':
    289                 case '\t':
    290                 case '\n':
    291                     foundSeparator = true;
    292                     break;
    293                 case '-':
    294                     // The negative sign following a 'e' or 'E' is not a separator.
    295                     if (currentIndex != start && !isPrevExponential) {
    296                         foundSeparator = true;
    297                         result.mEndWithNegOrDot = true;
    298                     }
    299                     break;
    300                 case '.':
    301                     if (!secondDot) {
    302                         secondDot = true;
    303                     } else {
    304                         // This is the second dot, and it is considered as a separator.
    305                         foundSeparator = true;
    306                         result.mEndWithNegOrDot = true;
    307                     }
    308                     break;
    309                 case 'e':
    310                 case 'E':
    311                     isExponential = true;
    312                     break;
    313             }
    314             if (foundSeparator) {
    315                 break;
    316             }
    317         }
    318         // When there is nothing found, then we put the end position to the end
    319         // of the string.
    320         result.mEndPosition = currentIndex;
    321     }
    322 
    323     /**
    324      * Parse the floats in the string. This is an optimized version of
    325      * parseFloat(s.split(",|\\s"));
    326      *
    327      * @param s the string containing a command and list of floats
    328      *
    329      * @return array of floats
    330      */
    331     @NonNull
    332     private static float[] getFloats(@NonNull String s) {
    333         if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
    334             return new float[0];
    335         }
    336         try {
    337             float[] results = new float[s.length()];
    338             int count = 0;
    339             int startPosition = 1;
    340             int endPosition;
    341 
    342             ExtractFloatResult result = new ExtractFloatResult();
    343             int totalLength = s.length();
    344 
    345             // The startPosition should always be the first character of the
    346             // current number, and endPosition is the character after the current
    347             // number.
    348             while (startPosition < totalLength) {
    349                 extract(s, startPosition, result);
    350                 endPosition = result.mEndPosition;
    351 
    352                 if (startPosition < endPosition) {
    353                     results[count++] = Float.parseFloat(
    354                             s.substring(startPosition, endPosition));
    355                 }
    356 
    357                 if (result.mEndWithNegOrDot) {
    358                     // Keep the '-' or '.' sign with next number.
    359                     startPosition = endPosition;
    360                 } else {
    361                     startPosition = endPosition + 1;
    362                 }
    363             }
    364             return Arrays.copyOf(results, count);
    365         } catch (NumberFormatException e) {
    366             assert false : "error in parsing \"" + s + "\"" + e;
    367             return new float[0];
    368         }
    369     }
    370 
    371 
    372     private static void addNode(@NonNull ArrayList<PathDataNode> list, char cmd,
    373             @NonNull float[] val) {
    374         list.add(new PathDataNode(cmd, val));
    375     }
    376 
    377     private static class ExtractFloatResult {
    378         // We need to return the position of the next separator and whether the
    379         // next float starts with a '-' or a '.'.
    380         private int mEndPosition;
    381         private boolean mEndWithNegOrDot;
    382     }
    383 
    384     /**
    385      * Each PathDataNode represents one command in the "d" attribute of the svg file. An array of
    386      * PathDataNode can represent the whole "d" attribute.
    387      */
    388     public static class PathDataNode {
    389         private char mType;
    390         @NonNull
    391         private float[] mParams;
    392 
    393         private PathDataNode(char type, @NonNull float[] params) {
    394             mType = type;
    395             mParams = params;
    396         }
    397 
    398         public char getType() {
    399             return mType;
    400         }
    401 
    402         @NonNull
    403         public float[] getParams() {
    404             return mParams;
    405         }
    406 
    407         private PathDataNode(@NonNull PathDataNode n) {
    408             mType = n.mType;
    409             mParams = Arrays.copyOf(n.mParams, n.mParams.length);
    410         }
    411 
    412         /**
    413          * Convert an array of PathDataNode to Path. Reset the passed path as needed before
    414          * calling this method.
    415          *
    416          * @param node The source array of PathDataNode.
    417          * @param path The target Path object.
    418          */
    419         public static void nodesToPath(@NonNull PathDataNode[] node, @NonNull Path_Delegate path) {
    420             float[] current = new float[6];
    421             char previousCommand = 'm';
    422             //noinspection ForLoopReplaceableByForEach
    423             for (int i = 0; i < node.length; i++) {
    424                 addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
    425                 previousCommand = node[i].mType;
    426             }
    427         }
    428 
    429         /**
    430          * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and
    431          * <code>nodeTo</code> according to the <code>fraction</code>.
    432          *
    433          * @param nodeFrom The start value as a PathDataNode.
    434          * @param nodeTo The end value as a PathDataNode
    435          * @param fraction The fraction to interpolate.
    436          */
    437         private void interpolatePathDataNode(@NonNull PathDataNode nodeFrom,
    438                 @NonNull PathDataNode nodeTo, float fraction) {
    439             for (int i = 0; i < nodeFrom.mParams.length; i++) {
    440                 mParams[i] = nodeFrom.mParams[i] * (1 - fraction)
    441                         + nodeTo.mParams[i] * fraction;
    442             }
    443         }
    444 
    445         @SuppressWarnings("PointlessArithmeticExpression")
    446         private static void addCommand(@NonNull Path_Delegate path, float[] current,
    447                 char previousCmd, char cmd, @NonNull float[] val) {
    448 
    449             int incr = 2;
    450             float currentX = current[0];
    451             float currentY = current[1];
    452             float ctrlPointX = current[2];
    453             float ctrlPointY = current[3];
    454             float currentSegmentStartX = current[4];
    455             float currentSegmentStartY = current[5];
    456             float reflectiveCtrlPointX;
    457             float reflectiveCtrlPointY;
    458 
    459             switch (cmd) {
    460                 case 'z':
    461                 case 'Z':
    462                     path.close();
    463                     // Path is closed here, but we need to move the pen to the
    464                     // closed position. So we cache the segment's starting position,
    465                     // and restore it here.
    466                     currentX = currentSegmentStartX;
    467                     currentY = currentSegmentStartY;
    468                     ctrlPointX = currentSegmentStartX;
    469                     ctrlPointY = currentSegmentStartY;
    470                     path.moveTo(currentX, currentY);
    471                     break;
    472                 case 'm':
    473                 case 'M':
    474                 case 'l':
    475                 case 'L':
    476                 case 't':
    477                 case 'T':
    478                     incr = 2;
    479                     break;
    480                 case 'h':
    481                 case 'H':
    482                 case 'v':
    483                 case 'V':
    484                     incr = 1;
    485                     break;
    486                 case 'c':
    487                 case 'C':
    488                     incr = 6;
    489                     break;
    490                 case 's':
    491                 case 'S':
    492                 case 'q':
    493                 case 'Q':
    494                     incr = 4;
    495                     break;
    496                 case 'a':
    497                 case 'A':
    498                     incr = 7;
    499                     break;
    500             }
    501 
    502             for (int k = 0; k < val.length; k += incr) {
    503                 switch (cmd) {
    504                     case 'm': // moveto - Start a new sub-path (relative)
    505                         currentX += val[k + 0];
    506                         currentY += val[k + 1];
    507 
    508                         if (k > 0) {
    509                             // According to the spec, if a moveto is followed by multiple
    510                             // pairs of coordinates, the subsequent pairs are treated as
    511                             // implicit lineto commands.
    512                             path.rLineTo(val[k + 0], val[k + 1]);
    513                         } else {
    514                             path.rMoveTo(val[k + 0], val[k + 1]);
    515                             currentSegmentStartX = currentX;
    516                             currentSegmentStartY = currentY;
    517                         }
    518                         break;
    519                     case 'M': // moveto - Start a new sub-path
    520                         currentX = val[k + 0];
    521                         currentY = val[k + 1];
    522 
    523                         if (k > 0) {
    524                             // According to the spec, if a moveto is followed by multiple
    525                             // pairs of coordinates, the subsequent pairs are treated as
    526                             // implicit lineto commands.
    527                             path.lineTo(val[k + 0], val[k + 1]);
    528                         } else {
    529                             path.moveTo(val[k + 0], val[k + 1]);
    530                             currentSegmentStartX = currentX;
    531                             currentSegmentStartY = currentY;
    532                         }
    533                         break;
    534                     case 'l': // lineto - Draw a line from the current point (relative)
    535                         path.rLineTo(val[k + 0], val[k + 1]);
    536                         currentX += val[k + 0];
    537                         currentY += val[k + 1];
    538                         break;
    539                     case 'L': // lineto - Draw a line from the current point
    540                         path.lineTo(val[k + 0], val[k + 1]);
    541                         currentX = val[k + 0];
    542                         currentY = val[k + 1];
    543                         break;
    544                     case 'h': // horizontal lineto - Draws a horizontal line (relative)
    545                         path.rLineTo(val[k + 0], 0);
    546                         currentX += val[k + 0];
    547                         break;
    548                     case 'H': // horizontal lineto - Draws a horizontal line
    549                         path.lineTo(val[k + 0], currentY);
    550                         currentX = val[k + 0];
    551                         break;
    552                     case 'v': // vertical lineto - Draws a vertical line from the current point (r)
    553                         path.rLineTo(0, val[k + 0]);
    554                         currentY += val[k + 0];
    555                         break;
    556                     case 'V': // vertical lineto - Draws a vertical line from the current point
    557                         path.lineTo(currentX, val[k + 0]);
    558                         currentY = val[k + 0];
    559                         break;
    560                     case 'c': // curveto - Draws a cubic Bzier curve (relative)
    561                         path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
    562                                 val[k + 4], val[k + 5]);
    563 
    564                         ctrlPointX = currentX + val[k + 2];
    565                         ctrlPointY = currentY + val[k + 3];
    566                         currentX += val[k + 4];
    567                         currentY += val[k + 5];
    568 
    569                         break;
    570                     case 'C': // curveto - Draws a cubic Bzier curve
    571                         path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
    572                                 val[k + 4], val[k + 5]);
    573                         currentX = val[k + 4];
    574                         currentY = val[k + 5];
    575                         ctrlPointX = val[k + 2];
    576                         ctrlPointY = val[k + 3];
    577                         break;
    578                     case 's': // smooth curveto - Draws a cubic Bzier curve (reflective cp)
    579                         reflectiveCtrlPointX = 0;
    580                         reflectiveCtrlPointY = 0;
    581                         if (previousCmd == 'c' || previousCmd == 's'
    582                                 || previousCmd == 'C' || previousCmd == 'S') {
    583                             reflectiveCtrlPointX = currentX - ctrlPointX;
    584                             reflectiveCtrlPointY = currentY - ctrlPointY;
    585                         }
    586                         path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
    587                                 val[k + 0], val[k + 1],
    588                                 val[k + 2], val[k + 3]);
    589 
    590                         ctrlPointX = currentX + val[k + 0];
    591                         ctrlPointY = currentY + val[k + 1];
    592                         currentX += val[k + 2];
    593                         currentY += val[k + 3];
    594                         break;
    595                     case 'S': // shorthand/smooth curveto Draws a cubic Bzier curve(reflective cp)
    596                         reflectiveCtrlPointX = currentX;
    597                         reflectiveCtrlPointY = currentY;
    598                         if (previousCmd == 'c' || previousCmd == 's'
    599                                 || previousCmd == 'C' || previousCmd == 'S') {
    600                             reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
    601                             reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
    602                         }
    603                         path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
    604                                 val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
    605                         ctrlPointX = val[k + 0];
    606                         ctrlPointY = val[k + 1];
    607                         currentX = val[k + 2];
    608                         currentY = val[k + 3];
    609                         break;
    610                     case 'q': // Draws a quadratic Bzier (relative)
    611                         path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
    612                         ctrlPointX = currentX + val[k + 0];
    613                         ctrlPointY = currentY + val[k + 1];
    614                         currentX += val[k + 2];
    615                         currentY += val[k + 3];
    616                         break;
    617                     case 'Q': // Draws a quadratic Bzier
    618                         path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
    619                         ctrlPointX = val[k + 0];
    620                         ctrlPointY = val[k + 1];
    621                         currentX = val[k + 2];
    622                         currentY = val[k + 3];
    623                         break;
    624                     case 't': // Draws a quadratic Bzier curve(reflective control point)(relative)
    625                         reflectiveCtrlPointX = 0;
    626                         reflectiveCtrlPointY = 0;
    627                         if (previousCmd == 'q' || previousCmd == 't'
    628                                 || previousCmd == 'Q' || previousCmd == 'T') {
    629                             reflectiveCtrlPointX = currentX - ctrlPointX;
    630                             reflectiveCtrlPointY = currentY - ctrlPointY;
    631                         }
    632                         path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
    633                                 val[k + 0], val[k + 1]);
    634                         ctrlPointX = currentX + reflectiveCtrlPointX;
    635                         ctrlPointY = currentY + reflectiveCtrlPointY;
    636                         currentX += val[k + 0];
    637                         currentY += val[k + 1];
    638                         break;
    639                     case 'T': // Draws a quadratic Bzier curve (reflective control point)
    640                         reflectiveCtrlPointX = currentX;
    641                         reflectiveCtrlPointY = currentY;
    642                         if (previousCmd == 'q' || previousCmd == 't'
    643                                 || previousCmd == 'Q' || previousCmd == 'T') {
    644                             reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
    645                             reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
    646                         }
    647                         path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
    648                                 val[k + 0], val[k + 1]);
    649                         ctrlPointX = reflectiveCtrlPointX;
    650                         ctrlPointY = reflectiveCtrlPointY;
    651                         currentX = val[k + 0];
    652                         currentY = val[k + 1];
    653                         break;
    654                     case 'a': // Draws an elliptical arc
    655                         // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
    656                         drawArc(path,
    657                                 currentX,
    658                                 currentY,
    659                                 val[k + 5] + currentX,
    660                                 val[k + 6] + currentY,
    661                                 val[k + 0],
    662                                 val[k + 1],
    663                                 val[k + 2],
    664                                 val[k + 3] != 0,
    665                                 val[k + 4] != 0);
    666                         currentX += val[k + 5];
    667                         currentY += val[k + 6];
    668                         ctrlPointX = currentX;
    669                         ctrlPointY = currentY;
    670                         break;
    671                     case 'A': // Draws an elliptical arc
    672                         drawArc(path,
    673                                 currentX,
    674                                 currentY,
    675                                 val[k + 5],
    676                                 val[k + 6],
    677                                 val[k + 0],
    678                                 val[k + 1],
    679                                 val[k + 2],
    680                                 val[k + 3] != 0,
    681                                 val[k + 4] != 0);
    682                         currentX = val[k + 5];
    683                         currentY = val[k + 6];
    684                         ctrlPointX = currentX;
    685                         ctrlPointY = currentY;
    686                         break;
    687                 }
    688                 previousCmd = cmd;
    689             }
    690             current[0] = currentX;
    691             current[1] = currentY;
    692             current[2] = ctrlPointX;
    693             current[3] = ctrlPointY;
    694             current[4] = currentSegmentStartX;
    695             current[5] = currentSegmentStartY;
    696         }
    697 
    698         private static void drawArc(@NonNull Path_Delegate p, float x0, float y0, float x1,
    699                 float y1, float a, float b, float theta, boolean isMoreThanHalf,
    700                 boolean isPositiveArc) {
    701 
    702             LOGGER.log(Level.FINE, "(" + x0 + "," + y0 + ")-(" + x1 + "," + y1
    703                     + ") {" + a + " " + b + "}");
    704         /* Convert rotation angle from degrees to radians */
    705             double thetaD = theta * Math.PI / 180.0f;
    706         /* Pre-compute rotation matrix entries */
    707             double cosTheta = Math.cos(thetaD);
    708             double sinTheta = Math.sin(thetaD);
    709         /* Transform (x0, y0) and (x1, y1) into unit space */
    710         /* using (inverse) rotation, followed by (inverse) scale */
    711             double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
    712             double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
    713             double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
    714             double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
    715             LOGGER.log(Level.FINE, "unit space (" + x0p + "," + y0p + ")-(" + x1p
    716                     + "," + y1p + ")");
    717         /* Compute differences and averages */
    718             double dx = x0p - x1p;
    719             double dy = y0p - y1p;
    720             double xm = (x0p + x1p) / 2;
    721             double ym = (y0p + y1p) / 2;
    722         /* Solve for intersecting unit circles */
    723             double dsq = dx * dx + dy * dy;
    724             if (dsq == 0.0) {
    725                 LOGGER.log(Level.FINE, " Points are coincident");
    726                 return; /* Points are coincident */
    727             }
    728             double disc = 1.0 / dsq - 1.0 / 4.0;
    729             if (disc < 0.0) {
    730                 LOGGER.log(Level.FINE, "Points are too far apart " + dsq);
    731                 float adjust = (float) (Math.sqrt(dsq) / 1.99999);
    732                 drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta,
    733                         isMoreThanHalf, isPositiveArc);
    734                 return; /* Points are too far apart */
    735             }
    736             double s = Math.sqrt(disc);
    737             double sdx = s * dx;
    738             double sdy = s * dy;
    739             double cx;
    740             double cy;
    741             if (isMoreThanHalf == isPositiveArc) {
    742                 cx = xm - sdy;
    743                 cy = ym + sdx;
    744             } else {
    745                 cx = xm + sdy;
    746                 cy = ym - sdx;
    747             }
    748 
    749             double eta0 = Math.atan2((y0p - cy), (x0p - cx));
    750             LOGGER.log(Level.FINE, "eta0 = Math.atan2( " + (y0p - cy) + " , "
    751                     + (x0p - cx) + ") = " + Math.toDegrees(eta0));
    752 
    753             double eta1 = Math.atan2((y1p - cy), (x1p - cx));
    754             LOGGER.log(Level.FINE, "eta1 = Math.atan2( " + (y1p - cy) + " , "
    755                     + (x1p - cx) + ") = " + Math.toDegrees(eta1));
    756             double sweep = (eta1 - eta0);
    757             if (isPositiveArc != (sweep >= 0)) {
    758                 if (sweep > 0) {
    759                     sweep -= 2 * Math.PI;
    760                 } else {
    761                     sweep += 2 * Math.PI;
    762                 }
    763             }
    764 
    765             cx *= a;
    766             cy *= b;
    767             double tcx = cx;
    768             cx = cx * cosTheta - cy * sinTheta;
    769             cy = tcx * sinTheta + cy * cosTheta;
    770             LOGGER.log(
    771                     Level.FINE,
    772                     "cx, cy, a, b, x0, y0, thetaD, eta0, sweep = " + cx + " , "
    773                             + cy + " , " + a + " , " + b + " , " + x0 + " , " + y0
    774                             + " , " + Math.toDegrees(thetaD) + " , "
    775                             + Math.toDegrees(eta0) + " , " + Math.toDegrees(sweep));
    776 
    777             arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
    778         }
    779 
    780         /**
    781          * Converts an arc to cubic Bezier segments and records them in p.
    782          *
    783          * @param p The target for the cubic Bezier segments
    784          * @param cx The x coordinate center of the ellipse
    785          * @param cy The y coordinate center of the ellipse
    786          * @param a The radius of the ellipse in the horizontal direction
    787          * @param b The radius of the ellipse in the vertical direction
    788          * @param e1x E(eta1) x coordinate of the starting point of the arc
    789          * @param e1y E(eta2) y coordinate of the starting point of the arc
    790          * @param theta The angle that the ellipse bounding rectangle makes with the horizontal
    791          * plane
    792          * @param start The start angle of the arc on the ellipse
    793          * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
    794          */
    795         private static void arcToBezier(@NonNull Path_Delegate p, double cx, double cy, double a,
    796                 double b, double e1x, double e1y, double theta, double start,
    797                 double sweep) {
    798             // Taken from equations at:
    799             // http://spaceroots.org/documents/ellipse/node8.html
    800             // and http://www.spaceroots.org/documents/ellipse/node22.html
    801             // Maximum of 45 degrees per cubic Bezier segment
    802             int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));
    803 
    804 
    805             double eta1 = start;
    806             double cosTheta = Math.cos(theta);
    807             double sinTheta = Math.sin(theta);
    808             double cosEta1 = Math.cos(eta1);
    809             double sinEta1 = Math.sin(eta1);
    810             double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
    811             double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
    812 
    813             double anglePerSegment = sweep / numSegments;
    814             for (int i = 0; i < numSegments; i++) {
    815                 double eta2 = eta1 + anglePerSegment;
    816                 double sinEta2 = Math.sin(eta2);
    817                 double cosEta2 = Math.cos(eta2);
    818                 double e2x = cx + (a * cosTheta * cosEta2)
    819                         - (b * sinTheta * sinEta2);
    820                 double e2y = cy + (a * sinTheta * cosEta2)
    821                         + (b * cosTheta * sinEta2);
    822                 double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
    823                 double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
    824                 double tanDiff2 = Math.tan((eta2 - eta1) / 2);
    825                 double alpha = Math.sin(eta2 - eta1)
    826                         * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
    827                 double q1x = e1x + alpha * ep1x;
    828                 double q1y = e1y + alpha * ep1y;
    829                 double q2x = e2x - alpha * ep2x;
    830                 double q2y = e2y - alpha * ep2y;
    831 
    832                 p.cubicTo((float) q1x,
    833                         (float) q1y,
    834                         (float) q2x,
    835                         (float) q2y,
    836                         (float) e2x,
    837                         (float) e2y);
    838                 eta1 = eta2;
    839                 e1x = e2x;
    840                 e1y = e2y;
    841                 ep1x = ep2x;
    842                 ep1y = ep2y;
    843             }
    844         }
    845     }
    846 }
    847