1 /*
   2  * Copyright (c) 2017, 2018, 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.Stroke;
  28 import java.awt.Shape;
  29 import java.awt.geom.CubicCurve2D;
  30 import java.awt.geom.Ellipse2D;
  31 import java.awt.geom.Line2D;
  32 import java.awt.geom.Path2D;
  33 import java.awt.geom.PathIterator;
  34 import java.awt.geom.QuadCurve2D;
  35 import java.awt.image.BufferedImage;
  36 import java.awt.image.DataBufferInt;
  37 import java.io.File;
  38 import java.io.FileOutputStream;
  39 import java.io.IOException;
  40 import java.util.Arrays;
  41 import java.util.Iterator;
  42 import java.util.Locale;
  43 import java.util.Random;
  44 import java.util.concurrent.atomic.AtomicBoolean;
  45 import java.util.concurrent.atomic.AtomicInteger;
  46 import java.util.logging.Handler;
  47 import java.util.logging.LogRecord;
  48 import java.util.logging.Logger;
  49 import javax.imageio.IIOImage;
  50 import javax.imageio.ImageIO;
  51 import javax.imageio.ImageWriteParam;
  52 import javax.imageio.ImageWriter;
  53 import javax.imageio.stream.ImageOutputStream;
  54 
  55 /**
  56  * @test
  57  * @bug 8191814
  58  * @summary Verifies that Marlin rendering generates the same
  59  * images with and without clipping optimization with all possible
  60  * stroke (cap/join) and/or dashes or fill modes (EO rules)
  61  * for paths made of either 9 lines, 4 quads, 2 cubics (random)
  62  * Note: Use the argument -slow to run more intensive tests (too much time)
  63  *
  64  * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly
  65  * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly -doDash
  66  * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic
  67  * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic -doDash
  68  * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly
  69  * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly -doDash
  70  * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic
  71  * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic -doDash
  72 */
  73 public final class ClipShapeTest {
  74 
  75     // test options:
  76     static int NUM_TESTS;
  77 
  78     // shape settings:
  79     static ShapeMode SHAPE_MODE;
  80 
  81     static boolean USE_DASHES;
  82     static boolean USE_VAR_STROKE;
  83 
  84     static int THRESHOLD_DELTA;
  85     static long THRESHOLD_NBPIX;
  86 
  87     // constants:
  88     static final boolean DO_FAIL = Boolean.valueOf(System.getProperty("ClipShapeTest.fail", "true"));
  89 
  90     static final boolean TEST_STROKER = true;
  91     static final boolean TEST_FILLER = true;
  92 
  93     static final boolean SUBDIVIDE_CURVE = true;
  94     static final double SUBDIVIDE_LEN_TH = 50.0;
  95     static final boolean TRACE_SUBDIVIDE_CURVE = false;
  96 
  97     static final int TESTW = 100;
  98     static final int TESTH = 100;
  99 
 100     // dump path on console:
 101     static final boolean DUMP_SHAPE = true;
 102 
 103     static final boolean SHOW_DETAILS = false; // disabled
 104     static final boolean SHOW_OUTLINE = true;
 105     static final boolean SHOW_POINTS = true;
 106     static final boolean SHOW_INFO = false;
 107 
 108     static final int MAX_SHOW_FRAMES = 10;
 109     static final int MAX_SAVE_FRAMES = 100;
 110 
 111     // use fixed seed to reproduce always same polygons between tests
 112     static final boolean FIXED_SEED = true;
 113 
 114     static final double RAND_SCALE = 3.0;
 115     static final double RANDW = TESTW * RAND_SCALE;
 116     static final double OFFW = (TESTW - RANDW) / 2.0;
 117     static final double RANDH = TESTH * RAND_SCALE;
 118     static final double OFFH = (TESTH - RANDH) / 2.0;
 119 
 120     static enum ShapeMode {
 121         TWO_CUBICS,
 122         FOUR_QUADS,
 123         FIVE_LINE_POLYS,
 124         NINE_LINE_POLYS,
 125         FIFTY_LINE_POLYS,
 126         MIXED
 127     }
 128 
 129     static final long SEED = 1666133789L;
 130     // Fixed seed to avoid any difference between runs:
 131     static final Random RANDOM = new Random(SEED);
 132 
 133     static final File OUTPUT_DIR = new File(".");
 134 
 135     static final AtomicBoolean isMarlin = new AtomicBoolean();
 136     static final AtomicBoolean isMarlinFloat = new AtomicBoolean();
 137     static final AtomicBoolean isClipRuntime = new AtomicBoolean();
 138 
 139     static {
 140         Locale.setDefault(Locale.US);
 141 
 142         // FIRST: Get Marlin runtime state from its log:
 143 
 144         // initialize j.u.l Looger:
 145         final Logger log = Logger.getLogger("sun.java2d.marlin");
 146         log.addHandler(new Handler() {
 147             @Override
 148             public void publish(LogRecord record) {
 149                 final String msg = record.getMessage();
 150                 if (msg != null) {
 151                     // last space to avoid matching other settings:
 152                     if (msg.startsWith("sun.java2d.renderer ")) {
 153                         isMarlin.set(msg.contains("MarlinRenderingEngine"));
 154                         isMarlinFloat.set(!msg.contains("DMarlinRenderingEngine"));
 155                     }
 156                     if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) {
 157                         isClipRuntime.set(msg.contains("true"));
 158                     }
 159                 }
 160 
 161                 final Throwable th = record.getThrown();
 162                 // detect any Throwable:
 163                 if (th != null) {
 164                     System.out.println("Test failed:\n" + record.getMessage());
 165                     th.printStackTrace(System.out);
 166 
 167                     throw new RuntimeException("Test failed: ", th);
 168                 }
 169             }
 170 
 171             @Override
 172             public void flush() {
 173             }
 174 
 175             @Override
 176             public void close() throws SecurityException {
 177             }
 178         });
 179 
 180         // enable Marlin logging & internal checks:
 181         System.setProperty("sun.java2d.renderer.log", "true");
 182         System.setProperty("sun.java2d.renderer.useLogger", "true");
 183 
 184         // disable static clipping setting:
 185         System.setProperty("sun.java2d.renderer.clip", "false");
 186         System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true");
 187 
 188         // enable subdivider:
 189         System.setProperty("sun.java2d.renderer.clip.subdivider", "true");
 190 
 191         // disable min length check: always subdivide curves at clip edges
 192         System.setProperty("sun.java2d.renderer.clip.subdivider.minLength", "-1");
 193 
 194         // If any curve, increase curve accuracy:
 195         // curve length max error:
 196         System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4");
 197 
 198         // cubic min/max error:
 199         System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3");
 200         System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4");
 201 
 202         // quad max error:
 203         System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4");
 204     }
 205 
 206     private static void resetOptions() {
 207         NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 5000);
 208 
 209         // shape settings:
 210         SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
 211 
 212         USE_DASHES = false;
 213         USE_VAR_STROKE = false;
 214     }
 215 
 216     /**
 217      * Test
 218      * @param args
 219      */
 220     public static void main(String[] args) {
 221         {
 222             // Bootstrap: init Renderer now:
 223             final BufferedImage img = newImage(TESTW, TESTH);
 224             final Graphics2D g2d = initialize(img, null);
 225 
 226             try {
 227                 paintShape(new Line2D.Double(0,0,100,100), g2d, true, false);
 228             } finally {
 229                 g2d.dispose();
 230             }
 231 
 232             if (!isMarlin.get()) {
 233                 throw new RuntimeException("Marlin renderer not used at runtime !");
 234             }
 235             if (!isClipRuntime.get()) {
 236                 throw new RuntimeException("Marlin clipping not enabled at runtime !");
 237             }
 238         }
 239 
 240         System.out.println("---------------------------------------");
 241         System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH);
 242 
 243         resetOptions();
 244 
 245         boolean runSlowTests = false;
 246 
 247         for (String arg : args) {
 248             if ("-slow".equals(arg)) {
 249                 runSlowTests = true;
 250             } else if ("-doDash".equals(arg)) {
 251                 USE_DASHES = true;
 252             } else if ("-doVarStroke".equals(arg)) {
 253                 USE_VAR_STROKE = true;
 254             } else {
 255                 // shape mode:
 256                 if (arg.equalsIgnoreCase("-poly")) {
 257                     SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
 258                 } else if (arg.equalsIgnoreCase("-bigpoly")) {
 259                     SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS;
 260                 } else if (arg.equalsIgnoreCase("-quad")) {
 261                     SHAPE_MODE = ShapeMode.FOUR_QUADS;
 262                 } else if (arg.equalsIgnoreCase("-cubic")) {
 263                     SHAPE_MODE = ShapeMode.TWO_CUBICS;
 264                 } else if (arg.equalsIgnoreCase("-mixed")) {
 265                     SHAPE_MODE = ShapeMode.MIXED;
 266                 }
 267             }
 268         }
 269 
 270         System.out.println("Shape mode: " + SHAPE_MODE);
 271 
 272         // adjust image comparison thresholds:
 273         switch (SHAPE_MODE) {
 274             case TWO_CUBICS:
 275                 // Define uncertainty for curves:
 276                 THRESHOLD_DELTA = 32;
 277                 THRESHOLD_NBPIX = (USE_DASHES) ? 50 : 200;
 278                 if (SUBDIVIDE_CURVE) {
 279                     THRESHOLD_NBPIX = 4;
 280                 }
 281                 break;
 282             case FOUR_QUADS:
 283             case MIXED:
 284                 // Define uncertainty for quads:
 285                 // curve subdivision causes curves to be smaller
 286                 // then curve offsets are different (more accurate)
 287                 THRESHOLD_DELTA = 64;
 288                 THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 420;
 289                 if (SUBDIVIDE_CURVE) {
 290                     THRESHOLD_NBPIX = 10;
 291                 }
 292                 break;
 293             default:
 294                 // Define uncertainty for lines:
 295                 // float variant have higher uncertainty
 296                 THRESHOLD_DELTA = 2;
 297                 THRESHOLD_NBPIX = (USE_DASHES) ?
 298                     // float variant have higher uncertainty
 299                     ((isMarlinFloat.get()) ? 30 : 6) // low for double
 300                     : (isMarlinFloat.get()) ? 10 : 0;
 301         }
 302 
 303 // Visual inspection (low threshold):
 304 //        THRESHOLD_NBPIX = 2;
 305 
 306         System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA);
 307         System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX);
 308 
 309         if (runSlowTests) {
 310             NUM_TESTS = 10000; // or 100000 (very slow)
 311             USE_VAR_STROKE = true;
 312         }
 313 
 314         System.out.println("NUM_TESTS: " + NUM_TESTS);
 315 
 316         if (USE_DASHES) {
 317             System.out.println("USE_DASHES: enabled.");
 318         }
 319         if (USE_VAR_STROKE) {
 320             System.out.println("USE_VAR_STROKE: enabled.");
 321         }
 322         if (!DO_FAIL) {
 323             System.out.println("DO_FAIL: disabled.");
 324         }
 325 
 326         System.out.println("---------------------------------------");
 327 
 328         final DiffContext allCtx = new DiffContext("All Test setups");
 329         final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)");
 330 
 331         int failures = 0;
 332         final long start = System.nanoTime();
 333         try {
 334             if (TEST_STROKER) {
 335                 final float[][] dashArrays = (USE_DASHES) ?
 336 // small
 337 //                        new float[][]{new float[]{1f, 2f}}
 338 // normal
 339                         new float[][]{new float[]{13f, 7f}}
 340 // large (prime)
 341 //                        new float[][]{new float[]{41f, 7f}}
 342 // none
 343                         : new float[][]{null};
 344 
 345                 System.out.println("dashes: " + Arrays.deepToString(dashArrays));
 346 
 347                 final float[] strokeWidths = (USE_VAR_STROKE)
 348                                                 ? new float[5] :
 349                                                   new float[]{10f};
 350 
 351                 int nsw = 0;
 352                 if (USE_VAR_STROKE) {
 353                     for (float width = 0.25f; width < 110f; width *= 5f) {
 354                         strokeWidths[nsw++] = width;
 355                     }
 356                 } else {
 357                     nsw = 1;
 358                 }
 359 
 360                 System.out.println("stroke widths: " + Arrays.toString(strokeWidths));
 361 
 362                 // Stroker tests:
 363                 for (int w = 0; w < nsw; w++) {
 364                     final float width = strokeWidths[w];
 365 
 366                     for (float[] dashes : dashArrays) {
 367 
 368                         for (int cap = 0; cap <= 2; cap++) {
 369 
 370                             for (int join = 0; join <= 2; join++) {
 371 
 372                                 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes));
 373                                 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes));
 374                             }
 375                         }
 376                     }
 377                 }
 378             }
 379 
 380             if (TEST_FILLER) {
 381                 // Filler tests:
 382                 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO));
 383                 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO));
 384 
 385                 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD));
 386                 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD));
 387             }
 388         } catch (IOException ioe) {
 389             throw new RuntimeException(ioe);
 390         }
 391         System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
 392 
 393         allWorstCtx.dump();
 394         allCtx.dump();
 395 
 396         if (DO_FAIL && (failures != 0)) {
 397             throw new RuntimeException("Clip test failures : " + failures);
 398         }
 399     }
 400 
 401     static int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException {
 402         final long start = System.nanoTime();
 403 
 404         if (FIXED_SEED) {
 405             // Reset seed for random numbers:
 406             RANDOM.setSeed(SEED);
 407         }
 408 
 409         System.out.println("paintPaths: " + NUM_TESTS
 410                 + " paths (" + SHAPE_MODE + ") - setup: " + ts);
 411 
 412         final boolean fill = !ts.isStroke();
 413         final Path2D p2d = new Path2D.Double(ts.windingRule);
 414 
 415         final Stroke stroke = (!fill) ? createStroke(ts) : null;
 416 
 417         final BufferedImage imgOn = newImage(TESTW, TESTH);
 418         final Graphics2D g2dOn = initialize(imgOn, stroke);
 419 
 420         final BufferedImage imgOff = newImage(TESTW, TESTH);
 421         final Graphics2D g2dOff = initialize(imgOff, stroke);
 422 
 423         final BufferedImage imgDiff = newImage(TESTW, TESTH);
 424 
 425         final DiffContext testSetupCtx = new DiffContext("Test setup");
 426         final DiffContext testWorstCtx = new DiffContext("Worst");
 427         final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)");
 428 
 429         int nd = 0;
 430         try {
 431             final DiffContext testCtx = new DiffContext("Test");
 432             final DiffContext testThCtx = new DiffContext("Test(>threshold)");
 433             BufferedImage diffImage;
 434 
 435             for (int n = 0; n < NUM_TESTS; n++) {
 436                 genShape(p2d, ts);
 437 
 438                 // Runtime clip setting OFF:
 439                 paintShape(p2d, g2dOff, fill, false);
 440 
 441                 // Runtime clip setting ON:
 442                 paintShape(p2d, g2dOn, fill, true);
 443 
 444                 /* compute image difference if possible */
 445                 diffImage = computeDiffImage(testCtx, testThCtx, imgOn, imgOff, imgDiff);
 446 
 447                 // Worst (total)
 448                 if (testCtx.isDiff()) {
 449                     if (testWorstCtx.isWorse(testCtx, false)) {
 450                         testWorstCtx.set(testCtx);
 451                     }
 452                     if (testWorstThCtx.isWorse(testCtx, true)) {
 453                         testWorstThCtx.set(testCtx);
 454                     }
 455                     // accumulate data:
 456                     testSetupCtx.add(testCtx);
 457                 }
 458                 if (diffImage != null) {
 459                     nd++;
 460 
 461                     testThCtx.dump();
 462                     testCtx.dump();
 463 
 464                     if (nd < MAX_SHOW_FRAMES) {
 465                         if (SHOW_DETAILS) {
 466                             paintShapeDetails(g2dOff, p2d);
 467                             paintShapeDetails(g2dOn, p2d);
 468                         }
 469 
 470                         if (nd < MAX_SAVE_FRAMES) {
 471                             if (DUMP_SHAPE) {
 472                                 dumpShape(p2d);
 473                             }
 474 
 475                             final String testName = "Setup_" + ts.id + "_test_" + n;
 476 
 477                             saveImage(imgOff, OUTPUT_DIR, testName + "-off.png");
 478                             saveImage(imgOn, OUTPUT_DIR, testName + "-on.png");
 479                             saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png");
 480                         }
 481                     }
 482                 }
 483             }
 484         } finally {
 485             g2dOff.dispose();
 486             g2dOn.dispose();
 487 
 488             if (nd != 0) {
 489                 System.out.println("paintPaths: " + NUM_TESTS + " paths - "
 490                         + "Number of differences = " + nd
 491                         + " ratio = " + (100f * nd) / NUM_TESTS + " %");
 492             }
 493 
 494             if (testWorstCtx.isDiff()) {
 495                 testWorstCtx.dump();
 496                 if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) {
 497                     testWorstThCtx.dump();
 498                 }
 499                 if (allWorstCtx.isWorse(testWorstThCtx, true)) {
 500                     allWorstCtx.set(testWorstThCtx);
 501                 }
 502             }
 503             testSetupCtx.dump();
 504 
 505             // accumulate data:
 506             allCtx.add(testSetupCtx);
 507         }
 508         System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
 509         return nd;
 510     }
 511 
 512     private static void paintShape(final Shape p2d, final Graphics2D g2d,
 513                                    final boolean fill, final boolean clip) {
 514         reset(g2d);
 515 
 516         setClip(g2d, clip);
 517 
 518         if (fill) {
 519             g2d.fill(p2d);
 520         } else {
 521             g2d.draw(p2d);
 522         }
 523     }
 524 
 525     private static Graphics2D initialize(final BufferedImage img,
 526                                          final Stroke s) {
 527         final Graphics2D g2d = (Graphics2D) img.getGraphics();
 528         g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
 529                 RenderingHints.VALUE_RENDER_QUALITY);
 530         g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
 531 // Test normalize:
 532 //                RenderingHints.VALUE_STROKE_NORMALIZE
 533                 RenderingHints.VALUE_STROKE_PURE
 534         );
 535 
 536         if (s != null) {
 537             g2d.setStroke(s);
 538         }
 539         g2d.setColor(Color.BLACK);
 540 
 541         return g2d;
 542     }
 543 
 544     private static void reset(final Graphics2D g2d) {
 545         // Disable antialiasing:
 546         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 547                 RenderingHints.VALUE_ANTIALIAS_OFF);
 548         g2d.setBackground(Color.WHITE);
 549         g2d.clearRect(0, 0, TESTW, TESTH);
 550     }
 551 
 552     private static void setClip(final Graphics2D g2d, final boolean clip) {
 553         // Enable antialiasing:
 554         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 555                 RenderingHints.VALUE_ANTIALIAS_ON);
 556 
 557         // Enable or Disable clipping:
 558         System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false");
 559     }
 560 
 561     static void genShape(final Path2D p2d, final TestSetup ts) {
 562         p2d.reset();
 563 
 564         /*
 565             Test closed path:
 566             0: moveTo + (draw)To + closePath
 567             1: (draw)To + closePath (closePath + (draw)To sequence)
 568         */
 569         final int end  = (ts.closed) ? 2 : 1;
 570 
 571         final double[] in = new double[8];
 572 
 573         double sx0 = 0.0, sy0 = 0.0, x0 = 0.0, y0 = 0.0;
 574 
 575         for (int p = 0; p < end; p++) {
 576             if (p <= 0) {
 577                 x0 = randX(); y0 = randY();
 578                 p2d.moveTo(x0, y0);
 579                 sx0 = x0; sy0 = y0;
 580             }
 581 
 582             switch (ts.shapeMode) {
 583                 case MIXED:
 584                 case FIVE_LINE_POLYS:
 585                 case NINE_LINE_POLYS:
 586                 case FIFTY_LINE_POLYS:
 587                     p2d.lineTo(randX(), randY());
 588                     p2d.lineTo(randX(), randY());
 589                     p2d.lineTo(randX(), randY());
 590                     p2d.lineTo(randX(), randY());
 591                     x0 = randX(); y0 = randY();
 592                     p2d.lineTo(x0, y0);
 593                     if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) {
 594                         // And an implicit close makes 5 lines
 595                         break;
 596                     }
 597                     p2d.lineTo(randX(), randY());
 598                     p2d.lineTo(randX(), randY());
 599                     p2d.lineTo(randX(), randY());
 600                     x0 = randX(); y0 = randY();
 601                     p2d.lineTo(x0, y0);
 602                     if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) {
 603                         // And an implicit close makes 9 lines
 604                         break;
 605                     }
 606                     if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) {
 607                         for (int i = 0; i < 41; i++) {
 608                             x0 = randX(); y0 = randY();
 609                             p2d.lineTo(x0, y0);
 610                         }
 611                         // And an implicit close makes 50 lines
 612                         break;
 613                     }
 614                 case TWO_CUBICS:
 615                     if (SUBDIVIDE_CURVE) {
 616                         in[0] = x0; in[1] = y0;
 617                         in[2] = randX(); in[3] = randY();
 618                         in[4] = randX(); in[5] = randY();
 619                         x0 = randX(); y0 = randY();
 620                         in[6] = x0; in[7] = y0;
 621                         subdivide(p2d, 8, in);
 622                         in[0] = x0; in[1] = y0;
 623                         in[2] = randX(); in[3] = randY();
 624                         in[4] = randX(); in[5] = randY();
 625                         x0 = randX(); y0 = randY();
 626                         in[6] = x0; in[7] = y0;
 627                         subdivide(p2d, 8, in);
 628                     } else {
 629                         x0 = randX(); y0 = randY();
 630                         p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);
 631                         x0 = randX(); y0 = randY();
 632                         p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);
 633                     }
 634                     if (ts.shapeMode == ShapeMode.TWO_CUBICS) {
 635                         break;
 636                     }
 637                 case FOUR_QUADS:
 638                     if (SUBDIVIDE_CURVE) {
 639                         in[0] = x0; in[1] = y0;
 640                         in[2] = randX(); in[3] = randY();
 641                         x0 = randX(); y0 = randY();
 642                         in[4] = x0; in[5] = y0;
 643                         subdivide(p2d, 6, in);
 644                         in[0] = x0; in[1] = y0;
 645                         in[2] = randX(); in[3] = randY();
 646                         x0 = randX(); y0 = randY();
 647                         in[4] = x0; in[5] = y0;
 648                         subdivide(p2d, 6, in);
 649                         in[0] = x0; in[1] = y0;
 650                         in[2] = randX(); in[3] = randY();
 651                         x0 = randX(); y0 = randY();
 652                         in[4] = x0; in[5] = y0;
 653                         subdivide(p2d, 6, in);
 654                         in[0] = x0; in[1] = y0;
 655                         in[2] = randX(); in[3] = randY();
 656                         x0 = randX(); y0 = randY();
 657                         in[4] = x0; in[5] = y0;
 658                         subdivide(p2d, 6, in);
 659                     } else {
 660                         x0 = randX(); y0 = randY();
 661                         p2d.quadTo(randX(), randY(), x0, y0);
 662                         x0 = randX(); y0 = randY();
 663                         p2d.quadTo(randX(), randY(), x0, y0);
 664                         x0 = randX(); y0 = randY();
 665                         p2d.quadTo(randX(), randY(), x0, y0);
 666                         x0 = randX(); y0 = randY();
 667                         p2d.quadTo(randX(), randY(), x0, y0);
 668                     }
 669                     if (ts.shapeMode == ShapeMode.FOUR_QUADS) {
 670                         break;
 671                     }
 672                 default:
 673             }
 674 
 675             if (ts.closed) {
 676                 p2d.closePath();
 677                 x0 = sx0; y0 = sy0;
 678             }
 679         }
 680     }
 681 
 682     static final int SUBDIVIDE_LIMIT = 5;
 683     static final double[][] SUBDIVIDE_CURVES = new double[SUBDIVIDE_LIMIT + 1][];
 684 
 685     static {
 686         for (int i = 0, n = 1; i < SUBDIVIDE_LIMIT; i++, n *= 2) {
 687             SUBDIVIDE_CURVES[i] = new double[8 * n];
 688         }
 689     }
 690 
 691     static void subdivide(final Path2D p2d, final int type, final double[] in) {
 692         if (TRACE_SUBDIVIDE_CURVE) {
 693             System.out.println("subdivide: " + Arrays.toString(Arrays.copyOf(in, type)));
 694         }
 695 
 696         double curveLen = ((type == 8)
 697                 ? curvelen(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7])
 698                 : quadlen(in[0], in[1], in[2], in[3], in[4], in[5]));
 699 
 700         if (curveLen > SUBDIVIDE_LEN_TH) {
 701             if (TRACE_SUBDIVIDE_CURVE) {
 702                 System.out.println("curvelen: " + curveLen);
 703             }
 704 
 705             System.arraycopy(in, 0, SUBDIVIDE_CURVES[0], 0, 8);
 706 
 707             int level = 0;
 708             while (curveLen >= SUBDIVIDE_LEN_TH) {
 709                 level++;
 710                 curveLen /= 2.0;
 711                 if (TRACE_SUBDIVIDE_CURVE) {
 712                     System.out.println("curvelen: " + curveLen);
 713                 }
 714             }
 715 
 716             if (TRACE_SUBDIVIDE_CURVE) {
 717                 System.out.println("level: " + level);
 718             }
 719 
 720             if (level > SUBDIVIDE_LIMIT) {
 721                 if (TRACE_SUBDIVIDE_CURVE) {
 722                     System.out.println("max level reached : " + level);
 723                 }
 724                 level = SUBDIVIDE_LIMIT;
 725             }
 726 
 727             for (int l = 0; l < level; l++) {
 728                 if (TRACE_SUBDIVIDE_CURVE) {
 729                     System.out.println("level: " + l);
 730                 }
 731 
 732                 double[] src = SUBDIVIDE_CURVES[l];
 733                 double[] dst = SUBDIVIDE_CURVES[l + 1];
 734 
 735                 for (int i = 0, j = 0; i < src.length; i += 8, j += 16) {
 736                     if (TRACE_SUBDIVIDE_CURVE) {
 737                         System.out.println("subdivide: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));
 738                     }
 739                     if (type == 8) {
 740                         CubicCurve2D.subdivide(src, i, dst, j, dst, j + 8);
 741                     } else {
 742                         QuadCurve2D.subdivide(src, i, dst, j, dst, j + 8);
 743                     }
 744                     if (TRACE_SUBDIVIDE_CURVE) {
 745                         System.out.println("left: " + Arrays.toString(Arrays.copyOfRange(dst, j, j + type)));
 746                         System.out.println("right: " + Arrays.toString(Arrays.copyOfRange(dst, j + 8, j + 8 + type)));
 747                     }
 748                 }
 749             }
 750 
 751             // Emit curves at last level:
 752             double[] src = SUBDIVIDE_CURVES[level];
 753 
 754             double len = 0.0;
 755 
 756             for (int i = 0; i < src.length; i += 8) {
 757                 if (TRACE_SUBDIVIDE_CURVE) {
 758                     System.out.println("curve: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));
 759                 }
 760 
 761                 if (type == 8) {
 762                     if (TRACE_SUBDIVIDE_CURVE) {
 763                         len += curvelen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);
 764                     }
 765                     p2d.curveTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);
 766                 } else {
 767                     if (TRACE_SUBDIVIDE_CURVE) {
 768                         len += quadlen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5]);
 769                     }
 770                     p2d.quadTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5]);
 771                 }
 772             }
 773 
 774             if (TRACE_SUBDIVIDE_CURVE) {
 775                 System.out.println("curveLen (final) = " + len);
 776             }
 777         } else {
 778             if (type == 8) {
 779                 p2d.curveTo(in[2], in[3], in[4], in[5], in[6], in[7]);
 780             } else {
 781                 p2d.quadTo(in[2], in[3], in[4], in[5]);
 782             }
 783         }
 784     }
 785 
 786     static final float POINT_RADIUS = 2f;
 787     static final float LINE_WIDTH = 1f;
 788 
 789     static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH);
 790     static final int COLOR_ALPHA = 128;
 791     static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA);
 792     static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA);
 793     static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA);
 794 
 795     static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float();
 796 
 797     private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) {
 798 
 799         final Stroke oldStroke = g2d.getStroke();
 800         final Color oldColor = g2d.getColor();
 801 
 802         setClip(g2d, false);
 803 
 804         if (SHOW_OUTLINE) {
 805             g2d.setStroke(OUTLINE_STROKE);
 806             g2d.setColor(COLOR_LINETO_ODD);
 807             g2d.draw(shape);
 808         }
 809 
 810         final float[] coords = new float[6];
 811         float px, py;
 812 
 813         int nMove = 0;
 814         int nLine = 0;
 815         int n = 0;
 816 
 817         for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {
 818             int type = it.currentSegment(coords);
 819             switch (type) {
 820                 case PathIterator.SEG_MOVETO:
 821                     if (SHOW_POINTS) {
 822                         g2d.setColor(COLOR_MOVETO);
 823                     }
 824                     break;
 825                 case PathIterator.SEG_LINETO:
 826                 case PathIterator.SEG_QUADTO:
 827                 case PathIterator.SEG_CUBICTO:
 828                     if (SHOW_POINTS) {
 829                         g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN);
 830                     }
 831                     nLine++;
 832                     break;
 833                 case PathIterator.SEG_CLOSE:
 834                     continue;
 835                 default:
 836                     System.out.println("unsupported segment type= " + type);
 837                     continue;
 838             }
 839             px = coords[0];
 840             py = coords[1];
 841 
 842             if (SHOW_INFO) {
 843                 System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py);
 844             }
 845 
 846             if (SHOW_POINTS) {
 847                 ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS,
 848                         POINT_RADIUS * 2f, POINT_RADIUS * 2f);
 849                 g2d.fill(ELL_POINT);
 850             }
 851         }
 852         if (SHOW_INFO) {
 853             System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine);
 854             System.out.println("--------------------------------------------------");
 855         }
 856 
 857         g2d.setStroke(oldStroke);
 858         g2d.setColor(oldColor);
 859     }
 860 
 861     private static void dumpShape(final Shape shape) {
 862         final float[] coords = new float[6];
 863 
 864         for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {
 865             final int type = it.currentSegment(coords);
 866             switch (type) {
 867                 case PathIterator.SEG_MOVETO:
 868                     System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");");
 869                     break;
 870                 case PathIterator.SEG_LINETO:
 871                     System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");");
 872                     break;
 873                 case PathIterator.SEG_QUADTO:
 874                     System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");");
 875                     break;
 876                 case PathIterator.SEG_CUBICTO:
 877                     System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");");
 878                     break;
 879                 case PathIterator.SEG_CLOSE:
 880                     System.out.println("p2d.closePath();");
 881                     break;
 882                 default:
 883                     System.out.println("// Unsupported segment type= " + type);
 884             }
 885         }
 886         System.out.println("--------------------------------------------------");
 887     }
 888 
 889     static double randX() {
 890         return RANDOM.nextDouble() * RANDW + OFFW;
 891     }
 892 
 893     static double randY() {
 894         return RANDOM.nextDouble() * RANDH + OFFH;
 895     }
 896 
 897     private static BasicStroke createStroke(final TestSetup ts) {
 898         return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f);
 899     }
 900 
 901     private final static class TestSetup {
 902 
 903         static final AtomicInteger COUNT = new AtomicInteger();
 904 
 905         final int id;
 906         final ShapeMode shapeMode;
 907         final boolean closed;
 908         // stroke
 909         final float strokeWidth;
 910         final int strokeCap;
 911         final int strokeJoin;
 912         final float[] dashes;
 913         // fill
 914         final int windingRule;
 915 
 916         TestSetup(ShapeMode shapeMode, final boolean closed,
 917                   final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) {
 918             this.id = COUNT.incrementAndGet();
 919             this.shapeMode = shapeMode;
 920             this.closed = closed;
 921             this.strokeWidth = strokeWidth;
 922             this.strokeCap = strokeCap;
 923             this.strokeJoin = strokeJoin;
 924             this.dashes = dashes;
 925             this.windingRule = Path2D.WIND_NON_ZERO;
 926         }
 927 
 928         TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) {
 929             this.id = COUNT.incrementAndGet();
 930             this.shapeMode = shapeMode;
 931             this.closed = closed;
 932             this.strokeWidth = 0f;
 933             this.strokeCap = this.strokeJoin = -1; // invalid
 934             this.dashes = null;
 935             this.windingRule = windingRule;
 936         }
 937 
 938         boolean isStroke() {
 939             return this.strokeWidth > 0f;
 940         }
 941 
 942         @Override
 943         public String toString() {
 944             if (isStroke()) {
 945                 return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed
 946                         + ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin)
 947                         + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "")
 948                         + '}';
 949             }
 950             return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed
 951                     + ", fill"
 952                     + ", windingRule=" + getWindingRule(windingRule) + '}';
 953         }
 954 
 955         private static String getCap(final int cap) {
 956             switch (cap) {
 957                 case BasicStroke.CAP_BUTT:
 958                     return "CAP_BUTT";
 959                 case BasicStroke.CAP_ROUND:
 960                     return "CAP_ROUND";
 961                 case BasicStroke.CAP_SQUARE:
 962                     return "CAP_SQUARE";
 963                 default:
 964                     return "";
 965             }
 966 
 967         }
 968 
 969         private static String getJoin(final int join) {
 970             switch (join) {
 971                 case BasicStroke.JOIN_MITER:
 972                     return "JOIN_MITER";
 973                 case BasicStroke.JOIN_ROUND:
 974                     return "JOIN_ROUND";
 975                 case BasicStroke.JOIN_BEVEL:
 976                     return "JOIN_BEVEL";
 977                 default:
 978                     return "";
 979             }
 980 
 981         }
 982 
 983         private static String getWindingRule(final int rule) {
 984             switch (rule) {
 985                 case PathIterator.WIND_EVEN_ODD:
 986                     return "WIND_EVEN_ODD";
 987                 case PathIterator.WIND_NON_ZERO:
 988                     return "WIND_NON_ZERO";
 989                 default:
 990                     return "";
 991             }
 992         }
 993     }
 994 
 995     // --- utilities ---
 996     private static final int DCM_ALPHA_MASK = 0xff000000;
 997 
 998     public static BufferedImage newImage(final int w, final int h) {
 999         return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
1000     }
1001 
1002     public static BufferedImage computeDiffImage(final DiffContext testCtx,
1003                                                  final DiffContext testThCtx,
1004                                                  final BufferedImage tstImage,
1005                                                  final BufferedImage refImage,
1006                                                  final BufferedImage diffImage) {
1007 
1008         final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData();
1009         final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData();
1010         final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData();
1011 
1012         // reset diff contexts:
1013         testCtx.reset();
1014         testThCtx.reset();
1015 
1016         int ref, tst, dg, v;
1017         for (int i = 0, len = aRefPix.length; i < len; i++) {
1018             ref = aRefPix[i];
1019             tst = aTstPix[i];
1020 
1021             // grayscale diff:
1022             dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst));
1023 
1024             // max difference on grayscale values:
1025             v = (int) Math.ceil(Math.abs(dg / 3.0));
1026             if (v <= THRESHOLD_DELTA) {
1027                 aDifPix[i] = 0;
1028             } else {
1029                 aDifPix[i] = toInt(v, v, v);
1030                 testThCtx.add(v);
1031             }
1032 
1033             if (v != 0) {
1034                 testCtx.add(v);
1035             }
1036         }
1037 
1038         testCtx.addNbPix(testThCtx.histPix.count);
1039 
1040         if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) {
1041             return null;
1042         }
1043 
1044         return diffImage;
1045     }
1046 
1047     static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException {
1048         final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG");
1049         if (itWriters.hasNext()) {
1050             final ImageWriter writer = itWriters.next();
1051 
1052             final ImageWriteParam writerParams = writer.getDefaultWriteParam();
1053             writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED);
1054 
1055             final File imgFile = new File(resDirectory, imageFileName);
1056 
1057             if (!imgFile.exists() || imgFile.canWrite()) {
1058                 System.out.println("saveImage: saving image as PNG [" + imgFile + "]...");
1059                 imgFile.delete();
1060 
1061                 // disable cache in temporary files:
1062                 ImageIO.setUseCache(false);
1063 
1064                 final long start = System.nanoTime();
1065 
1066                 // PNG uses already buffering:
1067                 final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile));
1068 
1069                 writer.setOutput(imgOutStream);
1070                 try {
1071                     writer.write(null, new IIOImage(image, null, null), writerParams);
1072                 } finally {
1073                     imgOutStream.close();
1074 
1075                     final long time = System.nanoTime() - start;
1076                     System.out.println("saveImage: duration= " + (time / 1000000l) + " ms.");
1077                 }
1078             }
1079         }
1080     }
1081 
1082     static int r(final int v) {
1083         return (v >> 16 & 0xff);
1084     }
1085 
1086     static int g(final int v) {
1087         return (v >> 8 & 0xff);
1088     }
1089 
1090     static int b(final int v) {
1091         return (v & 0xff);
1092     }
1093 
1094     static int clamp127(final int v) {
1095         return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255;
1096     }
1097 
1098     static int toInt(final int r, final int g, final int b) {
1099         return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b;
1100     }
1101 
1102     /* stats */
1103     static class StatInteger {
1104 
1105         public final String name;
1106         public long count = 0l;
1107         public long sum = 0l;
1108         public long min = Integer.MAX_VALUE;
1109         public long max = Integer.MIN_VALUE;
1110 
1111         StatInteger(String name) {
1112             this.name = name;
1113         }
1114 
1115         void reset() {
1116             count = 0l;
1117             sum = 0l;
1118             min = Integer.MAX_VALUE;
1119             max = Integer.MIN_VALUE;
1120         }
1121 
1122         void add(int val) {
1123             count++;
1124             sum += val;
1125             if (val < min) {
1126                 min = val;
1127             }
1128             if (val > max) {
1129                 max = val;
1130             }
1131         }
1132 
1133         void add(long val) {
1134             count++;
1135             sum += val;
1136             if (val < min) {
1137                 min = val;
1138             }
1139             if (val > max) {
1140                 max = val;
1141             }
1142         }
1143 
1144         void add(StatInteger stat) {
1145             count += stat.count;
1146             sum += stat.sum;
1147             if (stat.min < min) {
1148                 min = stat.min;
1149             }
1150             if (stat.max > max) {
1151                 max = stat.max;
1152             }
1153         }
1154 
1155         public final double average() {
1156             return ((double) sum) / count;
1157         }
1158 
1159         @Override
1160         public String toString() {
1161             final StringBuilder sb = new StringBuilder(128);
1162             toString(sb);
1163             return sb.toString();
1164         }
1165 
1166         public final StringBuilder toString(final StringBuilder sb) {
1167             sb.append(name).append("[n: ").append(count);
1168             sb.append("] ");
1169             if (count != 0) {
1170                 sb.append("sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average()));
1171                 sb.append(" [").append(min).append(" | ").append(max).append("]");
1172             }
1173             return sb;
1174         }
1175 
1176     }
1177 
1178     final static class Histogram extends StatInteger {
1179 
1180         static final int BUCKET = 2;
1181         static final int MAX = 20;
1182         static final int LAST = MAX - 1;
1183         static final int[] STEPS = new int[MAX];
1184         static final int BUCKET_TH;
1185 
1186         static {
1187             STEPS[0] = 0;
1188             STEPS[1] = 1;
1189 
1190             for (int i = 2; i < MAX; i++) {
1191                 STEPS[i] = STEPS[i - 1] * BUCKET;
1192             }
1193 //            System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS));
1194 
1195             if (THRESHOLD_DELTA % 2 != 0) {
1196                 throw new IllegalStateException("THRESHOLD_DELTA must be odd");
1197             }
1198 
1199             BUCKET_TH = bucket(THRESHOLD_DELTA);
1200         }
1201 
1202         static int bucket(int val) {
1203             for (int i = 1; i < MAX; i++) {
1204                 if (val < STEPS[i]) {
1205                     return i - 1;
1206                 }
1207             }
1208             return LAST;
1209         }
1210 
1211         private final StatInteger[] stats = new StatInteger[MAX];
1212 
1213         public Histogram(String name) {
1214             super(name);
1215             for (int i = 0; i < MAX; i++) {
1216                 stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~")));
1217             }
1218         }
1219 
1220         @Override
1221         final void reset() {
1222             super.reset();
1223             for (int i = 0; i < MAX; i++) {
1224                 stats[i].reset();
1225             }
1226         }
1227 
1228         @Override
1229         final void add(int val) {
1230             super.add(val);
1231             stats[bucket(val)].add(val);
1232         }
1233 
1234         @Override
1235         final void add(long val) {
1236             add((int) val);
1237         }
1238 
1239         void add(Histogram hist) {
1240             super.add(hist);
1241             for (int i = 0; i < MAX; i++) {
1242                 stats[i].add(hist.stats[i]);
1243             }
1244         }
1245 
1246         boolean isWorse(Histogram hist, boolean useTh) {
1247             boolean worst = false;
1248             if (!useTh && (hist.sum > sum)) {
1249                 worst = true;
1250             } else {
1251                 long sumLoc = 0l;
1252                 long sumHist = 0l;
1253                 // use running sum:
1254                 for (int i = MAX - 1; i >= BUCKET_TH; i--) {
1255                     sumLoc += stats[i].sum;
1256                     sumHist += hist.stats[i].sum;
1257                 }
1258                 if (sumHist > sumLoc) {
1259                     worst = true;
1260                 }
1261             }
1262             /*
1263             System.out.println("running sum worst:");
1264             System.out.println("this ? " + toString());
1265             System.out.println("worst ? " + hist.toString());
1266              */
1267             return worst;
1268         }
1269 
1270         @Override
1271         public final String toString() {
1272             final StringBuilder sb = new StringBuilder(2048);
1273             super.toString(sb).append(" { ");
1274 
1275             for (int i = 0; i < MAX; i++) {
1276                 if (stats[i].count != 0l) {
1277                     sb.append("\n        ").append(stats[i].toString());
1278                 }
1279             }
1280 
1281             return sb.append(" }").toString();
1282         }
1283     }
1284 
1285     /**
1286      * Adjust the given double value to keep only 3 decimal digits
1287      * @param value value to adjust
1288      * @return double value with only 3 decimal digits
1289      */
1290     static double trimTo3Digits(final double value) {
1291         return ((long) (1e3d * value)) / 1e3d;
1292     }
1293 
1294     static final class DiffContext {
1295 
1296         public final Histogram histPix;
1297 
1298         public final StatInteger nbPix;
1299 
1300         DiffContext(String name) {
1301             histPix = new Histogram("Diff Pixels [" + name + "]");
1302             nbPix = new StatInteger("NbPixels [" + name + "]");
1303         }
1304 
1305         void reset() {
1306             histPix.reset();
1307             nbPix.reset();
1308         }
1309 
1310         void dump() {
1311             if (isDiff()) {
1312                 System.out.println("Differences [" + histPix.name + "]:\n"
1313                         + ((nbPix.count != 0) ? (nbPix.toString() + "\n") : "")
1314                         + histPix.toString()
1315                 );
1316             } else {
1317                 System.out.println("No difference for [" + histPix.name + "].");
1318             }
1319         }
1320 
1321         void add(int val) {
1322             histPix.add(val);
1323         }
1324 
1325         void add(DiffContext ctx) {
1326             histPix.add(ctx.histPix);
1327             if (ctx.nbPix.count != 0L) {
1328                 nbPix.add(ctx.nbPix);
1329             }
1330         }
1331 
1332         void addNbPix(long val) {
1333             if (val != 0L) {
1334                 nbPix.add(val);
1335             }
1336         }
1337 
1338         void set(DiffContext ctx) {
1339             reset();
1340             add(ctx);
1341         }
1342 
1343         boolean isWorse(DiffContext ctx, boolean useTh) {
1344             return histPix.isWorse(ctx.histPix, useTh);
1345         }
1346 
1347         boolean isDiff() {
1348             return histPix.sum != 0l;
1349         }
1350     }
1351 
1352 
1353     static double linelen(final double x0, final double y0,
1354                           final double x1, final double y1)
1355     {
1356         final double dx = x1 - x0;
1357         final double dy = y1 - y0;
1358         return Math.sqrt(dx * dx + dy * dy);
1359     }
1360 
1361     static double quadlen(final double x0, final double y0,
1362                           final double x1, final double y1,
1363                           final double x2, final double y2)
1364     {
1365         return (linelen(x0, y0, x1, y1)
1366                 + linelen(x1, y1, x2, y2)
1367                 + linelen(x0, y0, x2, y2)) / 2.0d;
1368     }
1369 
1370     static double curvelen(final double x0, final double y0,
1371                            final double x1, final double y1,
1372                            final double x2, final double y2,
1373                            final double x3, final double y3)
1374     {
1375         return (linelen(x0, y0, x1, y1)
1376               + linelen(x1, y1, x2, y2)
1377               + linelen(x2, y2, x3, y3)
1378               + linelen(x0, y0, x3, y3)) / 2.0d;
1379     }
1380 }