/* * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.Shape; import java.awt.geom.CubicCurve2D; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.QuadCurve2D; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Iterator; import java.util.Locale; import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Handler; import java.util.logging.LogRecord; import java.util.logging.Logger; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; /** * @test * @bug 8191814 * @summary Verifies that Marlin rendering generates the same * images with and without clipping optimization with all possible * stroke (cap/join) and/or dashes or fill modes (EO rules) * for paths made of either 9 lines, 4 quads, 2 cubics (random) * Note: Use the argument -slow to run more intensive tests (too much time) * * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -poly -doDash * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.MarlinRenderingEngine ClipShapeTest -cubic -doDash * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly -doDash * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic -doDash */ public final class ClipShapeTest { // test options: static int NUM_TESTS; // shape settings: static ShapeMode SHAPE_MODE; static boolean USE_DASHES; static boolean USE_VAR_STROKE; static int THRESHOLD_DELTA; static long THRESHOLD_NBPIX; // constants: static final boolean DO_FAIL = Boolean.valueOf(System.getProperty("ClipShapeTest.fail", "true")); static final boolean TEST_STROKER = true; static final boolean TEST_FILLER = true; static final boolean SUBDIVIDE_CURVE = true; static final double SUBDIVIDE_LEN_TH = 50.0; static final boolean TRACE_SUBDIVIDE_CURVE = false; static final int TESTW = 100; static final int TESTH = 100; // dump path on console: static final boolean DUMP_SHAPE = true; static final boolean SHOW_DETAILS = false; // disabled static final boolean SHOW_OUTLINE = true; static final boolean SHOW_POINTS = true; static final boolean SHOW_INFO = false; static final int MAX_SHOW_FRAMES = 10; static final int MAX_SAVE_FRAMES = 100; // use fixed seed to reproduce always same polygons between tests static final boolean FIXED_SEED = true; static final double RAND_SCALE = 3.0; static final double RANDW = TESTW * RAND_SCALE; static final double OFFW = (TESTW - RANDW) / 2.0; static final double RANDH = TESTH * RAND_SCALE; static final double OFFH = (TESTH - RANDH) / 2.0; static enum ShapeMode { TWO_CUBICS, FOUR_QUADS, FIVE_LINE_POLYS, NINE_LINE_POLYS, FIFTY_LINE_POLYS, MIXED } static final long SEED = 1666133789L; // Fixed seed to avoid any difference between runs: static final Random RANDOM = new Random(SEED); static final File OUTPUT_DIR = new File("."); static final AtomicBoolean isMarlin = new AtomicBoolean(); static final AtomicBoolean isMarlinFloat = new AtomicBoolean(); static final AtomicBoolean isClipRuntime = new AtomicBoolean(); static { Locale.setDefault(Locale.US); // FIRST: Get Marlin runtime state from its log: // initialize j.u.l Looger: final Logger log = Logger.getLogger("sun.java2d.marlin"); log.addHandler(new Handler() { @Override public void publish(LogRecord record) { final String msg = record.getMessage(); if (msg != null) { // last space to avoid matching other settings: if (msg.startsWith("sun.java2d.renderer ")) { isMarlin.set(msg.contains("MarlinRenderingEngine")); isMarlinFloat.set(!msg.contains("DMarlinRenderingEngine")); } if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) { isClipRuntime.set(msg.contains("true")); } } final Throwable th = record.getThrown(); // detect any Throwable: if (th != null) { System.out.println("Test failed:\n" + record.getMessage()); th.printStackTrace(System.out); throw new RuntimeException("Test failed: ", th); } } @Override public void flush() { } @Override public void close() throws SecurityException { } }); // enable Marlin logging & internal checks: System.setProperty("sun.java2d.renderer.log", "true"); System.setProperty("sun.java2d.renderer.useLogger", "true"); // disable static clipping setting: System.setProperty("sun.java2d.renderer.clip", "false"); System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true"); // enable subdivider: System.setProperty("sun.java2d.renderer.clip.subdivider", "true"); // disable min length check: always subdivide curves at clip edges System.setProperty("sun.java2d.renderer.clip.subdivider.minLength", "-1"); // If any curve, increase curve accuracy: // curve length max error: System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4"); // cubic min/max error: System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3"); System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4"); // quad max error: System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4"); } private static void resetOptions() { NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 5000); // shape settings: SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; USE_DASHES = false; USE_VAR_STROKE = false; } /** * Test * @param args */ public static void main(String[] args) { { // Bootstrap: init Renderer now: final BufferedImage img = newImage(TESTW, TESTH); final Graphics2D g2d = initialize(img, null); try { paintShape(new Line2D.Double(0,0,100,100), g2d, true, false); } finally { g2d.dispose(); } if (!isMarlin.get()) { throw new RuntimeException("Marlin renderer not used at runtime !"); } if (!isClipRuntime.get()) { throw new RuntimeException("Marlin clipping not enabled at runtime !"); } } System.out.println("---------------------------------------"); System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH); resetOptions(); boolean runSlowTests = false; for (String arg : args) { if ("-slow".equals(arg)) { runSlowTests = true; } else if ("-doDash".equals(arg)) { USE_DASHES = true; } else if ("-doVarStroke".equals(arg)) { USE_VAR_STROKE = true; } else { // shape mode: if (arg.equalsIgnoreCase("-poly")) { SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; } else if (arg.equalsIgnoreCase("-bigpoly")) { SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS; } else if (arg.equalsIgnoreCase("-quad")) { SHAPE_MODE = ShapeMode.FOUR_QUADS; } else if (arg.equalsIgnoreCase("-cubic")) { SHAPE_MODE = ShapeMode.TWO_CUBICS; } else if (arg.equalsIgnoreCase("-mixed")) { SHAPE_MODE = ShapeMode.MIXED; } } } System.out.println("Shape mode: " + SHAPE_MODE); // adjust image comparison thresholds: switch (SHAPE_MODE) { case TWO_CUBICS: // Define uncertainty for curves: THRESHOLD_DELTA = 32; THRESHOLD_NBPIX = (USE_DASHES) ? 50 : 200; if (SUBDIVIDE_CURVE) { THRESHOLD_NBPIX = 4; } break; case FOUR_QUADS: case MIXED: // Define uncertainty for quads: // curve subdivision causes curves to be smaller // then curve offsets are different (more accurate) THRESHOLD_DELTA = 64; THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 420; if (SUBDIVIDE_CURVE) { THRESHOLD_NBPIX = 10; } break; default: // Define uncertainty for lines: // float variant have higher uncertainty THRESHOLD_DELTA = 2; THRESHOLD_NBPIX = (USE_DASHES) ? // float variant have higher uncertainty ((isMarlinFloat.get()) ? 30 : 6) // low for double : (isMarlinFloat.get()) ? 10 : 0; } // Visual inspection (low threshold): // THRESHOLD_NBPIX = 2; System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA); System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX); if (runSlowTests) { NUM_TESTS = 10000; // or 100000 (very slow) USE_VAR_STROKE = true; } System.out.println("NUM_TESTS: " + NUM_TESTS); if (USE_DASHES) { System.out.println("USE_DASHES: enabled."); } if (USE_VAR_STROKE) { System.out.println("USE_VAR_STROKE: enabled."); } if (!DO_FAIL) { System.out.println("DO_FAIL: disabled."); } System.out.println("---------------------------------------"); final DiffContext allCtx = new DiffContext("All Test setups"); final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)"); int failures = 0; final long start = System.nanoTime(); try { if (TEST_STROKER) { final float[][] dashArrays = (USE_DASHES) ? // small // new float[][]{new float[]{1f, 2f}} // normal new float[][]{new float[]{13f, 7f}} // large (prime) // new float[][]{new float[]{41f, 7f}} // none : new float[][]{null}; System.out.println("dashes: " + Arrays.deepToString(dashArrays)); final float[] strokeWidths = (USE_VAR_STROKE) ? new float[5] : new float[]{10f}; int nsw = 0; if (USE_VAR_STROKE) { for (float width = 0.25f; width < 110f; width *= 5f) { strokeWidths[nsw++] = width; } } else { nsw = 1; } System.out.println("stroke widths: " + Arrays.toString(strokeWidths)); // Stroker tests: for (int w = 0; w < nsw; w++) { final float width = strokeWidths[w]; for (float[] dashes : dashArrays) { for (int cap = 0; cap <= 2; cap++) { for (int join = 0; join <= 2; join++) { failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes)); } } } } } if (TEST_FILLER) { // Filler tests: failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD)); failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD)); } } catch (IOException ioe) { throw new RuntimeException(ioe); } System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); allWorstCtx.dump(); allCtx.dump(); if (DO_FAIL && (failures != 0)) { throw new RuntimeException("Clip test failures : " + failures); } } static int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException { final long start = System.nanoTime(); if (FIXED_SEED) { // Reset seed for random numbers: RANDOM.setSeed(SEED); } System.out.println("paintPaths: " + NUM_TESTS + " paths (" + SHAPE_MODE + ") - setup: " + ts); final boolean fill = !ts.isStroke(); final Path2D p2d = new Path2D.Double(ts.windingRule); final Stroke stroke = (!fill) ? createStroke(ts) : null; final BufferedImage imgOn = newImage(TESTW, TESTH); final Graphics2D g2dOn = initialize(imgOn, stroke); final BufferedImage imgOff = newImage(TESTW, TESTH); final Graphics2D g2dOff = initialize(imgOff, stroke); final BufferedImage imgDiff = newImage(TESTW, TESTH); final DiffContext testSetupCtx = new DiffContext("Test setup"); final DiffContext testWorstCtx = new DiffContext("Worst"); final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)"); int nd = 0; try { final DiffContext testCtx = new DiffContext("Test"); final DiffContext testThCtx = new DiffContext("Test(>threshold)"); BufferedImage diffImage; for (int n = 0; n < NUM_TESTS; n++) { genShape(p2d, ts); // Runtime clip setting OFF: paintShape(p2d, g2dOff, fill, false); // Runtime clip setting ON: paintShape(p2d, g2dOn, fill, true); /* compute image difference if possible */ diffImage = computeDiffImage(testCtx, testThCtx, imgOn, imgOff, imgDiff); // Worst (total) if (testCtx.isDiff()) { if (testWorstCtx.isWorse(testCtx, false)) { testWorstCtx.set(testCtx); } if (testWorstThCtx.isWorse(testCtx, true)) { testWorstThCtx.set(testCtx); } // accumulate data: testSetupCtx.add(testCtx); } if (diffImage != null) { nd++; testThCtx.dump(); testCtx.dump(); if (nd < MAX_SHOW_FRAMES) { if (SHOW_DETAILS) { paintShapeDetails(g2dOff, p2d); paintShapeDetails(g2dOn, p2d); } if (nd < MAX_SAVE_FRAMES) { if (DUMP_SHAPE) { dumpShape(p2d); } final String testName = "Setup_" + ts.id + "_test_" + n; saveImage(imgOff, OUTPUT_DIR, testName + "-off.png"); saveImage(imgOn, OUTPUT_DIR, testName + "-on.png"); saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png"); } } } } } finally { g2dOff.dispose(); g2dOn.dispose(); if (nd != 0) { System.out.println("paintPaths: " + NUM_TESTS + " paths - " + "Number of differences = " + nd + " ratio = " + (100f * nd) / NUM_TESTS + " %"); } if (testWorstCtx.isDiff()) { testWorstCtx.dump(); if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) { testWorstThCtx.dump(); } if (allWorstCtx.isWorse(testWorstThCtx, true)) { allWorstCtx.set(testWorstThCtx); } } testSetupCtx.dump(); // accumulate data: allCtx.add(testSetupCtx); } System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); return nd; } private static void paintShape(final Shape p2d, final Graphics2D g2d, final boolean fill, final boolean clip) { reset(g2d); setClip(g2d, clip); if (fill) { g2d.fill(p2d); } else { g2d.draw(p2d); } } private static Graphics2D initialize(final BufferedImage img, final Stroke s) { final Graphics2D g2d = (Graphics2D) img.getGraphics(); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, // Test normalize: // RenderingHints.VALUE_STROKE_NORMALIZE RenderingHints.VALUE_STROKE_PURE ); if (s != null) { g2d.setStroke(s); } g2d.setColor(Color.BLACK); return g2d; } private static void reset(final Graphics2D g2d) { // Disable antialiasing: g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g2d.setBackground(Color.WHITE); g2d.clearRect(0, 0, TESTW, TESTH); } private static void setClip(final Graphics2D g2d, final boolean clip) { // Enable antialiasing: g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Enable or Disable clipping: System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false"); } static void genShape(final Path2D p2d, final TestSetup ts) { p2d.reset(); /* Test closed path: 0: moveTo + (draw)To + closePath 1: (draw)To + closePath (closePath + (draw)To sequence) */ final int end = (ts.closed) ? 2 : 1; final double[] in = new double[8]; double sx0 = 0.0, sy0 = 0.0, x0 = 0.0, y0 = 0.0; for (int p = 0; p < end; p++) { if (p <= 0) { x0 = randX(); y0 = randY(); p2d.moveTo(x0, y0); sx0 = x0; sy0 = y0; } switch (ts.shapeMode) { case MIXED: case FIVE_LINE_POLYS: case NINE_LINE_POLYS: case FIFTY_LINE_POLYS: p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY()); x0 = randX(); y0 = randY(); p2d.lineTo(x0, y0); if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) { // And an implicit close makes 5 lines break; } p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY()); p2d.lineTo(randX(), randY()); x0 = randX(); y0 = randY(); p2d.lineTo(x0, y0); if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) { // And an implicit close makes 9 lines break; } if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) { for (int i = 0; i < 41; i++) { x0 = randX(); y0 = randY(); p2d.lineTo(x0, y0); } // And an implicit close makes 50 lines break; } case TWO_CUBICS: if (SUBDIVIDE_CURVE) { in[0] = x0; in[1] = y0; in[2] = randX(); in[3] = randY(); in[4] = randX(); in[5] = randY(); x0 = randX(); y0 = randY(); in[6] = x0; in[7] = y0; subdivide(p2d, 8, in); in[0] = x0; in[1] = y0; in[2] = randX(); in[3] = randY(); in[4] = randX(); in[5] = randY(); x0 = randX(); y0 = randY(); in[6] = x0; in[7] = y0; subdivide(p2d, 8, in); } else { x0 = randX(); y0 = randY(); p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0); x0 = randX(); y0 = randY(); p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0); } if (ts.shapeMode == ShapeMode.TWO_CUBICS) { break; } case FOUR_QUADS: if (SUBDIVIDE_CURVE) { in[0] = x0; in[1] = y0; in[2] = randX(); in[3] = randY(); x0 = randX(); y0 = randY(); in[4] = x0; in[5] = y0; subdivide(p2d, 6, in); in[0] = x0; in[1] = y0; in[2] = randX(); in[3] = randY(); x0 = randX(); y0 = randY(); in[4] = x0; in[5] = y0; subdivide(p2d, 6, in); in[0] = x0; in[1] = y0; in[2] = randX(); in[3] = randY(); x0 = randX(); y0 = randY(); in[4] = x0; in[5] = y0; subdivide(p2d, 6, in); in[0] = x0; in[1] = y0; in[2] = randX(); in[3] = randY(); x0 = randX(); y0 = randY(); in[4] = x0; in[5] = y0; subdivide(p2d, 6, in); } else { x0 = randX(); y0 = randY(); p2d.quadTo(randX(), randY(), x0, y0); x0 = randX(); y0 = randY(); p2d.quadTo(randX(), randY(), x0, y0); x0 = randX(); y0 = randY(); p2d.quadTo(randX(), randY(), x0, y0); x0 = randX(); y0 = randY(); p2d.quadTo(randX(), randY(), x0, y0); } if (ts.shapeMode == ShapeMode.FOUR_QUADS) { break; } default: } if (ts.closed) { p2d.closePath(); x0 = sx0; y0 = sy0; } } } static final int SUBDIVIDE_LIMIT = 5; static final double[][] SUBDIVIDE_CURVES = new double[SUBDIVIDE_LIMIT + 1][]; static { for (int i = 0, n = 1; i < SUBDIVIDE_LIMIT; i++, n *= 2) { SUBDIVIDE_CURVES[i] = new double[8 * n]; } } static void subdivide(final Path2D p2d, final int type, final double[] in) { if (TRACE_SUBDIVIDE_CURVE) { System.out.println("subdivide: " + Arrays.toString(Arrays.copyOf(in, type))); } double curveLen = ((type == 8) ? curvelen(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7]) : quadlen(in[0], in[1], in[2], in[3], in[4], in[5])); if (curveLen > SUBDIVIDE_LEN_TH) { if (TRACE_SUBDIVIDE_CURVE) { System.out.println("curvelen: " + curveLen); } System.arraycopy(in, 0, SUBDIVIDE_CURVES[0], 0, 8); int level = 0; while (curveLen >= SUBDIVIDE_LEN_TH) { level++; curveLen /= 2.0; if (TRACE_SUBDIVIDE_CURVE) { System.out.println("curvelen: " + curveLen); } } if (TRACE_SUBDIVIDE_CURVE) { System.out.println("level: " + level); } if (level > SUBDIVIDE_LIMIT) { if (TRACE_SUBDIVIDE_CURVE) { System.out.println("max level reached : " + level); } level = SUBDIVIDE_LIMIT; } for (int l = 0; l < level; l++) { if (TRACE_SUBDIVIDE_CURVE) { System.out.println("level: " + l); } double[] src = SUBDIVIDE_CURVES[l]; double[] dst = SUBDIVIDE_CURVES[l + 1]; for (int i = 0, j = 0; i < src.length; i += 8, j += 16) { if (TRACE_SUBDIVIDE_CURVE) { System.out.println("subdivide: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type))); } if (type == 8) { CubicCurve2D.subdivide(src, i, dst, j, dst, j + 8); } else { QuadCurve2D.subdivide(src, i, dst, j, dst, j + 8); } if (TRACE_SUBDIVIDE_CURVE) { System.out.println("left: " + Arrays.toString(Arrays.copyOfRange(dst, j, j + type))); System.out.println("right: " + Arrays.toString(Arrays.copyOfRange(dst, j + 8, j + 8 + type))); } } } // Emit curves at last level: double[] src = SUBDIVIDE_CURVES[level]; double len = 0.0; for (int i = 0; i < src.length; i += 8) { if (TRACE_SUBDIVIDE_CURVE) { System.out.println("curve: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type))); } if (type == 8) { if (TRACE_SUBDIVIDE_CURVE) { 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]); } p2d.curveTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]); } else { if (TRACE_SUBDIVIDE_CURVE) { len += quadlen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5]); } p2d.quadTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5]); } } if (TRACE_SUBDIVIDE_CURVE) { System.out.println("curveLen (final) = " + len); } } else { if (type == 8) { p2d.curveTo(in[2], in[3], in[4], in[5], in[6], in[7]); } else { p2d.quadTo(in[2], in[3], in[4], in[5]); } } } static final float POINT_RADIUS = 2f; static final float LINE_WIDTH = 1f; static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH); static final int COLOR_ALPHA = 128; static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA); static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA); static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA); static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float(); private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) { final Stroke oldStroke = g2d.getStroke(); final Color oldColor = g2d.getColor(); setClip(g2d, false); if (SHOW_OUTLINE) { g2d.setStroke(OUTLINE_STROKE); g2d.setColor(COLOR_LINETO_ODD); g2d.draw(shape); } final float[] coords = new float[6]; float px, py; int nMove = 0; int nLine = 0; int n = 0; for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { int type = it.currentSegment(coords); switch (type) { case PathIterator.SEG_MOVETO: if (SHOW_POINTS) { g2d.setColor(COLOR_MOVETO); } break; case PathIterator.SEG_LINETO: case PathIterator.SEG_QUADTO: case PathIterator.SEG_CUBICTO: if (SHOW_POINTS) { g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN); } nLine++; break; case PathIterator.SEG_CLOSE: continue; default: System.out.println("unsupported segment type= " + type); continue; } px = coords[0]; py = coords[1]; if (SHOW_INFO) { System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py); } if (SHOW_POINTS) { ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS, POINT_RADIUS * 2f, POINT_RADIUS * 2f); g2d.fill(ELL_POINT); } } if (SHOW_INFO) { System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine); System.out.println("--------------------------------------------------"); } g2d.setStroke(oldStroke); g2d.setColor(oldColor); } private static void dumpShape(final Shape shape) { final float[] coords = new float[6]; for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { final int type = it.currentSegment(coords); switch (type) { case PathIterator.SEG_MOVETO: System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");"); break; case PathIterator.SEG_LINETO: System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");"); break; case PathIterator.SEG_QUADTO: System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");"); break; case PathIterator.SEG_CUBICTO: System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");"); break; case PathIterator.SEG_CLOSE: System.out.println("p2d.closePath();"); break; default: System.out.println("// Unsupported segment type= " + type); } } System.out.println("--------------------------------------------------"); } static double randX() { return RANDOM.nextDouble() * RANDW + OFFW; } static double randY() { return RANDOM.nextDouble() * RANDH + OFFH; } private static BasicStroke createStroke(final TestSetup ts) { return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f); } private final static class TestSetup { static final AtomicInteger COUNT = new AtomicInteger(); final int id; final ShapeMode shapeMode; final boolean closed; // stroke final float strokeWidth; final int strokeCap; final int strokeJoin; final float[] dashes; // fill final int windingRule; TestSetup(ShapeMode shapeMode, final boolean closed, final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) { this.id = COUNT.incrementAndGet(); this.shapeMode = shapeMode; this.closed = closed; this.strokeWidth = strokeWidth; this.strokeCap = strokeCap; this.strokeJoin = strokeJoin; this.dashes = dashes; this.windingRule = Path2D.WIND_NON_ZERO; } TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) { this.id = COUNT.incrementAndGet(); this.shapeMode = shapeMode; this.closed = closed; this.strokeWidth = 0f; this.strokeCap = this.strokeJoin = -1; // invalid this.dashes = null; this.windingRule = windingRule; } boolean isStroke() { return this.strokeWidth > 0f; } @Override public String toString() { if (isStroke()) { return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed + ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin) + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "") + '}'; } return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed + ", fill" + ", windingRule=" + getWindingRule(windingRule) + '}'; } private static String getCap(final int cap) { switch (cap) { case BasicStroke.CAP_BUTT: return "CAP_BUTT"; case BasicStroke.CAP_ROUND: return "CAP_ROUND"; case BasicStroke.CAP_SQUARE: return "CAP_SQUARE"; default: return ""; } } private static String getJoin(final int join) { switch (join) { case BasicStroke.JOIN_MITER: return "JOIN_MITER"; case BasicStroke.JOIN_ROUND: return "JOIN_ROUND"; case BasicStroke.JOIN_BEVEL: return "JOIN_BEVEL"; default: return ""; } } private static String getWindingRule(final int rule) { switch (rule) { case PathIterator.WIND_EVEN_ODD: return "WIND_EVEN_ODD"; case PathIterator.WIND_NON_ZERO: return "WIND_NON_ZERO"; default: return ""; } } } // --- utilities --- private static final int DCM_ALPHA_MASK = 0xff000000; public static BufferedImage newImage(final int w, final int h) { return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); } public static BufferedImage computeDiffImage(final DiffContext testCtx, final DiffContext testThCtx, final BufferedImage tstImage, final BufferedImage refImage, final BufferedImage diffImage) { final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData(); final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData(); final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData(); // reset diff contexts: testCtx.reset(); testThCtx.reset(); int ref, tst, dg, v; for (int i = 0, len = aRefPix.length; i < len; i++) { ref = aRefPix[i]; tst = aTstPix[i]; // grayscale diff: dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst)); // max difference on grayscale values: v = (int) Math.ceil(Math.abs(dg / 3.0)); if (v <= THRESHOLD_DELTA) { aDifPix[i] = 0; } else { aDifPix[i] = toInt(v, v, v); testThCtx.add(v); } if (v != 0) { testCtx.add(v); } } testCtx.addNbPix(testThCtx.histPix.count); if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) { return null; } return diffImage; } static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException { final Iterator itWriters = ImageIO.getImageWritersByFormatName("PNG"); if (itWriters.hasNext()) { final ImageWriter writer = itWriters.next(); final ImageWriteParam writerParams = writer.getDefaultWriteParam(); writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED); final File imgFile = new File(resDirectory, imageFileName); if (!imgFile.exists() || imgFile.canWrite()) { System.out.println("saveImage: saving image as PNG [" + imgFile + "]..."); imgFile.delete(); // disable cache in temporary files: ImageIO.setUseCache(false); final long start = System.nanoTime(); // PNG uses already buffering: final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile)); writer.setOutput(imgOutStream); try { writer.write(null, new IIOImage(image, null, null), writerParams); } finally { imgOutStream.close(); final long time = System.nanoTime() - start; System.out.println("saveImage: duration= " + (time / 1000000l) + " ms."); } } } } static int r(final int v) { return (v >> 16 & 0xff); } static int g(final int v) { return (v >> 8 & 0xff); } static int b(final int v) { return (v & 0xff); } static int clamp127(final int v) { return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255; } static int toInt(final int r, final int g, final int b) { return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b; } /* stats */ static class StatInteger { public final String name; public long count = 0l; public long sum = 0l; public long min = Integer.MAX_VALUE; public long max = Integer.MIN_VALUE; StatInteger(String name) { this.name = name; } void reset() { count = 0l; sum = 0l; min = Integer.MAX_VALUE; max = Integer.MIN_VALUE; } void add(int val) { count++; sum += val; if (val < min) { min = val; } if (val > max) { max = val; } } void add(long val) { count++; sum += val; if (val < min) { min = val; } if (val > max) { max = val; } } void add(StatInteger stat) { count += stat.count; sum += stat.sum; if (stat.min < min) { min = stat.min; } if (stat.max > max) { max = stat.max; } } public final double average() { return ((double) sum) / count; } @Override public String toString() { final StringBuilder sb = new StringBuilder(128); toString(sb); return sb.toString(); } public final StringBuilder toString(final StringBuilder sb) { sb.append(name).append("[n: ").append(count); sb.append("] "); if (count != 0) { sb.append("sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average())); sb.append(" [").append(min).append(" | ").append(max).append("]"); } return sb; } } final static class Histogram extends StatInteger { static final int BUCKET = 2; static final int MAX = 20; static final int LAST = MAX - 1; static final int[] STEPS = new int[MAX]; static final int BUCKET_TH; static { STEPS[0] = 0; STEPS[1] = 1; for (int i = 2; i < MAX; i++) { STEPS[i] = STEPS[i - 1] * BUCKET; } // System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS)); if (THRESHOLD_DELTA % 2 != 0) { throw new IllegalStateException("THRESHOLD_DELTA must be odd"); } BUCKET_TH = bucket(THRESHOLD_DELTA); } static int bucket(int val) { for (int i = 1; i < MAX; i++) { if (val < STEPS[i]) { return i - 1; } } return LAST; } private final StatInteger[] stats = new StatInteger[MAX]; public Histogram(String name) { super(name); for (int i = 0; i < MAX; i++) { stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~"))); } } @Override final void reset() { super.reset(); for (int i = 0; i < MAX; i++) { stats[i].reset(); } } @Override final void add(int val) { super.add(val); stats[bucket(val)].add(val); } @Override final void add(long val) { add((int) val); } void add(Histogram hist) { super.add(hist); for (int i = 0; i < MAX; i++) { stats[i].add(hist.stats[i]); } } boolean isWorse(Histogram hist, boolean useTh) { boolean worst = false; if (!useTh && (hist.sum > sum)) { worst = true; } else { long sumLoc = 0l; long sumHist = 0l; // use running sum: for (int i = MAX - 1; i >= BUCKET_TH; i--) { sumLoc += stats[i].sum; sumHist += hist.stats[i].sum; } if (sumHist > sumLoc) { worst = true; } } /* System.out.println("running sum worst:"); System.out.println("this ? " + toString()); System.out.println("worst ? " + hist.toString()); */ return worst; } @Override public final String toString() { final StringBuilder sb = new StringBuilder(2048); super.toString(sb).append(" { "); for (int i = 0; i < MAX; i++) { if (stats[i].count != 0l) { sb.append("\n ").append(stats[i].toString()); } } return sb.append(" }").toString(); } } /** * Adjust the given double value to keep only 3 decimal digits * @param value value to adjust * @return double value with only 3 decimal digits */ static double trimTo3Digits(final double value) { return ((long) (1e3d * value)) / 1e3d; } static final class DiffContext { public final Histogram histPix; public final StatInteger nbPix; DiffContext(String name) { histPix = new Histogram("Diff Pixels [" + name + "]"); nbPix = new StatInteger("NbPixels [" + name + "]"); } void reset() { histPix.reset(); nbPix.reset(); } void dump() { if (isDiff()) { System.out.println("Differences [" + histPix.name + "]:\n" + ((nbPix.count != 0) ? (nbPix.toString() + "\n") : "") + histPix.toString() ); } else { System.out.println("No difference for [" + histPix.name + "]."); } } void add(int val) { histPix.add(val); } void add(DiffContext ctx) { histPix.add(ctx.histPix); if (ctx.nbPix.count != 0L) { nbPix.add(ctx.nbPix); } } void addNbPix(long val) { if (val != 0L) { nbPix.add(val); } } void set(DiffContext ctx) { reset(); add(ctx); } boolean isWorse(DiffContext ctx, boolean useTh) { return histPix.isWorse(ctx.histPix, useTh); } boolean isDiff() { return histPix.sum != 0l; } } static double linelen(final double x0, final double y0, final double x1, final double y1) { final double dx = x1 - x0; final double dy = y1 - y0; return Math.sqrt(dx * dx + dy * dy); } static double quadlen(final double x0, final double y0, final double x1, final double y1, final double x2, final double y2) { return (linelen(x0, y0, x1, y1) + linelen(x1, y1, x2, y2) + linelen(x0, y0, x2, y2)) / 2.0d; } static double curvelen(final double x0, final double y0, final double x1, final double y1, final double x2, final double y2, final double x3, final double y3) { return (linelen(x0, y0, x1, y1) + linelen(x1, y1, x2, y2) + linelen(x2, y2, x3, y3) + linelen(x0, y0, x3, y3)) / 2.0d; } }