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