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                     foundSeparator = true;
    290                     break;
    291                 case '-':
    292                     // The negative sign following a 'e' or 'E' is not a separator.
    293                     if (currentIndex != start && !isPrevExponential) {
    294                         foundSeparator = true;
    295                         result.mEndWithNegOrDot = true;
    296                     }
    297                     break;
    298                 case '.':
    299                     if (!secondDot) {
    300                         secondDot = true;
    301                     } else {
    302                         // This is the second dot, and it is considered as a separator.
    303                         foundSeparator = true;
    304                         result.mEndWithNegOrDot = true;
    305                     }
    306                     break;
    307                 case 'e':
    308                 case 'E':
    309                     isExponential = true;
    310                     break;
    311             }
    312             if (foundSeparator) {
    313                 break;
    314             }
    315         }
    316         // When there is nothing found, then we put the end position to the end
    317         // of the string.
    318         result.mEndPosition = currentIndex;
    319     }
    320 
    321     /**
    322      * Parse the floats in the string. This is an optimized version of
    323      * parseFloat(s.split(",|\\s"));
    324      *
    325      * @param s the string containing a command and list of floats
    326      *
    327      * @return array of floats
    328      */
    329     @NonNull
    330     private static float[] getFloats(@NonNull String s) {
    331         if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
    332             return new float[0];
    333         }
    334         try {
    335             float[] results = new float[s.length()];
    336             int count = 0;
    337             int startPosition = 1;
    338             int endPosition;
    339 
    340             ExtractFloatResult result = new ExtractFloatResult();
    341             int totalLength = s.length();
    342 
    343             // The startPosition should always be the first character of the
    344             // current number, and endPosition is the character after the current
    345             // number.
    346             while (startPosition < totalLength) {
    347                 extract(s, startPosition, result);
    348                 endPosition = result.mEndPosition;
    349 
    350                 if (startPosition < endPosition) {
    351                     results[count++] = Float.parseFloat(
    352                             s.substring(startPosition, endPosition));
    353                 }
    354 
    355                 if (result.mEndWithNegOrDot) {
    356                     // Keep the '-' or '.' sign with next number.
    357                     startPosition = endPosition;
    358                 } else {
    359                     startPosition = endPosition + 1;
    360                 }
    361             }
    362             return Arrays.copyOf(results, count);
    363         } catch (NumberFormatException e) {
    364             throw new RuntimeException("error in parsing \"" + s + "\"", e);
    365         }
    366     }
    367 
    368 
    369     private static void addNode(@NonNull ArrayList<PathDataNode> list, char cmd,
    370             @NonNull float[] val) {
    371         list.add(new PathDataNode(cmd, val));
    372     }
    373 
    374     private static class ExtractFloatResult {
    375         // We need to return the position of the next separator and whether the
    376         // next float starts with a '-' or a '.'.
    377         private int mEndPosition;
    378         private boolean mEndWithNegOrDot;
    379     }
    380 
    381     /**
    382      * Each PathDataNode represents one command in the "d" attribute of the svg file. An array of
    383      * PathDataNode can represent the whole "d" attribute.
    384      */
    385     public static class PathDataNode {
    386         private char mType;
    387         @NonNull
    388         private float[] mParams;
    389 
    390         private PathDataNode(char type, @NonNull float[] params) {
    391             mType = type;
    392             mParams = params;
    393         }
    394 
    395         public char getType() {
    396             return mType;
    397         }
    398 
    399         @NonNull
    400         public float[] getParams() {
    401             return mParams;
    402         }
    403 
    404         private PathDataNode(@NonNull PathDataNode n) {
    405             mType = n.mType;
    406             mParams = Arrays.copyOf(n.mParams, n.mParams.length);
    407         }
    408 
    409         /**
    410          * Convert an array of PathDataNode to Path. Reset the passed path as needed before
    411          * calling this method.
    412          *
    413          * @param node The source array of PathDataNode.
    414          * @param path The target Path object.
    415          */
    416         public static void nodesToPath(@NonNull PathDataNode[] node, @NonNull Path_Delegate path) {
    417             float[] current = new float[6];
    418             char previousCommand = 'm';
    419             //noinspection ForLoopReplaceableByForEach
    420             for (int i = 0; i < node.length; i++) {
    421                 addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
    422                 previousCommand = node[i].mType;
    423             }
    424         }
    425 
    426         /**
    427          * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and
    428          * <code>nodeTo</code> according to the <code>fraction</code>.
    429          *
    430          * @param nodeFrom The start value as a PathDataNode.
    431          * @param nodeTo The end value as a PathDataNode
    432          * @param fraction The fraction to interpolate.
    433          */
    434         private void interpolatePathDataNode(@NonNull PathDataNode nodeFrom,
    435                 @NonNull PathDataNode nodeTo, float fraction) {
    436             for (int i = 0; i < nodeFrom.mParams.length; i++) {
    437                 mParams[i] = nodeFrom.mParams[i] * (1 - fraction)
    438                         + nodeTo.mParams[i] * fraction;
    439             }
    440         }
    441 
    442         @SuppressWarnings("PointlessArithmeticExpression")
    443         private static void addCommand(@NonNull Path_Delegate path, float[] current,
    444                 char previousCmd, char cmd, @NonNull float[] val) {
    445 
    446             int incr = 2;
    447             float currentX = current[0];
    448             float currentY = current[1];
    449             float ctrlPointX = current[2];
    450             float ctrlPointY = current[3];
    451             float currentSegmentStartX = current[4];
    452             float currentSegmentStartY = current[5];
    453             float reflectiveCtrlPointX;
    454             float reflectiveCtrlPointY;
    455 
    456             switch (cmd) {
    457                 case 'z':
    458                 case 'Z':
    459                     path.close();
    460                     // Path is closed here, but we need to move the pen to the
    461                     // closed position. So we cache the segment's starting position,
    462                     // and restore it here.
    463                     currentX = currentSegmentStartX;
    464                     currentY = currentSegmentStartY;
    465                     ctrlPointX = currentSegmentStartX;
    466                     ctrlPointY = currentSegmentStartY;
    467                     path.moveTo(currentX, currentY);
    468                     break;
    469                 case 'm':
    470                 case 'M':
    471                 case 'l':
    472                 case 'L':
    473                 case 't':
    474                 case 'T':
    475                     incr = 2;
    476                     break;
    477                 case 'h':
    478                 case 'H':
    479                 case 'v':
    480                 case 'V':
    481                     incr = 1;
    482                     break;
    483                 case 'c':
    484                 case 'C':
    485                     incr = 6;
    486                     break;
    487                 case 's':
    488                 case 'S':
    489                 case 'q':
    490                 case 'Q':
    491                     incr = 4;
    492                     break;
    493                 case 'a':
    494                 case 'A':
    495                     incr = 7;
    496                     break;
    497             }
    498 
    499             for (int k = 0; k < val.length; k += incr) {
    500                 switch (cmd) {
    501                     case 'm': // moveto - Start a new sub-path (relative)
    502                         currentX += val[k + 0];
    503                         currentY += val[k + 1];
    504 
    505                         if (k > 0) {
    506                             // According to the spec, if a moveto is followed by multiple
    507                             // pairs of coordinates, the subsequent pairs are treated as
    508                             // implicit lineto commands.
    509                             path.rLineTo(val[k + 0], val[k + 1]);
    510                         } else {
    511                             path.rMoveTo(val[k + 0], val[k + 1]);
    512                             currentSegmentStartX = currentX;
    513                             currentSegmentStartY = currentY;
    514                         }
    515                         break;
    516                     case 'M': // moveto - Start a new sub-path
    517                         currentX = val[k + 0];
    518                         currentY = val[k + 1];
    519 
    520                         if (k > 0) {
    521                             // According to the spec, if a moveto is followed by multiple
    522                             // pairs of coordinates, the subsequent pairs are treated as
    523                             // implicit lineto commands.
    524                             path.lineTo(val[k + 0], val[k + 1]);
    525                         } else {
    526                             path.moveTo(val[k + 0], val[k + 1]);
    527                             currentSegmentStartX = currentX;
    528                             currentSegmentStartY = currentY;
    529                         }
    530                         break;
    531                     case 'l': // lineto - Draw a line from the current point (relative)
    532                         path.rLineTo(val[k + 0], val[k + 1]);
    533                         currentX += val[k + 0];
    534                         currentY += val[k + 1];
    535                         break;
    536                     case 'L': // lineto - Draw a line from the current point
    537                         path.lineTo(val[k + 0], val[k + 1]);
    538                         currentX = val[k + 0];
    539                         currentY = val[k + 1];
    540                         break;
    541                     case 'h': // horizontal lineto - Draws a horizontal line (relative)
    542                         path.rLineTo(val[k + 0], 0);
    543                         currentX += val[k + 0];
    544                         break;
    545                     case 'H': // horizontal lineto - Draws a horizontal line
    546                         path.lineTo(val[k + 0], currentY);
    547                         currentX = val[k + 0];
    548                         break;
    549                     case 'v': // vertical lineto - Draws a vertical line from the current point (r)
    550                         path.rLineTo(0, val[k + 0]);
    551                         currentY += val[k + 0];
    552                         break;
    553                     case 'V': // vertical lineto - Draws a vertical line from the current point
    554                         path.lineTo(currentX, val[k + 0]);
    555                         currentY = val[k + 0];
    556                         break;
    557                     case 'c': // curveto - Draws a cubic Bzier curve (relative)
    558                         path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
    559                                 val[k + 4], val[k + 5]);
    560 
    561                         ctrlPointX = currentX + val[k + 2];
    562                         ctrlPointY = currentY + val[k + 3];
    563                         currentX += val[k + 4];
    564                         currentY += val[k + 5];
    565 
    566                         break;
    567                     case 'C': // curveto - Draws a cubic Bzier curve
    568                         path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
    569                                 val[k + 4], val[k + 5]);
    570                         currentX = val[k + 4];
    571                         currentY = val[k + 5];
    572                         ctrlPointX = val[k + 2];
    573                         ctrlPointY = val[k + 3];
    574                         break;
    575                     case 's': // smooth curveto - Draws a cubic Bzier curve (reflective cp)
    576                         reflectiveCtrlPointX = 0;
    577                         reflectiveCtrlPointY = 0;
    578                         if (previousCmd == 'c' || previousCmd == 's'
    579                                 || previousCmd == 'C' || previousCmd == 'S') {
    580                             reflectiveCtrlPointX = currentX - ctrlPointX;
    581                             reflectiveCtrlPointY = currentY - ctrlPointY;
    582                         }
    583                         path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
    584                                 val[k + 0], val[k + 1],
    585                                 val[k + 2], val[k + 3]);
    586 
    587                         ctrlPointX = currentX + val[k + 0];
    588                         ctrlPointY = currentY + val[k + 1];
    589                         currentX += val[k + 2];
    590                         currentY += val[k + 3];
    591                         break;
    592                     case 'S': // shorthand/smooth curveto Draws a cubic Bzier curve(reflective cp)
    593                         reflectiveCtrlPointX = currentX;
    594                         reflectiveCtrlPointY = currentY;
    595                         if (previousCmd == 'c' || previousCmd == 's'
    596                                 || previousCmd == 'C' || previousCmd == 'S') {
    597                             reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
    598                             reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
    599                         }
    600                         path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
    601                                 val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
    602                         ctrlPointX = val[k + 0];
    603                         ctrlPointY = val[k + 1];
    604                         currentX = val[k + 2];
    605                         currentY = val[k + 3];
    606                         break;
    607                     case 'q': // Draws a quadratic Bzier (relative)
    608                         path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
    609                         ctrlPointX = currentX + val[k + 0];
    610                         ctrlPointY = currentY + val[k + 1];
    611                         currentX += val[k + 2];
    612                         currentY += val[k + 3];
    613                         break;
    614                     case 'Q': // Draws a quadratic Bzier
    615                         path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
    616                         ctrlPointX = val[k + 0];
    617                         ctrlPointY = val[k + 1];
    618                         currentX = val[k + 2];
    619                         currentY = val[k + 3];
    620                         break;
    621                     case 't': // Draws a quadratic Bzier curve(reflective control point)(relative)
    622                         reflectiveCtrlPointX = 0;
    623                         reflectiveCtrlPointY = 0;
    624                         if (previousCmd == 'q' || previousCmd == 't'
    625                                 || previousCmd == 'Q' || previousCmd == 'T') {
    626                             reflectiveCtrlPointX = currentX - ctrlPointX;
    627                             reflectiveCtrlPointY = currentY - ctrlPointY;
    628                         }
    629                         path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
    630                                 val[k + 0], val[k + 1]);
    631                         ctrlPointX = currentX + reflectiveCtrlPointX;
    632                         ctrlPointY = currentY + reflectiveCtrlPointY;
    633                         currentX += val[k + 0];
    634                         currentY += val[k + 1];
    635                         break;
    636                     case 'T': // Draws a quadratic Bzier curve (reflective control point)
    637                         reflectiveCtrlPointX = currentX;
    638                         reflectiveCtrlPointY = currentY;
    639                         if (previousCmd == 'q' || previousCmd == 't'
    640                                 || previousCmd == 'Q' || previousCmd == 'T') {
    641                             reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
    642                             reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
    643                         }
    644                         path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
    645                                 val[k + 0], val[k + 1]);
    646                         ctrlPointX = reflectiveCtrlPointX;
    647                         ctrlPointY = reflectiveCtrlPointY;
    648                         currentX = val[k + 0];
    649                         currentY = val[k + 1];
    650                         break;
    651                     case 'a': // Draws an elliptical arc
    652                         // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
    653                         drawArc(path,
    654                                 currentX,
    655                                 currentY,
    656                                 val[k + 5] + currentX,
    657                                 val[k + 6] + currentY,
    658                                 val[k + 0],
    659                                 val[k + 1],
    660                                 val[k + 2],
    661                                 val[k + 3] != 0,
    662                                 val[k + 4] != 0);
    663                         currentX += val[k + 5];
    664                         currentY += val[k + 6];
    665                         ctrlPointX = currentX;
    666                         ctrlPointY = currentY;
    667                         break;
    668                     case 'A': // Draws an elliptical arc
    669                         drawArc(path,
    670                                 currentX,
    671                                 currentY,
    672                                 val[k + 5],
    673                                 val[k + 6],
    674                                 val[k + 0],
    675                                 val[k + 1],
    676                                 val[k + 2],
    677                                 val[k + 3] != 0,
    678                                 val[k + 4] != 0);
    679                         currentX = val[k + 5];
    680                         currentY = val[k + 6];
    681                         ctrlPointX = currentX;
    682                         ctrlPointY = currentY;
    683                         break;
    684                 }
    685                 previousCmd = cmd;
    686             }
    687             current[0] = currentX;
    688             current[1] = currentY;
    689             current[2] = ctrlPointX;
    690             current[3] = ctrlPointY;
    691             current[4] = currentSegmentStartX;
    692             current[5] = currentSegmentStartY;
    693         }
    694 
    695         private static void drawArc(@NonNull Path_Delegate p, float x0, float y0, float x1,
    696                 float y1, float a, float b, float theta, boolean isMoreThanHalf,
    697                 boolean isPositiveArc) {
    698 
    699             LOGGER.log(Level.FINE, "(" + x0 + "," + y0 + ")-(" + x1 + "," + y1
    700                     + ") {" + a + " " + b + "}");
    701         /* Convert rotation angle from degrees to radians */
    702             double thetaD = theta * Math.PI / 180.0f;
    703         /* Pre-compute rotation matrix entries */
    704             double cosTheta = Math.cos(thetaD);
    705             double sinTheta = Math.sin(thetaD);
    706         /* Transform (x0, y0) and (x1, y1) into unit space */
    707         /* using (inverse) rotation, followed by (inverse) scale */
    708             double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
    709             double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
    710             double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
    711             double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
    712             LOGGER.log(Level.FINE, "unit space (" + x0p + "," + y0p + ")-(" + x1p
    713                     + "," + y1p + ")");
    714         /* Compute differences and averages */
    715             double dx = x0p - x1p;
    716             double dy = y0p - y1p;
    717             double xm = (x0p + x1p) / 2;
    718             double ym = (y0p + y1p) / 2;
    719         /* Solve for intersecting unit circles */
    720             double dsq = dx * dx + dy * dy;
    721             if (dsq == 0.0) {
    722                 LOGGER.log(Level.FINE, " Points are coincident");
    723                 return; /* Points are coincident */
    724             }
    725             double disc = 1.0 / dsq - 1.0 / 4.0;
    726             if (disc < 0.0) {
    727                 LOGGER.log(Level.FINE, "Points are too far apart " + dsq);
    728                 float adjust = (float) (Math.sqrt(dsq) / 1.99999);
    729                 drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta,
    730                         isMoreThanHalf, isPositiveArc);
    731                 return; /* Points are too far apart */
    732             }
    733             double s = Math.sqrt(disc);
    734             double sdx = s * dx;
    735             double sdy = s * dy;
    736             double cx;
    737             double cy;
    738             if (isMoreThanHalf == isPositiveArc) {
    739                 cx = xm - sdy;
    740                 cy = ym + sdx;
    741             } else {
    742                 cx = xm + sdy;
    743                 cy = ym - sdx;
    744             }
    745 
    746             double eta0 = Math.atan2((y0p - cy), (x0p - cx));
    747             LOGGER.log(Level.FINE, "eta0 = Math.atan2( " + (y0p - cy) + " , "
    748                     + (x0p - cx) + ") = " + Math.toDegrees(eta0));
    749 
    750             double eta1 = Math.atan2((y1p - cy), (x1p - cx));
    751             LOGGER.log(Level.FINE, "eta1 = Math.atan2( " + (y1p - cy) + " , "
    752                     + (x1p - cx) + ") = " + Math.toDegrees(eta1));
    753             double sweep = (eta1 - eta0);
    754             if (isPositiveArc != (sweep >= 0)) {
    755                 if (sweep > 0) {
    756                     sweep -= 2 * Math.PI;
    757                 } else {
    758                     sweep += 2 * Math.PI;
    759                 }
    760             }
    761 
    762             cx *= a;
    763             cy *= b;
    764             double tcx = cx;
    765             cx = cx * cosTheta - cy * sinTheta;
    766             cy = tcx * sinTheta + cy * cosTheta;
    767             LOGGER.log(
    768                     Level.FINE,
    769                     "cx, cy, a, b, x0, y0, thetaD, eta0, sweep = " + cx + " , "
    770                             + cy + " , " + a + " , " + b + " , " + x0 + " , " + y0
    771                             + " , " + Math.toDegrees(thetaD) + " , "
    772                             + Math.toDegrees(eta0) + " , " + Math.toDegrees(sweep));
    773 
    774             arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
    775         }
    776 
    777         /**
    778          * Converts an arc to cubic Bezier segments and records them in p.
    779          *
    780          * @param p The target for the cubic Bezier segments
    781          * @param cx The x coordinate center of the ellipse
    782          * @param cy The y coordinate center of the ellipse
    783          * @param a The radius of the ellipse in the horizontal direction
    784          * @param b The radius of the ellipse in the vertical direction
    785          * @param e1x E(eta1) x coordinate of the starting point of the arc
    786          * @param e1y E(eta2) y coordinate of the starting point of the arc
    787          * @param theta The angle that the ellipse bounding rectangle makes with the horizontal
    788          * plane
    789          * @param start The start angle of the arc on the ellipse
    790          * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
    791          */
    792         private static void arcToBezier(@NonNull Path_Delegate p, double cx, double cy, double a,
    793                 double b, double e1x, double e1y, double theta, double start,
    794                 double sweep) {
    795             // Taken from equations at:
    796             // http://spaceroots.org/documents/ellipse/node8.html
    797             // and http://www.spaceroots.org/documents/ellipse/node22.html
    798             // Maximum of 45 degrees per cubic Bezier segment
    799             int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));
    800 
    801 
    802             double eta1 = start;
    803             double cosTheta = Math.cos(theta);
    804             double sinTheta = Math.sin(theta);
    805             double cosEta1 = Math.cos(eta1);
    806             double sinEta1 = Math.sin(eta1);
    807             double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
    808             double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
    809 
    810             double anglePerSegment = sweep / numSegments;
    811             for (int i = 0; i < numSegments; i++) {
    812                 double eta2 = eta1 + anglePerSegment;
    813                 double sinEta2 = Math.sin(eta2);
    814                 double cosEta2 = Math.cos(eta2);
    815                 double e2x = cx + (a * cosTheta * cosEta2)
    816                         - (b * sinTheta * sinEta2);
    817                 double e2y = cy + (a * sinTheta * cosEta2)
    818                         + (b * cosTheta * sinEta2);
    819                 double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
    820                 double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
    821                 double tanDiff2 = Math.tan((eta2 - eta1) / 2);
    822                 double alpha = Math.sin(eta2 - eta1)
    823                         * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
    824                 double q1x = e1x + alpha * ep1x;
    825                 double q1y = e1y + alpha * ep1y;
    826                 double q2x = e2x - alpha * ep2x;
    827                 double q2y = e2y - alpha * ep2y;
    828 
    829                 p.cubicTo((float) q1x,
    830                         (float) q1y,
    831                         (float) q2x,
    832                         (float) q2y,
    833                         (float) e2x,
    834                         (float) e2y);
    835                 eta1 = eta2;
    836                 e1x = e2x;
    837                 e1y = e2y;
    838                 ep1x = ep2x;
    839                 ep1y = ep2y;
    840             }
    841         }
    842     }
    843 }
    844