1 /* 2 * Copyright (c) 2017, 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. 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.Stroke; 28 import java.awt.Shape; 29 import java.awt.geom.CubicCurve2D; 30 import java.awt.geom.Ellipse2D; 31 import java.awt.geom.Line2D; 32 import java.awt.geom.Path2D; 33 import java.awt.geom.PathIterator; 34 import java.awt.geom.QuadCurve2D; 35 import java.awt.image.BufferedImage; 36 import java.awt.image.DataBufferInt; 37 import java.io.File; 38 import java.io.FileOutputStream; 39 import java.io.IOException; 40 import java.util.Arrays; 41 import java.util.Iterator; 42 import java.util.Locale; 43 import java.util.Random; 44 import java.util.concurrent.atomic.AtomicBoolean; 45 import java.util.concurrent.atomic.AtomicInteger; 46 import java.util.logging.Handler; 47 import java.util.logging.LogRecord; 48 import java.util.logging.Logger; 49 import javax.imageio.IIOImage; 50 import javax.imageio.ImageIO; 51 import javax.imageio.ImageWriteParam; 52 import javax.imageio.ImageWriter; 53 import javax.imageio.stream.ImageOutputStream; 54 55 /** 56 * @test 57 * @bug 8191814 58 * @summary Verifies that Marlin rendering generates the same 59 * images with and without clipping optimization with all possible 60 * stroke (cap/join) and/or dashes or fill modes (EO rules) 61 * for paths made of either 9 lines, 4 quads, 2 cubics (random) 62 * Note: Use the argument -slow to run more intensive tests (too much time) 63 * 64 * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly 65 * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly -doDash 66 * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic 67 * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic -doDash 68 * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly 69 * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly -doDash 70 * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic 71 * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic -doDash 72 */ 73 public final class ClipShapeTest { 74 75 // test options: 76 static int NUM_TESTS; 77 78 // shape settings: 79 static ShapeMode SHAPE_MODE; 80 81 static boolean USE_DASHES; 82 static boolean USE_VAR_STROKE; 83 84 static int THRESHOLD_DELTA; 85 static long THRESHOLD_NBPIX; 86 87 // constants: 88 static final boolean DO_FAIL = Boolean.valueOf(System.getProperty("ClipShapeTest.fail", "true")); 89 90 static final boolean TEST_STROKER = true; 91 static final boolean TEST_FILLER = true; 92 93 static final boolean SUBDIVIDE_CURVE = true; 94 static final double SUBDIVIDE_LEN_TH = 50.0; 95 static final boolean TRACE_SUBDIVIDE_CURVE = false; 96 97 static final int TESTW = 100; 98 static final int TESTH = 100; 99 100 // dump path on console: 101 static final boolean DUMP_SHAPE = true; 102 103 static final boolean SHOW_DETAILS = false; // disabled 104 static final boolean SHOW_OUTLINE = true; 105 static final boolean SHOW_POINTS = true; 106 static final boolean SHOW_INFO = false; 107 108 static final int MAX_SHOW_FRAMES = 10; 109 static final int MAX_SAVE_FRAMES = 100; 110 111 // use fixed seed to reproduce always same polygons between tests 112 static final boolean FIXED_SEED = true; 113 114 static final double RAND_SCALE = 3.0; 115 static final double RANDW = TESTW * RAND_SCALE; 116 static final double OFFW = (TESTW - RANDW) / 2.0; 117 static final double RANDH = TESTH * RAND_SCALE; 118 static final double OFFH = (TESTH - RANDH) / 2.0; 119 120 static enum ShapeMode { 121 TWO_CUBICS, 122 FOUR_QUADS, 123 FIVE_LINE_POLYS, 124 NINE_LINE_POLYS, 125 FIFTY_LINE_POLYS, 126 MIXED 127 } 128 129 static final long SEED = 1666133789L; 130 // Fixed seed to avoid any difference between runs: 131 static final Random RANDOM = new Random(SEED); 132 133 static final File OUTPUT_DIR = new File("."); 134 135 static final AtomicBoolean isMarlin = new AtomicBoolean(); 136 static final AtomicBoolean isMarlinFloat = new AtomicBoolean(); 137 static final AtomicBoolean isClipRuntime = new AtomicBoolean(); 138 139 static { 140 Locale.setDefault(Locale.US); 141 142 // FIRST: Get Marlin runtime state from its log: 143 144 // initialize j.u.l Looger: 145 final Logger log = Logger.getLogger("sun.java2d.marlin"); 146 log.addHandler(new Handler() { 147 @Override 148 public void publish(LogRecord record) { 149 final String msg = record.getMessage(); 150 if (msg != null) { 151 // last space to avoid matching other settings: 152 if (msg.startsWith("sun.java2d.renderer ")) { 153 isMarlin.set(msg.contains("MarlinRenderingEngine")); 154 isMarlinFloat.set(!msg.contains("DMarlinRenderingEngine")); 155 } 156 if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) { 157 isClipRuntime.set(msg.contains("true")); 158 } 159 } 160 161 final Throwable th = record.getThrown(); 162 // detect any Throwable: 163 if (th != null) { 164 System.out.println("Test failed:\n" + record.getMessage()); 165 th.printStackTrace(System.out); 166 167 throw new RuntimeException("Test failed: ", th); 168 } 169 } 170 171 @Override 172 public void flush() { 173 } 174 175 @Override 176 public void close() throws SecurityException { 177 } 178 }); 179 180 // enable Marlin logging & internal checks: 181 System.setProperty("sun.java2d.renderer.log", "true"); 182 System.setProperty("sun.java2d.renderer.useLogger", "true"); 183 184 // disable static clipping setting: 185 System.setProperty("sun.java2d.renderer.clip", "false"); 186 System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true"); 187 188 // enable subdivider: 189 System.setProperty("sun.java2d.renderer.clip.subdivider", "true"); 190 191 // disable min length check: always subdivide curves at clip edges 192 System.setProperty("sun.java2d.renderer.clip.subdivider.minLength", "-1"); 193 194 // If any curve, increase curve accuracy: 195 // curve length max error: 196 System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4"); 197 198 // cubic min/max error: 199 System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3"); 200 System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4"); 201 202 // quad max error: 203 System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4"); 204 } 205 206 private static void resetOptions() { 207 NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 5000); 208 209 // shape settings: 210 SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; 211 212 USE_DASHES = false; 213 USE_VAR_STROKE = false; 214 } 215 216 /** 217 * Test 218 * @param args 219 */ 220 public static void main(String[] args) { 221 { 222 // Bootstrap: init Renderer now: 223 final BufferedImage img = newImage(TESTW, TESTH); 224 final Graphics2D g2d = initialize(img, null); 225 226 try { 227 paintShape(new Line2D.Double(0,0,100,100), g2d, true, false); 228 } finally { 229 g2d.dispose(); 230 } 231 232 if (!isMarlin.get()) { 233 throw new RuntimeException("Marlin renderer not used at runtime !"); 234 } 235 if (!isClipRuntime.get()) { 236 throw new RuntimeException("Marlin clipping not enabled at runtime !"); 237 } 238 } 239 240 System.out.println("---------------------------------------"); 241 System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH); 242 243 resetOptions(); 244 245 boolean runSlowTests = false; 246 247 for (String arg : args) { 248 if ("-slow".equals(arg)) { 249 runSlowTests = true; 250 } else if ("-doDash".equals(arg)) { 251 USE_DASHES = true; 252 } else if ("-doVarStroke".equals(arg)) { 253 USE_VAR_STROKE = true; 254 } else { 255 // shape mode: 256 if (arg.equalsIgnoreCase("-poly")) { 257 SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; 258 } else if (arg.equalsIgnoreCase("-bigpoly")) { 259 SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS; 260 } else if (arg.equalsIgnoreCase("-quad")) { 261 SHAPE_MODE = ShapeMode.FOUR_QUADS; 262 } else if (arg.equalsIgnoreCase("-cubic")) { 263 SHAPE_MODE = ShapeMode.TWO_CUBICS; 264 } else if (arg.equalsIgnoreCase("-mixed")) { 265 SHAPE_MODE = ShapeMode.MIXED; 266 } 267 } 268 } 269 270 System.out.println("Shape mode: " + SHAPE_MODE); 271 272 // adjust image comparison thresholds: 273 switch (SHAPE_MODE) { 274 case TWO_CUBICS: 275 // Define uncertainty for curves: 276 THRESHOLD_DELTA = 32; 277 THRESHOLD_NBPIX = (USE_DASHES) ? 50 : 200; 278 if (SUBDIVIDE_CURVE) { 279 THRESHOLD_NBPIX = 4; 280 } 281 break; 282 case FOUR_QUADS: 283 case MIXED: 284 // Define uncertainty for quads: 285 // curve subdivision causes curves to be smaller 286 // then curve offsets are different (more accurate) 287 THRESHOLD_DELTA = 64; 288 THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 420; 289 if (SUBDIVIDE_CURVE) { 290 THRESHOLD_NBPIX = 10; 291 } 292 break; 293 default: 294 // Define uncertainty for lines: 295 // float variant have higher uncertainty 296 THRESHOLD_DELTA = 2; 297 THRESHOLD_NBPIX = (USE_DASHES) ? 298 // float variant have higher uncertainty 299 ((isMarlinFloat.get()) ? 30 : 6) // low for double 300 : (isMarlinFloat.get()) ? 10 : 0; 301 } 302 303 // Visual inspection (low threshold): 304 // THRESHOLD_NBPIX = 2; 305 306 System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA); 307 System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX); 308 309 if (runSlowTests) { 310 NUM_TESTS = 10000; // or 100000 (very slow) 311 USE_VAR_STROKE = true; 312 } 313 314 System.out.println("NUM_TESTS: " + NUM_TESTS); 315 316 if (USE_DASHES) { 317 System.out.println("USE_DASHES: enabled."); 318 } 319 if (USE_VAR_STROKE) { 320 System.out.println("USE_VAR_STROKE: enabled."); 321 } 322 if (!DO_FAIL) { 323 System.out.println("DO_FAIL: disabled."); 324 } 325 326 System.out.println("---------------------------------------"); 327 328 final DiffContext allCtx = new DiffContext("All Test setups"); 329 final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)"); 330 331 int failures = 0; 332 final long start = System.nanoTime(); 333 try { 334 if (TEST_STROKER) { 335 final float[][] dashArrays = (USE_DASHES) ? 336 // small 337 // new float[][]{new float[]{1f, 2f}} 338 // normal 339 new float[][]{new float[]{13f, 7f}} 340 // large (prime) 341 // new float[][]{new float[]{41f, 7f}} 342 // none 343 : new float[][]{null}; 344 345 System.out.println("dashes: " + Arrays.deepToString(dashArrays)); 346 347 final float[] strokeWidths = (USE_VAR_STROKE) 348 ? new float[5] : 349 new float[]{10f}; 350 351 int nsw = 0; 352 if (USE_VAR_STROKE) { 353 for (float width = 0.25f; width < 110f; width *= 5f) { 354 strokeWidths[nsw++] = width; 355 } 356 } else { 357 nsw = 1; 358 } 359 360 System.out.println("stroke widths: " + Arrays.toString(strokeWidths)); 361 362 // Stroker tests: 363 for (int w = 0; w < nsw; w++) { 364 final float width = strokeWidths[w]; 365 366 for (float[] dashes : dashArrays) { 367 368 for (int cap = 0; cap <= 2; cap++) { 369 370 for (int join = 0; join <= 2; join++) { 371 372 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes)); 373 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes)); 374 } 375 } 376 } 377 } 378 } 379 380 if (TEST_FILLER) { 381 // Filler tests: 382 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO)); 383 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO)); 384 385 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD)); 386 failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD)); 387 } 388 } catch (IOException ioe) { 389 throw new RuntimeException(ioe); 390 } 391 System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); 392 393 allWorstCtx.dump(); 394 allCtx.dump(); 395 396 if (DO_FAIL && (failures != 0)) { 397 throw new RuntimeException("Clip test failures : " + failures); 398 } 399 } 400 401 static int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException { 402 final long start = System.nanoTime(); 403 404 if (FIXED_SEED) { 405 // Reset seed for random numbers: 406 RANDOM.setSeed(SEED); 407 } 408 409 System.out.println("paintPaths: " + NUM_TESTS 410 + " paths (" + SHAPE_MODE + ") - setup: " + ts); 411 412 final boolean fill = !ts.isStroke(); 413 final Path2D p2d = new Path2D.Double(ts.windingRule); 414 415 final Stroke stroke = (!fill) ? createStroke(ts) : null; 416 417 final BufferedImage imgOn = newImage(TESTW, TESTH); 418 final Graphics2D g2dOn = initialize(imgOn, stroke); 419 420 final BufferedImage imgOff = newImage(TESTW, TESTH); 421 final Graphics2D g2dOff = initialize(imgOff, stroke); 422 423 final BufferedImage imgDiff = newImage(TESTW, TESTH); 424 425 final DiffContext testSetupCtx = new DiffContext("Test setup"); 426 final DiffContext testWorstCtx = new DiffContext("Worst"); 427 final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)"); 428 429 int nd = 0; 430 try { 431 final DiffContext testCtx = new DiffContext("Test"); 432 final DiffContext testThCtx = new DiffContext("Test(>threshold)"); 433 BufferedImage diffImage; 434 435 for (int n = 0; n < NUM_TESTS; n++) { 436 genShape(p2d, ts); 437 438 // Runtime clip setting OFF: 439 paintShape(p2d, g2dOff, fill, false); 440 441 // Runtime clip setting ON: 442 paintShape(p2d, g2dOn, fill, true); 443 444 /* compute image difference if possible */ 445 diffImage = computeDiffImage(testCtx, testThCtx, imgOn, imgOff, imgDiff); 446 447 // Worst (total) 448 if (testCtx.isDiff()) { 449 if (testWorstCtx.isWorse(testCtx, false)) { 450 testWorstCtx.set(testCtx); 451 } 452 if (testWorstThCtx.isWorse(testCtx, true)) { 453 testWorstThCtx.set(testCtx); 454 } 455 // accumulate data: 456 testSetupCtx.add(testCtx); 457 } 458 if (diffImage != null) { 459 nd++; 460 461 testThCtx.dump(); 462 testCtx.dump(); 463 464 if (nd < MAX_SHOW_FRAMES) { 465 if (SHOW_DETAILS) { 466 paintShapeDetails(g2dOff, p2d); 467 paintShapeDetails(g2dOn, p2d); 468 } 469 470 if (nd < MAX_SAVE_FRAMES) { 471 if (DUMP_SHAPE) { 472 dumpShape(p2d); 473 } 474 475 final String testName = "Setup_" + ts.id + "_test_" + n; 476 477 saveImage(imgOff, OUTPUT_DIR, testName + "-off.png"); 478 saveImage(imgOn, OUTPUT_DIR, testName + "-on.png"); 479 saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png"); 480 } 481 } 482 } 483 } 484 } finally { 485 g2dOff.dispose(); 486 g2dOn.dispose(); 487 488 if (nd != 0) { 489 System.out.println("paintPaths: " + NUM_TESTS + " paths - " 490 + "Number of differences = " + nd 491 + " ratio = " + (100f * nd) / NUM_TESTS + " %"); 492 } 493 494 if (testWorstCtx.isDiff()) { 495 testWorstCtx.dump(); 496 if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) { 497 testWorstThCtx.dump(); 498 } 499 if (allWorstCtx.isWorse(testWorstThCtx, true)) { 500 allWorstCtx.set(testWorstThCtx); 501 } 502 } 503 testSetupCtx.dump(); 504 505 // accumulate data: 506 allCtx.add(testSetupCtx); 507 } 508 System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); 509 return nd; 510 } 511 512 private static void paintShape(final Shape p2d, final Graphics2D g2d, 513 final boolean fill, final boolean clip) { 514 reset(g2d); 515 516 setClip(g2d, clip); 517 518 if (fill) { 519 g2d.fill(p2d); 520 } else { 521 g2d.draw(p2d); 522 } 523 } 524 525 private static Graphics2D initialize(final BufferedImage img, 526 final Stroke s) { 527 final Graphics2D g2d = (Graphics2D) img.getGraphics(); 528 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 529 RenderingHints.VALUE_RENDER_QUALITY); 530 g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 531 // Test normalize: 532 // RenderingHints.VALUE_STROKE_NORMALIZE 533 RenderingHints.VALUE_STROKE_PURE 534 ); 535 536 if (s != null) { 537 g2d.setStroke(s); 538 } 539 g2d.setColor(Color.BLACK); 540 541 return g2d; 542 } 543 544 private static void reset(final Graphics2D g2d) { 545 // Disable antialiasing: 546 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 547 RenderingHints.VALUE_ANTIALIAS_OFF); 548 g2d.setBackground(Color.WHITE); 549 g2d.clearRect(0, 0, TESTW, TESTH); 550 } 551 552 private static void setClip(final Graphics2D g2d, final boolean clip) { 553 // Enable antialiasing: 554 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 555 RenderingHints.VALUE_ANTIALIAS_ON); 556 557 // Enable or Disable clipping: 558 System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false"); 559 } 560 561 static void genShape(final Path2D p2d, final TestSetup ts) { 562 p2d.reset(); 563 564 /* 565 Test closed path: 566 0: moveTo + (draw)To + closePath 567 1: (draw)To + closePath (closePath + (draw)To sequence) 568 */ 569 final int end = (ts.closed) ? 2 : 1; 570 571 final double[] in = new double[8]; 572 573 double sx0 = 0.0, sy0 = 0.0, x0 = 0.0, y0 = 0.0; 574 575 for (int p = 0; p < end; p++) { 576 if (p <= 0) { 577 x0 = randX(); y0 = randY(); 578 p2d.moveTo(x0, y0); 579 sx0 = x0; sy0 = y0; 580 } 581 582 switch (ts.shapeMode) { 583 case MIXED: 584 case FIVE_LINE_POLYS: 585 case NINE_LINE_POLYS: 586 case FIFTY_LINE_POLYS: 587 p2d.lineTo(randX(), randY()); 588 p2d.lineTo(randX(), randY()); 589 p2d.lineTo(randX(), randY()); 590 p2d.lineTo(randX(), randY()); 591 x0 = randX(); y0 = randY(); 592 p2d.lineTo(x0, y0); 593 if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) { 594 // And an implicit close makes 5 lines 595 break; 596 } 597 p2d.lineTo(randX(), randY()); 598 p2d.lineTo(randX(), randY()); 599 p2d.lineTo(randX(), randY()); 600 x0 = randX(); y0 = randY(); 601 p2d.lineTo(x0, y0); 602 if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) { 603 // And an implicit close makes 9 lines 604 break; 605 } 606 if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) { 607 for (int i = 0; i < 41; i++) { 608 x0 = randX(); y0 = randY(); 609 p2d.lineTo(x0, y0); 610 } 611 // And an implicit close makes 50 lines 612 break; 613 } 614 case TWO_CUBICS: 615 if (SUBDIVIDE_CURVE) { 616 in[0] = x0; in[1] = y0; 617 in[2] = randX(); in[3] = randY(); 618 in[4] = randX(); in[5] = randY(); 619 x0 = randX(); y0 = randY(); 620 in[6] = x0; in[7] = y0; 621 subdivide(p2d, 8, in); 622 in[0] = x0; in[1] = y0; 623 in[2] = randX(); in[3] = randY(); 624 in[4] = randX(); in[5] = randY(); 625 x0 = randX(); y0 = randY(); 626 in[6] = x0; in[7] = y0; 627 subdivide(p2d, 8, in); 628 } else { 629 x0 = randX(); y0 = randY(); 630 p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0); 631 x0 = randX(); y0 = randY(); 632 p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0); 633 } 634 if (ts.shapeMode == ShapeMode.TWO_CUBICS) { 635 break; 636 } 637 case FOUR_QUADS: 638 if (SUBDIVIDE_CURVE) { 639 in[0] = x0; in[1] = y0; 640 in[2] = randX(); in[3] = randY(); 641 x0 = randX(); y0 = randY(); 642 in[4] = x0; in[5] = y0; 643 subdivide(p2d, 6, in); 644 in[0] = x0; in[1] = y0; 645 in[2] = randX(); in[3] = randY(); 646 x0 = randX(); y0 = randY(); 647 in[4] = x0; in[5] = y0; 648 subdivide(p2d, 6, in); 649 in[0] = x0; in[1] = y0; 650 in[2] = randX(); in[3] = randY(); 651 x0 = randX(); y0 = randY(); 652 in[4] = x0; in[5] = y0; 653 subdivide(p2d, 6, in); 654 in[0] = x0; in[1] = y0; 655 in[2] = randX(); in[3] = randY(); 656 x0 = randX(); y0 = randY(); 657 in[4] = x0; in[5] = y0; 658 subdivide(p2d, 6, in); 659 } else { 660 x0 = randX(); y0 = randY(); 661 p2d.quadTo(randX(), randY(), x0, y0); 662 x0 = randX(); y0 = randY(); 663 p2d.quadTo(randX(), randY(), x0, y0); 664 x0 = randX(); y0 = randY(); 665 p2d.quadTo(randX(), randY(), x0, y0); 666 x0 = randX(); y0 = randY(); 667 p2d.quadTo(randX(), randY(), x0, y0); 668 } 669 if (ts.shapeMode == ShapeMode.FOUR_QUADS) { 670 break; 671 } 672 default: 673 } 674 675 if (ts.closed) { 676 p2d.closePath(); 677 x0 = sx0; y0 = sy0; 678 } 679 } 680 } 681 682 static final int SUBDIVIDE_LIMIT = 5; 683 static final double[][] SUBDIVIDE_CURVES = new double[SUBDIVIDE_LIMIT + 1][]; 684 685 static { 686 for (int i = 0, n = 1; i < SUBDIVIDE_LIMIT; i++, n *= 2) { 687 SUBDIVIDE_CURVES[i] = new double[8 * n]; 688 } 689 } 690 691 static void subdivide(final Path2D p2d, final int type, final double[] in) { 692 if (TRACE_SUBDIVIDE_CURVE) { 693 System.out.println("subdivide: " + Arrays.toString(Arrays.copyOf(in, type))); 694 } 695 696 double curveLen = ((type == 8) 697 ? curvelen(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7]) 698 : quadlen(in[0], in[1], in[2], in[3], in[4], in[5])); 699 700 if (curveLen > SUBDIVIDE_LEN_TH) { 701 if (TRACE_SUBDIVIDE_CURVE) { 702 System.out.println("curvelen: " + curveLen); 703 } 704 705 System.arraycopy(in, 0, SUBDIVIDE_CURVES[0], 0, 8); 706 707 int level = 0; 708 while (curveLen >= SUBDIVIDE_LEN_TH) { 709 level++; 710 curveLen /= 2.0; 711 if (TRACE_SUBDIVIDE_CURVE) { 712 System.out.println("curvelen: " + curveLen); 713 } 714 } 715 716 if (TRACE_SUBDIVIDE_CURVE) { 717 System.out.println("level: " + level); 718 } 719 720 if (level > SUBDIVIDE_LIMIT) { 721 if (TRACE_SUBDIVIDE_CURVE) { 722 System.out.println("max level reached : " + level); 723 } 724 level = SUBDIVIDE_LIMIT; 725 } 726 727 for (int l = 0; l < level; l++) { 728 if (TRACE_SUBDIVIDE_CURVE) { 729 System.out.println("level: " + l); 730 } 731 732 double[] src = SUBDIVIDE_CURVES[l]; 733 double[] dst = SUBDIVIDE_CURVES[l + 1]; 734 735 for (int i = 0, j = 0; i < src.length; i += 8, j += 16) { 736 if (TRACE_SUBDIVIDE_CURVE) { 737 System.out.println("subdivide: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type))); 738 } 739 if (type == 8) { 740 CubicCurve2D.subdivide(src, i, dst, j, dst, j + 8); 741 } else { 742 QuadCurve2D.subdivide(src, i, dst, j, dst, j + 8); 743 } 744 if (TRACE_SUBDIVIDE_CURVE) { 745 System.out.println("left: " + Arrays.toString(Arrays.copyOfRange(dst, j, j + type))); 746 System.out.println("right: " + Arrays.toString(Arrays.copyOfRange(dst, j + 8, j + 8 + type))); 747 } 748 } 749 } 750 751 // Emit curves at last level: 752 double[] src = SUBDIVIDE_CURVES[level]; 753 754 double len = 0.0; 755 756 for (int i = 0; i < src.length; i += 8) { 757 if (TRACE_SUBDIVIDE_CURVE) { 758 System.out.println("curve: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type))); 759 } 760 761 if (type == 8) { 762 if (TRACE_SUBDIVIDE_CURVE) { 763 len += curvelen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]); 764 } 765 p2d.curveTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]); 766 } else { 767 if (TRACE_SUBDIVIDE_CURVE) { 768 len += quadlen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5]); 769 } 770 p2d.quadTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5]); 771 } 772 } 773 774 if (TRACE_SUBDIVIDE_CURVE) { 775 System.out.println("curveLen (final) = " + len); 776 } 777 } else { 778 if (type == 8) { 779 p2d.curveTo(in[2], in[3], in[4], in[5], in[6], in[7]); 780 } else { 781 p2d.quadTo(in[2], in[3], in[4], in[5]); 782 } 783 } 784 } 785 786 static final float POINT_RADIUS = 2f; 787 static final float LINE_WIDTH = 1f; 788 789 static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH); 790 static final int COLOR_ALPHA = 128; 791 static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA); 792 static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA); 793 static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA); 794 795 static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float(); 796 797 private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) { 798 799 final Stroke oldStroke = g2d.getStroke(); 800 final Color oldColor = g2d.getColor(); 801 802 setClip(g2d, false); 803 804 if (SHOW_OUTLINE) { 805 g2d.setStroke(OUTLINE_STROKE); 806 g2d.setColor(COLOR_LINETO_ODD); 807 g2d.draw(shape); 808 } 809 810 final float[] coords = new float[6]; 811 float px, py; 812 813 int nMove = 0; 814 int nLine = 0; 815 int n = 0; 816 817 for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { 818 int type = it.currentSegment(coords); 819 switch (type) { 820 case PathIterator.SEG_MOVETO: 821 if (SHOW_POINTS) { 822 g2d.setColor(COLOR_MOVETO); 823 } 824 break; 825 case PathIterator.SEG_LINETO: 826 case PathIterator.SEG_QUADTO: 827 case PathIterator.SEG_CUBICTO: 828 if (SHOW_POINTS) { 829 g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN); 830 } 831 nLine++; 832 break; 833 case PathIterator.SEG_CLOSE: 834 continue; 835 default: 836 System.out.println("unsupported segment type= " + type); 837 continue; 838 } 839 px = coords[0]; 840 py = coords[1]; 841 842 if (SHOW_INFO) { 843 System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py); 844 } 845 846 if (SHOW_POINTS) { 847 ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS, 848 POINT_RADIUS * 2f, POINT_RADIUS * 2f); 849 g2d.fill(ELL_POINT); 850 } 851 } 852 if (SHOW_INFO) { 853 System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine); 854 System.out.println("--------------------------------------------------"); 855 } 856 857 g2d.setStroke(oldStroke); 858 g2d.setColor(oldColor); 859 } 860 861 private static void dumpShape(final Shape shape) { 862 final float[] coords = new float[6]; 863 864 for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { 865 final int type = it.currentSegment(coords); 866 switch (type) { 867 case PathIterator.SEG_MOVETO: 868 System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");"); 869 break; 870 case PathIterator.SEG_LINETO: 871 System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");"); 872 break; 873 case PathIterator.SEG_QUADTO: 874 System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");"); 875 break; 876 case PathIterator.SEG_CUBICTO: 877 System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");"); 878 break; 879 case PathIterator.SEG_CLOSE: 880 System.out.println("p2d.closePath();"); 881 break; 882 default: 883 System.out.println("// Unsupported segment type= " + type); 884 } 885 } 886 System.out.println("--------------------------------------------------"); 887 } 888 889 static double randX() { 890 return RANDOM.nextDouble() * RANDW + OFFW; 891 } 892 893 static double randY() { 894 return RANDOM.nextDouble() * RANDH + OFFH; 895 } 896 897 private static BasicStroke createStroke(final TestSetup ts) { 898 return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f); 899 } 900 901 private final static class TestSetup { 902 903 static final AtomicInteger COUNT = new AtomicInteger(); 904 905 final int id; 906 final ShapeMode shapeMode; 907 final boolean closed; 908 // stroke 909 final float strokeWidth; 910 final int strokeCap; 911 final int strokeJoin; 912 final float[] dashes; 913 // fill 914 final int windingRule; 915 916 TestSetup(ShapeMode shapeMode, final boolean closed, 917 final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) { 918 this.id = COUNT.incrementAndGet(); 919 this.shapeMode = shapeMode; 920 this.closed = closed; 921 this.strokeWidth = strokeWidth; 922 this.strokeCap = strokeCap; 923 this.strokeJoin = strokeJoin; 924 this.dashes = dashes; 925 this.windingRule = Path2D.WIND_NON_ZERO; 926 } 927 928 TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) { 929 this.id = COUNT.incrementAndGet(); 930 this.shapeMode = shapeMode; 931 this.closed = closed; 932 this.strokeWidth = 0f; 933 this.strokeCap = this.strokeJoin = -1; // invalid 934 this.dashes = null; 935 this.windingRule = windingRule; 936 } 937 938 boolean isStroke() { 939 return this.strokeWidth > 0f; 940 } 941 942 @Override 943 public String toString() { 944 if (isStroke()) { 945 return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed 946 + ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin) 947 + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "") 948 + '}'; 949 } 950 return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed 951 + ", fill" 952 + ", windingRule=" + getWindingRule(windingRule) + '}'; 953 } 954 955 private static String getCap(final int cap) { 956 switch (cap) { 957 case BasicStroke.CAP_BUTT: 958 return "CAP_BUTT"; 959 case BasicStroke.CAP_ROUND: 960 return "CAP_ROUND"; 961 case BasicStroke.CAP_SQUARE: 962 return "CAP_SQUARE"; 963 default: 964 return ""; 965 } 966 967 } 968 969 private static String getJoin(final int join) { 970 switch (join) { 971 case BasicStroke.JOIN_MITER: 972 return "JOIN_MITER"; 973 case BasicStroke.JOIN_ROUND: 974 return "JOIN_ROUND"; 975 case BasicStroke.JOIN_BEVEL: 976 return "JOIN_BEVEL"; 977 default: 978 return ""; 979 } 980 981 } 982 983 private static String getWindingRule(final int rule) { 984 switch (rule) { 985 case PathIterator.WIND_EVEN_ODD: 986 return "WIND_EVEN_ODD"; 987 case PathIterator.WIND_NON_ZERO: 988 return "WIND_NON_ZERO"; 989 default: 990 return ""; 991 } 992 } 993 } 994 995 // --- utilities --- 996 private static final int DCM_ALPHA_MASK = 0xff000000; 997 998 public static BufferedImage newImage(final int w, final int h) { 999 return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); 1000 } 1001 1002 public static BufferedImage computeDiffImage(final DiffContext testCtx, 1003 final DiffContext testThCtx, 1004 final BufferedImage tstImage, 1005 final BufferedImage refImage, 1006 final BufferedImage diffImage) { 1007 1008 final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData(); 1009 final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData(); 1010 final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData(); 1011 1012 // reset diff contexts: 1013 testCtx.reset(); 1014 testThCtx.reset(); 1015 1016 int ref, tst, dg, v; 1017 for (int i = 0, len = aRefPix.length; i < len; i++) { 1018 ref = aRefPix[i]; 1019 tst = aTstPix[i]; 1020 1021 // grayscale diff: 1022 dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst)); 1023 1024 // max difference on grayscale values: 1025 v = (int) Math.ceil(Math.abs(dg / 3.0)); 1026 if (v <= THRESHOLD_DELTA) { 1027 aDifPix[i] = 0; 1028 } else { 1029 aDifPix[i] = toInt(v, v, v); 1030 testThCtx.add(v); 1031 } 1032 1033 if (v != 0) { 1034 testCtx.add(v); 1035 } 1036 } 1037 1038 testCtx.addNbPix(testThCtx.histPix.count); 1039 1040 if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) { 1041 return null; 1042 } 1043 1044 return diffImage; 1045 } 1046 1047 static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException { 1048 final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG"); 1049 if (itWriters.hasNext()) { 1050 final ImageWriter writer = itWriters.next(); 1051 1052 final ImageWriteParam writerParams = writer.getDefaultWriteParam(); 1053 writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED); 1054 1055 final File imgFile = new File(resDirectory, imageFileName); 1056 1057 if (!imgFile.exists() || imgFile.canWrite()) { 1058 System.out.println("saveImage: saving image as PNG [" + imgFile + "]..."); 1059 imgFile.delete(); 1060 1061 // disable cache in temporary files: 1062 ImageIO.setUseCache(false); 1063 1064 final long start = System.nanoTime(); 1065 1066 // PNG uses already buffering: 1067 final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile)); 1068 1069 writer.setOutput(imgOutStream); 1070 try { 1071 writer.write(null, new IIOImage(image, null, null), writerParams); 1072 } finally { 1073 imgOutStream.close(); 1074 1075 final long time = System.nanoTime() - start; 1076 System.out.println("saveImage: duration= " + (time / 1000000l) + " ms."); 1077 } 1078 } 1079 } 1080 } 1081 1082 static int r(final int v) { 1083 return (v >> 16 & 0xff); 1084 } 1085 1086 static int g(final int v) { 1087 return (v >> 8 & 0xff); 1088 } 1089 1090 static int b(final int v) { 1091 return (v & 0xff); 1092 } 1093 1094 static int clamp127(final int v) { 1095 return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255; 1096 } 1097 1098 static int toInt(final int r, final int g, final int b) { 1099 return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b; 1100 } 1101 1102 /* stats */ 1103 static class StatInteger { 1104 1105 public final String name; 1106 public long count = 0l; 1107 public long sum = 0l; 1108 public long min = Integer.MAX_VALUE; 1109 public long max = Integer.MIN_VALUE; 1110 1111 StatInteger(String name) { 1112 this.name = name; 1113 } 1114 1115 void reset() { 1116 count = 0l; 1117 sum = 0l; 1118 min = Integer.MAX_VALUE; 1119 max = Integer.MIN_VALUE; 1120 } 1121 1122 void add(int val) { 1123 count++; 1124 sum += val; 1125 if (val < min) { 1126 min = val; 1127 } 1128 if (val > max) { 1129 max = val; 1130 } 1131 } 1132 1133 void add(long val) { 1134 count++; 1135 sum += val; 1136 if (val < min) { 1137 min = val; 1138 } 1139 if (val > max) { 1140 max = val; 1141 } 1142 } 1143 1144 void add(StatInteger stat) { 1145 count += stat.count; 1146 sum += stat.sum; 1147 if (stat.min < min) { 1148 min = stat.min; 1149 } 1150 if (stat.max > max) { 1151 max = stat.max; 1152 } 1153 } 1154 1155 public final double average() { 1156 return ((double) sum) / count; 1157 } 1158 1159 @Override 1160 public String toString() { 1161 final StringBuilder sb = new StringBuilder(128); 1162 toString(sb); 1163 return sb.toString(); 1164 } 1165 1166 public final StringBuilder toString(final StringBuilder sb) { 1167 sb.append(name).append("[n: ").append(count); 1168 sb.append("] "); 1169 if (count != 0) { 1170 sb.append("sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average())); 1171 sb.append(" [").append(min).append(" | ").append(max).append("]"); 1172 } 1173 return sb; 1174 } 1175 1176 } 1177 1178 final static class Histogram extends StatInteger { 1179 1180 static final int BUCKET = 2; 1181 static final int MAX = 20; 1182 static final int LAST = MAX - 1; 1183 static final int[] STEPS = new int[MAX]; 1184 static final int BUCKET_TH; 1185 1186 static { 1187 STEPS[0] = 0; 1188 STEPS[1] = 1; 1189 1190 for (int i = 2; i < MAX; i++) { 1191 STEPS[i] = STEPS[i - 1] * BUCKET; 1192 } 1193 // System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS)); 1194 1195 if (THRESHOLD_DELTA % 2 != 0) { 1196 throw new IllegalStateException("THRESHOLD_DELTA must be odd"); 1197 } 1198 1199 BUCKET_TH = bucket(THRESHOLD_DELTA); 1200 } 1201 1202 static int bucket(int val) { 1203 for (int i = 1; i < MAX; i++) { 1204 if (val < STEPS[i]) { 1205 return i - 1; 1206 } 1207 } 1208 return LAST; 1209 } 1210 1211 private final StatInteger[] stats = new StatInteger[MAX]; 1212 1213 public Histogram(String name) { 1214 super(name); 1215 for (int i = 0; i < MAX; i++) { 1216 stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~"))); 1217 } 1218 } 1219 1220 @Override 1221 final void reset() { 1222 super.reset(); 1223 for (int i = 0; i < MAX; i++) { 1224 stats[i].reset(); 1225 } 1226 } 1227 1228 @Override 1229 final void add(int val) { 1230 super.add(val); 1231 stats[bucket(val)].add(val); 1232 } 1233 1234 @Override 1235 final void add(long val) { 1236 add((int) val); 1237 } 1238 1239 void add(Histogram hist) { 1240 super.add(hist); 1241 for (int i = 0; i < MAX; i++) { 1242 stats[i].add(hist.stats[i]); 1243 } 1244 } 1245 1246 boolean isWorse(Histogram hist, boolean useTh) { 1247 boolean worst = false; 1248 if (!useTh && (hist.sum > sum)) { 1249 worst = true; 1250 } else { 1251 long sumLoc = 0l; 1252 long sumHist = 0l; 1253 // use running sum: 1254 for (int i = MAX - 1; i >= BUCKET_TH; i--) { 1255 sumLoc += stats[i].sum; 1256 sumHist += hist.stats[i].sum; 1257 } 1258 if (sumHist > sumLoc) { 1259 worst = true; 1260 } 1261 } 1262 /* 1263 System.out.println("running sum worst:"); 1264 System.out.println("this ? " + toString()); 1265 System.out.println("worst ? " + hist.toString()); 1266 */ 1267 return worst; 1268 } 1269 1270 @Override 1271 public final String toString() { 1272 final StringBuilder sb = new StringBuilder(2048); 1273 super.toString(sb).append(" { "); 1274 1275 for (int i = 0; i < MAX; i++) { 1276 if (stats[i].count != 0l) { 1277 sb.append("\n ").append(stats[i].toString()); 1278 } 1279 } 1280 1281 return sb.append(" }").toString(); 1282 } 1283 } 1284 1285 /** 1286 * Adjust the given double value to keep only 3 decimal digits 1287 * @param value value to adjust 1288 * @return double value with only 3 decimal digits 1289 */ 1290 static double trimTo3Digits(final double value) { 1291 return ((long) (1e3d * value)) / 1e3d; 1292 } 1293 1294 static final class DiffContext { 1295 1296 public final Histogram histPix; 1297 1298 public final StatInteger nbPix; 1299 1300 DiffContext(String name) { 1301 histPix = new Histogram("Diff Pixels [" + name + "]"); 1302 nbPix = new StatInteger("NbPixels [" + name + "]"); 1303 } 1304 1305 void reset() { 1306 histPix.reset(); 1307 nbPix.reset(); 1308 } 1309 1310 void dump() { 1311 if (isDiff()) { 1312 System.out.println("Differences [" + histPix.name + "]:\n" 1313 + ((nbPix.count != 0) ? (nbPix.toString() + "\n") : "") 1314 + histPix.toString() 1315 ); 1316 } else { 1317 System.out.println("No difference for [" + histPix.name + "]."); 1318 } 1319 } 1320 1321 void add(int val) { 1322 histPix.add(val); 1323 } 1324 1325 void add(DiffContext ctx) { 1326 histPix.add(ctx.histPix); 1327 if (ctx.nbPix.count != 0L) { 1328 nbPix.add(ctx.nbPix); 1329 } 1330 } 1331 1332 void addNbPix(long val) { 1333 if (val != 0L) { 1334 nbPix.add(val); 1335 } 1336 } 1337 1338 void set(DiffContext ctx) { 1339 reset(); 1340 add(ctx); 1341 } 1342 1343 boolean isWorse(DiffContext ctx, boolean useTh) { 1344 return histPix.isWorse(ctx.histPix, useTh); 1345 } 1346 1347 boolean isDiff() { 1348 return histPix.sum != 0l; 1349 } 1350 } 1351 1352 1353 static double linelen(final double x0, final double y0, 1354 final double x1, final double y1) 1355 { 1356 final double dx = x1 - x0; 1357 final double dy = y1 - y0; 1358 return Math.sqrt(dx * dx + dy * dy); 1359 } 1360 1361 static double quadlen(final double x0, final double y0, 1362 final double x1, final double y1, 1363 final double x2, final double y2) 1364 { 1365 return (linelen(x0, y0, x1, y1) 1366 + linelen(x1, y1, x2, y2) 1367 + linelen(x0, y0, x2, y2)) / 2.0d; 1368 } 1369 1370 static double curvelen(final double x0, final double y0, 1371 final double x1, final double y1, 1372 final double x2, final double y2, 1373 final double x3, final double y3) 1374 { 1375 return (linelen(x0, y0, x1, y1) 1376 + linelen(x1, y1, x2, y2) 1377 + linelen(x2, y2, x3, y3) 1378 + linelen(x0, y0, x3, y3)) / 2.0d; 1379 } 1380 }