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 }