diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b4bca3d..3f5e8493a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to this project will be documented in this file. - Editor (JSyntaxPane) incorrectly draws line numbers panel on Hi-dpi displays - Substance LAF - Shadow in text (mostly in window titles) drawn incorrectly - Incorrect icons for `New empty` action - with dashed borders +- [#2443] SVG importer - converting cubic bezier curves to quadratic ## [22.0.2] - 2025-01-17 ### Added @@ -3730,6 +3731,7 @@ Major version of SWF to XML export changed to 2. [#2400]: https://www.free-decompiler.com/flash/issues/2400 [#2413]: https://www.free-decompiler.com/flash/issues/2413 [#2386]: https://www.free-decompiler.com/flash/issues/2386 +[#2443]: https://www.free-decompiler.com/flash/issues/2443 [#2375]: https://www.free-decompiler.com/flash/issues/2375 [#2374]: https://www.free-decompiler.com/flash/issues/2374 [#2389]: https://www.free-decompiler.com/flash/issues/2389 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/svg/CubicToQuad.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/svg/CubicToQuad.java index cdf29f0e2..eedf09020 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/svg/CubicToQuad.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/importers/svg/CubicToQuad.java @@ -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 cubicPoints = new ArrayList<>(); + List 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 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 cubicToQuad(double p1x, double p1y, double c1x, double c1y, double c2x, double c2y, double p2x, double p2y, double errorBound) { + List 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 result = new ArrayList<>(); + double[] curve = new double[]{p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y}; + double prevPoint = 0; + List 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 _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 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 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 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++; + } + } }