1 /* 2 * Copyright (c) 2018, 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package test.com.sun.marlin; 26 27 import java.awt.BasicStroke; 28 import java.awt.Shape; 29 import java.awt.geom.Path2D; 30 import java.awt.geom.PathIterator; 31 import java.awt.image.BufferedImage; 32 import java.io.File; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 import java.util.Arrays; 36 import java.util.Iterator; 37 import java.util.Locale; 38 import java.util.Random; 39 import java.util.concurrent.CountDownLatch; 40 import java.util.concurrent.TimeUnit; 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 47 import javafx.application.Application; 48 import javafx.application.Platform; 49 import javafx.collections.ObservableList; 50 import javafx.embed.swing.SwingFXUtils; 51 import javafx.geometry.Rectangle2D; 52 import javafx.scene.Scene; 53 import javafx.scene.SnapshotParameters; 54 import javafx.scene.image.PixelReader; 55 import javafx.scene.image.PixelWriter; 56 import javafx.scene.image.WritableImage; 57 import javafx.scene.layout.BorderPane; 58 import javafx.scene.paint.Color; 59 import javafx.scene.shape.ClosePath; 60 import javafx.scene.shape.CubicCurveTo; 61 import javafx.scene.shape.FillRule; 62 import javafx.scene.shape.LineTo; 63 import javafx.scene.shape.MoveTo; 64 import javafx.scene.shape.Path; 65 import javafx.scene.shape.PathElement; 66 import javafx.scene.shape.QuadCurveTo; 67 import javafx.scene.shape.StrokeLineCap; 68 import javafx.scene.shape.StrokeLineJoin; 69 import javafx.scene.text.Text; 70 import javafx.stage.Stage; 71 import javax.imageio.IIOImage; 72 import javax.imageio.ImageIO; 73 import javax.imageio.ImageWriteParam; 74 import javax.imageio.ImageWriter; 75 import javax.imageio.stream.ImageOutputStream; 76 77 import junit.framework.AssertionFailedError; 78 import org.junit.AfterClass; 79 import org.junit.Assert; 80 import static org.junit.Assert.assertEquals; 81 import org.junit.BeforeClass; 82 import org.junit.Test; 83 84 import static test.util.Util.TIMEOUT; 85 86 /** 87 * @test 88 * @bug 89 * @summary Verifies that Marlin rendering generates the same 90 * images with and without clipping optimization with all possible 91 * stroke (cap/join) and/or dashes or fill modes (EO rules) 92 * for paths made of either 9 lines, 4 quads, 2 cubics (random) 93 */ 94 public final class ClipShapeTest { 95 96 // test options: 97 static int NUM_TESTS; 98 99 // shape settings: 100 static ShapeMode SHAPE_MODE; 101 102 static boolean USE_DASHES; 103 static boolean USE_VAR_STROKE; 104 105 static int THRESHOLD_DELTA; 106 static long THRESHOLD_NBPIX; 107 108 // constants: 109 static final boolean DO_FAIL = true; 110 111 static final boolean TEST_STROKER = true; 112 static final boolean TEST_FILLER = true; 113 114 static final int TESTW = 100; 115 static final int TESTH = 100; 116 117 static final boolean SHAPE_REPEAT = true; 118 119 // dump path on console: 120 static final boolean DUMP_SHAPE = true; 121 122 static final int MAX_SHOW_FRAMES = 10; 123 static final int MAX_SAVE_FRAMES = 100; 124 125 // use fixed seed to reproduce always same polygons between tests 126 static final boolean FIXED_SEED = true; 127 128 static final double RAND_SCALE = 3.0; 129 static final double RANDW = TESTW * RAND_SCALE; 130 static final double OFFW = (TESTW - RANDW) / 2.0; 131 static final double RANDH = TESTH * RAND_SCALE; 132 static final double OFFH = (TESTH - RANDH) / 2.0; 133 134 static enum ShapeMode { 135 TWO_CUBICS, 136 FOUR_QUADS, 137 FIVE_LINE_POLYS, 138 NINE_LINE_POLYS, 139 FIFTY_LINE_POLYS, 140 MIXED 141 } 142 143 static final long SEED = 1666133789L; 144 // Fixed seed to avoid any difference between runs: 145 static final Random RANDOM = new Random(SEED); 146 147 static final File OUTPUT_DIR = new File("."); 148 149 static final AtomicBoolean isMarlin = new AtomicBoolean(); 150 static final AtomicBoolean isClipRuntime = new AtomicBoolean(); 151 152 // Used to launch the application before running any test 153 private static final CountDownLatch launchLatch = new CountDownLatch(1); 154 155 // Singleton Application instance 156 static MyApp myApp; 157 158 static boolean doChecksFailed = false; 159 160 private static final Logger log; 161 162 static { 163 Locale.setDefault(Locale.US); 164 165 // FIRST: Get Marlin runtime state from its log: 166 167 // initialize j.u.l Looger: 168 log = Logger.getLogger("prism.marlin"); 169 log.addHandler(new Handler() { 170 @Override 171 public void publish(LogRecord record) { 172 final String msg = record.getMessage(); 173 if (msg != null) { 174 // last space to avoid matching other settings: 175 if (msg.startsWith("prism.marlin ")) { 176 isMarlin.set(msg.contains("DRenderer")); 177 } 178 if (msg.startsWith("prism.marlin.clip.runtime.enable")) { 179 isClipRuntime.set(msg.contains("true")); 180 } 181 } 182 183 final Throwable th = record.getThrown(); 184 // detect any Throwable: 185 if (th != null) { 186 System.out.println("Test failed:\n" + record.getMessage()); 187 th.printStackTrace(System.out); 188 189 doChecksFailed = true; 190 191 throw new RuntimeException("Test failed: ", th); 192 } 193 } 194 195 @Override 196 public void flush() { 197 } 198 199 @Override 200 public void close() throws SecurityException { 201 } 202 }); 203 204 // enable Marlin logging & internal checks: 205 System.setProperty("prism.marlin.log", "true"); 206 System.setProperty("prism.marlin.useLogger", "true"); 207 208 // disable static clipping setting: 209 System.setProperty("prism.marlin.clip", "false"); 210 System.setProperty("prism.marlin.clip.runtime.enable", "true"); 211 212 // enable subdivider: 213 System.setProperty("prism.marlin.clip.subdivider", "true"); 214 215 // disable min length check: always subdivide curves at clip edges 216 System.setProperty("prism.marlin.clip.subdivider.minLength", "-1"); 217 218 // If any curve, increase curve accuracy: 219 // curve length max error: 220 System.setProperty("prism.marlin.curve_len_err", "1e-4"); 221 222 // cubic min/max error: 223 System.setProperty("prism.marlin.cubic_dec_d2", "1e-3"); 224 System.setProperty("prism.marlin.cubic_inc_d1", "1e-4"); 225 226 // quad max error: 227 System.setProperty("prism.marlin.quad_dec_d2", "5e-4"); 228 } 229 230 // Application class. An instance is created and initialized before running 231 // the first test, and it lives through the execution of all tests. 232 public static class MyApp extends Application { 233 234 Stage stage = null; 235 236 public MyApp() { 237 super(); 238 } 239 240 @Override 241 public void init() { 242 ClipShapeTest.myApp = this; 243 } 244 245 @Override 246 public void start(Stage primaryStage) throws Exception { 247 this.stage = primaryStage; 248 249 BorderPane root = new BorderPane(); 250 251 root.setBottom(new Text("running...")); 252 253 Scene scene = new Scene(root); 254 stage.setScene(scene); 255 stage.setTitle("Testing"); 256 stage.show(); 257 258 launchLatch.countDown(); 259 } 260 } 261 262 boolean done; 263 264 public synchronized void signalDone() { 265 done = true; 266 notifyAll(); 267 } 268 269 public synchronized void waitDone() throws InterruptedException { 270 while (!done) { 271 wait(); 272 } 273 } 274 275 private static void resetOptions() { 276 NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 100); 277 278 // shape settings: 279 SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; 280 281 USE_DASHES = false; 282 USE_VAR_STROKE = false; 283 } 284 285 /** 286 * Test 287 * @param args 288 */ 289 public static void initArgs(String[] args) { 290 System.out.println("---------------------------------------"); 291 System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH); 292 293 resetOptions(); 294 295 boolean runSlowTests = false; 296 297 for (String arg : args) { 298 if ("-slow".equals(arg)) { 299 runSlowTests = true; 300 } else if ("-doDash".equals(arg)) { 301 USE_DASHES = true; 302 } else if ("-doVarStroke".equals(arg)) { 303 USE_VAR_STROKE = true; 304 } else { 305 // shape mode: 306 if (arg.equalsIgnoreCase("-poly")) { 307 SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; 308 } else if (arg.equalsIgnoreCase("-bigpoly")) { 309 SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS; 310 } else if (arg.equalsIgnoreCase("-quad")) { 311 SHAPE_MODE = ShapeMode.FOUR_QUADS; 312 } else if (arg.equalsIgnoreCase("-cubic")) { 313 SHAPE_MODE = ShapeMode.TWO_CUBICS; 314 } else if (arg.equalsIgnoreCase("-mixed")) { 315 SHAPE_MODE = ShapeMode.MIXED; 316 } 317 } 318 } 319 320 System.out.println("Shape mode: " + SHAPE_MODE); 321 322 // adjust image comparison thresholds: 323 switch (SHAPE_MODE) { 324 case TWO_CUBICS: 325 // Define uncertainty for curves: 326 /* 327 Diff Pixels [Worst(All Test setups)][n: 647] sum: 15130 avg: 23.384 [1 | 174] { 328 1 .. 2[n: 93] sum: 93 avg: 1.0 [1 | 1] 329 2 .. 4[n: 92] sum: 223 avg: 2.423 [2 | 3] 330 4 .. 8[n: 135] sum: 732 avg: 5.422 [4 | 7] 331 8 .. 16[n: 109] sum: 1235 avg: 11.33 [8 | 15] 332 16 .. 32[n: 82] sum: 1782 avg: 21.731 [16 | 31] 333 32 .. 64[n: 59] sum: 2584 avg: 43.796 [32 | 62] 334 64 .. 128[n: 52] sum: 4929 avg: 94.788 [64 | 127] 335 128 .. 256[n: 25] sum: 3552 avg: 142.08 [129 | 174] } 336 337 DASH: Diff Pixels [Worst(All Test setups)][n: 128] sum: 5399 avg: 42.179 [1 | 255] { 338 1 .. 2[n: 54] sum: 54 avg: 1.0 [1 | 1] 339 2 .. 4[n: 28] sum: 63 avg: 2.25 [2 | 3] 340 4 .. 8[n: 6] sum: 33 avg: 5.5 [4 | 7] 341 8 .. 16[n: 3] sum: 33 avg: 11.0 [9 | 15] 342 16 .. 32[n: 4] sum: 87 avg: 21.75 [16 | 25] 343 32 .. 64[n: 6] sum: 276 avg: 46.0 [37 | 60] 344 64 .. 128[n: 6] sum: 568 avg: 94.666 [71 | 118] 345 128 .. 256[n: 21] sum: 4285 avg: 204.047 [128 | 255] } 346 */ 347 THRESHOLD_DELTA = 32; 348 THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 150; 349 break; 350 case FOUR_QUADS: 351 case MIXED: 352 // Define uncertainty for quads: 353 // curve subdivision causes curves to be smaller 354 // then curve offsets are different (more accurate) 355 /* 356 Diff Pixels [Worst(All Test setups)][n: 775] sum: 57659 avg: 74.398 [1 | 251] { 357 1 .. 2[n: 21] sum: 21 avg: 1.0 [1 | 1] 358 2 .. 4[n: 20] sum: 52 avg: 2.6 [2 | 3] 359 4 .. 8[n: 44] sum: 236 avg: 5.363 [4 | 7] 360 8 .. 16[n: 52] sum: 578 avg: 11.115 [8 | 15] 361 16 .. 32[n: 75] sum: 1729 avg: 23.053 [16 | 31] 362 32 .. 64[n: 152] sum: 7178 avg: 47.223 [32 | 63] 363 64 .. 128[n: 274] sum: 25741 avg: 93.945 [64 | 127] 364 128 .. 256[n: 137] sum: 22124 avg: 161.489 [128 | 251] } 365 366 DASH: Diff Pixels [Worst(All Test setups)][n: 354] sum: 29638 avg: 83.723 [1 | 254] { 367 1 .. 2[n: 31] sum: 31 avg: 1.0 [1 | 1] 368 2 .. 4[n: 45] sum: 111 avg: 2.466 [2 | 3] 369 4 .. 8[n: 22] sum: 113 avg: 5.136 [4 | 7] 370 8 .. 16[n: 25] sum: 247 avg: 9.88 [8 | 15] 371 16 .. 32[n: 26] sum: 579 avg: 22.269 [16 | 31] 372 32 .. 64[n: 39] sum: 1698 avg: 43.538 [32 | 62] 373 64 .. 128[n: 56] sum: 5284 avg: 94.357 [64 | 127] 374 128 .. 256[n: 110] sum: 21575 avg: 196.136 [128 | 254] } 375 */ 376 THRESHOLD_DELTA = 64; 377 THRESHOLD_NBPIX = (USE_DASHES) ? 180 : 420; 378 break; 379 default: 380 // Define uncertainty for lines: 381 // float variant have higher uncertainty 382 /* 383 DASH: Diff Pixels [Worst(All Test setups)][n: 7] sum: 8 avg: 1.142 [1 | 2] { 384 1 .. 2[n: 6] sum: 6 avg: 1.0 [1 | 1] 385 2 .. 4[n: 1] sum: 2 avg: 2.0 [2 | 2] } 386 */ 387 THRESHOLD_DELTA = 2; 388 THRESHOLD_NBPIX = 4; // very low 389 } 390 391 // TODO: define one more threshold on total result (total sum) ? 392 393 System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA); 394 System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX); 395 396 if (runSlowTests) { 397 NUM_TESTS = 10000; // or 100000 (very slow) 398 USE_DASHES = true; 399 USE_VAR_STROKE = true; 400 } 401 402 System.out.println("NUM_TESTS: " + NUM_TESTS); 403 404 if (USE_DASHES) { 405 System.out.println("USE_DASHES: enabled."); 406 } 407 if (USE_VAR_STROKE) { 408 System.out.println("USE_VAR_STROKE: enabled."); 409 } 410 411 System.out.println("---------------------------------------"); 412 } 413 414 private void runTests() { 415 416 final DiffContext allCtx = new DiffContext("All Test setups"); 417 final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)"); 418 419 int failures = 0; 420 final long start = System.nanoTime(); 421 try { 422 if (TEST_STROKER) { 423 final float[][] dashArrays = (USE_DASHES) ? 424 // small 425 // new float[][]{new float[]{1f, 2f}} 426 // normal 427 new float[][]{new float[]{13f, 7f}} 428 // large (prime) 429 // new float[][]{new float[]{41f, 7f}} 430 // none 431 : new float[][]{null}; 432 433 System.out.println("dashes: " + Arrays.deepToString(dashArrays)); 434 435 final float[] strokeWidths = (USE_VAR_STROKE) 436 ? new float[5] : 437 new float[]{10f}; 438 439 int nsw = 0; 440 if (USE_VAR_STROKE) { 441 for (float width = 0.1f; width < 110f; width *= 5f) { 442 strokeWidths[nsw++] = width; 443 } 444 } else { 445 nsw = 1; 446 } 447 448 System.out.println("stroke widths: " + Arrays.toString(strokeWidths)); 449 450 // Stroker tests: 451 for (int w = 0; w < nsw; w++) { 452 final float width = strokeWidths[w]; 453 454 for (float[] dashes : dashArrays) { 455 456 for (int cap = 0; cap <= 2; cap++) { 457 458 for (int join = 0; join <= 2; join++) { 459 460 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes)); 461 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes)); 462 } 463 } 464 } 465 } 466 } 467 468 if (TEST_FILLER) { 469 // Filler tests: 470 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO)); 471 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO)); 472 473 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD)); 474 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD)); 475 } 476 } catch (IOException ioe) { 477 throw new RuntimeException(ioe); 478 } 479 System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); 480 481 allWorstCtx.dump(); 482 allCtx.dump(); 483 484 if (DO_FAIL && (failures != 0)) { 485 throw new RuntimeException("Clip test failures : " + failures); 486 } 487 } 488 489 int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException { 490 final long start = System.nanoTime(); 491 492 if (FIXED_SEED) { 493 // Reset seed for random numbers: 494 RANDOM.setSeed(SEED); 495 } 496 497 System.out.println("paintPaths: " + NUM_TESTS 498 + " paths (" + SHAPE_MODE + ") - setup: " + ts); 499 500 final Path2D p2d = new Path2D.Double(ts.windingRule); 501 502 final Path p = makePath(ts); 503 504 final WritableImage imgOff = new WritableImage(TESTW, TESTH); 505 final PixelReader prOff = imgOff.getPixelReader(); 506 507 final WritableImage imgOn = new WritableImage(TESTW, TESTH); 508 final PixelReader prOn = imgOn.getPixelReader(); 509 510 final WritableImage imgDiff = new WritableImage(TESTW, TESTH); 511 final PixelWriter prDiff = imgDiff.getPixelWriter(); 512 513 final DiffContext testSetupCtx = new DiffContext("Test setup"); 514 final DiffContext testWorstCtx = new DiffContext("Worst"); 515 final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)"); 516 517 int nd = 0; 518 try { 519 final DiffContext testCtx = new DiffContext("Test"); 520 final DiffContext testThCtx = new DiffContext("Test(>threshold)"); 521 PixelWriter diffImage; 522 523 for (int n = 0; n < NUM_TESTS; n++) { 524 genShape(p2d, ts); 525 526 // Use directly Platform.runLater() instead of Util.runAndWait() 527 // that has too high latency (100ms polling) 528 done = false; 529 Platform.runLater(() -> { 530 /* 531 Note: as CachingShapeRep try caching the Path mask at the second rendering pass, 532 then its xformBounds corresponds to the shape not the giiiiven clip. 533 To avoid such side-effect, perform clipping first below (or call setPath again) 534 */ 535 setPath(p, p2d); 536 537 // Runtime clip setting ON (FIRST): 538 paintShape(p, imgOn, true); 539 540 // Runtime clip setting OFF (2ND): 541 paintShape(p, imgOff, false); 542 543 signalDone(); 544 }); 545 try { 546 waitDone(); 547 } catch (InterruptedException ex) { 548 break; 549 } 550 551 /* compute image difference if possible */ 552 diffImage = computeDiffImage(testCtx, testThCtx, prOn, prOff, prDiff); 553 554 // Worst (total) 555 if (testCtx.isDiff()) { 556 if (testWorstCtx.isWorse(testCtx, false)) { 557 testWorstCtx.set(testCtx); 558 } 559 if (testWorstThCtx.isWorse(testCtx, true)) { 560 testWorstThCtx.set(testCtx); 561 } 562 // accumulate data: 563 testSetupCtx.add(testCtx); 564 } 565 if (diffImage != null) { 566 nd++; 567 568 testThCtx.dump(); 569 testCtx.dump(); 570 571 if (nd < MAX_SHOW_FRAMES) { 572 if (nd < MAX_SAVE_FRAMES) { 573 if (DUMP_SHAPE) { 574 dumpShape(p2d); 575 } 576 577 final String testName = "Setup_" + ts.id + "_test_" + n; 578 579 saveImage(imgOff, OUTPUT_DIR, testName + "-off.png"); 580 saveImage(imgOn, OUTPUT_DIR, testName + "-on.png"); 581 saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png"); 582 } 583 } 584 } 585 } 586 } finally { 587 if (nd != 0) { 588 System.out.println("paintPaths: " + NUM_TESTS + " paths - " 589 + "Number of differences = " + nd 590 + " ratio = " + (100f * nd) / NUM_TESTS + " %"); 591 } 592 593 if (testWorstCtx.isDiff()) { 594 testWorstCtx.dump(); 595 if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) { 596 testWorstThCtx.dump(); 597 } 598 if (allWorstCtx.isWorse(testWorstThCtx, true)) { 599 allWorstCtx.set(testWorstThCtx); 600 } 601 } 602 testSetupCtx.dump(); 603 604 // accumulate data: 605 allCtx.add(testSetupCtx); 606 } 607 System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); 608 return nd; 609 } 610 611 public Path makePath(final TestSetup ts) { 612 final Path p = new Path(); 613 p.setCache(false); 614 p.setSmooth(true); 615 616 if (ts.isStroke()) { 617 p.setFill(null); 618 p.setStroke(Color.BLACK); 619 620 switch (ts.strokeCap) { 621 case BasicStroke.CAP_BUTT: 622 p.setStrokeLineCap(StrokeLineCap.BUTT); 623 break; 624 case BasicStroke.CAP_ROUND: 625 p.setStrokeLineCap(StrokeLineCap.ROUND); 626 break; 627 case BasicStroke.CAP_SQUARE: 628 p.setStrokeLineCap(StrokeLineCap.SQUARE); 629 break; 630 default: 631 } 632 633 p.setStrokeLineJoin(StrokeLineJoin.MITER); 634 switch (ts.strokeJoin) { 635 case BasicStroke.JOIN_MITER: 636 p.setStrokeLineJoin(StrokeLineJoin.MITER); 637 break; 638 case BasicStroke.JOIN_ROUND: 639 p.setStrokeLineJoin(StrokeLineJoin.ROUND); 640 break; 641 case BasicStroke.JOIN_BEVEL: 642 p.setStrokeLineJoin(StrokeLineJoin.BEVEL); 643 break; 644 default: 645 } 646 647 if (ts.dashes != null) { 648 ObservableList<Double> pDashes = p.getStrokeDashArray(); 649 pDashes.clear(); 650 for (float f : ts.dashes) { 651 pDashes.add(Double.valueOf(f)); 652 } 653 } 654 655 p.setStrokeMiterLimit(10.0); 656 p.setStrokeWidth(ts.strokeWidth); 657 658 } else { 659 p.setFill(Color.BLACK); 660 p.setStroke(null); 661 662 switch (ts.windingRule) { 663 case Path2D.WIND_EVEN_ODD: 664 p.setFillRule(FillRule.EVEN_ODD); 665 break; 666 case Path2D.WIND_NON_ZERO: 667 p.setFillRule(FillRule.NON_ZERO); 668 break; 669 } 670 } 671 return p; 672 } 673 674 public static void setPath(Path p, Path2D p2d) { 675 final ObservableList<PathElement> elements = p.getElements(); 676 elements.clear(); 677 678 final double[] coords = new double[6]; 679 for (PathIterator pi = p2d.getPathIterator(null); !pi.isDone(); pi.next()) { 680 switch (pi.currentSegment(coords)) { 681 case PathIterator.SEG_MOVETO: 682 elements.add(new MoveTo(coords[0], coords[1])); 683 break; 684 case PathIterator.SEG_LINETO: 685 elements.add(new LineTo(coords[0], coords[1])); 686 break; 687 case PathIterator.SEG_QUADTO: 688 elements.add(new QuadCurveTo(coords[0], coords[1], 689 coords[2], coords[3])); 690 break; 691 case PathIterator.SEG_CUBICTO: 692 elements.add(new CubicCurveTo(coords[0], coords[1], 693 coords[2], coords[3], 694 coords[4], coords[5])); 695 break; 696 case PathIterator.SEG_CLOSE: 697 elements.add(new ClosePath()); 698 break; 699 default: 700 throw new InternalError("unexpected segment type"); 701 } 702 } 703 } 704 705 private static void paintShape(Path p, WritableImage wimg, final boolean clip) { 706 // Enable or Disable clipping: 707 System.setProperty("prism.marlin.clip.runtime", (clip) ? "true" : "false"); 708 709 final SnapshotParameters sp = new SnapshotParameters(); 710 sp.setViewport(new Rectangle2D(0, 0, TESTW, TESTH)); 711 712 WritableImage out = p.snapshot(sp, wimg); 713 714 if (out != wimg) { 715 System.out.println("different images !"); 716 } 717 // Or use (faster?) 718 // ShapeUtil.getMaskData(p, null, b, BaseTransform.IDENTITY_TRANSFORM, true, aa); 719 } 720 721 static void genShape(final Path2D p2d, final TestSetup ts) { 722 p2d.reset(); 723 724 final int end = (SHAPE_REPEAT) ? 2 : 1; 725 726 for (int p = 0; p < end; p++) { 727 p2d.moveTo(randX(), randY()); 728 729 switch (ts.shapeMode) { 730 case MIXED: 731 case FIFTY_LINE_POLYS: 732 case NINE_LINE_POLYS: 733 case FIVE_LINE_POLYS: 734 p2d.lineTo(randX(), randY()); 735 p2d.lineTo(randX(), randY()); 736 p2d.lineTo(randX(), randY()); 737 p2d.lineTo(randX(), randY()); 738 if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) { 739 // And an implicit close makes 5 lines 740 break; 741 } 742 p2d.lineTo(randX(), randY()); 743 p2d.lineTo(randX(), randY()); 744 p2d.lineTo(randX(), randY()); 745 p2d.lineTo(randX(), randY()); 746 if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) { 747 // And an implicit close makes 9 lines 748 break; 749 } 750 if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) { 751 for (int i = 0; i < 41; i++) { 752 p2d.lineTo(randX(), randY()); 753 } 754 // And an implicit close makes 50 lines 755 break; 756 } 757 case TWO_CUBICS: 758 p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY()); 759 p2d.curveTo(randX(), randY(), randX(), randY(), randX(), randY()); 760 if (ts.shapeMode == ShapeMode.TWO_CUBICS) { 761 break; 762 } 763 case FOUR_QUADS: 764 p2d.quadTo(randX(), randY(), randX(), randY()); 765 p2d.quadTo(randX(), randY(), randX(), randY()); 766 p2d.quadTo(randX(), randY(), randX(), randY()); 767 p2d.quadTo(randX(), randY(), randX(), randY()); 768 if (ts.shapeMode == ShapeMode.FOUR_QUADS) { 769 break; 770 } 771 default: 772 } 773 774 if (ts.closed) { 775 p2d.closePath(); 776 } 777 } 778 } 779 780 @BeforeClass 781 public static void setupOnce() { 782 // Start the Application 783 new Thread(() -> Application.launch(MyApp.class, (String[]) null)).start(); 784 785 try { 786 if (!launchLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) { 787 throw new AssertionFailedError("Timeout waiting for Application to launch"); 788 } 789 790 } catch (InterruptedException ex) { 791 AssertionFailedError err = new AssertionFailedError("Unexpected exception"); 792 err.initCause(ex); 793 throw err; 794 } 795 796 assertEquals(0, launchLatch.getCount()); 797 } 798 799 private void checkMarlin() { 800 if (!isMarlin.get()) { 801 throw new RuntimeException("Marlin renderer not used at runtime !"); 802 } 803 if (!isClipRuntime.get()) { 804 throw new RuntimeException("Marlin clipping not enabled at runtime !"); 805 } 806 } 807 808 @AfterClass 809 public static void teardownOnce() { 810 Platform.exit(); 811 } 812 813 @Test(timeout = 600000) 814 public void TestPoly() throws InterruptedException { 815 test(new String[]{"-poly"}); 816 test(new String[]{"-poly", "-doDash"}); 817 } 818 819 @Test(timeout = 900000) 820 public void TestQuad() throws InterruptedException { 821 test(new String[]{"-quad"}); 822 test(new String[]{"-quad", "-doDash"}); 823 } 824 825 @Test(timeout = 900000) 826 public void TestCubic() throws InterruptedException { 827 test(new String[]{"-cubic"}); 828 test(new String[]{"-cubic", "-doDash"}); 829 } 830 831 private void test(String[] args) { 832 initArgs(args); 833 runTests(); 834 checkMarlin(); 835 Assert.assertFalse("Detected a problem.", doChecksFailed); 836 } 837 838 private static void dumpShape(final Shape shape) { 839 final float[] coords = new float[6]; 840 841 for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { 842 final int type = it.currentSegment(coords); 843 switch (type) { 844 case PathIterator.SEG_MOVETO: 845 System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");"); 846 break; 847 case PathIterator.SEG_LINETO: 848 System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");"); 849 break; 850 case PathIterator.SEG_QUADTO: 851 System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");"); 852 break; 853 case PathIterator.SEG_CUBICTO: 854 System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");"); 855 break; 856 case PathIterator.SEG_CLOSE: 857 System.out.println("p2d.closePath();"); 858 break; 859 default: 860 System.out.println("// Unsupported segment type= " + type); 861 } 862 } 863 System.out.println("--------------------------------------------------"); 864 } 865 866 static double randX() { 867 return RANDOM.nextDouble() * RANDW + OFFW; 868 } 869 870 static double randY() { 871 return RANDOM.nextDouble() * RANDH + OFFH; 872 } 873 874 private final static class TestSetup { 875 876 static final AtomicInteger COUNT = new AtomicInteger(); 877 878 final int id; 879 final ShapeMode shapeMode; 880 final boolean closed; 881 // stroke 882 final float strokeWidth; 883 final int strokeCap; 884 final int strokeJoin; 885 final float[] dashes; 886 // fill 887 final int windingRule; 888 889 TestSetup(ShapeMode shapeMode, final boolean closed, 890 final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) { 891 this.id = COUNT.incrementAndGet(); 892 this.shapeMode = shapeMode; 893 this.closed = closed; 894 this.strokeWidth = strokeWidth; 895 this.strokeCap = strokeCap; 896 this.strokeJoin = strokeJoin; 897 this.dashes = dashes; 898 this.windingRule = Path2D.WIND_NON_ZERO; 899 } 900 901 TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) { 902 this.id = COUNT.incrementAndGet(); 903 this.shapeMode = shapeMode; 904 this.closed = closed; 905 this.strokeWidth = 0f; 906 this.strokeCap = this.strokeJoin = -1; // invalid 907 this.dashes = null; 908 this.windingRule = windingRule; 909 } 910 911 boolean isStroke() { 912 return this.strokeWidth > 0f; 913 } 914 915 @Override 916 public String toString() { 917 if (isStroke()) { 918 return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed 919 + ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin) 920 + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "") 921 + '}'; 922 } 923 return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed 924 + ", fill" 925 + ", windingRule=" + getWindingRule(windingRule) + '}'; 926 } 927 928 private static String getCap(final int cap) { 929 switch (cap) { 930 case BasicStroke.CAP_BUTT: 931 return "CAP_BUTT"; 932 case BasicStroke.CAP_ROUND: 933 return "CAP_ROUND"; 934 case BasicStroke.CAP_SQUARE: 935 return "CAP_SQUARE"; 936 default: 937 return ""; 938 } 939 940 } 941 942 private static String getJoin(final int join) { 943 switch (join) { 944 case BasicStroke.JOIN_MITER: 945 return "JOIN_MITER"; 946 case BasicStroke.JOIN_ROUND: 947 return "JOIN_ROUND"; 948 case BasicStroke.JOIN_BEVEL: 949 return "JOIN_BEVEL"; 950 default: 951 return ""; 952 } 953 954 } 955 956 private static String getWindingRule(final int rule) { 957 switch (rule) { 958 case PathIterator.WIND_EVEN_ODD: 959 return "WIND_EVEN_ODD"; 960 case PathIterator.WIND_NON_ZERO: 961 return "WIND_NON_ZERO"; 962 default: 963 return ""; 964 } 965 } 966 } 967 968 // --- utilities --- 969 private static final int DCM_ALPHA_MASK = 0xff000000; 970 971 public static BufferedImage newImage(final int w, final int h) { 972 return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); 973 } 974 975 public static PixelWriter computeDiffImage(final DiffContext testCtx, 976 final DiffContext testThCtx, 977 final PixelReader tstImage, 978 final PixelReader refImage, 979 final PixelWriter diffImage) { 980 981 // reset diff contexts: 982 testCtx.reset(); 983 testThCtx.reset(); 984 985 int ref, tst, dg, v; 986 987 for (int y = 0; y < TESTH; y++) { 988 for (int x = 0; x < TESTW; x++) { 989 ref = refImage.getArgb(x, y); 990 tst = tstImage.getArgb(x, y); 991 992 // grayscale diff: 993 dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst)); 994 995 // max difference on grayscale values: 996 v = (int) Math.ceil(Math.abs(dg / 3.0)); 997 998 if (v <= THRESHOLD_DELTA) { 999 diffImage.setArgb(x, y, 0); 1000 } else { 1001 diffImage.setArgb(x, y, toInt(v, v, v)); 1002 testThCtx.add(v); 1003 } 1004 1005 if (v != 0) { 1006 testCtx.add(v); 1007 } 1008 } 1009 } 1010 1011 if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) { 1012 return null; 1013 } 1014 1015 return diffImage; 1016 } 1017 1018 static void saveImage(final WritableImage image, final File resDirectory, final String imageFileName) throws IOException { 1019 saveImage(SwingFXUtils.fromFXImage(image, null), resDirectory, imageFileName); 1020 } 1021 1022 static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException { 1023 final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG"); 1024 if (itWriters.hasNext()) { 1025 final ImageWriter writer = itWriters.next(); 1026 1027 final ImageWriteParam writerParams = writer.getDefaultWriteParam(); 1028 writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED); 1029 1030 final File imgFile = new File(resDirectory, imageFileName); 1031 1032 if (!imgFile.exists() || imgFile.canWrite()) { 1033 System.out.println("saveImage: saving image as PNG [" + imgFile + "]..."); 1034 imgFile.delete(); 1035 1036 // disable cache in temporary files: 1037 ImageIO.setUseCache(false); 1038 1039 final long start = System.nanoTime(); 1040 1041 // PNG uses already buffering: 1042 final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile)); 1043 1044 writer.setOutput(imgOutStream); 1045 try { 1046 writer.write(null, new IIOImage(image, null, null), writerParams); 1047 } finally { 1048 imgOutStream.close(); 1049 1050 final long time = System.nanoTime() - start; 1051 System.out.println("saveImage: duration= " + (time / 1000000l) + " ms."); 1052 } 1053 } 1054 } 1055 } 1056 1057 static int r(final int v) { 1058 return (v >> 16 & 0xff); 1059 } 1060 1061 static int g(final int v) { 1062 return (v >> 8 & 0xff); 1063 } 1064 1065 static int b(final int v) { 1066 return (v & 0xff); 1067 } 1068 1069 static int clamp127(final int v) { 1070 return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255; 1071 } 1072 1073 static int toInt(final int r, final int g, final int b) { 1074 return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b; 1075 } 1076 1077 /* stats */ 1078 static class StatInteger { 1079 1080 public final String name; 1081 public long count = 0l; 1082 public long sum = 0l; 1083 public long min = Integer.MAX_VALUE; 1084 public long max = Integer.MIN_VALUE; 1085 1086 StatInteger(String name) { 1087 this.name = name; 1088 } 1089 1090 void reset() { 1091 count = 0l; 1092 sum = 0l; 1093 min = Integer.MAX_VALUE; 1094 max = Integer.MIN_VALUE; 1095 } 1096 1097 void add(int val) { 1098 count++; 1099 sum += val; 1100 if (val < min) { 1101 min = val; 1102 } 1103 if (val > max) { 1104 max = val; 1105 } 1106 } 1107 1108 void add(long val) { 1109 count++; 1110 sum += val; 1111 if (val < min) { 1112 min = val; 1113 } 1114 if (val > max) { 1115 max = val; 1116 } 1117 } 1118 1119 void add(StatInteger stat) { 1120 count += stat.count; 1121 sum += stat.sum; 1122 if (stat.min < min) { 1123 min = stat.min; 1124 } 1125 if (stat.max > max) { 1126 max = stat.max; 1127 } 1128 } 1129 1130 public final double average() { 1131 return ((double) sum) / count; 1132 } 1133 1134 @Override 1135 public String toString() { 1136 final StringBuilder sb = new StringBuilder(128); 1137 toString(sb); 1138 return sb.toString(); 1139 } 1140 1141 public final StringBuilder toString(final StringBuilder sb) { 1142 sb.append(name).append("[n: ").append(count); 1143 sb.append("] sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average())); 1144 sb.append(" [").append(min).append(" | ").append(max).append("]"); 1145 return sb; 1146 } 1147 1148 } 1149 1150 final static class Histogram extends StatInteger { 1151 1152 static final int BUCKET = 2; 1153 static final int MAX = 20; 1154 static final int LAST = MAX - 1; 1155 static final int[] STEPS = new int[MAX]; 1156 static final int BUCKET_TH; 1157 1158 static { 1159 STEPS[0] = 0; 1160 STEPS[1] = 1; 1161 1162 for (int i = 2; i < MAX; i++) { 1163 STEPS[i] = STEPS[i - 1] * BUCKET; 1164 } 1165 // System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS)); 1166 1167 if (THRESHOLD_DELTA % 2 != 0) { 1168 throw new IllegalStateException("THRESHOLD_DELTA must be odd"); 1169 } 1170 1171 BUCKET_TH = bucket(THRESHOLD_DELTA); 1172 } 1173 1174 static int bucket(int val) { 1175 for (int i = 1; i < MAX; i++) { 1176 if (val < STEPS[i]) { 1177 return i - 1; 1178 } 1179 } 1180 return LAST; 1181 } 1182 1183 private final StatInteger[] stats = new StatInteger[MAX]; 1184 1185 public Histogram(String name) { 1186 super(name); 1187 for (int i = 0; i < MAX; i++) { 1188 stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~"))); 1189 } 1190 } 1191 1192 @Override 1193 final void reset() { 1194 super.reset(); 1195 for (int i = 0; i < MAX; i++) { 1196 stats[i].reset(); 1197 } 1198 } 1199 1200 @Override 1201 final void add(int val) { 1202 super.add(val); 1203 stats[bucket(val)].add(val); 1204 } 1205 1206 @Override 1207 final void add(long val) { 1208 add((int) val); 1209 } 1210 1211 void add(Histogram hist) { 1212 super.add(hist); 1213 for (int i = 0; i < MAX; i++) { 1214 stats[i].add(hist.stats[i]); 1215 } 1216 } 1217 1218 boolean isWorse(Histogram hist, boolean useTh) { 1219 boolean worst = false; 1220 if (!useTh && (hist.sum > sum)) { 1221 worst = true; 1222 } else { 1223 long sumLoc = 0l; 1224 long sumHist = 0l; 1225 // use running sum: 1226 for (int i = MAX - 1; i >= BUCKET_TH; i--) { 1227 sumLoc += stats[i].sum; 1228 sumHist += hist.stats[i].sum; 1229 } 1230 if (sumHist > sumLoc) { 1231 worst = true; 1232 } 1233 } 1234 /* 1235 System.out.println("running sum worst:"); 1236 System.out.println("this ? " + toString()); 1237 System.out.println("worst ? " + hist.toString()); 1238 */ 1239 return worst; 1240 } 1241 1242 @Override 1243 public final String toString() { 1244 final StringBuilder sb = new StringBuilder(2048); 1245 super.toString(sb).append(" { "); 1246 1247 for (int i = 0; i < MAX; i++) { 1248 if (stats[i].count != 0l) { 1249 sb.append("\n ").append(stats[i].toString()); 1250 } 1251 } 1252 1253 return sb.append(" }").toString(); 1254 } 1255 } 1256 1257 /** 1258 * Adjust the given double value to keep only 3 decimal digits 1259 * @param value value to adjust 1260 * @return double value with only 3 decimal digits 1261 */ 1262 static double trimTo3Digits(final double value) { 1263 return ((long) (1e3d * value)) / 1e3d; 1264 } 1265 1266 static final class DiffContext { 1267 1268 public final Histogram histPix; 1269 1270 DiffContext(String name) { 1271 histPix = new Histogram("Diff Pixels [" + name + "]"); 1272 } 1273 1274 void reset() { 1275 histPix.reset(); 1276 } 1277 1278 void dump() { 1279 if (isDiff()) { 1280 System.out.println("Differences [" + histPix.name + "]:\n" + histPix.toString()); 1281 } else { 1282 System.out.println("No difference for [" + histPix.name + "]."); 1283 } 1284 } 1285 1286 void add(int val) { 1287 histPix.add(val); 1288 } 1289 1290 void add(DiffContext ctx) { 1291 histPix.add(ctx.histPix); 1292 } 1293 1294 void set(DiffContext ctx) { 1295 reset(); 1296 add(ctx); 1297 } 1298 1299 boolean isWorse(DiffContext ctx, boolean useTh) { 1300 return histPix.isWorse(ctx.histPix, useTh); 1301 } 1302 1303 boolean isDiff() { 1304 return histPix.sum != 0l; 1305 } 1306 } 1307 }