Fixed: #2443 SVG importer - converting cubic bezier curves to quadratic

This commit is contained in:
Jindra Petřík
2025-04-20 22:46:45 +02:00
parent bfc420719a
commit d5ff99490c
2 changed files with 247 additions and 6 deletions

View File

@@ -17,6 +17,7 @@
package com.jpexs.decompiler.flash.importers.svg;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
@@ -28,6 +29,12 @@ import java.util.List;
*/
public class CubicToQuad {
// Precision used to check determinant in quad and cubic solvers,
// any number lower than this is considered to be zero.
// `8.67e-19` is an example of real error occurring in tests.
private static final double EPSILON = 1e-16;
class Point {
public double x;
@@ -66,6 +73,11 @@ public class CubicToQuad {
public double dot(Point point) {
return this.x * point.x + this.y * point.y;
}
@Override
public String toString() {
return "[" + x + ", " + y + "]";
}
}
private Point[] calcPowerCoefficients(Point p1, Point c1, Point c2, Point p2) {
@@ -103,7 +115,7 @@ public class CubicToQuad {
return (b == 0) ? new double[0] : new double[]{-c / b};
}
double D = b * b - 4 * a * c;
if (D < 0) {
if (Math.abs(D) < EPSILON) {
return new double[0];
} else if (D == 0) {
return new double[]{-b / (2 * a)};
@@ -228,6 +240,7 @@ public class CubicToQuad {
return new Point[]{f1, new Point(cx, cy), f2};
}
/*
private boolean isSegmentApproximationClose(Point a, Point b, Point c, Point d, double tmin, double tmax, Point p1, Point c1, Point p2, double errorBound) {
// a,b,c,d define cubic curve
// tmin, tmax are boundary points on cubic curve
@@ -256,6 +269,111 @@ public class CubicToQuad {
}
return true;
}
*/
private Point[] calcPowerCoefficientsQuad(Point p1, Point c1, Point p2) {
// point(t) = p1*(1-t)^2 + c1*t*(1-t) + p2*t^2 = a*t^2 + b*t + c
// for each t value, so
// a = p1 + p2 - 2 * c1
// b = 2 * (c1 - p1)
// c = p1
Point a = c1.mul(-2).add(p1).add(p2);
Point b = c1.sub(p1).mul(2);
Point c = p1;
return new Point[]{a, b, c};
}
/*
* Calculate a distance between a `point` and a line segment `p1, p2`
* (result is squared for performance reasons), see details here:
* https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
*/
private double minDistanceToLineSq(Point point, Point p1, Point p2) {
Point p1p2 = p2.sub(p1);
double dot = point.sub(p1).dot(p1p2);
double lenSq = p1p2.sqr();
double param = 0;
Point diff;
if (lenSq != 0) {
param = dot / lenSq;
}
if (param <= 0) {
diff = point.sub(p1);
} else if (param >= 1) {
diff = point.sub(p2);
} else {
diff = point.sub(p1.add(p1p2.mul(param)));
}
return diff.sqr();
}
/*
* Divide cubic and quadratic curves into 10 points and 9 line segments.
* Calculate distances between each point on cubic and nearest line segment
* on quadratic (and vice versa), and make sure all distances are less
* than `errorBound`.
*
* We need to calculate BOTH distance from all points on quadratic to any cubic,
* and all points on cubic to any quadratic.
*
* If we do it only one way, it may lead to an error if the entire original curve
* falls within errorBound (then **any** quad will erroneously treated as good):
* https://github.com/fontello/svg2ttf/issues/105#issuecomment-842558027
*
* - a,b,c,d define cubic curve (power coefficients)
* - tmin, tmax are boundary points on cubic curve (in 0-1 range)
* - p1, c1, p2 define quadratic curve (control points)
* - errorBound is maximum allowed distance
*/
private boolean isSegmentApproximationClose(Point a, Point b, Point c, Point d, double tmin, double tmax, Point p1, Point c1, Point p2, double errorBound) {
int n = 10; // number of points
double t;
double dt;
Point[] p = calcPowerCoefficientsQuad(p1, c1, p2);
Point qa = p[0];
Point qb = p[1];
Point qc = p[2];
int i;
int j;
double distSq;
double errorBoundSq = errorBound * errorBound;
List<Point> cubicPoints = new ArrayList<>();
List<Point> quadPoints = new ArrayList<>();
double minDistSq;
dt = (tmax - tmin) / ((double) n);
for (i = 0, t = tmin; i <= n; i++, t += dt) {
cubicPoints.add(calcPoint(a, b, c, d, t));
}
dt = 1 / ((double) n);
for (i = 0, t = 0; i <= n; i++, t += dt) {
quadPoints.add(calcPointQuad(qa, qb, qc, t));
}
for (i = 1; i < cubicPoints.size() - 1; i++) {
minDistSq = Double.MAX_VALUE;
for (j = 0; j < quadPoints.size() - 1; j++) {
distSq = minDistanceToLineSq(cubicPoints.get(i), quadPoints.get(j), quadPoints.get(j + 1));
minDistSq = Math.min(minDistSq, distSq);
}
if (minDistSq > errorBoundSq) {
return false;
}
}
for (i = 1; i < quadPoints.size() - 1; i++) {
minDistSq = Double.MAX_VALUE;
for (j = 0; j < cubicPoints.size() - 1; j++) {
distSq = minDistanceToLineSq(quadPoints.get(i), cubicPoints.get(j), cubicPoints.get(j + 1));
minDistSq = Math.min(minDistSq, distSq);
}
if (minDistSq > errorBoundSq) {
return false;
}
}
return true;
}
private boolean _isApproximationClose(Point a, Point b, Point c, Point d, List<Point[]> quadCurves, double errorBound) {
double dt = 1.0 / quadCurves.size();
@@ -307,12 +425,42 @@ public class CubicToQuad {
return _isApproximationClose(pc[0], pc[1], pc[2], pc[3], fromFlatArray(quads), errorBound);
}
/*
* Split cubic bézier curve into two cubic curves, see details here:
* https://math.stackexchange.com/questions/877725
*/
private double[][] subdivideCubic(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4, double t) {
double u = 1 - t;
double v = t;
double bx = x1 * u + x2 * v;
double sx = x2 * u + x3 * v;
double fx = x3 * u + x4 * v;
double cx = bx * u + sx * v;
double ex = sx * u + fx * v;
double dx = cx * u + ex * v;
double by = y1 * u + y2 * v;
double sy = y2 * u + y3 * v;
double fy = y3 * u + y4 * v;
double cy = by * u + sy * v;
double ey = sy * u + fy * v;
double dy = cy * u + ey * v;
return new double[][]{
{x1, y1, bx, by, cx, cy, dx, dy},
{dx, dy, ex, ey, fx, fy, x4, y4}
};
}
/**
* Approximate cubic Bezier curve defined with base points p1, p2 and control points c1, c2 with
* with a few quadratic Bezier curves.
* The function uses tangent method to find quadratic approximation of cubic curve segment and
* simplified Hausdorff distance to determine number of segments that is enough to make error small.
* In general the method is the same as described here: https://fontforge.github.io/bezier.html.
* Approximate cubic Bezier curve defined with base points p1, p2 and
* control points c1, c2 with with a few quadratic Bezier curves. The
* function uses tangent method to find quadratic approximation of cubic
* curve segment and simplified Hausdorff distance to determine number of
* segments that is enough to make error small. In general the method is the
* same as described here: https://fontforge.github.io/bezier.html.
*
* @param p1x Base point 1 x coordinate
* @param p1y Base point 1 y coordinate
* @param c1x Control point 1 x coordinate
@@ -325,6 +473,66 @@ public class CubicToQuad {
* @return List of quadratic Bezier curve points
*/
public List<Double> cubicToQuad(double p1x, double p1y, double c1x, double c1y, double c2x, double c2y, double p2x, double p2y, double errorBound) {
List<Double> inflections = solveInflections(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
if (inflections.isEmpty()) {
return _cubicToQuad(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, errorBound);
}
List<Double> result = new ArrayList<>();
double[] curve = new double[]{p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y};
double prevPoint = 0;
List<Double> quad;
double[][] split;
for (int inflectionIdx = 0; inflectionIdx < inflections.size(); inflectionIdx++) {
split = subdivideCubic(
curve[0], curve[1], curve[2], curve[3],
curve[4], curve[5], curve[6], curve[7],
// we make a new curve, so adjust inflection point accordingly
1 - (1 - inflections.get(inflectionIdx)) / (1 - prevPoint)
);
quad = _cubicToQuad(
split[0][0], split[0][1], split[0][2], split[0][3],
split[0][4], split[0][5], split[0][6], split[0][7],
errorBound
);
result.addAll(quad.subList(0, quad.size() - 2));
curve = split[1];
prevPoint = inflections.get(inflectionIdx);
}
quad = _cubicToQuad(
curve[0], curve[1], curve[2], curve[3],
curve[4], curve[5], curve[6], curve[7],
errorBound
);
result.addAll(quad);
return result;
}
/**
* Approximate cubic Bezier curve defined with base points p1, p2 and
* control points c1, c2 with with a few quadratic Bezier curves. The
* function uses tangent method to find quadratic approximation of cubic
* curve segment and simplified Hausdorff distance to determine number of
* segments that is enough to make error small. In general the method is the
* same as described here: https://fontforge.github.io/bezier.html.
*
* @param p1x Base point 1 x coordinate
* @param p1y Base point 1 y coordinate
* @param c1x Control point 1 x coordinate
* @param c1y Control point 1 y coordinate
* @param c2x Control point 2 x coordinate
* @param c2y Control point 2 y coordinate
* @param p2x Base point 2 x coordinate
* @param p2y Base point 2 y coordinate
* @param errorBound Error bound
* @return List of quadratic Bezier curve points
*/
private List<Double> _cubicToQuad(double p1x, double p1y, double c1x, double c1y, double c2x, double c2y, double p2x, double p2y, double errorBound) {
Point p1 = new Point(p1x, p1y);
Point c1 = new Point(c1x, c1y);
Point c2 = new Point(c2x, c2y);
@@ -352,4 +560,35 @@ public class CubicToQuad {
}
return toFlatArray(approximation);
}
/*
* Find inflection points on a cubic curve, algorithm is similar to this one:
* http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html
*/
private List<Double> solveInflections(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) {
double p = -(x4 * (y1 - 2 * y2 + y3)) + x3 * (2 * y1 - 3 * y2 + y4)
+ x1 * (y2 - 2 * y3 + y4) - x2 * (y1 - 3 * y3 + 2 * y4);
double q = x4 * (y1 - y2) + 3 * x3 * (-y1 + y2) + x2 * (2 * y1 - 3 * y3 + y4) - x1 * (2 * y2 - 3 * y3 + y4);
double r = x3 * (y1 - y2) + x1 * (y2 - y3) + x2 * (-y1 + y3);
double[] arr = quadSolve(p, q, r);
List<Double> inf = new ArrayList<>();
for (double t : arr) {
if (t > 1e-8 && t < 1 - 1e-8) {
inf.add(t);
}
}
Collections.sort(inf);
return inf;
// return quadSolve(p, q, r).filter(function (t) { return t > 1e-8 && t < 1 - 1e-8 }).sort(byNumber)
}
public static void main(String[] args) {
List<Double> quadCoordinates = new CubicToQuad().cubicToQuad(7217.0, 4004.0, 7155.32, 4019.7800000000007, 6403.46, 3544.120000000001, 6280.699999999999, 3559.46, 0.1);
int r = 0;
for (Double d : quadCoordinates) {
System.err.println("" + r + ": " + d);
r++;
}
}
}