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 }