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 }