mirror of
https://github.com/JetBrains/JetBrainsRuntime.git
synced 2025-12-06 09:29:38 +01:00
JDK-8341381 Random lines appear in graphic causing by the fix of JDK-8297230
- Fix cubic offsetting artefacts (sort cubic roots + fixed numerical accuracy problem in ROC^2-w^2 = 0 solver + fixed EliminateInf) - Restored lower precision using ulp(float) in point, line or flat bezier curve checks (cherry picked from commit e72b87e6538dda97e6f0f2840040c6864b3f146e)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2007, 2021, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -144,7 +144,9 @@ final class Curve {
|
||||
// finds points where the first and second derivative are
|
||||
// perpendicular. This happens when g(t) = f'(t)*f''(t) == 0 (where
|
||||
// * is a dot product). Unfortunately, we have to solve a cubic.
|
||||
private int perpendiculardfddf(final double[] pts, final int off) {
|
||||
private int perpendiculardfddf(final double[] pts, final int off,
|
||||
final double A, final double B)
|
||||
{
|
||||
assert pts.length >= off + 4;
|
||||
|
||||
// these are the coefficients of some multiple of g(t) (not g(t),
|
||||
@@ -155,7 +157,7 @@ final class Curve {
|
||||
final double c = 2.0d * (dax * cx + day * cy) + dbx * dbx + dby * dby;
|
||||
final double d = dbx * cx + dby * cy;
|
||||
|
||||
return Helpers.cubicRootsInAB(a, b, c, d, pts, off, 0.0d, 1.0d);
|
||||
return Helpers.cubicRootsInAB(a, b, c, d, pts, off, A, B);
|
||||
}
|
||||
|
||||
// Tries to find the roots of the function ROC(t)-w in [0, 1). It uses
|
||||
@@ -171,35 +173,43 @@ final class Curve {
|
||||
// at most 4 sub-intervals of (0,1). ROC has asymptotes at inflection
|
||||
// points, so roc-w can have at least 6 roots. This shouldn't be a
|
||||
// problem for what we're trying to do (draw a nice looking curve).
|
||||
int rootsOfROCMinusW(final double[] roots, final int off, final double w2, final double err) {
|
||||
int rootsOfROCMinusW(final double[] roots, final int off, final double w2,
|
||||
final double A, final double B)
|
||||
{
|
||||
// no OOB exception, because by now off<=6, and roots.length >= 10
|
||||
assert off <= 6 && roots.length >= 10;
|
||||
|
||||
int ret = off;
|
||||
final int end = off + perpendiculardfddf(roots, off);
|
||||
final int end = off + perpendiculardfddf(roots, off, A, B);
|
||||
Helpers.isort(roots, off, end);
|
||||
roots[end] = 1.0d; // always check interval end points
|
||||
|
||||
double t0 = 0.0d, ft0 = ROCsq(t0) - w2;
|
||||
double t0 = 0.0d;
|
||||
double ft0 = eliminateInf(ROCsq(t0) - w2);
|
||||
double t1, ft1;
|
||||
|
||||
for (int i = off; i <= end; i++) {
|
||||
double t1 = roots[i], ft1 = ROCsq(t1) - w2;
|
||||
t1 = roots[i];
|
||||
ft1 = eliminateInf(ROCsq(t1) - w2);
|
||||
if (ft0 == 0.0d) {
|
||||
roots[ret++] = t0;
|
||||
} else if (ft1 * ft0 < 0.0d) { // have opposite signs
|
||||
// (ROC(t)^2 == w^2) == (ROC(t) == w) is true because
|
||||
// ROC(t) >= 0 for all t.
|
||||
roots[ret++] = falsePositionROCsqMinusX(t0, t1, w2, err);
|
||||
roots[ret++] = falsePositionROCsqMinusX(t0, t1, ft0, ft1, w2, A); // A = err
|
||||
}
|
||||
t0 = t1;
|
||||
ft0 = ft1;
|
||||
}
|
||||
|
||||
return ret - off;
|
||||
}
|
||||
|
||||
private static double eliminateInf(final double x) {
|
||||
return (x == Double.POSITIVE_INFINITY ? Double.MAX_VALUE :
|
||||
(x == Double.NEGATIVE_INFINITY ? Double.MIN_VALUE : x));
|
||||
private final static double MAX_ROC_SQ = 1e20;
|
||||
|
||||
private static double eliminateInf(final double x2) {
|
||||
// limit the value of x to avoid numerical problems (smaller step):
|
||||
// must handle NaN and +Infinity:
|
||||
return (x2 <= MAX_ROC_SQ) ? x2 : MAX_ROC_SQ;
|
||||
}
|
||||
|
||||
// A slight modification of the false position algorithm on wikipedia.
|
||||
@@ -210,17 +220,18 @@ final class Curve {
|
||||
// and turn out. Same goes for the newton's method
|
||||
// algorithm in Helpers.java
|
||||
private double falsePositionROCsqMinusX(final double t0, final double t1,
|
||||
final double ft0, final double ft1,
|
||||
final double w2, final double err)
|
||||
{
|
||||
final int iterLimit = 100;
|
||||
int side = 0;
|
||||
double t = t1, ft = eliminateInf(ROCsq(t) - w2);
|
||||
double s = t0, fs = eliminateInf(ROCsq(s) - w2);
|
||||
double s = t0, fs = eliminateInf(ft0);
|
||||
double t = t1, ft = eliminateInf(ft1);
|
||||
double r = s, fr;
|
||||
|
||||
for (int i = 0; i < iterLimit && Math.abs(t - s) > err * Math.abs(t + s); i++) {
|
||||
for (int i = 0; i < iterLimit && Math.abs(t - s) > err; i++) {
|
||||
r = (fs * t - ft * s) / (fs - ft);
|
||||
fr = ROCsq(r) - w2;
|
||||
fr = eliminateInf(ROCsq(r) - w2);
|
||||
if (sameSign(fr, ft)) {
|
||||
ft = fr; t = r;
|
||||
if (side < 0) {
|
||||
@@ -241,7 +252,7 @@ final class Curve {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
return (Math.abs(ft) <= Math.abs(fs)) ? t : s;
|
||||
}
|
||||
|
||||
private static boolean sameSign(final double x, final double y) {
|
||||
@@ -256,9 +267,9 @@ final class Curve {
|
||||
final double dy = t * (t * day + dby) + cy;
|
||||
final double ddx = 2.0d * dax * t + dbx;
|
||||
final double ddy = 2.0d * day * t + dby;
|
||||
final double dx2dy2 = dx * dx + dy * dy;
|
||||
final double ddx2ddy2 = ddx * ddx + ddy * ddy;
|
||||
final double ddxdxddydy = ddx * dx + ddy * dy;
|
||||
return dx2dy2 * ((dx2dy2 * dx2dy2) / (dx2dy2 * ddx2ddy2 - ddxdxddydy * ddxdxddydy));
|
||||
final double dx2dy2 = dx * dx + dy * dy; // positive
|
||||
final double dxddyddxdy = dx * ddy - dy * ddx;
|
||||
// may return +Infinity if dxddyddxdy = 0 or NaN if 0/0:
|
||||
return (dx2dy2 * dx2dy2 * dx2dy2) / (dxddyddxdy * dxddyddxdy); // both positive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2007, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -566,7 +566,7 @@ public final class DMarlinRenderingEngine extends RenderingEngine
|
||||
}
|
||||
|
||||
private static boolean nearZero(final double num) {
|
||||
return Math.abs(num) < 2.0d * Math.ulp(num);
|
||||
return Math.abs(num) < 2.0d * Helpers.ulp(num);
|
||||
}
|
||||
|
||||
abstract static class NormalizingPathIterator implements PathIterator {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2007, 2022, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -31,12 +31,19 @@ import sun.java2d.marlin.stats.StatLong;
|
||||
|
||||
final class Helpers implements MarlinConst {
|
||||
|
||||
private final static double T_ERR = 1e-4;
|
||||
private final static double T_A = T_ERR;
|
||||
private final static double T_B = 1.0 - T_ERR;
|
||||
|
||||
private static final double EPS = 1e-9d;
|
||||
|
||||
private Helpers() {
|
||||
throw new Error("This is a non instantiable class");
|
||||
}
|
||||
|
||||
/** use lower precision like former Pisces and Marlin (float-precision) */
|
||||
static double ulp(final double value) { return Math.ulp((float)value); }
|
||||
|
||||
static boolean within(final double x, final double y) {
|
||||
return within(x, y, EPS);
|
||||
}
|
||||
@@ -322,10 +329,10 @@ final class Helpers implements MarlinConst {
|
||||
|
||||
// now we must subdivide at points where one of the offset curves will have
|
||||
// a cusp. This happens at ts where the radius of curvature is equal to w.
|
||||
ret += c.rootsOfROCMinusW(ts, ret, w2, 0.0001d);
|
||||
ret += c.rootsOfROCMinusW(ts, ret, w2, T_A, T_B);
|
||||
|
||||
ret = filterOutNotInAB(ts, 0, ret, 0.0001d, 0.9999d);
|
||||
isort(ts, ret);
|
||||
ret = filterOutNotInAB(ts, 0, ret, T_A, T_B);
|
||||
isort(ts, 0, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -354,7 +361,7 @@ final class Helpers implements MarlinConst {
|
||||
if ((outCodeOR & OUTCODE_BOTTOM) != 0) {
|
||||
ret += curve.yPoints(ts, ret, clipRect[1]);
|
||||
}
|
||||
isort(ts, ret);
|
||||
isort(ts, 0, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -374,11 +381,11 @@ final class Helpers implements MarlinConst {
|
||||
}
|
||||
}
|
||||
|
||||
static void isort(final double[] a, final int len) {
|
||||
for (int i = 1, j; i < len; i++) {
|
||||
static void isort(final double[] a, final int off, final int len) {
|
||||
for (int i = off + 1, j; i < len; i++) {
|
||||
final double ai = a[i];
|
||||
j = i - 1;
|
||||
for (; j >= 0 && a[j] > ai; j--) {
|
||||
for (; j >= off && a[j] > ai; j--) {
|
||||
a[j + 1] = a[j];
|
||||
}
|
||||
a[j + 1] = ai;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2007, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -886,8 +886,8 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst {
|
||||
|
||||
// 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));
|
||||
final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Math.ulp(y4));
|
||||
final boolean p1eqp2 = Helpers.withinD(dx1, dy1, 6.0d * Helpers.ulp(y2));
|
||||
final boolean p3eqp4 = Helpers.withinD(dx4, dy4, 6.0d * Helpers.ulp(y4));
|
||||
|
||||
if (p1eqp2 && p3eqp4) {
|
||||
return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff);
|
||||
@@ -905,7 +905,7 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst {
|
||||
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))) {
|
||||
if (Helpers.within(dotsq, l1sq * l4sq, 4.0d * Helpers.ulp(dotsq))) {
|
||||
return getLineOffsets(x1, y1, x4, y4, leftOff, rightOff);
|
||||
}
|
||||
|
||||
@@ -1078,8 +1078,8 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst {
|
||||
// equal if they're very close to each other.
|
||||
|
||||
// if p1 == p2 or p2 == p3: draw line from p1->p3
|
||||
final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Math.ulp(y2));
|
||||
final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Math.ulp(y3));
|
||||
final boolean p1eqp2 = Helpers.withinD(dx12, dy12, 6.0d * Helpers.ulp(y2));
|
||||
final boolean p2eqp3 = Helpers.withinD(dx23, dy23, 6.0d * Helpers.ulp(y3));
|
||||
|
||||
if (p1eqp2 || p2eqp3) {
|
||||
return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff);
|
||||
@@ -1091,7 +1091,7 @@ final class Stroker implements StartFlagPathConsumer2D, MarlinConst {
|
||||
final double l1sq = dx12 * dx12 + dy12 * dy12;
|
||||
final double l3sq = dx23 * dx23 + dy23 * dy23;
|
||||
|
||||
if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Math.ulp(dotsq))) {
|
||||
if (Helpers.within(dotsq, l1sq * l3sq, 4.0d * Helpers.ulp(dotsq))) {
|
||||
return getLineOffsets(x1, y1, x3, y3, leftOff, rightOff);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@@ -27,7 +27,7 @@ package sun.java2d.marlin;
|
||||
|
||||
public final class Version {
|
||||
|
||||
private static final String VERSION = "marlin-0.9.4.7-Unsafe-OpenJDK";
|
||||
private static final String VERSION = "marlin-0.9.4.9-Unsafe-OpenJDK";
|
||||
|
||||
public static String getVersion() {
|
||||
return VERSION;
|
||||
|
||||
601
test/jdk/sun/java2d/marlin/Bug8341381.java
Normal file
601
test/jdk/sun/java2d/marlin/Bug8341381.java
Normal file
@@ -0,0 +1,601 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.Stroke;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.CubicCurve2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.Raster;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
|
||||
import static java.lang.System.out;
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @bug 8341381
|
||||
* @summary fix cubic offsetting issue (numerical accuracy)
|
||||
* @run main/othervm/timeout=20 Bug8341381
|
||||
* @modules java.desktop/sun.java2d.marlin
|
||||
*/
|
||||
public final class Bug8341381 {
|
||||
|
||||
static final boolean SHOW_GUI = false;
|
||||
|
||||
static final boolean CHECK_PIXELS = true;
|
||||
static final boolean TRACE_ALL = false;
|
||||
static final boolean TRACE_CHECK_PIXELS = false;
|
||||
|
||||
static final boolean SAVE_IMAGE = false;
|
||||
|
||||
static final boolean INTENSIVE = false;
|
||||
|
||||
static final double DPI = 96;
|
||||
static final float STROKE_WIDTH = 15f;
|
||||
|
||||
// delay is 1 frame at 60hz
|
||||
static final int DELAY = 16;
|
||||
// off-screen test step (1.0 by default)
|
||||
static final double STEP = (INTENSIVE) ? 1.0 / 117 : 1.0;
|
||||
|
||||
// stats:
|
||||
static int N_TEST = 0;
|
||||
static int N_FAIL = 0;
|
||||
|
||||
static final AtomicBoolean isMarlin = new AtomicBoolean();
|
||||
static final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
public static void main(final String[] args) {
|
||||
Locale.setDefault(Locale.US);
|
||||
|
||||
// FIRST: Get Marlin runtime state from its log:
|
||||
|
||||
// initialize j.u.l Logger:
|
||||
final Logger log = Logger.getLogger("sun.java2d.marlin");
|
||||
log.addHandler(new Handler() {
|
||||
@Override
|
||||
public void publish(LogRecord record) {
|
||||
final String msg = record.getMessage();
|
||||
if (msg != null) {
|
||||
// last space to avoid matching other settings:
|
||||
if (msg.startsWith("sun.java2d.renderer ")) {
|
||||
isMarlin.set(msg.contains("DMarlinRenderingEngine"));
|
||||
}
|
||||
}
|
||||
|
||||
final Throwable th = record.getThrown();
|
||||
// detect any Throwable:
|
||||
if (th != null) {
|
||||
out.println("Test failed:\n" + record.getMessage());
|
||||
th.printStackTrace(out);
|
||||
throw new RuntimeException("Test failed: ", th);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws SecurityException {
|
||||
}
|
||||
});
|
||||
|
||||
out.println("Bug8341381: start");
|
||||
final long startTime = System.currentTimeMillis();
|
||||
|
||||
// enable Marlin logging & internal checks:
|
||||
System.setProperty("sun.java2d.renderer.log", "true");
|
||||
System.setProperty("sun.java2d.renderer.useLogger", "true");
|
||||
|
||||
try {
|
||||
startTest();
|
||||
|
||||
out.println("WAITING ...");
|
||||
latch.await(15, TimeUnit.SECONDS); // 2s typically
|
||||
|
||||
if (isMarlin.get()) {
|
||||
out.println("Marlin renderer used at runtime.");
|
||||
} else {
|
||||
throw new RuntimeException("Marlin renderer NOT used at runtime !");
|
||||
}
|
||||
|
||||
// show test report:
|
||||
out.println("TESTS: " + N_TEST + " FAILS: " + N_FAIL);
|
||||
|
||||
if (N_FAIL > 0) {
|
||||
throw new RuntimeException("Bug8341381: " + N_FAIL + " / " + N_TEST + " test(s) failed !");
|
||||
}
|
||||
|
||||
} catch (InterruptedException ie) {
|
||||
throw new RuntimeException(ie);
|
||||
} catch (InvocationTargetException ite) {
|
||||
throw new RuntimeException(ite);
|
||||
} finally {
|
||||
final double elapsed = (System.currentTimeMillis() - startTime) / 1000.0;
|
||||
out.println("Bug8341381: end (" + elapsed + " s)");
|
||||
}
|
||||
}
|
||||
|
||||
private static void startTest() throws InterruptedException, InvocationTargetException {
|
||||
if (SHOW_GUI) {
|
||||
SwingUtilities.invokeAndWait(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final JFrame viewer = new JFrame();
|
||||
viewer.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
viewer.setContentPane(new CanvasPanel(viewer));
|
||||
viewer.pack();
|
||||
viewer.setVisible(true);
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
out.println("STEP: " + STEP);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final Context ctx = new Context();
|
||||
final Dimension initialDim = ctx.bugDisplay.getSize(DPI);
|
||||
|
||||
double w = initialDim.width;
|
||||
double h = initialDim.height;
|
||||
do {
|
||||
ctx.shouldScale(w, h);
|
||||
ctx.paintImage();
|
||||
|
||||
// resize component:
|
||||
w -= STEP;
|
||||
h -= STEP;
|
||||
|
||||
} while (ctx.iterate());
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
static final class Context {
|
||||
|
||||
final BugDisplay bugDisplay = new BugDisplay();
|
||||
double width = 0.0, height = 0.0;
|
||||
|
||||
BufferedImage bimg = null;
|
||||
|
||||
boolean shouldScale(final double w, final double h) {
|
||||
if ((w != width) || (h != height) || !bugDisplay.isScaled) {
|
||||
width = w;
|
||||
height = h;
|
||||
bugDisplay.scale(width, height);
|
||||
N_TEST++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void paintImage() {
|
||||
final int w = bugDisplay.canvasWidth;
|
||||
final int h = bugDisplay.canvasHeight;
|
||||
|
||||
if ((bimg == null) || (w > bimg.getWidth()) || (h > bimg.getHeight())) {
|
||||
bimg = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
|
||||
}
|
||||
final Graphics gi = bimg.getGraphics();
|
||||
try {
|
||||
bugDisplay.paint(gi);
|
||||
} finally {
|
||||
gi.dispose();
|
||||
}
|
||||
if (!bugDisplay.checkImage(bimg)) {
|
||||
N_FAIL++;
|
||||
}
|
||||
}
|
||||
|
||||
boolean iterate() {
|
||||
if ((bugDisplay.canvasWidth > 10) || (bugDisplay.canvasHeight > 10)) {
|
||||
// continue:
|
||||
return true;
|
||||
}
|
||||
out.println("Stop");
|
||||
latch.countDown();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static final class CanvasPanel extends JPanel {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Context ctx = new Context();
|
||||
private boolean resized = false;
|
||||
private Timer timer = null;
|
||||
|
||||
public CanvasPanel(final JFrame frame) {
|
||||
timer = new Timer(DELAY, e -> {
|
||||
if (resized) {
|
||||
resized = false;
|
||||
|
||||
if (ctx.iterate()) {
|
||||
// resize component:
|
||||
setSize((int) Math.round(ctx.width - 1), (int) Math.round(ctx.height - 1));
|
||||
} else {
|
||||
timer.stop();
|
||||
if (frame != null) {
|
||||
frame.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
timer.setCoalesce(true);
|
||||
timer.setRepeats(true);
|
||||
timer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(final Graphics g) {
|
||||
final Dimension dim = getSize();
|
||||
if (ctx.shouldScale(dim.width, dim.height)) {
|
||||
this.resized = true;
|
||||
}
|
||||
super.paint(g);
|
||||
|
||||
// paint on buffered image:
|
||||
if (CHECK_PIXELS) {
|
||||
final int w = ctx.bugDisplay.canvasWidth;
|
||||
final int h = ctx.bugDisplay.canvasHeight;
|
||||
if (this.resized) {
|
||||
ctx.paintImage();
|
||||
}
|
||||
g.drawImage(ctx.bimg.getSubimage(0, 0, w, h), 0, 0, null);
|
||||
} else {
|
||||
ctx.bugDisplay.paint(g);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
return ctx.bugDisplay.getSize(DPI);
|
||||
}
|
||||
}
|
||||
|
||||
static final class BugDisplay {
|
||||
|
||||
boolean isScaled = false;
|
||||
int canvasWidth;
|
||||
int canvasHeight;
|
||||
|
||||
private final static java.util.List<CubicCurve2D> curves1 = Arrays.asList(
|
||||
new CubicCurve2D.Double(2191.0, 7621.0, 2191.0, 7619.0, 2191.0, 7618.0, 2191.0, 7617.0),
|
||||
new CubicCurve2D.Double(2191.0, 7617.0, 2191.0, 7617.0, 2191.0, 7616.0, 2191.0, 7615.0),
|
||||
new CubicCurve2D.Double(2198.0, 7602.0, 2200.0, 7599.0, 2203.0, 7595.0, 2205.0, 7590.0),
|
||||
new CubicCurve2D.Double(2205.0, 7590.0, 2212.0, 7580.0, 2220.0, 7571.0, 2228.0, 7563.0),
|
||||
new CubicCurve2D.Double(2228.0, 7563.0, 2233.0, 7557.0, 2239.0, 7551.0, 2245.0, 7546.0),
|
||||
new CubicCurve2D.Double(2245.0, 7546.0, 2252.0, 7540.0, 2260.0, 7534.0, 2267.0, 7528.0),
|
||||
new CubicCurve2D.Double(2267.0, 7528.0, 2271.0, 7526.0, 2275.0, 7524.0, 2279.0, 7521.0),
|
||||
new CubicCurve2D.Double(2279.0, 7521.0, 2279.0, 7520.0, 2280.0, 7520.0, 2281.0, 7519.0)
|
||||
);
|
||||
private final static java.util.List<CubicCurve2D> curves2 = Arrays.asList(
|
||||
new CubicCurve2D.Double(2281.0, 7519.0, 2282.0, 7518.0, 2282.0, 7517.0, 2283.0, 7516.0),
|
||||
new CubicCurve2D.Double(2283.0, 7516.0, 2284.0, 7515.0, 2284.0, 7515.0, 2285.0, 7514.0),
|
||||
new CubicCurve2D.Double(2291.0, 7496.0, 2292.0, 7495.0, 2292.0, 7494.0, 2291.0, 7493.0),
|
||||
new CubicCurve2D.Double(2291.0, 7493.0, 2290.0, 7492.0, 2290.0, 7492.0, 2289.0, 7492.0),
|
||||
new CubicCurve2D.Double(2289.0, 7492.0, 2288.0, 7491.0, 2286.0, 7492.0, 2285.0, 7492.0),
|
||||
new CubicCurve2D.Double(2262.0, 7496.0, 2260.0, 7497.0, 2259.0, 7497.0, 2257.0, 7498.0),
|
||||
new CubicCurve2D.Double(2257.0, 7498.0, 2254.0, 7498.0, 2251.0, 7499.0, 2248.0, 7501.0),
|
||||
new CubicCurve2D.Double(2248.0, 7501.0, 2247.0, 7501.0, 2245.0, 7502.0, 2244.0, 7503.0),
|
||||
new CubicCurve2D.Double(2207.0, 7523.0, 2203.0, 7525.0, 2199.0, 7528.0, 2195.0, 7530.0),
|
||||
new CubicCurve2D.Double(2195.0, 7530.0, 2191.0, 7534.0, 2186.0, 7538.0, 2182.0, 7541.0)
|
||||
);
|
||||
private final static java.util.List<CubicCurve2D> curves3 = Arrays.asList(
|
||||
new CubicCurve2D.Double(2182.0, 7541.0, 2178.0, 7544.0, 2174.0, 7547.0, 2170.0, 7551.0),
|
||||
new CubicCurve2D.Double(2170.0, 7551.0, 2164.0, 7556.0, 2158.0, 7563.0, 2152.0, 7569.0),
|
||||
new CubicCurve2D.Double(2152.0, 7569.0, 2148.0, 7573.0, 2145.0, 7577.0, 2141.0, 7582.0),
|
||||
new CubicCurve2D.Double(2141.0, 7582.0, 2138.0, 7588.0, 2134.0, 7595.0, 2132.0, 7602.0),
|
||||
new CubicCurve2D.Double(2132.0, 7602.0, 2132.0, 7605.0, 2131.0, 7608.0, 2131.0, 7617.0),
|
||||
new CubicCurve2D.Double(2131.0, 7617.0, 2131.0, 7620.0, 2131.0, 7622.0, 2131.0, 7624.0),
|
||||
new CubicCurve2D.Double(2131.0, 7624.0, 2131.0, 7630.0, 2132.0, 7636.0, 2135.0, 7641.0),
|
||||
new CubicCurve2D.Double(2135.0, 7641.0, 2136.0, 7644.0, 2137.0, 7647.0, 2139.0, 7650.0),
|
||||
new CubicCurve2D.Double(2139.0, 7650.0, 2143.0, 7658.0, 2149.0, 7664.0, 2155.0, 7670.0),
|
||||
new CubicCurve2D.Double(2155.0, 7670.0, 2160.0, 7676.0, 2165.0, 7681.0, 2171.0, 7686.0)
|
||||
);
|
||||
private final static java.util.List<CubicCurve2D> curves4 = Arrays.asList(
|
||||
new CubicCurve2D.Double(2171.0, 7686.0, 2174.0, 7689.0, 2177.0, 7692.0, 2180.0, 7694.0),
|
||||
new CubicCurve2D.Double(2180.0, 7694.0, 2185.0, 7698.0, 2191.0, 7702.0, 2196.0, 7706.0),
|
||||
new CubicCurve2D.Double(2196.0, 7706.0, 2199.0, 7708.0, 2203.0, 7711.0, 2207.0, 7713.0),
|
||||
new CubicCurve2D.Double(2244.0, 7734.0, 2245.0, 7734.0, 2247.0, 7735.0, 2248.0, 7736.0),
|
||||
new CubicCurve2D.Double(2248.0, 7736.0, 2251.0, 7738.0, 2254.0, 7739.0, 2257.0, 7739.0),
|
||||
new CubicCurve2D.Double(2257.0, 7739.0, 2259.0, 7739.0, 2260.0, 7739.0, 2262.0, 7740.0),
|
||||
new CubicCurve2D.Double(2285.0, 7745.0, 2286.0, 7745.0, 2288.0, 7745.0, 2289.0, 7745.0),
|
||||
new CubicCurve2D.Double(2289.0, 7745.0, 2290.0, 7745.0, 2290.0, 7744.0, 2291.0, 7743.0),
|
||||
new CubicCurve2D.Double(2291.0, 7743.0, 2292.0, 7742.0, 2292.0, 7741.0, 2291.0, 7740.0),
|
||||
new CubicCurve2D.Double(2285.0, 7722.0, 2284.0, 7721.0, 2284.0, 7721.0, 2283.0, 7720.0),
|
||||
new CubicCurve2D.Double(2283.0, 7720.0, 2282.0, 7719.0, 2282.0, 7719.0, 2281.0, 7718.0),
|
||||
new CubicCurve2D.Double(2281.0, 7718.0, 2280.0, 7717.0, 2279.0, 7716.0, 2279.0, 7716.0),
|
||||
new CubicCurve2D.Double(2279.0, 7716.0, 2275.0, 7712.0, 2271.0, 7710.0, 2267.0, 7708.0),
|
||||
new CubicCurve2D.Double(2267.0, 7708.0, 2260.0, 7702.0, 2252.0, 7697.0, 2245.0, 7691.0),
|
||||
new CubicCurve2D.Double(2245.0, 7691.0, 2239.0, 7685.0, 2233.0, 7679.0, 2228.0, 7673.0),
|
||||
new CubicCurve2D.Double(2228.0, 7673.0, 2220.0, 7665.0, 2212.0, 7656.0, 2205.0, 7646.0),
|
||||
new CubicCurve2D.Double(2205.0, 7646.0, 2203.0, 7641.0, 2200.0, 7637.0, 2198.0, 7634.0)
|
||||
);
|
||||
|
||||
private final static Point2D.Double[] extent = {new Point2D.Double(0.0, 0.0), new Point2D.Double(7777.0, 10005.0)};
|
||||
|
||||
private final static Stroke STROKE = new BasicStroke(STROKE_WIDTH);
|
||||
private final static Stroke STROKE_DASHED = new BasicStroke(STROKE_WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL,
|
||||
10.0f, new float[] {100f, 0f}, 0.0f);
|
||||
|
||||
// members:
|
||||
private final java.util.List<CubicCurve2D> allCurves = new ArrayList<>();
|
||||
private final Rectangle2D bboxAllCurves = new Rectangle2D.Double();
|
||||
|
||||
BugDisplay() {
|
||||
allCurves.addAll(curves1);
|
||||
allCurves.addAll(curves2);
|
||||
allCurves.addAll(curves3);
|
||||
allCurves.addAll(curves4);
|
||||
|
||||
// initialize bounding box:
|
||||
double x1 = Double.POSITIVE_INFINITY;
|
||||
double y1 = Double.POSITIVE_INFINITY;
|
||||
double x2 = Double.NEGATIVE_INFINITY;
|
||||
double y2 = Double.NEGATIVE_INFINITY;
|
||||
|
||||
for (final CubicCurve2D c : allCurves) {
|
||||
final Rectangle2D r = c.getBounds2D();
|
||||
if (r.getMinX() < x1) {
|
||||
x1 = r.getMinX();
|
||||
}
|
||||
if (r.getMinY() < y1) {
|
||||
y1 = r.getMinY();
|
||||
}
|
||||
if (r.getMaxX() > x2) {
|
||||
x2 = r.getMaxX();
|
||||
}
|
||||
if (r.getMaxY() > y2) {
|
||||
y2 = r.getMaxY();
|
||||
}
|
||||
}
|
||||
// add margin of 10%:
|
||||
final double m = 1.1 * STROKE_WIDTH;
|
||||
bboxAllCurves.setFrameFromDiagonal(x1 - m, y1 - m, x2 + m, y2 + m);
|
||||
}
|
||||
|
||||
public void paint(final Graphics g) {
|
||||
final Graphics2D g2d = (Graphics2D) g;
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
|
||||
|
||||
// ------ scale
|
||||
final AffineTransform tx_orig = g2d.getTransform();
|
||||
final AffineTransform tx = getDrawTransform();
|
||||
g2d.transform(tx);
|
||||
|
||||
// draw bbox:
|
||||
if (!CHECK_PIXELS) {
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.setStroke(STROKE);
|
||||
g2d.draw(bboxAllCurves);
|
||||
}
|
||||
// draw curves:
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
|
||||
g2d.setColor(Color.BLACK);
|
||||
|
||||
// dasher + stroker:
|
||||
g2d.setStroke(STROKE_DASHED);
|
||||
this.allCurves.forEach(g2d::draw);
|
||||
|
||||
// reset
|
||||
g2d.setTransform(tx_orig);
|
||||
}
|
||||
|
||||
private AffineTransform getDrawTransform() {
|
||||
// ------ scale
|
||||
double minX = extent[0].x, maxX = extent[1].x;
|
||||
double minY = extent[0].y, maxY = extent[1].y;
|
||||
|
||||
// we're scaling and respecting the proportions, check which scale to use
|
||||
double sx = this.canvasWidth / Math.abs(maxX - minX);
|
||||
double sy = this.canvasHeight / Math.abs(maxY - minY);
|
||||
double s = Math.min(sx, sy);
|
||||
|
||||
double m00, m11, m02, m12;
|
||||
if (minX < maxX) {
|
||||
m00 = s;
|
||||
m02 = -s * minX;
|
||||
} else {
|
||||
// inverted X axis
|
||||
m00 = -s;
|
||||
m02 = this.canvasWidth + s * maxX;
|
||||
}
|
||||
if (minY < maxY) {
|
||||
m11 = s;
|
||||
m12 = -s * minY;
|
||||
} else {
|
||||
// inverted Y axis
|
||||
m11 = -s;
|
||||
m12 = this.canvasHeight + s * maxY;
|
||||
}
|
||||
|
||||
// scale to the available view port
|
||||
AffineTransform scaleTransform = new AffineTransform(m00, 0, 0, m11, m02, m12);
|
||||
|
||||
// invert the Y axis since (0, 0) is at top left for AWT
|
||||
AffineTransform invertY = new AffineTransform(1, 0, 0, -1, 0, this.canvasHeight);
|
||||
invertY.concatenate(scaleTransform);
|
||||
|
||||
return invertY;
|
||||
}
|
||||
|
||||
public Dimension getSize(double dpi) {
|
||||
double metricScalingFactor = 0.02539999969303608;
|
||||
// 1 inch = 25,4 millimeter
|
||||
final double factor = dpi * metricScalingFactor / 25.4;
|
||||
|
||||
int width = (int) Math.ceil(Math.abs(extent[1].x - extent[0].x) * factor);
|
||||
int height = (int) Math.ceil(Math.abs(extent[1].y - extent[0].y) * factor);
|
||||
|
||||
return new Dimension(width, height);
|
||||
}
|
||||
|
||||
public void scale(double w, double h) {
|
||||
double extentWidth = Math.abs(extent[1].x - extent[0].x);
|
||||
double extentHeight = Math.abs(extent[1].y - extent[0].y);
|
||||
|
||||
double fx = w / extentWidth;
|
||||
if (fx * extentHeight > h) {
|
||||
fx = h / extentHeight;
|
||||
}
|
||||
this.canvasWidth = (int) Math.round(fx * extentWidth);
|
||||
this.canvasHeight = (int) Math.round(fx * extentHeight);
|
||||
|
||||
// out.println("canvas scaled (" + canvasWidth + " x " + canvasHeight + ")");
|
||||
|
||||
this.isScaled = true;
|
||||
}
|
||||
|
||||
protected boolean checkImage(BufferedImage image) {
|
||||
final AffineTransform tx = getDrawTransform();
|
||||
|
||||
final Point2D pMin = new Point2D.Double(bboxAllCurves.getMinX(), bboxAllCurves.getMinY());
|
||||
final Point2D pMax = new Point2D.Double(bboxAllCurves.getMaxX(), bboxAllCurves.getMaxY());
|
||||
|
||||
final Point2D tMin = tx.transform(pMin, null);
|
||||
final Point2D tMax = tx.transform(pMax, null);
|
||||
|
||||
int xMin = (int) tMin.getX();
|
||||
int xMax = (int) tMax.getX();
|
||||
if (xMin > xMax) {
|
||||
int t = xMin;
|
||||
xMin = xMax;
|
||||
xMax = t;
|
||||
}
|
||||
|
||||
int yMin = (int) tMin.getY();
|
||||
int yMax = (int) tMax.getY();
|
||||
if (yMin > yMax) {
|
||||
int t = yMin;
|
||||
yMin = yMax;
|
||||
yMax = t;
|
||||
}
|
||||
// add pixel margin (AA):
|
||||
xMin -= 3;
|
||||
xMax += 4;
|
||||
yMin -= 3;
|
||||
yMax += 4;
|
||||
|
||||
if (xMin < 0 || xMax > image.getWidth()
|
||||
|| yMin < 0 || yMax > image.getHeight()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// out.println("Checking rectangle: " + tMin + " to " + tMax);
|
||||
// out.println("X min: " + xMin + " - max: " + xMax);
|
||||
// out.println("Y min: " + yMin + " - max: " + yMax);
|
||||
|
||||
final Raster raster = image.getData();
|
||||
final int expected = Color.WHITE.getRGB();
|
||||
int nBadPixels = 0;
|
||||
|
||||
// horizontal lines:
|
||||
for (int x = xMin; x <= xMax; x++) {
|
||||
if (!checkPixel(raster, x, yMin, expected)) {
|
||||
nBadPixels++;
|
||||
}
|
||||
if (!checkPixel(raster, x, yMax, expected)) {
|
||||
nBadPixels++;
|
||||
}
|
||||
}
|
||||
|
||||
// vertical lines:
|
||||
for (int y = yMin; y <= yMax; y++) {
|
||||
if (!checkPixel(raster, xMin, y, expected)) {
|
||||
nBadPixels++;
|
||||
}
|
||||
if (!checkPixel(raster, xMax, y, expected)) {
|
||||
nBadPixels++;
|
||||
}
|
||||
}
|
||||
|
||||
if (nBadPixels != 0) {
|
||||
out.println("(" + canvasWidth + " x " + canvasHeight + ") BAD pixels = " + nBadPixels);
|
||||
|
||||
if (SAVE_IMAGE) {
|
||||
try {
|
||||
final File file = new File("Bug8341381-" + canvasWidth + "-" + canvasHeight + ".png");
|
||||
|
||||
out.println("Writing file: " + file.getAbsolutePath());
|
||||
ImageIO.write(image.getSubimage(0, 0, canvasWidth, canvasHeight), "PNG", file);
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (TRACE_ALL) {
|
||||
out.println("(" + canvasWidth + " x " + canvasHeight + ") OK");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private final static int[] TMP_RGB = new int[1];
|
||||
|
||||
private static boolean checkPixel(final Raster raster,
|
||||
final int x, final int y,
|
||||
final int expected) {
|
||||
|
||||
final int[] rgb = (int[]) raster.getDataElements(x, y, TMP_RGB);
|
||||
|
||||
if (rgb[0] != expected) {
|
||||
if (TRACE_CHECK_PIXELS) {
|
||||
out.println("bad pixel at (" + x + ", " + y + ") = " + rgb[0]
|
||||
+ " expected = " + expected);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user