Path: blob/master/test/jdk/sun/java2d/marlin/ClipShapeTest.java
41149 views
/*1* Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/22import java.awt.BasicStroke;23import java.awt.Color;24import java.awt.Graphics2D;25import java.awt.RenderingHints;26import java.awt.Stroke;27import java.awt.Shape;28import java.awt.geom.CubicCurve2D;29import java.awt.geom.Ellipse2D;30import java.awt.geom.Line2D;31import java.awt.geom.Path2D;32import java.awt.geom.PathIterator;33import java.awt.geom.QuadCurve2D;34import java.awt.image.BufferedImage;35import java.awt.image.DataBufferInt;36import java.io.File;37import java.io.FileOutputStream;38import java.io.IOException;39import java.util.Arrays;40import java.util.Iterator;41import java.util.Locale;42import java.util.Random;43import java.util.concurrent.atomic.AtomicBoolean;44import java.util.concurrent.atomic.AtomicInteger;45import java.util.logging.Handler;46import java.util.logging.LogRecord;47import java.util.logging.Logger;48import javax.imageio.IIOImage;49import javax.imageio.ImageIO;50import javax.imageio.ImageWriteParam;51import javax.imageio.ImageWriter;52import javax.imageio.stream.ImageOutputStream;5354/**55* @test56* @bug 819181457* @summary Verifies that Marlin rendering generates the same58* images with and without clipping optimization with all possible59* stroke (cap/join) and/or dashes or fill modes (EO rules)60* for paths made of either 9 lines, 4 quads, 2 cubics (random)61* Note: Use the argument -slow to run more intensive tests (too much time)62*63* @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly64* @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly -doDash65* @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic66* @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic -doDash67*/68public final class ClipShapeTest {6970// test options:71static int NUM_TESTS;7273// shape settings:74static ShapeMode SHAPE_MODE;7576static boolean USE_DASHES;77static boolean USE_VAR_STROKE;7879static int THRESHOLD_DELTA;80static long THRESHOLD_NBPIX;8182// constants:83static final boolean DO_FAIL = Boolean.valueOf(System.getProperty("ClipShapeTest.fail", "true"));8485static final boolean TEST_STROKER = true;86static final boolean TEST_FILLER = true;8788static final boolean SUBDIVIDE_CURVE = true;89static final double SUBDIVIDE_LEN_TH = 50.0;90static final boolean TRACE_SUBDIVIDE_CURVE = false;9192static final int TESTW = 100;93static final int TESTH = 100;9495// dump path on console:96static final boolean DUMP_SHAPE = true;9798static final boolean SHOW_DETAILS = false; // disabled99static final boolean SHOW_OUTLINE = true;100static final boolean SHOW_POINTS = true;101static final boolean SHOW_INFO = false;102103static final int MAX_SHOW_FRAMES = 10;104static final int MAX_SAVE_FRAMES = 100;105106// use fixed seed to reproduce always same polygons between tests107static final boolean FIXED_SEED = true;108109static final double RAND_SCALE = 3.0;110static final double RANDW = TESTW * RAND_SCALE;111static final double OFFW = (TESTW - RANDW) / 2.0;112static final double RANDH = TESTH * RAND_SCALE;113static final double OFFH = (TESTH - RANDH) / 2.0;114115static enum ShapeMode {116TWO_CUBICS,117FOUR_QUADS,118FIVE_LINE_POLYS,119NINE_LINE_POLYS,120FIFTY_LINE_POLYS,121MIXED122}123124static final long SEED = 1666133789L;125// Fixed seed to avoid any difference between runs:126static final Random RANDOM = new Random(SEED);127128static final File OUTPUT_DIR = new File(".");129130static final AtomicBoolean isMarlin = new AtomicBoolean();131static final AtomicBoolean isClipRuntime = new AtomicBoolean();132133static {134Locale.setDefault(Locale.US);135136// FIRST: Get Marlin runtime state from its log:137138// initialize j.u.l Looger:139final Logger log = Logger.getLogger("sun.java2d.marlin");140log.addHandler(new Handler() {141@Override142public void publish(LogRecord record) {143final String msg = record.getMessage();144if (msg != null) {145// last space to avoid matching other settings:146if (msg.startsWith("sun.java2d.renderer ")) {147isMarlin.set(msg.contains("DMarlinRenderingEngine"));148}149if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) {150isClipRuntime.set(msg.contains("true"));151}152}153154final Throwable th = record.getThrown();155// detect any Throwable:156if (th != null) {157System.out.println("Test failed:\n" + record.getMessage());158th.printStackTrace(System.out);159160throw new RuntimeException("Test failed: ", th);161}162}163164@Override165public void flush() {166}167168@Override169public void close() throws SecurityException {170}171});172173// enable Marlin logging & internal checks:174System.setProperty("sun.java2d.renderer.log", "true");175System.setProperty("sun.java2d.renderer.useLogger", "true");176177// disable static clipping setting:178System.setProperty("sun.java2d.renderer.clip", "false");179System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true");180181// enable subdivider:182System.setProperty("sun.java2d.renderer.clip.subdivider", "true");183184// disable min length check: always subdivide curves at clip edges185System.setProperty("sun.java2d.renderer.clip.subdivider.minLength", "-1");186187// If any curve, increase curve accuracy:188// curve length max error:189System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4");190191// cubic min/max error:192System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3");193System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4");194195// quad max error:196System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4");197}198199private static void resetOptions() {200NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 5000);201202// shape settings:203SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;204205USE_DASHES = false;206USE_VAR_STROKE = false;207}208209/**210* Test211* @param args212*/213public static void main(String[] args) {214{215// Bootstrap: init Renderer now:216final BufferedImage img = newImage(TESTW, TESTH);217final Graphics2D g2d = initialize(img, null);218219try {220paintShape(new Line2D.Double(0,0,100,100), g2d, true, false);221} finally {222g2d.dispose();223}224225if (!isMarlin.get()) {226throw new RuntimeException("Marlin renderer not used at runtime !");227}228if (!isClipRuntime.get()) {229throw new RuntimeException("Marlin clipping not enabled at runtime !");230}231}232233System.out.println("---------------------------------------");234System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH);235236resetOptions();237238boolean runSlowTests = false;239240for (String arg : args) {241if ("-slow".equals(arg)) {242runSlowTests = true;243} else if ("-doDash".equals(arg)) {244USE_DASHES = true;245} else if ("-doVarStroke".equals(arg)) {246USE_VAR_STROKE = true;247} else {248// shape mode:249if (arg.equalsIgnoreCase("-poly")) {250SHAPE_MODE = ShapeMode.NINE_LINE_POLYS;251} else if (arg.equalsIgnoreCase("-bigpoly")) {252SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS;253} else if (arg.equalsIgnoreCase("-quad")) {254SHAPE_MODE = ShapeMode.FOUR_QUADS;255} else if (arg.equalsIgnoreCase("-cubic")) {256SHAPE_MODE = ShapeMode.TWO_CUBICS;257} else if (arg.equalsIgnoreCase("-mixed")) {258SHAPE_MODE = ShapeMode.MIXED;259}260}261}262263System.out.println("Shape mode: " + SHAPE_MODE);264265// adjust image comparison thresholds:266switch (SHAPE_MODE) {267case TWO_CUBICS:268// Define uncertainty for curves:269THRESHOLD_DELTA = 32;270THRESHOLD_NBPIX = (USE_DASHES) ? 50 : 200;271if (SUBDIVIDE_CURVE) {272THRESHOLD_NBPIX = 4;273}274break;275case FOUR_QUADS:276case MIXED:277// Define uncertainty for quads:278// curve subdivision causes curves to be smaller279// then curve offsets are different (more accurate)280THRESHOLD_DELTA = 64;281THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 420;282if (SUBDIVIDE_CURVE) {283THRESHOLD_NBPIX = 10;284}285break;286default:287// Define uncertainty for lines:288// float variant have higher uncertainty289THRESHOLD_DELTA = 2;290THRESHOLD_NBPIX = (USE_DASHES) ? 6 : 0;291}292293// Visual inspection (low threshold):294// THRESHOLD_NBPIX = 2;295296System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA);297System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX);298299if (runSlowTests) {300NUM_TESTS = 10000; // or 100000 (very slow)301USE_VAR_STROKE = true;302}303304System.out.println("NUM_TESTS: " + NUM_TESTS);305306if (USE_DASHES) {307System.out.println("USE_DASHES: enabled.");308}309if (USE_VAR_STROKE) {310System.out.println("USE_VAR_STROKE: enabled.");311}312if (!DO_FAIL) {313System.out.println("DO_FAIL: disabled.");314}315316System.out.println("---------------------------------------");317318final DiffContext allCtx = new DiffContext("All Test setups");319final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)");320321int failures = 0;322final long start = System.nanoTime();323try {324if (TEST_STROKER) {325final float[][] dashArrays = (USE_DASHES) ?326// small327// new float[][]{new float[]{1f, 2f}}328// normal329new float[][]{new float[]{13f, 7f}}330// large (prime)331// new float[][]{new float[]{41f, 7f}}332// none333: new float[][]{null};334335System.out.println("dashes: " + Arrays.deepToString(dashArrays));336337final float[] strokeWidths = (USE_VAR_STROKE)338? new float[5] :339new float[]{10f};340341int nsw = 0;342if (USE_VAR_STROKE) {343for (float width = 0.25f; width < 110f; width *= 5f) {344strokeWidths[nsw++] = width;345}346} else {347nsw = 1;348}349350System.out.println("stroke widths: " + Arrays.toString(strokeWidths));351352// Stroker tests:353for (int w = 0; w < nsw; w++) {354final float width = strokeWidths[w];355356for (float[] dashes : dashArrays) {357358for (int cap = 0; cap <= 2; cap++) {359360for (int join = 0; join <= 2; join++) {361362failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes));363failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes));364}365}366}367}368}369370if (TEST_FILLER) {371// Filler tests:372failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO));373failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO));374375failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD));376failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD));377}378} catch (IOException ioe) {379throw new RuntimeException(ioe);380}381System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");382383allWorstCtx.dump();384allCtx.dump();385386if (DO_FAIL && (failures != 0)) {387throw new RuntimeException("Clip test failures : " + failures);388}389}390391static int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException {392final long start = System.nanoTime();393394if (FIXED_SEED) {395// Reset seed for random numbers:396RANDOM.setSeed(SEED);397}398399System.out.println("paintPaths: " + NUM_TESTS400+ " paths (" + SHAPE_MODE + ") - setup: " + ts);401402final boolean fill = !ts.isStroke();403final Path2D p2d = new Path2D.Double(ts.windingRule);404405final Stroke stroke = (!fill) ? createStroke(ts) : null;406407final BufferedImage imgOn = newImage(TESTW, TESTH);408final Graphics2D g2dOn = initialize(imgOn, stroke);409410final BufferedImage imgOff = newImage(TESTW, TESTH);411final Graphics2D g2dOff = initialize(imgOff, stroke);412413final BufferedImage imgDiff = newImage(TESTW, TESTH);414415final DiffContext testSetupCtx = new DiffContext("Test setup");416final DiffContext testWorstCtx = new DiffContext("Worst");417final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)");418419int nd = 0;420try {421final DiffContext testCtx = new DiffContext("Test");422final DiffContext testThCtx = new DiffContext("Test(>threshold)");423BufferedImage diffImage;424425for (int n = 0; n < NUM_TESTS; n++) {426genShape(p2d, ts);427428// Runtime clip setting OFF:429paintShape(p2d, g2dOff, fill, false);430431// Runtime clip setting ON:432paintShape(p2d, g2dOn, fill, true);433434/* compute image difference if possible */435diffImage = computeDiffImage(testCtx, testThCtx, imgOn, imgOff, imgDiff);436437// Worst (total)438if (testCtx.isDiff()) {439if (testWorstCtx.isWorse(testCtx, false)) {440testWorstCtx.set(testCtx);441}442if (testWorstThCtx.isWorse(testCtx, true)) {443testWorstThCtx.set(testCtx);444}445// accumulate data:446testSetupCtx.add(testCtx);447}448if (diffImage != null) {449nd++;450451testThCtx.dump();452testCtx.dump();453454if (nd < MAX_SHOW_FRAMES) {455if (SHOW_DETAILS) {456paintShapeDetails(g2dOff, p2d);457paintShapeDetails(g2dOn, p2d);458}459460if (nd < MAX_SAVE_FRAMES) {461if (DUMP_SHAPE) {462dumpShape(p2d);463}464465final String testName = "Setup_" + ts.id + "_test_" + n;466467saveImage(imgOff, OUTPUT_DIR, testName + "-off.png");468saveImage(imgOn, OUTPUT_DIR, testName + "-on.png");469saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png");470}471}472}473}474} finally {475g2dOff.dispose();476g2dOn.dispose();477478if (nd != 0) {479System.out.println("paintPaths: " + NUM_TESTS + " paths - "480+ "Number of differences = " + nd481+ " ratio = " + (100f * nd) / NUM_TESTS + " %");482}483484if (testWorstCtx.isDiff()) {485testWorstCtx.dump();486if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) {487testWorstThCtx.dump();488}489if (allWorstCtx.isWorse(testWorstThCtx, true)) {490allWorstCtx.set(testWorstThCtx);491}492}493testSetupCtx.dump();494495// accumulate data:496allCtx.add(testSetupCtx);497}498System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms.");499return nd;500}501502private static void paintShape(final Shape p2d, final Graphics2D g2d,503final boolean fill, final boolean clip) {504reset(g2d);505506setClip(g2d, clip);507508if (fill) {509g2d.fill(p2d);510} else {511g2d.draw(p2d);512}513}514515private static Graphics2D initialize(final BufferedImage img,516final Stroke s) {517final Graphics2D g2d = (Graphics2D) img.getGraphics();518g2d.setRenderingHint(RenderingHints.KEY_RENDERING,519RenderingHints.VALUE_RENDER_QUALITY);520g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,521// Test normalize:522// RenderingHints.VALUE_STROKE_NORMALIZE523RenderingHints.VALUE_STROKE_PURE524);525526if (s != null) {527g2d.setStroke(s);528}529g2d.setColor(Color.BLACK);530531return g2d;532}533534private static void reset(final Graphics2D g2d) {535// Disable antialiasing:536g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,537RenderingHints.VALUE_ANTIALIAS_OFF);538g2d.setBackground(Color.WHITE);539g2d.clearRect(0, 0, TESTW, TESTH);540}541542private static void setClip(final Graphics2D g2d, final boolean clip) {543// Enable antialiasing:544g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,545RenderingHints.VALUE_ANTIALIAS_ON);546547// Enable or Disable clipping:548System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false");549}550551static void genShape(final Path2D p2d, final TestSetup ts) {552p2d.reset();553554/*555Test closed path:5560: moveTo + (draw)To + closePath5571: (draw)To + closePath (closePath + (draw)To sequence)558*/559final int end = (ts.closed) ? 2 : 1;560561final double[] in = new double[8];562563double sx0 = 0.0, sy0 = 0.0, x0 = 0.0, y0 = 0.0;564565for (int p = 0; p < end; p++) {566if (p <= 0) {567x0 = randX(); y0 = randY();568p2d.moveTo(x0, y0);569sx0 = x0; sy0 = y0;570}571572switch (ts.shapeMode) {573case MIXED:574case FIVE_LINE_POLYS:575case NINE_LINE_POLYS:576case FIFTY_LINE_POLYS:577p2d.lineTo(randX(), randY());578p2d.lineTo(randX(), randY());579p2d.lineTo(randX(), randY());580p2d.lineTo(randX(), randY());581x0 = randX(); y0 = randY();582p2d.lineTo(x0, y0);583if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) {584// And an implicit close makes 5 lines585break;586}587p2d.lineTo(randX(), randY());588p2d.lineTo(randX(), randY());589p2d.lineTo(randX(), randY());590x0 = randX(); y0 = randY();591p2d.lineTo(x0, y0);592if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) {593// And an implicit close makes 9 lines594break;595}596if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) {597for (int i = 0; i < 41; i++) {598x0 = randX(); y0 = randY();599p2d.lineTo(x0, y0);600}601// And an implicit close makes 50 lines602break;603}604case TWO_CUBICS:605if (SUBDIVIDE_CURVE) {606in[0] = x0; in[1] = y0;607in[2] = randX(); in[3] = randY();608in[4] = randX(); in[5] = randY();609x0 = randX(); y0 = randY();610in[6] = x0; in[7] = y0;611subdivide(p2d, 8, in);612in[0] = x0; in[1] = y0;613in[2] = randX(); in[3] = randY();614in[4] = randX(); in[5] = randY();615x0 = randX(); y0 = randY();616in[6] = x0; in[7] = y0;617subdivide(p2d, 8, in);618} else {619x0 = randX(); y0 = randY();620p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);621x0 = randX(); y0 = randY();622p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0);623}624if (ts.shapeMode == ShapeMode.TWO_CUBICS) {625break;626}627case FOUR_QUADS:628if (SUBDIVIDE_CURVE) {629in[0] = x0; in[1] = y0;630in[2] = randX(); in[3] = randY();631x0 = randX(); y0 = randY();632in[4] = x0; in[5] = y0;633subdivide(p2d, 6, in);634in[0] = x0; in[1] = y0;635in[2] = randX(); in[3] = randY();636x0 = randX(); y0 = randY();637in[4] = x0; in[5] = y0;638subdivide(p2d, 6, in);639in[0] = x0; in[1] = y0;640in[2] = randX(); in[3] = randY();641x0 = randX(); y0 = randY();642in[4] = x0; in[5] = y0;643subdivide(p2d, 6, in);644in[0] = x0; in[1] = y0;645in[2] = randX(); in[3] = randY();646x0 = randX(); y0 = randY();647in[4] = x0; in[5] = y0;648subdivide(p2d, 6, in);649} else {650x0 = randX(); y0 = randY();651p2d.quadTo(randX(), randY(), x0, y0);652x0 = randX(); y0 = randY();653p2d.quadTo(randX(), randY(), x0, y0);654x0 = randX(); y0 = randY();655p2d.quadTo(randX(), randY(), x0, y0);656x0 = randX(); y0 = randY();657p2d.quadTo(randX(), randY(), x0, y0);658}659if (ts.shapeMode == ShapeMode.FOUR_QUADS) {660break;661}662default:663}664665if (ts.closed) {666p2d.closePath();667x0 = sx0; y0 = sy0;668}669}670}671672static final int SUBDIVIDE_LIMIT = 5;673static final double[][] SUBDIVIDE_CURVES = new double[SUBDIVIDE_LIMIT + 1][];674675static {676for (int i = 0, n = 1; i < SUBDIVIDE_LIMIT; i++, n *= 2) {677SUBDIVIDE_CURVES[i] = new double[8 * n];678}679}680681static void subdivide(final Path2D p2d, final int type, final double[] in) {682if (TRACE_SUBDIVIDE_CURVE) {683System.out.println("subdivide: " + Arrays.toString(Arrays.copyOf(in, type)));684}685686double curveLen = ((type == 8)687? curvelen(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7])688: quadlen(in[0], in[1], in[2], in[3], in[4], in[5]));689690if (curveLen > SUBDIVIDE_LEN_TH) {691if (TRACE_SUBDIVIDE_CURVE) {692System.out.println("curvelen: " + curveLen);693}694695System.arraycopy(in, 0, SUBDIVIDE_CURVES[0], 0, 8);696697int level = 0;698while (curveLen >= SUBDIVIDE_LEN_TH) {699level++;700curveLen /= 2.0;701if (TRACE_SUBDIVIDE_CURVE) {702System.out.println("curvelen: " + curveLen);703}704}705706if (TRACE_SUBDIVIDE_CURVE) {707System.out.println("level: " + level);708}709710if (level > SUBDIVIDE_LIMIT) {711if (TRACE_SUBDIVIDE_CURVE) {712System.out.println("max level reached : " + level);713}714level = SUBDIVIDE_LIMIT;715}716717for (int l = 0; l < level; l++) {718if (TRACE_SUBDIVIDE_CURVE) {719System.out.println("level: " + l);720}721722double[] src = SUBDIVIDE_CURVES[l];723double[] dst = SUBDIVIDE_CURVES[l + 1];724725for (int i = 0, j = 0; i < src.length; i += 8, j += 16) {726if (TRACE_SUBDIVIDE_CURVE) {727System.out.println("subdivide: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));728}729if (type == 8) {730CubicCurve2D.subdivide(src, i, dst, j, dst, j + 8);731} else {732QuadCurve2D.subdivide(src, i, dst, j, dst, j + 8);733}734if (TRACE_SUBDIVIDE_CURVE) {735System.out.println("left: " + Arrays.toString(Arrays.copyOfRange(dst, j, j + type)));736System.out.println("right: " + Arrays.toString(Arrays.copyOfRange(dst, j + 8, j + 8 + type)));737}738}739}740741// Emit curves at last level:742double[] src = SUBDIVIDE_CURVES[level];743744double len = 0.0;745746for (int i = 0; i < src.length; i += 8) {747if (TRACE_SUBDIVIDE_CURVE) {748System.out.println("curve: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type)));749}750751if (type == 8) {752if (TRACE_SUBDIVIDE_CURVE) {753len += 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]);754}755p2d.curveTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]);756} else {757if (TRACE_SUBDIVIDE_CURVE) {758len += quadlen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5]);759}760p2d.quadTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5]);761}762}763764if (TRACE_SUBDIVIDE_CURVE) {765System.out.println("curveLen (final) = " + len);766}767} else {768if (type == 8) {769p2d.curveTo(in[2], in[3], in[4], in[5], in[6], in[7]);770} else {771p2d.quadTo(in[2], in[3], in[4], in[5]);772}773}774}775776static final float POINT_RADIUS = 2f;777static final float LINE_WIDTH = 1f;778779static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH);780static final int COLOR_ALPHA = 128;781static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA);782static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA);783static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA);784785static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float();786787private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) {788789final Stroke oldStroke = g2d.getStroke();790final Color oldColor = g2d.getColor();791792setClip(g2d, false);793794if (SHOW_OUTLINE) {795g2d.setStroke(OUTLINE_STROKE);796g2d.setColor(COLOR_LINETO_ODD);797g2d.draw(shape);798}799800final float[] coords = new float[6];801float px, py;802803int nMove = 0;804int nLine = 0;805int n = 0;806807for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {808int type = it.currentSegment(coords);809switch (type) {810case PathIterator.SEG_MOVETO:811if (SHOW_POINTS) {812g2d.setColor(COLOR_MOVETO);813}814break;815case PathIterator.SEG_LINETO:816case PathIterator.SEG_QUADTO:817case PathIterator.SEG_CUBICTO:818if (SHOW_POINTS) {819g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN);820}821nLine++;822break;823case PathIterator.SEG_CLOSE:824continue;825default:826System.out.println("unsupported segment type= " + type);827continue;828}829px = coords[0];830py = coords[1];831832if (SHOW_INFO) {833System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py);834}835836if (SHOW_POINTS) {837ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS,838POINT_RADIUS * 2f, POINT_RADIUS * 2f);839g2d.fill(ELL_POINT);840}841}842if (SHOW_INFO) {843System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine);844System.out.println("--------------------------------------------------");845}846847g2d.setStroke(oldStroke);848g2d.setColor(oldColor);849}850851private static void dumpShape(final Shape shape) {852final float[] coords = new float[6];853854for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) {855final int type = it.currentSegment(coords);856switch (type) {857case PathIterator.SEG_MOVETO:858System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");");859break;860case PathIterator.SEG_LINETO:861System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");");862break;863case PathIterator.SEG_QUADTO:864System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");");865break;866case PathIterator.SEG_CUBICTO:867System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");");868break;869case PathIterator.SEG_CLOSE:870System.out.println("p2d.closePath();");871break;872default:873System.out.println("// Unsupported segment type= " + type);874}875}876System.out.println("--------------------------------------------------");877}878879static double randX() {880return RANDOM.nextDouble() * RANDW + OFFW;881}882883static double randY() {884return RANDOM.nextDouble() * RANDH + OFFH;885}886887private static BasicStroke createStroke(final TestSetup ts) {888return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f);889}890891private final static class TestSetup {892893static final AtomicInteger COUNT = new AtomicInteger();894895final int id;896final ShapeMode shapeMode;897final boolean closed;898// stroke899final float strokeWidth;900final int strokeCap;901final int strokeJoin;902final float[] dashes;903// fill904final int windingRule;905906TestSetup(ShapeMode shapeMode, final boolean closed,907final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) {908this.id = COUNT.incrementAndGet();909this.shapeMode = shapeMode;910this.closed = closed;911this.strokeWidth = strokeWidth;912this.strokeCap = strokeCap;913this.strokeJoin = strokeJoin;914this.dashes = dashes;915this.windingRule = Path2D.WIND_NON_ZERO;916}917918TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) {919this.id = COUNT.incrementAndGet();920this.shapeMode = shapeMode;921this.closed = closed;922this.strokeWidth = 0f;923this.strokeCap = this.strokeJoin = -1; // invalid924this.dashes = null;925this.windingRule = windingRule;926}927928boolean isStroke() {929return this.strokeWidth > 0f;930}931932@Override933public String toString() {934if (isStroke()) {935return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed936+ ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin)937+ ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "")938+ '}';939}940return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed941+ ", fill"942+ ", windingRule=" + getWindingRule(windingRule) + '}';943}944945private static String getCap(final int cap) {946switch (cap) {947case BasicStroke.CAP_BUTT:948return "CAP_BUTT";949case BasicStroke.CAP_ROUND:950return "CAP_ROUND";951case BasicStroke.CAP_SQUARE:952return "CAP_SQUARE";953default:954return "";955}956957}958959private static String getJoin(final int join) {960switch (join) {961case BasicStroke.JOIN_MITER:962return "JOIN_MITER";963case BasicStroke.JOIN_ROUND:964return "JOIN_ROUND";965case BasicStroke.JOIN_BEVEL:966return "JOIN_BEVEL";967default:968return "";969}970971}972973private static String getWindingRule(final int rule) {974switch (rule) {975case PathIterator.WIND_EVEN_ODD:976return "WIND_EVEN_ODD";977case PathIterator.WIND_NON_ZERO:978return "WIND_NON_ZERO";979default:980return "";981}982}983}984985// --- utilities ---986private static final int DCM_ALPHA_MASK = 0xff000000;987988public static BufferedImage newImage(final int w, final int h) {989return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);990}991992public static BufferedImage computeDiffImage(final DiffContext testCtx,993final DiffContext testThCtx,994final BufferedImage tstImage,995final BufferedImage refImage,996final BufferedImage diffImage) {997998final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData();999final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData();1000final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData();10011002// reset diff contexts:1003testCtx.reset();1004testThCtx.reset();10051006int ref, tst, dg, v;1007for (int i = 0, len = aRefPix.length; i < len; i++) {1008ref = aRefPix[i];1009tst = aTstPix[i];10101011// grayscale diff:1012dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst));10131014// max difference on grayscale values:1015v = (int) Math.ceil(Math.abs(dg / 3.0));1016if (v <= THRESHOLD_DELTA) {1017aDifPix[i] = 0;1018} else {1019aDifPix[i] = toInt(v, v, v);1020testThCtx.add(v);1021}10221023if (v != 0) {1024testCtx.add(v);1025}1026}10271028testCtx.addNbPix(testThCtx.histPix.count);10291030if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) {1031return null;1032}10331034return diffImage;1035}10361037static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException {1038final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG");1039if (itWriters.hasNext()) {1040final ImageWriter writer = itWriters.next();10411042final ImageWriteParam writerParams = writer.getDefaultWriteParam();1043writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED);10441045final File imgFile = new File(resDirectory, imageFileName);10461047if (!imgFile.exists() || imgFile.canWrite()) {1048System.out.println("saveImage: saving image as PNG [" + imgFile + "]...");1049imgFile.delete();10501051// disable cache in temporary files:1052ImageIO.setUseCache(false);10531054final long start = System.nanoTime();10551056// PNG uses already buffering:1057final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile));10581059writer.setOutput(imgOutStream);1060try {1061writer.write(null, new IIOImage(image, null, null), writerParams);1062} finally {1063imgOutStream.close();10641065final long time = System.nanoTime() - start;1066System.out.println("saveImage: duration= " + (time / 1000000l) + " ms.");1067}1068}1069}1070}10711072static int r(final int v) {1073return (v >> 16 & 0xff);1074}10751076static int g(final int v) {1077return (v >> 8 & 0xff);1078}10791080static int b(final int v) {1081return (v & 0xff);1082}10831084static int clamp127(final int v) {1085return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255;1086}10871088static int toInt(final int r, final int g, final int b) {1089return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b;1090}10911092/* stats */1093static class StatInteger {10941095public final String name;1096public long count = 0l;1097public long sum = 0l;1098public long min = Integer.MAX_VALUE;1099public long max = Integer.MIN_VALUE;11001101StatInteger(String name) {1102this.name = name;1103}11041105void reset() {1106count = 0l;1107sum = 0l;1108min = Integer.MAX_VALUE;1109max = Integer.MIN_VALUE;1110}11111112void add(int val) {1113count++;1114sum += val;1115if (val < min) {1116min = val;1117}1118if (val > max) {1119max = val;1120}1121}11221123void add(long val) {1124count++;1125sum += val;1126if (val < min) {1127min = val;1128}1129if (val > max) {1130max = val;1131}1132}11331134void add(StatInteger stat) {1135count += stat.count;1136sum += stat.sum;1137if (stat.min < min) {1138min = stat.min;1139}1140if (stat.max > max) {1141max = stat.max;1142}1143}11441145public final double average() {1146return ((double) sum) / count;1147}11481149@Override1150public String toString() {1151final StringBuilder sb = new StringBuilder(128);1152toString(sb);1153return sb.toString();1154}11551156public final StringBuilder toString(final StringBuilder sb) {1157sb.append(name).append("[n: ").append(count);1158sb.append("] ");1159if (count != 0) {1160sb.append("sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average()));1161sb.append(" [").append(min).append(" | ").append(max).append("]");1162}1163return sb;1164}11651166}11671168final static class Histogram extends StatInteger {11691170static final int BUCKET = 2;1171static final int MAX = 20;1172static final int LAST = MAX - 1;1173static final int[] STEPS = new int[MAX];1174static final int BUCKET_TH;11751176static {1177STEPS[0] = 0;1178STEPS[1] = 1;11791180for (int i = 2; i < MAX; i++) {1181STEPS[i] = STEPS[i - 1] * BUCKET;1182}1183// System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS));11841185if (THRESHOLD_DELTA % 2 != 0) {1186throw new IllegalStateException("THRESHOLD_DELTA must be odd");1187}11881189BUCKET_TH = bucket(THRESHOLD_DELTA);1190}11911192static int bucket(int val) {1193for (int i = 1; i < MAX; i++) {1194if (val < STEPS[i]) {1195return i - 1;1196}1197}1198return LAST;1199}12001201private final StatInteger[] stats = new StatInteger[MAX];12021203public Histogram(String name) {1204super(name);1205for (int i = 0; i < MAX; i++) {1206stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~")));1207}1208}12091210@Override1211final void reset() {1212super.reset();1213for (int i = 0; i < MAX; i++) {1214stats[i].reset();1215}1216}12171218@Override1219final void add(int val) {1220super.add(val);1221stats[bucket(val)].add(val);1222}12231224@Override1225final void add(long val) {1226add((int) val);1227}12281229void add(Histogram hist) {1230super.add(hist);1231for (int i = 0; i < MAX; i++) {1232stats[i].add(hist.stats[i]);1233}1234}12351236boolean isWorse(Histogram hist, boolean useTh) {1237boolean worst = false;1238if (!useTh && (hist.sum > sum)) {1239worst = true;1240} else {1241long sumLoc = 0l;1242long sumHist = 0l;1243// use running sum:1244for (int i = MAX - 1; i >= BUCKET_TH; i--) {1245sumLoc += stats[i].sum;1246sumHist += hist.stats[i].sum;1247}1248if (sumHist > sumLoc) {1249worst = true;1250}1251}1252/*1253System.out.println("running sum worst:");1254System.out.println("this ? " + toString());1255System.out.println("worst ? " + hist.toString());1256*/1257return worst;1258}12591260@Override1261public final String toString() {1262final StringBuilder sb = new StringBuilder(2048);1263super.toString(sb).append(" { ");12641265for (int i = 0; i < MAX; i++) {1266if (stats[i].count != 0l) {1267sb.append("\n ").append(stats[i].toString());1268}1269}12701271return sb.append(" }").toString();1272}1273}12741275/**1276* Adjust the given double value to keep only 3 decimal digits1277* @param value value to adjust1278* @return double value with only 3 decimal digits1279*/1280static double trimTo3Digits(final double value) {1281return ((long) (1e3d * value)) / 1e3d;1282}12831284static final class DiffContext {12851286public final Histogram histPix;12871288public final StatInteger nbPix;12891290DiffContext(String name) {1291histPix = new Histogram("Diff Pixels [" + name + "]");1292nbPix = new StatInteger("NbPixels [" + name + "]");1293}12941295void reset() {1296histPix.reset();1297nbPix.reset();1298}12991300void dump() {1301if (isDiff()) {1302System.out.println("Differences [" + histPix.name + "]:\n"1303+ ((nbPix.count != 0) ? (nbPix.toString() + "\n") : "")1304+ histPix.toString()1305);1306} else {1307System.out.println("No difference for [" + histPix.name + "].");1308}1309}13101311void add(int val) {1312histPix.add(val);1313}13141315void add(DiffContext ctx) {1316histPix.add(ctx.histPix);1317if (ctx.nbPix.count != 0L) {1318nbPix.add(ctx.nbPix);1319}1320}13211322void addNbPix(long val) {1323if (val != 0L) {1324nbPix.add(val);1325}1326}13271328void set(DiffContext ctx) {1329reset();1330add(ctx);1331}13321333boolean isWorse(DiffContext ctx, boolean useTh) {1334return histPix.isWorse(ctx.histPix, useTh);1335}13361337boolean isDiff() {1338return histPix.sum != 0l;1339}1340}134113421343static double linelen(final double x0, final double y0,1344final double x1, final double y1)1345{1346final double dx = x1 - x0;1347final double dy = y1 - y0;1348return Math.sqrt(dx * dx + dy * dy);1349}13501351static double quadlen(final double x0, final double y0,1352final double x1, final double y1,1353final double x2, final double y2)1354{1355return (linelen(x0, y0, x1, y1)1356+ linelen(x1, y1, x2, y2)1357+ linelen(x0, y0, x2, y2)) / 2.0d;1358}13591360static double curvelen(final double x0, final double y0,1361final double x1, final double y1,1362final double x2, final double y2,1363final double x3, final double y3)1364{1365return (linelen(x0, y0, x1, y1)1366+ linelen(x1, y1, x2, y2)1367+ linelen(x2, y2, x3, y3)1368+ linelen(x0, y0, x3, y3)) / 2.0d;1369}1370}137113721373