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 fill modes (EO rules)
  58  * Note: Use the argument -slow to run more intensive tests (too much time)
  59  * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest
  60  * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest
  61  */
  62 public final class ClipShapeTest {
  63 
  64     static final boolean TEST_STROKER = true;
  65     static final boolean TEST_FILLER = true;
  66 
  67     // complementary tests in slow mode:
  68     static boolean USE_DASHES = false;
  69     static boolean USE_VAR_STROKE = false;
  70 
  71     static int NUM_TESTS = 5000;
  72     static final int TESTW = 100;
  73     static final int TESTH = 100;
  74 
  75     // shape settings:
  76     static final ShapeMode SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;
  77     static final boolean SHAPE_REPEAT = true;
  78 
  79     // dump path on console:
  80     static final boolean DUMP_SHAPE = true;
  81 
  82     static final boolean SHOW_DETAILS = true;
  83     static final boolean SHOW_OUTLINE = true;
  84     static final boolean SHOW_POINTS = true;
  85     static final boolean SHOW_INFO = false;
  86 
  87     static final int MAX_SHOW_FRAMES = 10;
  88 
  89     // use fixed seed to reproduce always same polygons between tests
  90     static final boolean FIXED_SEED = false;
  91     static final double RAND_SCALE = 3.0;
  92     static final double RANDW = TESTW * RAND_SCALE;
  93     static final double OFFW = (TESTW - RANDW) / 2.0;
  94     static final double RANDH = TESTH * RAND_SCALE;
  95     static final double OFFH = (TESTH - RANDH) / 2.0;
  96 
  97     static enum ShapeMode {
  98         TWO_CUBICS,
  99         FOUR_QUADS,
 100         FIVE_LINE_POLYS,
 101         NINE_LINE_POLYS,
 102         FIFTY_LINE_POLYS,
 103         MIXED
 104     }
 105 
 106     static final long SEED = 1666133789L;
 107     // Fixed seed to avoid any difference between runs:
 108     static final Random RANDOM = new Random(SEED);
 109 
 110     static final File OUTPUT_DIR = new File(".");
 111 
 112     /**
 113      * Test
 114      * @param args
 115      */
 116     public static void main(String[] args) {
 117         boolean runSlowTests = (args.length != 0 && "-slow".equals(args[0]));
 118 
 119         if (runSlowTests) {
 120             NUM_TESTS = 20000; // or 100000 (very slow)
 121             USE_DASHES = true;
 122             USE_VAR_STROKE = true;
 123         }
 124 
 125         Locale.setDefault(Locale.US);
 126 
 127         // Get Marlin runtime state from its log:
 128         final AtomicBoolean isMarlin = new AtomicBoolean();
 129         final AtomicBoolean isClipRuntime = new AtomicBoolean();
 130 
 131         // initialize j.u.l Looger:
 132         final Logger log = Logger.getLogger("sun.java2d.marlin");
 133         log.addHandler(new Handler() {
 134             @Override
 135             public void publish(LogRecord record) {
 136                 final String msg = record.getMessage();
 137                 if (msg != null) {
 138                     // last space to avoid matching other settings:
 139                     if (msg.startsWith("sun.java2d.renderer ")) {
 140                         isMarlin.set(msg.contains("MarlinRenderingEngine"));
 141                     }
 142                     if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) {
 143                         isClipRuntime.set(msg.contains("true"));
 144                     }
 145                 }
 146 
 147                 final Throwable th = record.getThrown();
 148                 // detect any Throwable:
 149                 if (th != null) {
 150                     System.out.println("Test failed:\n" + record.getMessage());
 151                     th.printStackTrace(System.out);
 152 
 153                     throw new RuntimeException("Test failed: ", th);
 154                 }
 155             }
 156 
 157             @Override
 158             public void flush() {
 159             }
 160 
 161             @Override
 162             public void close() throws SecurityException {
 163             }
 164         });
 165 
 166         // enable Marlin logging & internal checks:
 167         System.setProperty("sun.java2d.renderer.log", "true");
 168         System.setProperty("sun.java2d.renderer.useLogger", "true");
 169 
 170         // disable static clipping setting:
 171         System.setProperty("sun.java2d.renderer.clip", "false");
 172         System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true");
 173 
 174         System.out.println("ClipShapeTests: image = " + TESTW + " x " + TESTH);
 175 
 176         int failures = 0;
 177         final long start = System.nanoTime();
 178         try {
 179             // TODO: test affine transforms ?
 180 
 181             if (TEST_STROKER) {
 182                 final float[][] dashArrays = (USE_DASHES)
 183                         ? new float[][]{null, new float[]{1f, 2f}}
 184                         : new float[][]{null};
 185 
 186                 System.out.println("dashes: " + Arrays.toString(dashArrays));
 187 
 188                 final float[] strokeWidths = (USE_VAR_STROKE)
 189                         ? new float[5] : new float[]{8f};
 190 
 191                 int nsw = 0;
 192                 if (USE_VAR_STROKE) {
 193                     for (float width = 0.1f; width < 110f; width *= 5f) {
 194                         strokeWidths[nsw++] = width;
 195                     }
 196                 } else {
 197                     nsw = 1;
 198                 }
 199 
 200                 System.out.println("stroke widths: " + Arrays.toString(strokeWidths));
 201 
 202                 // Stroker tests:
 203                 for (int w = 0; w < nsw; w++) {
 204                     final float width = strokeWidths[w];
 205 
 206                     for (float[] dashes : dashArrays) {
 207 
 208                         for (int cap = 0; cap <= 2; cap++) {
 209 
 210                             for (int join = 0; join <= 2; join++) {
 211 
 212                                 failures += paintPaths(new TestSetup(SHAPE_MODE, false, width, cap, join, dashes));
 213                                 failures += paintPaths(new TestSetup(SHAPE_MODE, true, width, cap, join, dashes));
 214                             }
 215                         }
 216                     }
 217                 }
 218             }
 219 
 220             if (TEST_FILLER) {
 221                 // Filler tests:
 222                 failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO));
 223                 failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO));
 224 
 225                 failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD));
 226                 failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD));
 227             }
 228         } catch (IOException ioe) {
 229             throw new RuntimeException(ioe);
 230         }
 231         System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
 232 
 233         if (!isMarlin.get()) {
 234             throw new RuntimeException("Marlin renderer not used at runtime !");
 235         }
 236         if (!isClipRuntime.get()) {
 237             throw new RuntimeException("Marlin clipping not enabled at runtime !");
 238         }
 239         if (failures != 0) {
 240             throw new RuntimeException("Clip test failures : " + failures);
 241         }
 242     }
 243 
 244     static int paintPaths(final TestSetup ts) throws IOException {
 245         final long start = System.nanoTime();
 246 
 247         if (FIXED_SEED) {
 248             // Reset seed for random numbers:
 249             RANDOM.setSeed(SEED);
 250         }
 251 
 252         System.out.println("paintPaths: " + NUM_TESTS
 253                 + " paths (" + SHAPE_MODE + ") - setup: " + ts);
 254 
 255         final boolean fill = !ts.isStroke();
 256         final Path2D p2d = new Path2D.Double(ts.windingRule);
 257 
 258         final BufferedImage imgOn = newImage(TESTW, TESTH);
 259         final Graphics2D g2dOn = initialize(imgOn, ts);
 260 
 261         final BufferedImage imgOff = newImage(TESTW, TESTH);
 262         final Graphics2D g2dOff = initialize(imgOff, ts);
 263 
 264         final BufferedImage imgDiff = newImage(TESTW, TESTH);
 265 
 266         final DiffContext globalCtx = new DiffContext("All tests");
 267 
 268         int nd = 0;
 269         try {
 270             final DiffContext testCtx = new DiffContext("Test");
 271             BufferedImage diffImage;
 272 
 273             for (int n = 0; n < NUM_TESTS; n++) {
 274                 genShape(p2d, ts);
 275 
 276                 // Runtime clip setting OFF:
 277                 paintShape(p2d, g2dOff, fill, false);
 278 
 279                 // Runtime clip setting ON:
 280                 paintShape(p2d, g2dOn, fill, true);
 281 
 282                 /* compute image difference if possible */
 283                 diffImage = computeDiffImage(testCtx, imgOn, imgOff, imgDiff, globalCtx);
 284 
 285                 final String testName = "Setup_" + ts.id + "_test_" + n;
 286 
 287                 if (diffImage != null) {
 288                     nd++;
 289 
 290                     final double ratio = (100.0 * testCtx.histPix.count) / testCtx.histAll.count;
 291                     System.out.println("Diff ratio: " + testName + " = " + trimTo3Digits(ratio) + " %");
 292 
 293                     if (false) {
 294                         saveImage(diffImage, OUTPUT_DIR, testName + "-diff.png");
 295                     }
 296 
 297                     if (DUMP_SHAPE) {
 298                         dumpShape(p2d);
 299                     }
 300                     if (nd < MAX_SHOW_FRAMES) {
 301                         if (SHOW_DETAILS) {
 302                             paintShapeDetails(g2dOff, p2d);
 303                             paintShapeDetails(g2dOn, p2d);
 304                         }
 305 
 306                         saveImage(imgOff, OUTPUT_DIR, testName + "-off.png");
 307                         saveImage(imgOn, OUTPUT_DIR, testName + "-on.png");
 308                         saveImage(diffImage, OUTPUT_DIR, testName + "-diff.png");
 309                     }
 310                 }
 311             }
 312         } finally {
 313             g2dOff.dispose();
 314             g2dOn.dispose();
 315 
 316             if (nd != 0) {
 317                 System.out.println("paintPaths: " + NUM_TESTS + " paths - "
 318                         + "Number of differences = " + nd
 319                         + " ratio = " + (100f * nd) / NUM_TESTS + " %");
 320             }
 321 
 322             globalCtx.dump();
 323         }
 324         System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");
 325         return nd;
 326     }
 327 
 328     private static void paintShape(final Path2D p2d, final Graphics2D g2d,
 329                                    final boolean fill, final boolean clip) {
 330         reset(g2d);
 331 
 332         setClip(g2d, clip);
 333 
 334         if (fill) {
 335             g2d.fill(p2d);
 336         } else {
 337             g2d.draw(p2d);
 338         }
 339     }
 340 
 341     private static Graphics2D initialize(final BufferedImage img,
 342                                          final TestSetup ts) {
 343         final Graphics2D g2d = (Graphics2D) img.getGraphics();
 344         g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
 345                 RenderingHints.VALUE_RENDER_QUALITY);
 346         g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
 347                 RenderingHints.VALUE_STROKE_PURE);
 348 
 349         if (ts.isStroke()) {
 350             g2d.setStroke(createStroke(ts));
 351         }
 352         g2d.setColor(Color.GRAY);
 353 
 354         return g2d;
 355     }
 356 
 357     private static void reset(final Graphics2D g2d) {
 358         // Disable antialiasing:
 359         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 360                 RenderingHints.VALUE_ANTIALIAS_OFF);
 361         g2d.setBackground(Color.WHITE);
 362         g2d.clearRect(0, 0, TESTW, TESTH);
 363     }
 364 
 365     private static void setClip(final Graphics2D g2d, final boolean clip) {
 366         // Enable antialiasing:
 367         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
 368                 RenderingHints.VALUE_ANTIALIAS_ON);
 369 
 370         // Enable or Disable clipping:
 371         System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false");
 372     }
 373 
 374     static void genShape(final Path2D p2d, final TestSetup ts) {
 375         p2d.reset();
 376 
 377         final int end = (SHAPE_REPEAT) ? 2 : 1;
 378 
 379         for (int p = 0; p < end; p++) {
 380             p2d.moveTo(randX(), randY());
 381 
 382             switch (ts.shapeMode) {
 383                 case MIXED:
 384                 case FIFTY_LINE_POLYS:
 385                 case NINE_LINE_POLYS:
 386                 case FIVE_LINE_POLYS:
 387                     p2d.lineTo(randX(), randY());
 388                     p2d.lineTo(randX(), randY());
 389                     p2d.lineTo(randX(), randY());
 390                     p2d.lineTo(randX(), randY());
 391                     if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) {
 392                         // And an implicit close makes 5 lines
 393                         break;
 394                     }
 395                     p2d.lineTo(randX(), randY());
 396                     p2d.lineTo(randX(), randY());
 397                     p2d.lineTo(randX(), randY());
 398                     p2d.lineTo(randX(), randY());
 399                     if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) {
 400                         // And an implicit close makes 9 lines
 401                         break;
 402                     }
 403                     if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) {
 404                         for (int i = 0; i < 41; i++) {
 405                             p2d.lineTo(randX(), randY());
 406                         }
 407                         // And an implicit close makes 50 lines
 408                         break;
 409                     }
 410                 case TWO_CUBICS:
 411                     p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY());
 412                     p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY());
 413                     if (ts.shapeMode == ShapeMode.TWO_CUBICS) {
 414                         break;
 415                     }
 416                 case FOUR_QUADS:
 417                     p2d.quadTo(randX(), randY(), randX(), randY());
 418                     p2d.quadTo(randX(), randY(), randX(), randY());
 419                     p2d.quadTo(randX(), randY(), randX(), randY());
 420                     p2d.quadTo(randX(), randY(), randX(), randY());
 421                     if (ts.shapeMode == ShapeMode.FOUR_QUADS) {
 422                         break;
 423                     }
 424                 default:
 425             }
 426 
 427             if (ts.closed) {
 428                 p2d.closePath();
 429             }
 430         }
 431     }
 432 
 433     static final float POINT_RADIUS = 2f;
 434     static final float LINE_WIDTH = 1f;
 435 
 436     static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH);
 437     static final int COLOR_ALPHA = 128;
 438     static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA);
 439     static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA);
 440     static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA);
 441 
 442     static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float();
 443 
 444     private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) {
 445 
 446         final Stroke oldStroke = g2d.getStroke();
 447         final Color oldColor = g2d.getColor();
 448 
 449         setClip(g2d, false);
 450 
 451         if (SHOW_OUTLINE) {
 452             g2d.setStroke(OUTLINE_STROKE);
 453             g2d.setColor(COLOR_LINETO_ODD);
 454             g2d.draw(shape);
 455         }
 456 
 457         final float[] coords = new float[6];
 458         float px, py;
 459 
 460         int nMove = 0;
 461         int nLine = 0;
 462         int n = 0;
 463 
 464         for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {
 465             int type = it.currentSegment(coords);
 466             switch (type) {
 467                 case PathIterator.SEG_MOVETO:
 468                     if (SHOW_POINTS) {
 469                         g2d.setColor(COLOR_MOVETO);
 470                     }
 471                     break;
 472                 case PathIterator.SEG_LINETO:
 473                     if (SHOW_POINTS) {
 474                         g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN);
 475                     }
 476                     nLine++;
 477                     break;
 478                 case PathIterator.SEG_CLOSE:
 479                     continue;
 480                 default:
 481                     System.out.println("unsupported segment type= " + type);
 482                     continue;
 483             }
 484             px = coords[0];
 485             py = coords[1];
 486 
 487             if (SHOW_INFO) {
 488                 System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py);
 489             }
 490 
 491             if (SHOW_POINTS) {
 492                 ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS,
 493                         POINT_RADIUS * 2f, POINT_RADIUS * 2f);
 494                 g2d.fill(ELL_POINT);
 495             }
 496         }
 497         if (SHOW_INFO) {
 498             System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine);
 499             System.out.println("--------------------------------------------------");
 500         }
 501 
 502         g2d.setStroke(oldStroke);
 503         g2d.setColor(oldColor);
 504     }
 505 
 506     private static void dumpShape(final Shape shape) {
 507         final float[] coords = new float[6];
 508 
 509         for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {
 510             final int type = it.currentSegment(coords);
 511             switch (type) {
 512                 case PathIterator.SEG_MOVETO:
 513                     System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");");
 514                     break;
 515                 case PathIterator.SEG_LINETO:
 516                     System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");");
 517                     break;
 518                 case PathIterator.SEG_CLOSE:
 519                     System.out.println("p2d.closePath();");
 520                     break;
 521                 default:
 522                     System.out.println("// Unsupported segment type= " + type);
 523             }
 524         }
 525         System.out.println("--------------------------------------------------");
 526     }
 527 
 528     static double randX() {
 529         return RANDOM.nextDouble() * RANDW + OFFW;
 530     }
 531 
 532     static double randY() {
 533         return RANDOM.nextDouble() * RANDH + OFFH;
 534     }
 535 
 536     private static BasicStroke createStroke(final TestSetup ts) {
 537         return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f);
 538     }
 539 
 540     private final static class TestSetup {
 541 
 542         static final AtomicInteger COUNT = new AtomicInteger();
 543 
 544         final int id;
 545         final ShapeMode shapeMode;
 546         final boolean closed;
 547         // stroke
 548         final float strokeWidth;
 549         final int strokeCap;
 550         final int strokeJoin;
 551         final float[] dashes;
 552         // fill
 553         final int windingRule;
 554 
 555         TestSetup(ShapeMode shapeMode, final boolean closed,
 556                   final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) {
 557             this.id = COUNT.incrementAndGet();
 558             this.shapeMode = shapeMode;
 559             this.closed = closed;
 560             this.strokeWidth = strokeWidth;
 561             this.strokeCap = strokeCap;
 562             this.strokeJoin = strokeJoin;
 563             this.dashes = dashes;
 564             this.windingRule = Path2D.WIND_NON_ZERO;
 565         }
 566 
 567         TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) {
 568             this.id = COUNT.incrementAndGet();
 569             this.shapeMode = shapeMode;
 570             this.closed = closed;
 571             this.strokeWidth = 0f;
 572             this.strokeCap = this.strokeJoin = -1; // invalid
 573             this.dashes = null;
 574             this.windingRule = windingRule;
 575         }
 576 
 577         boolean isStroke() {
 578             return this.strokeWidth > 0f;
 579         }
 580 
 581         @Override
 582         public String toString() {
 583             return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed
 584                     + ", strokeWidth=" + strokeWidth + ", strokeCap=" + strokeCap + ", strokeJoin=" + strokeJoin
 585                     + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "")
 586                     + ", windingRule=" + windingRule + '}';
 587         }
 588     }
 589 
 590     // --- utilities ---
 591     private static final int DCM_ALPHA_MASK = 0xff000000;
 592 
 593     public static BufferedImage newImage(final int w, final int h) {
 594         return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
 595     }
 596 
 597     public static BufferedImage computeDiffImage(final DiffContext localCtx,
 598                                                  final BufferedImage tstImage,
 599                                                  final BufferedImage refImage,
 600                                                  final BufferedImage diffImage,
 601                                                  final DiffContext globalCtx) {
 602 
 603         final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData();
 604         final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData();
 605         final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData();
 606 
 607         // reset local diff context:
 608         localCtx.reset();
 609 
 610         int ref, tst, dg, v;
 611         for (int i = 0, len = aRefPix.length; i < len; i++) {
 612             ref = aRefPix[i];
 613             tst = aTstPix[i];
 614 
 615             // grayscale diff:
 616             dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst));
 617 
 618             // max difference on grayscale values:
 619             v = (int) Math.ceil(Math.abs(dg / 3.0));
 620 
 621             aDifPix[i] = toInt(v, v, v);
 622 
 623             localCtx.add(v);
 624             globalCtx.add(v);
 625         }
 626 
 627         if (!localCtx.isDiff()) {
 628             return null;
 629         }
 630 
 631         return diffImage;
 632     }
 633 
 634     static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException {
 635         final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG");
 636         if (itWriters.hasNext()) {
 637             final ImageWriter writer = itWriters.next();
 638 
 639             final ImageWriteParam writerParams = writer.getDefaultWriteParam();
 640             writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED);
 641 
 642             final File imgFile = new File(resDirectory, imageFileName);
 643 
 644             if (!imgFile.exists() || imgFile.canWrite()) {
 645                 System.out.println("saveImage: saving image as PNG [" + imgFile + "]...");
 646                 imgFile.delete();
 647 
 648                 // disable cache in temporary files:
 649                 ImageIO.setUseCache(false);
 650 
 651                 final long start = System.nanoTime();
 652 
 653                 // PNG uses already buffering:
 654                 final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile));
 655 
 656                 writer.setOutput(imgOutStream);
 657                 try {
 658                     writer.write(null, new IIOImage(image, null, null), writerParams);
 659                 } finally {
 660                     imgOutStream.close();
 661 
 662                     final long time = System.nanoTime() - start;
 663                     System.out.println("saveImage: duration= " + (time / 1000000l) + " ms.");
 664                 }
 665             }
 666         }
 667     }
 668 
 669     static int r(final int v) {
 670         return (v >> 16 & 0xff);
 671     }
 672 
 673     static int g(final int v) {
 674         return (v >> 8 & 0xff);
 675     }
 676 
 677     static int b(final int v) {
 678         return (v & 0xff);
 679     }
 680 
 681     static int clamp127(final int v) {
 682         return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255;
 683     }
 684 
 685     static int toInt(final int r, final int g, final int b) {
 686         return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b;
 687     }
 688 
 689     /* stats */
 690     static class StatInteger {
 691 
 692         public final String name;
 693         public long count = 0l;
 694         public long sum = 0l;
 695         public long min = Integer.MAX_VALUE;
 696         public long max = Integer.MIN_VALUE;
 697 
 698         StatInteger(String name) {
 699             this.name = name;
 700         }
 701 
 702         void reset() {
 703             count = 0l;
 704             sum = 0l;
 705             min = Integer.MAX_VALUE;
 706             max = Integer.MIN_VALUE;
 707         }
 708 
 709         void add(int val) {
 710             count++;
 711             sum += val;
 712             if (val < min) {
 713                 min = val;
 714             }
 715             if (val > max) {
 716                 max = val;
 717             }
 718         }
 719 
 720         void add(long val) {
 721             count++;
 722             sum += val;
 723             if (val < min) {
 724                 min = val;
 725             }
 726             if (val > max) {
 727                 max = val;
 728             }
 729         }
 730 
 731         public final double average() {
 732             return ((double) sum) / count;
 733         }
 734 
 735         @Override
 736         public String toString() {
 737             final StringBuilder sb = new StringBuilder(128);
 738             toString(sb);
 739             return sb.toString();
 740         }
 741 
 742         public final StringBuilder toString(final StringBuilder sb) {
 743             sb.append(name).append("[n: ").append(count);
 744             sb.append("] sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average()));
 745             sb.append(" [").append(min).append(" | ").append(max).append("]");
 746             return sb;
 747         }
 748 
 749     }
 750 
 751     final static class Histogram extends StatInteger {
 752 
 753         static final int BUCKET = 2;
 754         static final int MAX = 20;
 755         static final int LAST = MAX - 1;
 756         static final int[] STEPS = new int[MAX];
 757 
 758         static {
 759             STEPS[0] = 0;
 760             STEPS[1] = 1;
 761 
 762             for (int i = 2; i < MAX; i++) {
 763                 STEPS[i] = STEPS[i - 1] * BUCKET;
 764             }
 765 //            System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS));
 766         }
 767 
 768         static int bucket(int val) {
 769             for (int i = 1; i < MAX; i++) {
 770                 if (val < STEPS[i]) {
 771                     return i - 1;
 772                 }
 773             }
 774             return LAST;
 775         }
 776 
 777         private final StatInteger[] stats = new StatInteger[MAX];
 778 
 779         public Histogram(String name) {
 780             super(name);
 781             for (int i = 0; i < MAX; i++) {
 782                 stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~")));
 783             }
 784         }
 785 
 786         @Override
 787         final void reset() {
 788             super.reset();
 789             for (int i = 0; i < MAX; i++) {
 790                 stats[i].reset();
 791             }
 792         }
 793 
 794         @Override
 795         final void add(int val) {
 796             super.add(val);
 797             stats[bucket(val)].add(val);
 798         }
 799 
 800         @Override
 801         final void add(long val) {
 802             add((int) val);
 803         }
 804 
 805         @Override
 806         public final String toString() {
 807             final StringBuilder sb = new StringBuilder(2048);
 808             super.toString(sb).append(" { ");
 809 
 810             for (int i = 0; i < MAX; i++) {
 811                 if (stats[i].count != 0l) {
 812                     sb.append("\n        ").append(stats[i].toString());
 813                 }
 814             }
 815 
 816             return sb.append(" }").toString();
 817         }
 818     }
 819 
 820     /**
 821      * Adjust the given double value to keep only 3 decimal digits
 822      * @param value value to adjust
 823      * @return double value with only 3 decimal digits
 824      */
 825     static double trimTo3Digits(final double value) {
 826         return ((long) (1e3d * value)) / 1e3d;
 827     }
 828 
 829     static final class DiffContext {
 830 
 831         public final Histogram histAll;
 832         public final Histogram histPix;
 833 
 834         DiffContext(String name) {
 835             histAll = new Histogram("All  Pixels [" + name + "]");
 836             histPix = new Histogram("Diff Pixels [" + name + "]");
 837         }
 838 
 839         void reset() {
 840             histAll.reset();
 841             histPix.reset();
 842         }
 843 
 844         void dump() {
 845             if (isDiff()) {
 846                 System.out.println("Differences [" + histAll.name + "]:");
 847                 System.out.println("Total [all pixels]:\n" + histAll.toString());
 848                 System.out.println("Total [different pixels]:\n" + histPix.toString());
 849             } else {
 850                 System.out.println("No difference for [" + histAll.name + "].");
 851             }
 852         }
 853 
 854         void add(int val) {
 855             histAll.add(val);
 856             if (val != 0) {
 857                 histPix.add(val);
 858             }
 859         }
 860 
 861         boolean isDiff() {
 862             return histAll.sum != 0l;
 863         }
 864     }
 865 }