Skip to content

Commit

Permalink
Issue #155: Switched the arcTo() function back to using double precis…
Browse files Browse the repository at this point in the history
…ion.

Fixed the radius checking code to match the original Batik code - in case that was part of the problem.
Removed unnecessary calls toDegrees() and toRadians() calls.
  • Loading branch information
BigBadaboom committed Sep 23, 2018
1 parent a5bf988 commit bc32019
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2444,7 +2444,7 @@ private static int clamp255(float val)
}


static int colourWithOpacity(int colour, float opacity)
private static int colourWithOpacity(int colour, float opacity)
{
int alpha = (colour >> 24) & 0xff;
alpha = Math.round(alpha * opacity);
Expand Down Expand Up @@ -2575,15 +2575,15 @@ public void close()
// Handling of Arcs

/*
* SVG arc representation uses "endpoint parameterisation" where we specify the endpoint of the arc.
* SVG arc representation uses "endpoint parameterisation" where we specify the start and endpoint of the arc.
* This is to be consistent with the other path commands. However we need to convert this to "centre point
* parameterisation" in order to calculate the arc. Handily, the SVG spec provides all the required maths
* in section "F.6 Elliptical arc implementation notes".
*
* Some of this code has been borrowed from the Batik library (Apache-2 license).
*
* Note: the original version of this code used doubles. This version uses floats because of some
* sort Android JIT(?) bug. See Issue #62.
* Previously, to work around issue #62, we converted this function to use floats. However in issue #155,
* we discovered that there are some arcs that fail due of a lack of precision. So we have switched back to doubles.
*/

private static void arcTo(float lastX, float lastY, float rx, float ry, float angle, boolean largeArcFlag, boolean sweepFlag, float x, float y, PathInterface pather)
Expand All @@ -2606,77 +2606,85 @@ private static void arcTo(float lastX, float lastY, float rx, float ry, float an
ry = Math.abs(ry);

// Convert angle from degrees to radians
float angleRad = (float) Math.toRadians(angle % 360.0);
float cosAngle = (float) Math.cos(angleRad);
float sinAngle = (float) Math.sin(angleRad);
double angleRad = Math.toRadians(angle % 360.0);
double cosAngle = Math.cos(angleRad);
double sinAngle = Math.sin(angleRad);

// We simplify the calculations by transforming the arc so that the origin is at the
// midpoint calculated above followed by a rotation to line up the coordinate axes
// with the axes of the ellipse.

// Compute the midpoint of the line between the current and the end point
float dx2 = (lastX - x) / 2.0f;
float dy2 = (lastY - y) / 2.0f;
double dx2 = (lastX - x) / 2.0;
double dy2 = (lastY - y) / 2.0;

// Step 1 : Compute (x1', y1') - the transformed start point
float x1 = (cosAngle * dx2 + sinAngle * dy2);
float y1 = (-sinAngle * dx2 + cosAngle * dy2);
// Step 1 : Compute (x1', y1')
// x1,y1 is the midpoint vector rotated to take the arc's angle out of consideration
double x1 = (cosAngle * dx2 + sinAngle * dy2);
double y1 = (-sinAngle * dx2 + cosAngle * dy2);

float rx_sq = rx * rx;
float ry_sq = ry * ry;
float x1_sq = x1 * x1;
float y1_sq = y1 * y1;
double rx_sq = rx * rx;
double ry_sq = ry * ry;
double x1_sq = x1 * x1;
double y1_sq = y1 * y1;

// Check that radii are large enough.
// If they are not, the spec says to scale them up so they are.
// This is to compensate for potential rounding errors/differences between SVG implementations.
float radiiCheck = x1_sq / rx_sq + y1_sq / ry_sq;
if (radiiCheck > 1) {
rx = (float) Math.sqrt(radiiCheck) * rx;
ry = (float) Math.sqrt(radiiCheck) * ry;
double radiiCheck = x1_sq / rx_sq + y1_sq / ry_sq;
if (radiiCheck > 0.99999) {
double radiiScale = Math.sqrt(radiiCheck) * 1.00001;
rx = (float) (radiiScale * rx);
ry = (float) (radiiScale * ry);
rx_sq = rx * rx;
ry_sq = ry * ry;
}

// Step 2 : Compute (cx1, cy1) - the transformed centre point
float sign = (largeArcFlag == sweepFlag) ? -1 : 1;
float sq = ((rx_sq * ry_sq) - (rx_sq * y1_sq) - (ry_sq * x1_sq)) / ((rx_sq * y1_sq) + (ry_sq * x1_sq));
double sign = (largeArcFlag == sweepFlag) ? -1 : 1;
double sq = ((rx_sq * ry_sq) - (rx_sq * y1_sq) - (ry_sq * x1_sq)) / ((rx_sq * y1_sq) + (ry_sq * x1_sq));
sq = (sq < 0) ? 0 : sq;
float coef = (float) (sign * Math.sqrt(sq));
float cx1 = coef * ((rx * y1) / ry);
float cy1 = coef * -((ry * x1) / rx);
double coef = (sign * Math.sqrt(sq));
double cx1 = coef * ((rx * y1) / ry);
double cy1 = coef * -((ry * x1) / rx);

// Step 3 : Compute (cx, cy) from (cx1, cy1)
float sx2 = (lastX + x) / 2.0f;
float sy2 = (lastY + y) / 2.0f;
float cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
float cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);
double sx2 = (lastX + x) / 2.0;
double sy2 = (lastY + y) / 2.0;
double cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
double cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);

// Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
float ux = (x1 - cx1) / rx;
float uy = (y1 - cy1) / ry;
float vx = (-x1 - cx1) / rx;
float vy = (-y1 - cy1) / ry;
float p, n;

// Compute the angle start
n = (float) Math.sqrt((ux * ux) + (uy * uy));
p = ux; // (1 * ux) + (0 * uy)
sign = (uy < 0) ? -1.0f : 1.0f;
float angleStart = (float) Math.toDegrees(sign * Math.acos(p / n));
double ux = (x1 - cx1) / rx;
double uy = (y1 - cy1) / ry;
double vx = (-x1 - cx1) / rx;
double vy = (-y1 - cy1) / ry;
double p, n;

// Angle betwen two vectors is +/- acos( u.v / len(u) * len(v))
// Where '.' is the dot product. And +/- is calculated from the sign of the cross product (u x v)

final double TWO_PI = Math.PI * 2.0;

// Compute the start angle
// The angle between (ux,uy) and the 0deg angle (1,0)
n = Math.sqrt((ux * ux) + (uy * uy)); // len(u) * len(1,0) == len(u)
p = ux; // u.v == (ux,uy).(1,0) == (1 * ux) + (0 * uy) == ux
sign = (uy < 0) ? -1.0 : 1.0; // u x v == (1 * uy - ux * 0) == uy
double angleStart = sign * Math.acos(p / n); // No need for checkedArcCos() here

// Compute the angle extent
n = (float) Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
p = ux * vx + uy * vy;
sign = (ux * vy - uy * vx < 0) ? -1.0f : 1.0f;
double angleExtent = Math.toDegrees(sign * Math.acos(p / n));
double angleExtent = sign * checkedArcCos(p / n);
if (!sweepFlag && angleExtent > 0) {
angleExtent -= 360f;
angleExtent -= TWO_PI;
} else if (sweepFlag && angleExtent < 0) {
angleExtent += 360f;
angleExtent += TWO_PI;
}
angleExtent %= 360f;
angleStart %= 360f;
angleExtent %= TWO_PI;
angleStart %= TWO_PI;

// Many elliptical arc implementations including the Java2D and Android ones, only
// support arcs that are axis aligned. Therefore we need to substitute the arc
Expand All @@ -2688,7 +2696,7 @@ private static void arcTo(float lastX, float lastY, float rx, float ry, float an
Matrix m = new Matrix();
m.postScale(rx, ry);
m.postRotate(angle);
m.postTranslate(cx, cy);
m.postTranslate((float) cx, (float) cy);
m.mapPoints(bezierPoints);

// The last point in the bezier set should match exactly the last coord pair in the arc (ie: x,y). But
Expand All @@ -2706,6 +2714,14 @@ private static void arcTo(float lastX, float lastY, float rx, float ry, float an
}


// Check input to Math.acos() in case rounding or other errors result in a val < -1 or > +1.
// For example, see the possible KitKat JIT error described in issue #62.
private static double checkedArcCos(double val)
{
return (val < -1.0) ? Math.PI : (val > 1.0) ? 0 : Math.acos(val);
}


/*
* Generate the control points and endpoints for a set of bezier curves that match
* a circular arc starting from angle 'angleStart' and sweep the angle 'angleExtent'.
Expand All @@ -2722,11 +2738,9 @@ private static void arcTo(float lastX, float lastY, float rx, float ry, float an
*/
private static float[] arcToBeziers(double angleStart, double angleExtent)
{
int numSegments = (int) Math.ceil(Math.abs(angleExtent) / 90.0);

angleStart = Math.toRadians(angleStart);
angleExtent = Math.toRadians(angleExtent);
float angleIncrement = (float) (angleExtent / numSegments);
int numSegments = (int) Math.ceil(Math.abs(angleExtent) * 2.0 / Math.PI); // (angleExtent / 90deg)

double angleIncrement = angleExtent / numSegments;

// The length of each control point vector is given by the following formula.
double controlLength = 4.0 / 3.0 * Math.sin(angleIncrement / 2.0) / (1.0 + Math.cos(angleIncrement / 2.0));
Expand Down
60 changes: 60 additions & 0 deletions androidsvg/src/test/java/com/caverock/androidsvg/ArcTo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.caverock.androidsvg;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Build;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;

import java.util.List;

import static org.junit.Assert.assertEquals;

@RunWith(RobolectricTestRunner.class)
@Config(manifest=Config.NONE, sdk = Build.VERSION_CODES.KITKAT, shadows={MockCanvas.class, MockPath.class})
public class ArcTo
{
@Test
public void testIssue155() throws SVGParseException
{
String test = "<svg>" +
" <path d=\"M 163.637 412.021 a 646225.813 646225.813 0 0 1 -36.313 162\"/>" +
"</svg>";
SVG svg = SVG.getFromString(test);

Bitmap newBM = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(newBM);

svg.renderToCanvas(canvas);

List<String> ops = ((MockCanvas) Shadow.extract(canvas)).getOperations();
/**/System.out.println(String.join(",", ops));
assertEquals(6, ops.size());
assertEquals("drawPath('M 163.63701 412.02103 C 151.5 466.03125 139.375 520.0156 127.32401 574.021', Paint({color=#ff000000}))", ops.get(3));
}


@Test
public void testIssue156() throws SVGParseException
{
String test = "<svg>" +
" <path d=\"M 422.776 332.659 a 539896.23 539896.23 0 0 0-22.855-26.558\"/>" +
"</svg>";
SVG svg = SVG.getFromString(test);

Bitmap newBM = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(newBM);

svg.renderToCanvas(canvas);

List<String> ops = ((MockCanvas) Shadow.extract(canvas)).getOperations();
/**/System.out.println(String.join(",", ops));
assertEquals(6, ops.size());
assertEquals("drawPath('M 422.77603 332.65903 C 415.15625 323.8125 407.53125 314.96875 399.92102 306.101', Paint({color=#ff000000}))", ops.get(3));

}
}

0 comments on commit bc32019

Please sign in to comment.