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