JDK-8277574 : Long line when path.curveTo() is drawn with RenderingHint VALUE_ANTIALIAS_ON
  • Type: Bug
  • Component: client-libs
  • Sub-Component: 2d
  • Affected Version: 11,17,18
  • Priority: P3
  • Status: In Progress
  • Resolution: Unresolved
  • OS: windows_10
  • CPU: x86_64
  • Submitted: 2021-11-19
  • Updated: 2024-01-30
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
tbdUnresolved
Description
ADDITIONAL SYSTEM INFORMATION :
Windows 10.0.19043.1348

A DESCRIPTION OF THE PROBLEM :
Running the attached code creates a long line in the produced image

REGRESSION : Last worked in version 8u311

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
run code
open the result PNG image


EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
very tiny black line
ACTUAL -
long black line

---------- BEGIN SOURCE ----------

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

public class PDFBOX_5322
{
    public static void main(String[] args) throws IOException
    {
        BufferedImage bim = new BufferedImage(623, 311, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = (Graphics2D) bim.getGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setBackground(Color.white);
        g2d.clearRect(0, 0, bim.getWidth(), bim.getHeight());
        g2d.setColor(Color.black);
        g2d.transform(new AffineTransform(1f, 0f, 0f, -1f, 0, 311f));
        GeneralPath path = new GeneralPath();
        path.moveTo(438.5315f, 261.2291f);
        path.curveTo(437.8919f, 265.3086f, 437.6903f, 265.3338f, 437.6903f, 265.3338f);
        path.curveTo(437.5043f, 265.3571f, 437.3822f, 265.2621f, 437.3822f, 265.2621f);
        g2d.draw(path);
        g2d.dispose();
        ImageIO.write(bim, "png", new File("PDFBOX-5322.png"));
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Use jdk8, or don't use antialiasing

FREQUENCY : always



Comments
This bug is difficult but I should figure out how to stay in safe case: - coordinate rounding to .... - detect bad case: c1 & c2 sign differ - use safer approach : limit angle to 60 or 30 deg, use other offset algo... I will look to possible solutions
30-01-2024

as not on review (yet) and not a regression in 19, then move to "20" as a possible target
08-06-2022

Some more debugging information: This specific small curve test case causes several problems: - subdivision at particular points (extrema, inflexion, cusp points) have numerical accuracy issues - computeOffsetCubic() is mainly the basic Tiller & hanson approach: shift start/end point and fix slope at t=0.5. This algorithm expects well-formed curves (symetric legs, flat angles) that is not the case here: small loop, high radius of curvature. With this small loop curve, the offsetted thick curve is merely a pie / arcs... Testing offsetted points helps to detect such bad behaviour => use recursive subdivision approach, based on max(roc) or angle threshold... - the cusp area is not correctly handled as the offset curve has the opposite direction. For example thick ellipses have now an hole at the middle whereas it should be plain fill. Finally I propose to ignore too small curves compared to the stroke width and/or the device units (1/8th subpixel grid) as a first fix, based on curve flattness squared Other defaults will be tracked in their own bugs.
05-04-2022

I think a properly done JDK 19 fix is fine. It isn't an 18 regression (or even a 17 one) so there is absolutely no reason to rush a quick fix into JDK 18 I'm not sure anyone (?) is going to support 18 as an LTS either .. if someone does then they can backport it in to an 18 update.
05-01-2022

Phil could you tell me how urgent is this bug fix (present in both java2d & javafx) ?
20-12-2021

This bug in the Marlin-renderer for java 2D (11+) is also present in the Marlin-renderer for java FX (11+); should we open another javafx ticket ?
15-12-2021

I think JDK 8u 311 is in fact Oracle JDK8u so it uses ductus rendering engine, no GPL (Pisces or Marlin) renderer. I will test on openjdk8u as I think Pisces may work as it used float values: Math.ulp(1) ~ 1e-6 (float) not 1e-15 (double) Phil Race or Joe Darcy: how could we write an automated fuzzy test to assert degenerated conditions and ensure all Math.ulp conditions are valid and enough to fix degenerated cases? see Stroker.computeOffsetCubic() that uses a discrimminant and matrix inversion to compute the offsetted curves. I am not as good in maths as Joe Darcy, but I am sure I can make a fuzzy test to ensure good curve conditioning... Finally how urgent is this P3 bug ? Should I work now on this bug fix + test now to get it in 18.0.0 ? or you consider it is too late and let's backport fix later. Laurent
15-12-2021

Here are the internal debugging logs: INFO: Stroker.computeOffsetCubic(437.4173312717014, 45.489651150173614, 437.41733127170136, 45.48965115017361, 437.5, 45.5, 437.5, 45.5); dx1: -5.6843418860808015E-14 dy1: -7.105427357601002E-15 th: 4.263256414560601E-14 dx4: 0.0 dy4: 0.0 th: 4.263256414560601E-14 dotsq: 2.2778723994689156E-29 l1sq: 3.281661365719409E-27 l4sq: 0.006941217331244038 l1sq * l4sq: 2.277872474700554E-29 th: 1.1210387714598537E-44 th fixed: 1.0E-15 INFO: Stroker: p.lineTo(437.4794388031552, 44.993523490657715); As you can see both thresholds are too small compared to values (1e-14 or 1E-44) ! Here is the current changes to get trace: --- a/src/main/java/sun/java2d/marlin/Stroker.java +++ b/src/main/java/sun/java2d/marlin/Stroker.java @@ -856,9 +856,31 @@ final double x3 = pts[off + 4]; final double y3 = pts[off + 5]; final double x4 = pts[off + 6]; final double y4 = pts[off + 7]; + + if ((437.4173312717014 == pts[off + 0]) + && (45.489651150173614 == pts[off + 1])) + { + System.out.println("Bad case ?"); + } + + if (DEBUG_CUBIC_INFO) { + MarlinUtils.logInfo("Stroker.computeOffsetCubic(" + + pts[off + 0] + ", " + pts[off + 1] + ", " + + pts[off + 2] + ", " + pts[off + 3] + ", " + + pts[off + 4] + ", " + pts[off + 5] + ", " + + pts[off + 6] + ", " + pts[off + 7] + ");"); + } + double dx1 = x2 - x1; double dy1 = y2 - y1; double dx4 = x4 - x3; double dy4 = y4 - y3; + System.out.println("dx1: "+dx1); + System.out.println("dy1: "+dy1); + System.out.println("th: "+(6.0d * Math.ulp(y2))); + System.out.println("dx4: "+dx4); + System.out.println("dy4: "+dy4); + System.out.println("th: "+(6.0d * Math.ulp(y4))); + // if p1 == p2 && p3 == p4: draw line from p1->p4, unless p1 == p4, // in which case ignore if p1 == p2 final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Math.ulp(y2)); @@ -880,7 +902,17 @@ final double l1sq = dx1 * dx1 + dy1 * dy1; final double l4sq = dx4 * dx4 + dy4 * dy4; - if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Math.ulp(dotsq))) { + System.out.println("dotsq: "+dotsq); + System.out.println("l1sq: "+l1sq); + System.out.println("l4sq: "+l4sq); + + System.out.println("l1sq * l4sq: "+(l1sq * l4sq)); + System.out.println("th: "+(4.0d * Math.ulp(dotsq))); + + final double th = Math.max(1e-15, 4.0d * Math.ulp(dotsq)); + System.out.println("th fixed: "+th); + + if (Helpers.within(dotsq, l1sq * l4sq, th)) { return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff); }
02-12-2021

Help needed on FP maths, See dot product check: https://github.com/openjdk/jdk/blob/684edbb4c884cbc3e05118e4bc9808b5d5b71a74/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java#L869 I will propose a simpler solution to test degenerated cases: points within 1e-9 min... too much accuracy not required. I will publish real numbers asap. I can propose a quick fix for JDK18, before Dec 9th, but I doubt review can be done before RDP0. Phil do you want me to fix it as fast as possible ?
02-12-2021

This bug in the Marlin renderer is coming from Pisces, the former shape rasterizer in OpenJDK 6-8, it was removed in JDK10 or 11. As this bug is related to floating precision (double is not helping here), I will write an automated fuzzy test to ascertain both small or huge curves work. That's the tricky part: I will generate control points randomly within few ulps and check stroked shape is not too far from original curve + radius = stroke width / 2. Could joe darcy help me on this FP precision issue ?
02-12-2021

I had a quick look with path tracing enabled in Marlin renderer. The transformed curve in Stroker is having P1, P2 very close ~ 1e-14 and P3=P4. In this degenerated case, the detection is malformed and computed offset curves are totally wrong for numerical instabilities due to matrix inversion. See https://github.com/openjdk/jdk/blob/a363b7b9217cbb9a7580a87b812da8d5a4215326/src/java.desktop/share/classes/sun/java2d/marlin/Stroker.java#L853 I quickly hacked the detection and the curve is then considered a line from P1 to P4.
01-12-2021

Seems like it must be a marlin renderer bug.
22-11-2021

Checked with attached test case, issue is reproducible, from jdk 11 on-wards<attached screenshot> Test Result: ========= 8u311: Pass 11: Fail 11.0.13: Fail 17: Fail 18ea23: Fail
22-11-2021