1 /* 2 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 import java.awt.BasicStroke; 24 import java.awt.Color; 25 import java.awt.Graphics2D; 26 import java.awt.RenderingHints; 27 import java.awt.Shape; 28 import java.awt.Stroke; 29 import java.awt.geom.Ellipse2D; 30 import java.awt.geom.Path2D; 31 import java.awt.geom.PathIterator; 32 import java.awt.image.BufferedImage; 33 import java.awt.image.DataBufferInt; 34 import java.io.File; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 import java.util.Arrays; 38 import java.util.Iterator; 39 import java.util.Locale; 40 import java.util.Random; 41 import java.util.concurrent.atomic.AtomicBoolean; 42 import java.util.concurrent.atomic.AtomicInteger; 43 import java.util.logging.Handler; 44 import java.util.logging.LogRecord; 45 import java.util.logging.Logger; 46 import javax.imageio.IIOImage; 47 import javax.imageio.ImageIO; 48 import javax.imageio.ImageWriteParam; 49 import javax.imageio.ImageWriter; 50 import javax.imageio.stream.ImageOutputStream; 51 52 /** 53 * @test 54 * @bug 8191814 55 * @summary Verifies that Marlin rendering generates the same 56 * images with and without clipping optimization with all possible 57 * stroke (cap/join) and/or dashes or fill modes (EO rules) 58 * for paths made of either 9 lines, 4 quads, 2 cubics (random) 59 * Note: Use the argument -slow to run more intensive tests (too much time) 60 * 61 * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly 62 * @run main/othervm/timeout=240 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly -doDash 63 * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic 64 * @run main/othervm/timeout=240 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic -doDash 65 * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly 66 * @run main/othervm/timeout=240 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly -doDash 67 * @run main/othervm/timeout=120 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic 68 * @run main/othervm/timeout=240 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic -doDash 69 */ 70 public final class ClipShapeTest { 71 72 static boolean TX_SCALE = false; 73 static boolean TX_SHEAR = false; 74 75 static final boolean TEST_STROKER = true; 76 static final boolean TEST_FILLER = true; 77 78 // complementary tests in slow mode: 79 static boolean USE_DASHES = false; 80 static boolean USE_VAR_STROKE = false; 81 82 static int NUM_TESTS = 5000; 83 static final int TESTW = 100; 84 static final int TESTH = 100; 85 86 // shape settings: 87 static ShapeMode SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; 88 89 static int THRESHOLD_DELTA; 90 static long THRESHOLD_NBPIX; 91 92 static final boolean SHAPE_REPEAT = true; 93 94 // dump path on console: 95 static final boolean DUMP_SHAPE = true; 96 97 static final boolean SHOW_DETAILS = false; // disabled 98 static final boolean SHOW_OUTLINE = true; 99 static final boolean SHOW_POINTS = true; 100 static final boolean SHOW_INFO = false; 101 102 static final int MAX_SHOW_FRAMES = 10; 103 static final int MAX_SAVE_FRAMES = 100; 104 105 // use fixed seed to reproduce always same polygons between tests 106 static final boolean FIXED_SEED = false; 107 static final double RAND_SCALE = 3.0; 108 static final double RANDW = TESTW * RAND_SCALE; 109 static final double OFFW = (TESTW - RANDW) / 2.0; 110 static final double RANDH = TESTH * RAND_SCALE; 111 static final double OFFH = (TESTH - RANDH) / 2.0; 112 113 static enum ShapeMode { 114 TWO_CUBICS, 115 FOUR_QUADS, 116 FIVE_LINE_POLYS, 117 NINE_LINE_POLYS, 118 FIFTY_LINE_POLYS, 119 MIXED 120 } 121 122 static final long SEED = 1666133789L; 123 // Fixed seed to avoid any difference between runs: 124 static final Random RANDOM = new Random(SEED); 125 126 static final File OUTPUT_DIR = new File("."); 127 128 static final AtomicBoolean isMarlin = new AtomicBoolean(); 129 static final AtomicBoolean isClipRuntime = new AtomicBoolean(); 130 131 static { 132 Locale.setDefault(Locale.US); 133 134 // FIRST: Get Marlin runtime state from its log: 135 136 // initialize j.u.l Looger: 137 final Logger log = Logger.getLogger("sun.java2d.marlin"); 138 log.addHandler(new Handler() { 139 @Override 140 public void publish(LogRecord record) { 141 final String msg = record.getMessage(); 142 if (msg != null) { 143 // last space to avoid matching other settings: 144 if (msg.startsWith("sun.java2d.renderer ")) { 145 isMarlin.set(msg.contains("MarlinRenderingEngine")); 146 } 147 if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) { 148 isClipRuntime.set(msg.contains("true")); 149 } 150 } 151 152 final Throwable th = record.getThrown(); 153 // detect any Throwable: 154 if (th != null) { 155 System.out.println("Test failed:\n" + record.getMessage()); 156 th.printStackTrace(System.out); 157 158 throw new RuntimeException("Test failed: ", th); 159 } 160 } 161 162 @Override 163 public void flush() { 164 } 165 166 @Override 167 public void close() throws SecurityException { 168 } 169 }); 170 171 // enable Marlin logging & internal checks: 172 System.setProperty("sun.java2d.renderer.log", "true"); 173 System.setProperty("sun.java2d.renderer.useLogger", "true"); 174 175 // disable static clipping setting: 176 System.setProperty("sun.java2d.renderer.clip", "false"); 177 System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true"); 178 179 // enable subdivider: 180 System.setProperty("sun.java2d.renderer.clip.subdivider", "true"); 181 182 // disable min length check: always subdivide curves at clip edges 183 System.setProperty("sun.java2d.renderer.clip.subdivider.minLength", "-1"); 184 185 // If any curve, increase curve accuracy: 186 // curve length max error: 187 System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4"); 188 189 // quad max error: 190 System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4"); 191 192 // cubic min/max error: 193 System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3"); 194 System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4"); // or disabled ~ 1e-6 195 } 196 197 /** 198 * Test 199 * @param args 200 */ 201 public static void main(String[] args) { 202 boolean runSlowTests = false; 203 204 for (String arg : args) { 205 if ("-slow".equals(arg)) { 206 System.out.println("slow: enabled."); 207 runSlowTests = true; 208 } else if ("-doScale".equals(arg)) { 209 System.out.println("doScale: enabled."); 210 TX_SCALE = true; 211 } else if ("-doShear".equals(arg)) { 212 System.out.println("doShear: enabled."); 213 TX_SHEAR = true; 214 } else if ("-doDash".equals(arg)) { 215 System.out.println("doDash: enabled."); 216 USE_DASHES = true; 217 } else if ("-doVarStroke".equals(arg)) { 218 System.out.println("doVarStroke: enabled."); 219 USE_VAR_STROKE = true; 220 } 221 // shape mode: 222 else if (arg.equalsIgnoreCase("-poly")) { 223 SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; 224 } else if (arg.equalsIgnoreCase("-bigpoly")) { 225 SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS; 226 } else if (arg.equalsIgnoreCase("-quad")) { 227 SHAPE_MODE = ShapeMode.FOUR_QUADS; 228 } else if (arg.equalsIgnoreCase("-cubic")) { 229 SHAPE_MODE = ShapeMode.TWO_CUBICS; 230 } else if (arg.equalsIgnoreCase("-mixed")) { 231 SHAPE_MODE = ShapeMode.MIXED; 232 } 233 } 234 235 System.out.println("Shape mode: " + SHAPE_MODE); 236 237 // adjust image comparison thresholds: 238 switch(SHAPE_MODE) { 239 case TWO_CUBICS: 240 // Define uncertainty for curves: 241 THRESHOLD_DELTA = 32; // / 256 242 THRESHOLD_NBPIX = 128; // / 10000 243 break; 244 case FOUR_QUADS: 245 case MIXED: 246 // Define uncertainty for quads: 247 // curve subdivision causes curves to be smaller 248 // then curve offsets are different (more accurate) 249 THRESHOLD_DELTA = 64; // 64 / 256 250 THRESHOLD_NBPIX = 256; // 256 / 10000 251 break; 252 default: 253 // Define uncertainty for lines: 254 // float variant have higher uncertainty 255 THRESHOLD_DELTA = 8; 256 THRESHOLD_NBPIX = 8; 257 } 258 259 System.out.println("THRESHOLD_DELTA: "+THRESHOLD_DELTA); 260 System.out.println("THRESHOLD_NBPIX: "+THRESHOLD_NBPIX); 261 262 if (runSlowTests) { 263 NUM_TESTS = 10000; // or 100000 (very slow) 264 USE_DASHES = true; 265 USE_VAR_STROKE = true; 266 } 267 268 System.out.println("ClipShapeTests: image = " + TESTW + " x " + TESTH); 269 270 int failures = 0; 271 final long start = System.nanoTime(); 272 try { 273 // TODO: test affine transforms ? 274 275 if (TEST_STROKER) { 276 final float[][] dashArrays = (USE_DASHES) ? 277 // small 278 // new float[][]{new float[]{1f, 2f}} 279 // normal 280 new float[][]{new float[]{13f, 7f}} 281 // large (prime) 282 // new float[][]{new float[]{41f, 7f}} 283 // none 284 : new float[][]{null}; 285 286 System.out.println("dashes: " + Arrays.deepToString(dashArrays)); 287 288 final float[] strokeWidths = (USE_VAR_STROKE) 289 ? new float[5] : 290 new float[]{10f}; 291 292 int nsw = 0; 293 if (USE_VAR_STROKE) { 294 for (float width = 0.1f; width < 110f; width *= 5f) { 295 strokeWidths[nsw++] = width; 296 } 297 } else { 298 nsw = 1; 299 } 300 301 System.out.println("stroke widths: " + Arrays.toString(strokeWidths)); 302 303 // Stroker tests: 304 for (int w = 0; w < nsw; w++) { 305 final float width = strokeWidths[w]; 306 307 for (float[] dashes : dashArrays) { 308 309 for (int cap = 0; cap <= 2; cap++) { 310 311 for (int join = 0; join <= 2; join++) { 312 313 failures += paintPaths(new TestSetup(SHAPE_MODE, false, width, cap, join, dashes)); 314 failures += paintPaths(new TestSetup(SHAPE_MODE, true, width, cap, join, dashes)); 315 } 316 } 317 } 318 } 319 } 320 321 if (TEST_FILLER) { 322 // Filler tests: 323 failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO)); 324 failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO)); 325 326 failures += paintPaths(new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD)); 327 failures += paintPaths(new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD)); 328 } 329 } catch (IOException ioe) { 330 throw new RuntimeException(ioe); 331 } 332 System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); 333 334 if (!isMarlin.get()) { 335 throw new RuntimeException("Marlin renderer not used at runtime !"); 336 } 337 if (!isClipRuntime.get()) { 338 throw new RuntimeException("Marlin clipping not enabled at runtime !"); 339 } 340 if (failures != 0) { 341 throw new RuntimeException("Clip test failures : " + failures); 342 } 343 } 344 345 static int paintPaths(final TestSetup ts) throws IOException { 346 final long start = System.nanoTime(); 347 348 if (FIXED_SEED) { 349 // Reset seed for random numbers: 350 RANDOM.setSeed(SEED); 351 } 352 353 System.out.println("paintPaths: " + NUM_TESTS 354 + " paths (" + SHAPE_MODE + ") - setup: " + ts); 355 356 final boolean fill = !ts.isStroke(); 357 final Path2D p2d = new Path2D.Double(ts.windingRule); 358 359 final BufferedImage imgOn = newImage(TESTW, TESTH); 360 final Graphics2D g2dOn = initialize(imgOn, ts); 361 362 final BufferedImage imgOff = newImage(TESTW, TESTH); 363 final Graphics2D g2dOff = initialize(imgOff, ts); 364 365 final BufferedImage imgDiff = newImage(TESTW, TESTH); 366 367 final DiffContext globalCtx = new DiffContext("All tests"); 368 369 int nd = 0; 370 try { 371 final DiffContext testCtx = new DiffContext("Test"); 372 BufferedImage diffImage; 373 374 for (int n = 0; n < NUM_TESTS; n++) { 375 genShape(p2d, ts); 376 377 // Runtime clip setting OFF: 378 paintShape(p2d, g2dOff, fill, false); 379 380 // Runtime clip setting ON: 381 paintShape(p2d, g2dOn, fill, true); 382 383 /* compute image difference if possible */ 384 diffImage = computeDiffImage(testCtx, imgOn, imgOff, imgDiff, globalCtx); 385 386 final String testName = "Setup_" + ts.id + "_test_" + n; 387 388 if (diffImage != null) { 389 nd++; 390 391 final double ratio = (100.0 * testCtx.histPix.count) / testCtx.histAll.count; 392 System.out.println("Diff ratio: " + testName + " = " + trimTo3Digits(ratio) + " %"); 393 394 if (nd < MAX_SHOW_FRAMES) { 395 if (SHOW_DETAILS) { 396 paintShapeDetails(g2dOff, p2d); 397 paintShapeDetails(g2dOn, p2d); 398 } 399 400 if (nd < MAX_SAVE_FRAMES) { 401 if (DUMP_SHAPE) { 402 dumpShape(p2d); 403 } 404 saveImage(imgOff, OUTPUT_DIR, testName + "-off.png"); 405 saveImage(imgOn, OUTPUT_DIR, testName + "-on.png"); 406 saveImage(diffImage, OUTPUT_DIR, testName + "-diff.png"); 407 } 408 } 409 } 410 } 411 } finally { 412 g2dOff.dispose(); 413 g2dOn.dispose(); 414 415 if (nd != 0) { 416 System.out.println("paintPaths: " + NUM_TESTS + " paths - " 417 + "Number of differences = " + nd 418 + " ratio = " + (100f * nd) / NUM_TESTS + " %"); 419 } 420 421 globalCtx.dump(); 422 } 423 System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); 424 return nd; 425 } 426 427 private static void paintShape(final Path2D p2d, final Graphics2D g2d, 428 final boolean fill, final boolean clip) { 429 reset(g2d); 430 431 setClip(g2d, clip); 432 433 if (fill) { 434 g2d.fill(p2d); 435 } else { 436 g2d.draw(p2d); 437 } 438 } 439 440 private static Graphics2D initialize(final BufferedImage img, 441 final TestSetup ts) { 442 final Graphics2D g2d = (Graphics2D) img.getGraphics(); 443 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 444 RenderingHints.VALUE_RENDER_QUALITY); 445 g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 446 RenderingHints.VALUE_STROKE_PURE); 447 448 if (ts.isStroke()) { 449 g2d.setStroke(createStroke(ts)); 450 } 451 g2d.setColor(Color.GRAY); 452 453 // Test scale 454 if (TX_SCALE) { 455 g2d.scale(1.2, 1.2); 456 } 457 // Test shear 458 if (TX_SHEAR) { 459 g2d.shear(0.1, 0.2); 460 } 461 462 return g2d; 463 } 464 465 private static void reset(final Graphics2D g2d) { 466 // Disable antialiasing: 467 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 468 RenderingHints.VALUE_ANTIALIAS_OFF); 469 g2d.setBackground(Color.WHITE); 470 g2d.clearRect(0, 0, TESTW, TESTH); 471 } 472 473 private static void setClip(final Graphics2D g2d, final boolean clip) { 474 // Enable antialiasing: 475 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 476 RenderingHints.VALUE_ANTIALIAS_ON); 477 478 // Enable or Disable clipping: 479 System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false"); 480 } 481 482 static void genShape(final Path2D p2d, final TestSetup ts) { 483 p2d.reset(); 484 485 final int end = (SHAPE_REPEAT) ? 2 : 1; 486 487 for (int p = 0; p < end; p++) { 488 p2d.moveTo(randX(), randY()); 489 490 switch (ts.shapeMode) { 491 case MIXED: 492 case FIFTY_LINE_POLYS: 493 case NINE_LINE_POLYS: 494 case FIVE_LINE_POLYS: 495 p2d.lineTo(randX(), randY()); 496 p2d.lineTo(randX(), randY()); 497 p2d.lineTo(randX(), randY()); 498 p2d.lineTo(randX(), randY()); 499 if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) { 500 // And an implicit close makes 5 lines 501 break; 502 } 503 p2d.lineTo(randX(), randY()); 504 p2d.lineTo(randX(), randY()); 505 p2d.lineTo(randX(), randY()); 506 p2d.lineTo(randX(), randY()); 507 if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) { 508 // And an implicit close makes 9 lines 509 break; 510 } 511 if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) { 512 for (int i = 0; i < 41; i++) { 513 p2d.lineTo(randX(), randY()); 514 } 515 // And an implicit close makes 50 lines 516 break; 517 } 518 case TWO_CUBICS: 519 p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY()); 520 p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY()); 521 if (ts.shapeMode == ShapeMode.TWO_CUBICS) { 522 break; 523 } 524 case FOUR_QUADS: 525 p2d.quadTo(randX(), randY(), randX(), randY()); 526 p2d.quadTo(randX(), randY(), randX(), randY()); 527 p2d.quadTo(randX(), randY(), randX(), randY()); 528 p2d.quadTo(randX(), randY(), randX(), randY()); 529 if (ts.shapeMode == ShapeMode.FOUR_QUADS) { 530 break; 531 } 532 default: 533 } 534 535 if (ts.closed) { 536 p2d.closePath(); 537 } 538 } 539 } 540 541 static final float POINT_RADIUS = 2f; 542 static final float LINE_WIDTH = 1f; 543 544 static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH); 545 static final int COLOR_ALPHA = 128; 546 static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA); 547 static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA); 548 static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA); 549 550 static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float(); 551 552 private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) { 553 554 final Stroke oldStroke = g2d.getStroke(); 555 final Color oldColor = g2d.getColor(); 556 557 setClip(g2d, false); 558 559 if (SHOW_OUTLINE) { 560 g2d.setStroke(OUTLINE_STROKE); 561 g2d.setColor(COLOR_LINETO_ODD); 562 g2d.draw(shape); 563 } 564 565 final float[] coords = new float[6]; 566 float px, py; 567 568 int nMove = 0; 569 int nLine = 0; 570 int n = 0; 571 572 for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { 573 int type = it.currentSegment(coords); 574 switch (type) { 575 case PathIterator.SEG_MOVETO: 576 if (SHOW_POINTS) { 577 g2d.setColor(COLOR_MOVETO); 578 } 579 break; 580 case PathIterator.SEG_LINETO: 581 case PathIterator.SEG_QUADTO: 582 case PathIterator.SEG_CUBICTO: 583 if (SHOW_POINTS) { 584 g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN); 585 } 586 nLine++; 587 break; 588 case PathIterator.SEG_CLOSE: 589 continue; 590 default: 591 System.out.println("unsupported segment type= " + type); 592 continue; 593 } 594 px = coords[0]; 595 py = coords[1]; 596 597 if (SHOW_INFO) { 598 System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py); 599 } 600 601 if (SHOW_POINTS) { 602 ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS, 603 POINT_RADIUS * 2f, POINT_RADIUS * 2f); 604 g2d.fill(ELL_POINT); 605 } 606 } 607 if (SHOW_INFO) { 608 System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine); 609 System.out.println("--------------------------------------------------"); 610 } 611 612 g2d.setStroke(oldStroke); 613 g2d.setColor(oldColor); 614 } 615 616 private static void dumpShape(final Shape shape) { 617 final float[] coords = new float[6]; 618 619 for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { 620 final int type = it.currentSegment(coords); 621 switch (type) { 622 case PathIterator.SEG_MOVETO: 623 System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");"); 624 break; 625 case PathIterator.SEG_LINETO: 626 System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");"); 627 break; 628 case PathIterator.SEG_QUADTO: 629 System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");"); 630 break; 631 case PathIterator.SEG_CUBICTO: 632 System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");"); 633 break; 634 case PathIterator.SEG_CLOSE: 635 System.out.println("p2d.closePath();"); 636 break; 637 default: 638 System.out.println("// Unsupported segment type= " + type); 639 } 640 } 641 System.out.println("--------------------------------------------------"); 642 } 643 644 static double randX() { 645 return RANDOM.nextDouble() * RANDW + OFFW; 646 } 647 648 static double randY() { 649 return RANDOM.nextDouble() * RANDH + OFFH; 650 } 651 652 private static BasicStroke createStroke(final TestSetup ts) { 653 return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f); 654 } 655 656 private final static class TestSetup { 657 658 static final AtomicInteger COUNT = new AtomicInteger(); 659 660 final int id; 661 final ShapeMode shapeMode; 662 final boolean closed; 663 // stroke 664 final float strokeWidth; 665 final int strokeCap; 666 final int strokeJoin; 667 final float[] dashes; 668 // fill 669 final int windingRule; 670 671 TestSetup(ShapeMode shapeMode, final boolean closed, 672 final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) { 673 this.id = COUNT.incrementAndGet(); 674 this.shapeMode = shapeMode; 675 this.closed = closed; 676 this.strokeWidth = strokeWidth; 677 this.strokeCap = strokeCap; 678 this.strokeJoin = strokeJoin; 679 this.dashes = dashes; 680 this.windingRule = Path2D.WIND_NON_ZERO; 681 } 682 683 TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) { 684 this.id = COUNT.incrementAndGet(); 685 this.shapeMode = shapeMode; 686 this.closed = closed; 687 this.strokeWidth = 0f; 688 this.strokeCap = this.strokeJoin = -1; // invalid 689 this.dashes = null; 690 this.windingRule = windingRule; 691 } 692 693 boolean isStroke() { 694 return this.strokeWidth > 0f; 695 } 696 697 @Override 698 public String toString() { 699 if (isStroke()) { 700 return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed 701 + ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin) 702 + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "") 703 + '}'; 704 } 705 return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed 706 + ", fill" 707 + ", windingRule=" + getWindingRule(windingRule) + '}'; 708 } 709 710 private static String getCap(final int cap) { 711 switch (cap) { 712 case BasicStroke.CAP_BUTT: 713 return "CAP_BUTT"; 714 case BasicStroke.CAP_ROUND: 715 return "CAP_ROUND"; 716 case BasicStroke.CAP_SQUARE: 717 return "CAP_SQUARE"; 718 default: 719 return ""; 720 } 721 722 } 723 724 private static String getJoin(final int join) { 725 switch (join) { 726 case BasicStroke.JOIN_MITER: 727 return "JOIN_MITER"; 728 case BasicStroke.JOIN_ROUND: 729 return "JOIN_ROUND"; 730 case BasicStroke.JOIN_BEVEL: 731 return "JOIN_BEVEL"; 732 default: 733 return ""; 734 } 735 736 } 737 738 private static String getWindingRule(final int rule) { 739 switch (rule) { 740 case PathIterator.WIND_EVEN_ODD: 741 return "WIND_EVEN_ODD"; 742 case PathIterator.WIND_NON_ZERO: 743 return "WIND_NON_ZERO"; 744 default: 745 return ""; 746 } 747 } 748 } 749 750 // --- utilities --- 751 private static final int DCM_ALPHA_MASK = 0xff000000; 752 753 public static BufferedImage newImage(final int w, final int h) { 754 return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); 755 } 756 757 public static BufferedImage computeDiffImage(final DiffContext localCtx, 758 final BufferedImage tstImage, 759 final BufferedImage refImage, 760 final BufferedImage diffImage, 761 final DiffContext globalCtx) { 762 763 final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData(); 764 final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData(); 765 final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData(); 766 767 // reset local diff context: 768 localCtx.reset(); 769 770 int ref, tst, dg, v; 771 for (int i = 0, len = aRefPix.length; i < len; i++) { 772 ref = aRefPix[i]; 773 tst = aTstPix[i]; 774 775 // grayscale diff: 776 dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst)); 777 778 // max difference on grayscale values: 779 v = (int) Math.ceil(Math.abs(dg / 3.0)); 780 781 // TODO: count warnings 782 if (v <= THRESHOLD_DELTA) { 783 aDifPix[i] = 0; 784 } else { 785 aDifPix[i] = toInt(v, v, v); 786 787 localCtx.add(v); 788 } 789 globalCtx.add(v); 790 } 791 792 if (!localCtx.isDiff() || (localCtx.histPix.count <= THRESHOLD_NBPIX)) { 793 return null; 794 } 795 796 localCtx.dump(); 797 798 return diffImage; 799 } 800 801 static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException { 802 final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG"); 803 if (itWriters.hasNext()) { 804 final ImageWriter writer = itWriters.next(); 805 806 final ImageWriteParam writerParams = writer.getDefaultWriteParam(); 807 writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED); 808 809 final File imgFile = new File(resDirectory, imageFileName); 810 811 if (!imgFile.exists() || imgFile.canWrite()) { 812 System.out.println("saveImage: saving image as PNG [" + imgFile + "]..."); 813 imgFile.delete(); 814 815 // disable cache in temporary files: 816 ImageIO.setUseCache(false); 817 818 final long start = System.nanoTime(); 819 820 // PNG uses already buffering: 821 final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile)); 822 823 writer.setOutput(imgOutStream); 824 try { 825 writer.write(null, new IIOImage(image, null, null), writerParams); 826 } finally { 827 imgOutStream.close(); 828 829 final long time = System.nanoTime() - start; 830 System.out.println("saveImage: duration= " + (time / 1000000l) + " ms."); 831 } 832 } 833 } 834 } 835 836 static int r(final int v) { 837 return (v >> 16 & 0xff); 838 } 839 840 static int g(final int v) { 841 return (v >> 8 & 0xff); 842 } 843 844 static int b(final int v) { 845 return (v & 0xff); 846 } 847 848 static int clamp127(final int v) { 849 return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255; 850 } 851 852 static int toInt(final int r, final int g, final int b) { 853 return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b; 854 } 855 856 /* stats */ 857 static class StatInteger { 858 859 public final String name; 860 public long count = 0l; 861 public long sum = 0l; 862 public long min = Integer.MAX_VALUE; 863 public long max = Integer.MIN_VALUE; 864 865 StatInteger(String name) { 866 this.name = name; 867 } 868 869 void reset() { 870 count = 0l; 871 sum = 0l; 872 min = Integer.MAX_VALUE; 873 max = Integer.MIN_VALUE; 874 } 875 876 void add(int val) { 877 count++; 878 sum += val; 879 if (val < min) { 880 min = val; 881 } 882 if (val > max) { 883 max = val; 884 } 885 } 886 887 void add(long val) { 888 count++; 889 sum += val; 890 if (val < min) { 891 min = val; 892 } 893 if (val > max) { 894 max = val; 895 } 896 } 897 898 public final double average() { 899 return ((double) sum) / count; 900 } 901 902 @Override 903 public String toString() { 904 final StringBuilder sb = new StringBuilder(128); 905 toString(sb); 906 return sb.toString(); 907 } 908 909 public final StringBuilder toString(final StringBuilder sb) { 910 sb.append(name).append("[n: ").append(count); 911 sb.append("] sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average())); 912 sb.append(" [").append(min).append(" | ").append(max).append("]"); 913 return sb; 914 } 915 916 } 917 918 final static class Histogram extends StatInteger { 919 920 static final int BUCKET = 2; 921 static final int MAX = 20; 922 static final int LAST = MAX - 1; 923 static final int[] STEPS = new int[MAX]; 924 925 static { 926 STEPS[0] = 0; 927 STEPS[1] = 1; 928 929 for (int i = 2; i < MAX; i++) { 930 STEPS[i] = STEPS[i - 1] * BUCKET; 931 } 932 // System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS)); 933 } 934 935 static int bucket(int val) { 936 for (int i = 1; i < MAX; i++) { 937 if (val < STEPS[i]) { 938 return i - 1; 939 } 940 } 941 return LAST; 942 } 943 944 private final StatInteger[] stats = new StatInteger[MAX]; 945 946 public Histogram(String name) { 947 super(name); 948 for (int i = 0; i < MAX; i++) { 949 stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~"))); 950 } 951 } 952 953 @Override 954 final void reset() { 955 super.reset(); 956 for (int i = 0; i < MAX; i++) { 957 stats[i].reset(); 958 } 959 } 960 961 @Override 962 final void add(int val) { 963 super.add(val); 964 stats[bucket(val)].add(val); 965 } 966 967 @Override 968 final void add(long val) { 969 add((int) val); 970 } 971 972 @Override 973 public final String toString() { 974 final StringBuilder sb = new StringBuilder(2048); 975 super.toString(sb).append(" { "); 976 977 for (int i = 0; i < MAX; i++) { 978 if (stats[i].count != 0l) { 979 sb.append("\n ").append(stats[i].toString()); 980 } 981 } 982 983 return sb.append(" }").toString(); 984 } 985 } 986 987 /** 988 * Adjust the given double value to keep only 3 decimal digits 989 * @param value value to adjust 990 * @return double value with only 3 decimal digits 991 */ 992 static double trimTo3Digits(final double value) { 993 return ((long) (1e3d * value)) / 1e3d; 994 } 995 996 static final class DiffContext { 997 998 public final Histogram histAll; 999 public final Histogram histPix; 1000 1001 DiffContext(String name) { 1002 histAll = new Histogram("All Pixels [" + name + "]"); 1003 histPix = new Histogram("Diff Pixels [" + name + "]"); 1004 } 1005 1006 void reset() { 1007 histAll.reset(); 1008 histPix.reset(); 1009 } 1010 1011 void dump() { 1012 if (isDiff()) { 1013 System.out.println("Differences [" + histAll.name + "]:"); 1014 System.out.println("Total [all pixels]:\n" + histAll.toString()); 1015 System.out.println("Total [different pixels]:\n" + histPix.toString()); 1016 } else { 1017 System.out.println("No difference for [" + histAll.name + "]."); 1018 } 1019 } 1020 1021 void add(int val) { 1022 histAll.add(val); 1023 if (val != 0) { 1024 histPix.add(val); 1025 } 1026 } 1027 1028 boolean isDiff() { 1029 return histAll.sum != 0l; 1030 } 1031 } 1032 }