1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 import java.awt.BasicStroke;
  24 import java.awt.Color;
  25 import java.awt.Graphics2D;
  26 import java.awt.RenderingHints;
  27 import java.awt.Shape;
  28 import java.awt.Stroke;
  29 import java.awt.geom.Ellipse2D;
  30 import java.awt.geom.Path2D;
  31 import java.awt.geom.PathIterator;
  32 import java.awt.image.BufferedImage;
  33 import java.awt.image.DataBufferInt;
  34 import java.io.File;
  35 import java.io.FileOutputStream;
  36 import java.io.IOException;
  37 import java.util.Arrays;
  38 import java.util.Iterator;
  39 import java.util.Locale;
  40 import java.util.Random;
  41 import java.util.concurrent.atomic.AtomicBoolean;
  42 import java.util.concurrent.atomic.AtomicInteger;
  43 import java.util.logging.Handler;
  44 import java.util.logging.LogRecord;
  45 import java.util.logging.Logger;
  46 import javax.imageio.IIOImage;
  47 import javax.imageio.ImageIO;
  48 import javax.imageio.ImageWriteParam;
  49 import javax.imageio.ImageWriter;
  50 import javax.imageio.stream.ImageOutputStream;
  51 
  52 /**
  53  * @test
  54  * @bug 8191814
  55  * @summary Verifies that Marlin rendering generates the same
  56  * images with and without clipping optimization with all possible
  57  * stroke (cap/join) and/or dashes or fill modes (EO rules)
  58  * for paths made of either 9 lines, 4 quads, 2 cubics (random)
  59  * Note: Use the argument -slow to run more intensive tests (too much time)
  60  *
  61  * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly
  62  * @run main/othervm/timeout=240 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly -doDash
  63  * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic
  64  * @run main/othervm/timeout=240 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic -doDash
  65  * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly
  66  * @run main/othervm/timeout=240 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly -doDash
  67  * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic
  68  * @run main/othervm/timeout=240 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic -doDash
  69 */
  70 public final class ClipShapeTest {
  71 
  72     static boolean TX_SCALE = false;
  73     static boolean TX_SHEAR = false;
  74 
  75     static final boolean TEST_STROKER = true;
  76     static final boolean TEST_FILLER = true;
  77 
  78     // complementary tests in slow mode:
  79     static boolean USE_DASHES = false;
  80     static boolean USE_VAR_STROKE = false;
  81 
  82     static int NUM_TESTS = 5000;
  83     static final int TESTW = 100;
  84     static final int TESTH = 100;
  85 
  86     // shape settings:
  87     static ShapeMode SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
  88 
  89     static int THRESHOLD_DELTA;
  90     static long THRESHOLD_NBPIX;
  91 
  92     static final boolean SHAPE_REPEAT = true;
  93 
  94     // dump path on console:
  95     static final boolean DUMP_SHAPE = true;
  96 
  97     static final boolean SHOW_DETAILS = false; // disabled
  98     static final boolean SHOW_OUTLINE = true;
  99     static final boolean SHOW_POINTS = true;
 100     static final boolean SHOW_INFO = false;
 101 
 102     static final int MAX_SHOW_FRAMES = 10;
 103     static final int MAX_SAVE_FRAMES = 100;
 104 
 105     // use fixed seed to reproduce always same polygons between tests
 106     static final boolean FIXED_SEED = false;
 107     static final double RAND_SCALE = 3.0;
 108     static final double RANDW = TESTW * RAND_SCALE;
 109     static final double OFFW = (TESTW - RANDW) / 2.0;
 110     static final double RANDH = TESTH * RAND_SCALE;
 111     static final double OFFH = (TESTH - RANDH) / 2.0;
 112 
 113     static enum ShapeMode {
 114         TWO_CUBICS,
 115         FOUR_QUADS,
 116         FIVE_LINE_POLYS,
 117         NINE_LINE_POLYS,
 118         FIFTY_LINE_POLYS,
 119         MIXED
 120     }
 121 
 122     static final long SEED = 1666133789L;
 123     // Fixed seed to avoid any difference between runs:
 124     static final Random RANDOM = new Random(SEED);
 125 
 126     static final File OUTPUT_DIR = new File(".");
 127 
 128     static final AtomicBoolean isMarlin = new AtomicBoolean();
 129     static final AtomicBoolean isClipRuntime = new AtomicBoolean();
 130 
 131     static {
 132         Locale.setDefault(Locale.US);
 133 
 134         // FIRST: Get Marlin runtime state from its log:
 135 
 136         // initialize j.u.l Looger:
 137         final Logger log = Logger.getLogger("sun.java2d.marlin");
 138         log.addHandler(new Handler() {
 139             @Override
 140             public void publish(LogRecord record) {
 141                 final String msg = record.getMessage();
 142                 if (msg != null) {
 143                     // last space to avoid matching other settings:
 144                     if (msg.startsWith("sun.java2d.renderer ")) {
 145                         isMarlin.set(msg.contains("MarlinRenderingEngine"));
 146                     }
 147                     if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) {
 148                         isClipRuntime.set(msg.contains("true"));
 149                     }
 150                 }
 151 
 152                 final Throwable th = record.getThrown();
 153                 // detect any Throwable:
 154                 if (th != null) {
 155                     System.out.println("Test failed:\n" + record.getMessage());
 156                     th.printStackTrace(System.out);
 157 
 158                     throw new RuntimeException("Test failed: ", th);
 159                 }
 160             }
 161 
 162             @Override
 163             public void flush() {
 164             }
 165 
 166             @Override
 167             public void close() throws SecurityException {
 168             }
 169         });
 170 
 171         // enable Marlin logging & internal checks:
 172         System.setProperty("sun.java2d.renderer.log", "true");
 173         System.setProperty("sun.java2d.renderer.useLogger", "true");
 174 
 175         // disable static clipping setting:
 176         System.setProperty("sun.java2d.renderer.clip", "false");
 177         System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true");
 178 
 179         // enable subdivider:
 180         System.setProperty("sun.java2d.renderer.clip.subdivider", "true");
 181 
 182         // disable min length check: always subdivide curves at clip edges
 183         System.setProperty("sun.java2d.renderer.clip.subdivider.minLength", "-1");
 184 
 185         // If any curve, increase curve accuracy:
 186         // curve length max error:
 187         System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4");
 188 
 189         // quad max error:
 190         System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4");
 191 
 192         // cubic min/max error:
 193         System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3");
 194         System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4"); // or disabled ~ 1e-6
 195     }
 196 
 197     /**
 198      * Test
 199      * @param args
 200      */
 201     public static void main(String[] args) {
 202         boolean runSlowTests = false;
 203 
 204         for (String arg : args) {
 205             if ("-slow".equals(arg)) {
 206                 System.out.println("slow: enabled.");
 207                 runSlowTests = true;
 208             } else if ("-doScale".equals(arg)) {
 209                 System.out.println("doScale: enabled.");
 210                 TX_SCALE = true;
 211             } else if ("-doShear".equals(arg)) {
 212                 System.out.println("doShear: enabled.");
 213                 TX_SHEAR = true;
 214             } else if ("-doDash".equals(arg)) {
 215                 System.out.println("doDash: enabled.");
 216                 USE_DASHES = true;
 217             } else if ("-doVarStroke".equals(arg)) {
 218                 System.out.println("doVarStroke: enabled.");
 219                 USE_VAR_STROKE = true;
 220             }
 221             // shape mode:
 222             else if (arg.equalsIgnoreCase("-poly")) {
 223                 SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
 224             } else if (arg.equalsIgnoreCase("-bigpoly")) {
 225                 SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS;
 226             } else if (arg.equalsIgnoreCase("-quad")) {
 227                 SHAPE_MODE = ShapeMode.FOUR_QUADS;
 228             } else if (arg.equalsIgnoreCase("-cubic")) {
 229                 SHAPE_MODE = ShapeMode.TWO_CUBICS;
 230             } else if (arg.equalsIgnoreCase("-mixed")) {
 231                 SHAPE_MODE = ShapeMode.MIXED;
 232             }
 233         }
 234 
 235         System.out.println("Shape mode: " + SHAPE_MODE);
 236 
 237         // adjust image comparison thresholds:
 238         switch(SHAPE_MODE) {
 239             case TWO_CUBICS:
 240                 // Define uncertainty for curves:
 241                 THRESHOLD_DELTA = 32; //  / 256
 242                 THRESHOLD_NBPIX = 128; //  / 10000
 243                 break;
 244             case FOUR_QUADS:
 245             case MIXED:
 246                 // Define uncertainty for quads:
 247                 // curve subdivision causes curves to be smaller
 248                 // then curve offsets are different (more accurate)
 249                 THRESHOLD_DELTA = 64;  // 64 / 256
 250                 THRESHOLD_NBPIX = 256; // 256 / 10000
 251                 break;
 252             default:
 253                 // Define uncertainty for lines:
 254                 // float variant have higher uncertainty
 255                 THRESHOLD_DELTA = 8;
 256                 THRESHOLD_NBPIX = 8;
 257         }
 258 
 259         System.out.println("THRESHOLD_DELTA: "+THRESHOLD_DELTA);
 260         System.out.println("THRESHOLD_NBPIX: "+THRESHOLD_NBPIX);
 261 
 262         if (runSlowTests) {
 263             NUM_TESTS = 10000; // or 100000 (very slow)
 264             USE_DASHES = true;
 265             USE_VAR_STROKE = true;
 266         }
 267 
 268         System.out.println("ClipShapeTests: image = " + TESTW + " x " + TESTH);
 269 
 270         int failures = 0;
 271         final long start = System.nanoTime();
 272         try {
 273             // TODO: test affine transforms ?
 274 
 275             if (TEST_STROKER) {
 276                 final float[][] dashArrays = (USE_DASHES) ?
 277 // small
 278 //                        new float[][]{new float[]{1f, 2f}}
 279 // normal
 280                         new float[][]{new float[]{13f, 7f}}
 281 // large (prime)
 282 //                        new float[][]{new float[]{41f, 7f}}
 283 // none
 284                         : new float[][]{null};
 285 
 286                 System.out.println("dashes: " + Arrays.deepToString(dashArrays));
 287 
 288                 final float[] strokeWidths = (USE_VAR_STROKE)
 289                                                 ? new float[5] :
 290                                                   new float[]{10f};
 291 
 292                 int nsw = 0;
 293                 if (USE_VAR_STROKE) {
 294                     for (float width = 0.1f; width < 110f; width *= 5f) {
 295                         strokeWidths[nsw++] = width;
 296                     }
 297                 } else {
 298                     nsw = 1;
 299                 }
 300 
 301                 System.out.println("stroke widths: " + Arrays.toString(strokeWidths));
 302 
 303                 // Stroker tests:
 304                 for (int w = 0; w < nsw; w++) {
 305                     final float width = strokeWidths[w];
 306 
 307                     for (float[] dashes : dashArrays) {
 308 
 309                         for (int cap = 0; cap <= 2; cap++) {
 310 
 311                             for (int join = 0; join <= 2; join++) {
 312 
 313                                 failures += paintPaths(new TestSetup(SHAPE_MODE, false, width, cap, join, dashes));
 314                                 failures += paintPaths(new TestSetup(SHAPE_MODE, true, width, cap, join, dashes));
 315                             }
 316                         }
 317                     }
 318                 }
 319             }
 320 
 321             if (TEST_FILLER) {
 322                 // Filler tests:
 323                 failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO));
 324                 failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO));
 325 
 326                 failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD));
 327                 failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD));
 328             }
 329         } catch (IOException ioe) {
 330             throw new RuntimeException(ioe);
 331         }
 332         System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
 333 
 334         if (!isMarlin.get()) {
 335             throw new RuntimeException("Marlin renderer not used at runtime !");
 336         }
 337         if (!isClipRuntime.get()) {
 338             throw new RuntimeException("Marlin clipping not enabled at runtime !");
 339         }
 340         if (failures != 0) {
 341             throw new RuntimeException("Clip test failures : " + failures);
 342         }
 343     }
 344 
 345     static int paintPaths(final TestSetup ts) throws IOException {
 346         final long start = System.nanoTime();
 347 
 348         if (FIXED_SEED) {
 349             // Reset seed for random numbers:
 350             RANDOM.setSeed(SEED);
 351         }
 352 
 353         System.out.println("paintPaths: " + NUM_TESTS
 354                 + " paths (" + SHAPE_MODE + ") - setup: " + ts);
 355 
 356         final boolean fill = !ts.isStroke();
 357         final Path2D p2d = new Path2D.Double(ts.windingRule);
 358 
 359         final BufferedImage imgOn = newImage(TESTW, TESTH);
 360         final Graphics2D g2dOn = initialize(imgOn, ts);
 361 
 362         final BufferedImage imgOff = newImage(TESTW, TESTH);
 363         final Graphics2D g2dOff = initialize(imgOff, ts);
 364 
 365         final BufferedImage imgDiff = newImage(TESTW, TESTH);
 366 
 367         final DiffContext globalCtx = new DiffContext("All tests");
 368 
 369         int nd = 0;
 370         try {
 371             final DiffContext testCtx = new DiffContext("Test");
 372             BufferedImage diffImage;
 373 
 374             for (int n = 0; n < NUM_TESTS; n++) {
 375                 genShape(p2d, ts);
 376 
 377                 // Runtime clip setting OFF:
 378                 paintShape(p2d, g2dOff, fill, false);
 379 
 380                 // Runtime clip setting ON:
 381                 paintShape(p2d, g2dOn, fill, true);
 382 
 383                 /* compute image difference if possible */
 384                 diffImage = computeDiffImage(testCtx, imgOn, imgOff, imgDiff, globalCtx);
 385 
 386                 final String testName = "Setup_" + ts.id + "_test_" + n;
 387 
 388                 if (diffImage != null) {
 389                     nd++;
 390 
 391                     final double ratio = (100.0 * testCtx.histPix.count) / testCtx.histAll.count;
 392                     System.out.println("Diff ratio: " + testName + " = " + trimTo3Digits(ratio) + " %");
 393 
 394                     if (nd < MAX_SHOW_FRAMES) {
 395                         if (SHOW_DETAILS) {
 396                             paintShapeDetails(g2dOff, p2d);
 397                             paintShapeDetails(g2dOn, p2d);
 398                         }
 399 
 400                         if (nd < MAX_SAVE_FRAMES) {
 401                             if (DUMP_SHAPE) {
 402                                 dumpShape(p2d);
 403                             }
 404                             saveImage(imgOff, OUTPUT_DIR, testName + "-off.png");
 405                             saveImage(imgOn, OUTPUT_DIR, testName + "-on.png");
 406                             saveImage(diffImage, OUTPUT_DIR, testName + "-diff.png");
 407                         }
 408                     }
 409                 }
 410             }
 411         } finally {
 412             g2dOff.dispose();
 413             g2dOn.dispose();
 414 
 415             if (nd != 0) {
 416                 System.out.println("paintPaths: " + NUM_TESTS + " paths - "
 417                         + "Number of differences = " + nd
 418                         + " ratio = " + (100f * nd) / NUM_TESTS + " %");
 419             }
 420 
 421             globalCtx.dump();
 422         }
 423         System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
 424         return nd;
 425     }
 426 
 427     private static void paintShape(final Path2D p2d, final Graphics2D g2d,
 428                                    final boolean fill, final boolean clip) {
 429         reset(g2d);
 430 
 431         setClip(g2d, clip);
 432 
 433         if (fill) {
 434             g2d.fill(p2d);
 435         } else {
 436             g2d.draw(p2d);
 437         }
 438     }
 439 
 440     private static Graphics2D initialize(final BufferedImage img,
 441                                          final TestSetup ts) {
 442         final Graphics2D g2d = (Graphics2D) img.getGraphics();
 443         g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
 444                 RenderingHints.VALUE_RENDER_QUALITY);
 445         g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
 446                 RenderingHints.VALUE_STROKE_PURE);
 447 
 448         if (ts.isStroke()) {
 449             g2d.setStroke(createStroke(ts));
 450         }
 451         g2d.setColor(Color.GRAY);
 452 
 453         // Test scale
 454         if (TX_SCALE) {
 455             g2d.scale(1.2, 1.2);
 456         }
 457         // Test shear
 458         if (TX_SHEAR) {
 459             g2d.shear(0.1, 0.2);
 460         }
 461 
 462         return g2d;
 463     }
 464 
 465     private static void reset(final Graphics2D g2d) {
 466         // Disable antialiasing:
 467         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 468                 RenderingHints.VALUE_ANTIALIAS_OFF);
 469         g2d.setBackground(Color.WHITE);
 470         g2d.clearRect(0, 0, TESTW, TESTH);
 471     }
 472 
 473     private static void setClip(final Graphics2D g2d, final boolean clip) {
 474         // Enable antialiasing:
 475         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 476                 RenderingHints.VALUE_ANTIALIAS_ON);
 477 
 478         // Enable or Disable clipping:
 479         System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false");
 480     }
 481 
 482     static void genShape(final Path2D p2d, final TestSetup ts) {
 483         p2d.reset();
 484 
 485         final int end = (SHAPE_REPEAT) ? 2 : 1;
 486 
 487         for (int p = 0; p < end; p++) {
 488             p2d.moveTo(randX(), randY());
 489 
 490             switch (ts.shapeMode) {
 491                 case MIXED:
 492                 case FIFTY_LINE_POLYS:
 493                 case NINE_LINE_POLYS:
 494                 case FIVE_LINE_POLYS:
 495                     p2d.lineTo(randX(), randY());
 496                     p2d.lineTo(randX(), randY());
 497                     p2d.lineTo(randX(), randY());
 498                     p2d.lineTo(randX(), randY());
 499                     if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) {
 500                         // And an implicit close makes 5 lines
 501                         break;
 502                     }
 503                     p2d.lineTo(randX(), randY());
 504                     p2d.lineTo(randX(), randY());
 505                     p2d.lineTo(randX(), randY());
 506                     p2d.lineTo(randX(), randY());
 507                     if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) {
 508                         // And an implicit close makes 9 lines
 509                         break;
 510                     }
 511                     if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) {
 512                         for (int i = 0; i < 41; i++) {
 513                             p2d.lineTo(randX(), randY());
 514                         }
 515                         // And an implicit close makes 50 lines
 516                         break;
 517                     }
 518                 case TWO_CUBICS:
 519                     p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY());
 520                     p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY());
 521                     if (ts.shapeMode == ShapeMode.TWO_CUBICS) {
 522                         break;
 523                     }
 524                 case FOUR_QUADS:
 525                     p2d.quadTo(randX(), randY(), randX(), randY());
 526                     p2d.quadTo(randX(), randY(), randX(), randY());
 527                     p2d.quadTo(randX(), randY(), randX(), randY());
 528                     p2d.quadTo(randX(), randY(), randX(), randY());
 529                     if (ts.shapeMode == ShapeMode.FOUR_QUADS) {
 530                         break;
 531                     }
 532                 default:
 533             }
 534 
 535             if (ts.closed) {
 536                 p2d.closePath();
 537             }
 538         }
 539     }
 540 
 541     static final float POINT_RADIUS = 2f;
 542     static final float LINE_WIDTH = 1f;
 543 
 544     static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH);
 545     static final int COLOR_ALPHA = 128;
 546     static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA);
 547     static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA);
 548     static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA);
 549 
 550     static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float();
 551 
 552     private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) {
 553 
 554         final Stroke oldStroke = g2d.getStroke();
 555         final Color oldColor = g2d.getColor();
 556 
 557         setClip(g2d, false);
 558 
 559         if (SHOW_OUTLINE) {
 560             g2d.setStroke(OUTLINE_STROKE);
 561             g2d.setColor(COLOR_LINETO_ODD);
 562             g2d.draw(shape);
 563         }
 564 
 565         final float[] coords = new float[6];
 566         float px, py;
 567 
 568         int nMove = 0;
 569         int nLine = 0;
 570         int n = 0;
 571 
 572         for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {
 573             int type = it.currentSegment(coords);
 574             switch (type) {
 575                 case PathIterator.SEG_MOVETO:
 576                     if (SHOW_POINTS) {
 577                         g2d.setColor(COLOR_MOVETO);
 578                     }
 579                     break;
 580                 case PathIterator.SEG_LINETO:
 581                 case PathIterator.SEG_QUADTO:
 582                 case PathIterator.SEG_CUBICTO:
 583                     if (SHOW_POINTS) {
 584                         g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN);
 585                     }
 586                     nLine++;
 587                     break;
 588                 case PathIterator.SEG_CLOSE:
 589                     continue;
 590                 default:
 591                     System.out.println("unsupported segment type= " + type);
 592                     continue;
 593             }
 594             px = coords[0];
 595             py = coords[1];
 596 
 597             if (SHOW_INFO) {
 598                 System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py);
 599             }
 600 
 601             if (SHOW_POINTS) {
 602                 ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS,
 603                         POINT_RADIUS * 2f, POINT_RADIUS * 2f);
 604                 g2d.fill(ELL_POINT);
 605             }
 606         }
 607         if (SHOW_INFO) {
 608             System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine);
 609             System.out.println("--------------------------------------------------");
 610         }
 611 
 612         g2d.setStroke(oldStroke);
 613         g2d.setColor(oldColor);
 614     }
 615 
 616     private static void dumpShape(final Shape shape) {
 617         final float[] coords = new float[6];
 618 
 619         for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {
 620             final int type = it.currentSegment(coords);
 621             switch (type) {
 622                 case PathIterator.SEG_MOVETO:
 623                     System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");");
 624                     break;
 625                 case PathIterator.SEG_LINETO:
 626                     System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");");
 627                     break;
 628                 case PathIterator.SEG_QUADTO:
 629                     System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");");
 630                     break;
 631                 case PathIterator.SEG_CUBICTO:
 632                     System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");");
 633                     break;
 634                 case PathIterator.SEG_CLOSE:
 635                     System.out.println("p2d.closePath();");
 636                     break;
 637                 default:
 638                     System.out.println("// Unsupported segment type= " + type);
 639             }
 640         }
 641         System.out.println("--------------------------------------------------");
 642     }
 643 
 644     static double randX() {
 645         return RANDOM.nextDouble() * RANDW + OFFW;
 646     }
 647 
 648     static double randY() {
 649         return RANDOM.nextDouble() * RANDH + OFFH;
 650     }
 651 
 652     private static BasicStroke createStroke(final TestSetup ts) {
 653         return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f);
 654     }
 655 
 656     private final static class TestSetup {
 657 
 658         static final AtomicInteger COUNT = new AtomicInteger();
 659 
 660         final int id;
 661         final ShapeMode shapeMode;
 662         final boolean closed;
 663         // stroke
 664         final float strokeWidth;
 665         final int strokeCap;
 666         final int strokeJoin;
 667         final float[] dashes;
 668         // fill
 669         final int windingRule;
 670 
 671         TestSetup(ShapeMode shapeMode, final boolean closed,
 672                   final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) {
 673             this.id = COUNT.incrementAndGet();
 674             this.shapeMode = shapeMode;
 675             this.closed = closed;
 676             this.strokeWidth = strokeWidth;
 677             this.strokeCap = strokeCap;
 678             this.strokeJoin = strokeJoin;
 679             this.dashes = dashes;
 680             this.windingRule = Path2D.WIND_NON_ZERO;
 681         }
 682 
 683         TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) {
 684             this.id = COUNT.incrementAndGet();
 685             this.shapeMode = shapeMode;
 686             this.closed = closed;
 687             this.strokeWidth = 0f;
 688             this.strokeCap = this.strokeJoin = -1; // invalid
 689             this.dashes = null;
 690             this.windingRule = windingRule;
 691         }
 692 
 693         boolean isStroke() {
 694             return this.strokeWidth > 0f;
 695         }
 696 
 697         @Override
 698         public String toString() {
 699             if (isStroke()) {
 700                 return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed
 701                         + ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin)
 702                         + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "")
 703                         + '}';
 704             }
 705             return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed
 706                     + ", fill"
 707                     + ", windingRule=" + getWindingRule(windingRule) + '}';
 708         }
 709 
 710         private static String getCap(final int cap) {
 711             switch (cap) {
 712                 case BasicStroke.CAP_BUTT:
 713                     return "CAP_BUTT";
 714                 case BasicStroke.CAP_ROUND:
 715                     return "CAP_ROUND";
 716                 case BasicStroke.CAP_SQUARE:
 717                     return "CAP_SQUARE";
 718                 default:
 719                     return "";
 720             }
 721 
 722         }
 723 
 724         private static String getJoin(final int join) {
 725             switch (join) {
 726                 case BasicStroke.JOIN_MITER:
 727                     return "JOIN_MITER";
 728                 case BasicStroke.JOIN_ROUND:
 729                     return "JOIN_ROUND";
 730                 case BasicStroke.JOIN_BEVEL:
 731                     return "JOIN_BEVEL";
 732                 default:
 733                     return "";
 734             }
 735 
 736         }
 737 
 738         private static String getWindingRule(final int rule) {
 739             switch (rule) {
 740                 case PathIterator.WIND_EVEN_ODD:
 741                     return "WIND_EVEN_ODD";
 742                 case PathIterator.WIND_NON_ZERO:
 743                     return "WIND_NON_ZERO";
 744                 default:
 745                     return "";
 746             }
 747         }
 748     }
 749 
 750     // --- utilities ---
 751     private static final int DCM_ALPHA_MASK = 0xff000000;
 752 
 753     public static BufferedImage newImage(final int w, final int h) {
 754         return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
 755     }
 756 
 757     public static BufferedImage computeDiffImage(final DiffContext localCtx,
 758                                                  final BufferedImage tstImage,
 759                                                  final BufferedImage refImage,
 760                                                  final BufferedImage diffImage,
 761                                                  final DiffContext globalCtx) {
 762 
 763         final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData();
 764         final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData();
 765         final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData();
 766 
 767         // reset local diff context:
 768         localCtx.reset();
 769 
 770         int ref, tst, dg, v;
 771         for (int i = 0, len = aRefPix.length; i < len; i++) {
 772             ref = aRefPix[i];
 773             tst = aTstPix[i];
 774 
 775             // grayscale diff:
 776             dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst));
 777 
 778             // max difference on grayscale values:
 779             v = (int) Math.ceil(Math.abs(dg / 3.0));
 780 
 781 // TODO: count warnings
 782             if (v <= THRESHOLD_DELTA) {
 783                 aDifPix[i] = 0;
 784             } else {
 785                 aDifPix[i] = toInt(v, v, v);
 786 
 787                 localCtx.add(v);
 788             }
 789             globalCtx.add(v);
 790         }
 791 
 792         if (!localCtx.isDiff() || (localCtx.histPix.count <= THRESHOLD_NBPIX)) {
 793             return null;
 794         }
 795 
 796         localCtx.dump();
 797 
 798         return diffImage;
 799     }
 800 
 801     static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException {
 802         final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG");
 803         if (itWriters.hasNext()) {
 804             final ImageWriter writer = itWriters.next();
 805 
 806             final ImageWriteParam writerParams = writer.getDefaultWriteParam();
 807             writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED);
 808 
 809             final File imgFile = new File(resDirectory, imageFileName);
 810 
 811             if (!imgFile.exists() || imgFile.canWrite()) {
 812                 System.out.println("saveImage: saving image as PNG [" + imgFile + "]...");
 813                 imgFile.delete();
 814 
 815                 // disable cache in temporary files:
 816                 ImageIO.setUseCache(false);
 817 
 818                 final long start = System.nanoTime();
 819 
 820                 // PNG uses already buffering:
 821                 final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile));
 822 
 823                 writer.setOutput(imgOutStream);
 824                 try {
 825                     writer.write(null, new IIOImage(image, null, null), writerParams);
 826                 } finally {
 827                     imgOutStream.close();
 828 
 829                     final long time = System.nanoTime() - start;
 830                     System.out.println("saveImage: duration= " + (time / 1000000l) + " ms.");
 831                 }
 832             }
 833         }
 834     }
 835 
 836     static int r(final int v) {
 837         return (v >> 16 & 0xff);
 838     }
 839 
 840     static int g(final int v) {
 841         return (v >> 8 & 0xff);
 842     }
 843 
 844     static int b(final int v) {
 845         return (v & 0xff);
 846     }
 847 
 848     static int clamp127(final int v) {
 849         return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255;
 850     }
 851 
 852     static int toInt(final int r, final int g, final int b) {
 853         return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b;
 854     }
 855 
 856     /* stats */
 857     static class StatInteger {
 858 
 859         public final String name;
 860         public long count = 0l;
 861         public long sum = 0l;
 862         public long min = Integer.MAX_VALUE;
 863         public long max = Integer.MIN_VALUE;
 864 
 865         StatInteger(String name) {
 866             this.name = name;
 867         }
 868 
 869         void reset() {
 870             count = 0l;
 871             sum = 0l;
 872             min = Integer.MAX_VALUE;
 873             max = Integer.MIN_VALUE;
 874         }
 875 
 876         void add(int val) {
 877             count++;
 878             sum += val;
 879             if (val < min) {
 880                 min = val;
 881             }
 882             if (val > max) {
 883                 max = val;
 884             }
 885         }
 886 
 887         void add(long val) {
 888             count++;
 889             sum += val;
 890             if (val < min) {
 891                 min = val;
 892             }
 893             if (val > max) {
 894                 max = val;
 895             }
 896         }
 897 
 898         public final double average() {
 899             return ((double) sum) / count;
 900         }
 901 
 902         @Override
 903         public String toString() {
 904             final StringBuilder sb = new StringBuilder(128);
 905             toString(sb);
 906             return sb.toString();
 907         }
 908 
 909         public final StringBuilder toString(final StringBuilder sb) {
 910             sb.append(name).append("[n: ").append(count);
 911             sb.append("] sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average()));
 912             sb.append(" [").append(min).append(" | ").append(max).append("]");
 913             return sb;
 914         }
 915 
 916     }
 917 
 918     final static class Histogram extends StatInteger {
 919 
 920         static final int BUCKET = 2;
 921         static final int MAX = 20;
 922         static final int LAST = MAX - 1;
 923         static final int[] STEPS = new int[MAX];
 924 
 925         static {
 926             STEPS[0] = 0;
 927             STEPS[1] = 1;
 928 
 929             for (int i = 2; i < MAX; i++) {
 930                 STEPS[i] = STEPS[i - 1] * BUCKET;
 931             }
 932 //            System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS));
 933         }
 934 
 935         static int bucket(int val) {
 936             for (int i = 1; i < MAX; i++) {
 937                 if (val < STEPS[i]) {
 938                     return i - 1;
 939                 }
 940             }
 941             return LAST;
 942         }
 943 
 944         private final StatInteger[] stats = new StatInteger[MAX];
 945 
 946         public Histogram(String name) {
 947             super(name);
 948             for (int i = 0; i < MAX; i++) {
 949                 stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~")));
 950             }
 951         }
 952 
 953         @Override
 954         final void reset() {
 955             super.reset();
 956             for (int i = 0; i < MAX; i++) {
 957                 stats[i].reset();
 958             }
 959         }
 960 
 961         @Override
 962         final void add(int val) {
 963             super.add(val);
 964             stats[bucket(val)].add(val);
 965         }
 966 
 967         @Override
 968         final void add(long val) {
 969             add((int) val);
 970         }
 971 
 972         @Override
 973         public final String toString() {
 974             final StringBuilder sb = new StringBuilder(2048);
 975             super.toString(sb).append(" { ");
 976 
 977             for (int i = 0; i < MAX; i++) {
 978                 if (stats[i].count != 0l) {
 979                     sb.append("\n        ").append(stats[i].toString());
 980                 }
 981             }
 982 
 983             return sb.append(" }").toString();
 984         }
 985     }
 986 
 987     /**
 988      * Adjust the given double value to keep only 3 decimal digits
 989      * @param value value to adjust
 990      * @return double value with only 3 decimal digits
 991      */
 992     static double trimTo3Digits(final double value) {
 993         return ((long) (1e3d * value)) / 1e3d;
 994     }
 995 
 996     static final class DiffContext {
 997 
 998         public final Histogram histAll;
 999         public final Histogram histPix;
1000 
1001         DiffContext(String name) {
1002             histAll = new Histogram("All  Pixels [" + name + "]");
1003             histPix = new Histogram("Diff Pixels [" + name + "]");
1004         }
1005 
1006         void reset() {
1007             histAll.reset();
1008             histPix.reset();
1009         }
1010 
1011         void dump() {
1012             if (isDiff()) {
1013                 System.out.println("Differences [" + histAll.name + "]:");
1014                 System.out.println("Total [all pixels]:\n" + histAll.toString());
1015                 System.out.println("Total [different pixels]:\n" + histPix.toString());
1016             } else {
1017                 System.out.println("No difference for [" + histAll.name + "].");
1018             }
1019         }
1020 
1021         void add(int val) {
1022             histAll.add(val);
1023             if (val != 0) {
1024                 histPix.add(val);
1025             }
1026         }
1027 
1028         boolean isDiff() {
1029             return histAll.sum != 0l;
1030         }
1031     }
1032 }