diff --git a/scripts/common/java/ProcessScreenshots.java b/scripts/common/java/ProcessScreenshots.java index a0b941910e..3e941b54ae 100644 --- a/scripts/common/java/ProcessScreenshots.java +++ b/scripts/common/java/ProcessScreenshots.java @@ -41,7 +41,9 @@ public static void main(String[] args) throws Exception { arguments.referenceDir, arguments.actualEntries, arguments.emitBase64, - arguments.previewDir + arguments.previewDir, + arguments.maxChannelDelta, + arguments.maxMismatchPercent ); String json = JsonUtil.stringify(payload); System.out.print(json); @@ -51,7 +53,9 @@ static Map buildResults( Path referenceDir, List> actualEntries, boolean emitBase64, - Path previewDir + Path previewDir, + int maxChannelDelta, + double maxMismatchPercent ) throws IOException { List> results = new ArrayList<>(); for (Map.Entry entry : actualEntries) { @@ -75,7 +79,7 @@ static Map buildResults( try { PNGImage actual = loadPngWithRetry(actualPath); PNGImage expected = loadPngWithRetry(expectedPath); - Map outcome = compareImages(expected, actual); + Map outcome = compareImages(expected, actual, maxChannelDelta, maxMismatchPercent); if (Boolean.TRUE.equals(outcome.get("equal"))) { record.put("status", "equal"); } else { @@ -318,21 +322,60 @@ private static int clamp(int value) { return Math.max(0, Math.min(255, value)); } - private static Map compareImages(PNGImage expected, PNGImage actual) { + private static Map compareImages(PNGImage expected, PNGImage actual, int maxChannelDelta, double maxMismatchPercent) { boolean equal = expected.width == actual.width && expected.height == actual.height && expected.bitDepth == actual.bitDepth && expected.colorType == actual.colorType && java.util.Arrays.equals(expected.pixels, actual.pixels); Map result = new LinkedHashMap<>(); - result.put("equal", equal); result.put("width", actual.width); result.put("height", actual.height); result.put("bit_depth", actual.bitDepth); result.put("color_type", actual.colorType); + if (!equal && maxChannelDelta > 0 && maxMismatchPercent >= 0 && expected.width == actual.width && expected.height == actual.height) { + int totalPixels = actual.width * actual.height; + int mismatchCount = countMismatchedPixels(expected, actual, maxChannelDelta); + double mismatchPercent = totalPixels == 0 ? 0d : (mismatchCount * 100d) / totalPixels; + result.put("mismatch_count", mismatchCount); + result.put("mismatch_percent", mismatchPercent); + result.put("max_channel_delta", maxChannelDelta); + result.put("max_mismatch_percent", maxMismatchPercent); + equal = mismatchPercent <= maxMismatchPercent; + } + result.put("equal", equal); return result; } + private static int countMismatchedPixels(PNGImage expected, PNGImage actual, int maxChannelDelta) { + int[] expectedRgb = toRgbArray(expected); + int[] actualRgb = toRgbArray(actual); + int mismatched = 0; + for (int i = 0; i < expectedRgb.length; i++) { + int e = expectedRgb[i]; + int a = actualRgb[i]; + int er = (e >> 16) & 0xff; + int eg = (e >> 8) & 0xff; + int eb = e & 0xff; + int ar = (a >> 16) & 0xff; + int ag = (a >> 8) & 0xff; + int ab = a & 0xff; + if (Math.abs(er - ar) > maxChannelDelta + || Math.abs(eg - ag) > maxChannelDelta + || Math.abs(eb - ab) > maxChannelDelta) { + mismatched++; + } + } + return mismatched; + } + + private static int[] toRgbArray(PNGImage image) { + BufferedImage rgbImage = toRgbImage(image); + int[] pixels = new int[image.width * image.height]; + rgbImage.getRGB(0, 0, image.width, image.height, pixels, 0, image.width); + return pixels; + } + private static PNGImage loadPngWithRetry(Path path) throws IOException { int attempt = 0; long lastSize = -1; @@ -666,18 +709,25 @@ private static class Arguments { final List> actualEntries; final boolean emitBase64; final Path previewDir; + final int maxChannelDelta; + final double maxMismatchPercent; - private Arguments(Path referenceDir, List> actualEntries, boolean emitBase64, Path previewDir) { + private Arguments(Path referenceDir, List> actualEntries, boolean emitBase64, Path previewDir, + int maxChannelDelta, double maxMismatchPercent) { this.referenceDir = referenceDir; this.actualEntries = actualEntries; this.emitBase64 = emitBase64; this.previewDir = previewDir; + this.maxChannelDelta = maxChannelDelta; + this.maxMismatchPercent = maxMismatchPercent; } static Arguments parse(String[] args) { Path reference = null; boolean emitBase64 = false; Path previewDir = null; + int maxChannelDelta = 0; + double maxMismatchPercent = 0d; List> actuals = new ArrayList<>(); for (int i = 0; i < args.length; i++) { String arg = args[i]; @@ -712,6 +762,26 @@ static Arguments parse(String[] args) { Path path = Path.of(value.substring(idx + 1)); actuals.add(Map.entry(name, path)); } + case "--max-channel-delta" -> { + if (++i >= args.length) { + System.err.println("Missing value for --max-channel-delta"); + return null; + } + maxChannelDelta = parseIntArg("--max-channel-delta", args[i]); + if (maxChannelDelta < 0) { + return null; + } + } + case "--max-mismatch-percent" -> { + if (++i >= args.length) { + System.err.println("Missing value for --max-mismatch-percent"); + return null; + } + maxMismatchPercent = parseDoubleArg("--max-mismatch-percent", args[i]); + if (maxMismatchPercent < 0) { + return null; + } + } default -> { System.err.println("Unknown argument: " + arg); return null; @@ -722,7 +792,25 @@ static Arguments parse(String[] args) { System.err.println("--reference-dir is required"); return null; } - return new Arguments(reference, actuals, emitBase64, previewDir); + return new Arguments(reference, actuals, emitBase64, previewDir, maxChannelDelta, maxMismatchPercent); + } + + private static int parseIntArg(String flag, String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + System.err.println("Invalid integer for " + flag + ": " + value); + return -1; + } + } + + private static double parseDoubleArg(String flag, String value) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + System.err.println("Invalid number for " + flag + ": " + value); + return -1d; + } } } } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/DrawGradient.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/DrawGradient.java index db6a9ea069..08ad35d97f 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/DrawGradient.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/DrawGradient.java @@ -1,25 +1,20 @@ package com.codenameone.examples.hellocodenameone.tests.graphics; -import com.codename1.ui.EncodedImage; -import com.codename1.ui.FontImage; import com.codename1.ui.Graphics; -import com.codename1.ui.Image; -import com.codename1.ui.RGBImage; import com.codename1.ui.geom.Rectangle; import com.codenameone.examples.hellocodenameone.tests.AbstractGraphicsScreenshotTest; public class DrawGradient extends AbstractGraphicsScreenshotTest { - @Override protected void drawContent(Graphics g, Rectangle bounds) { int height = bounds.getHeight() / 3; int width = bounds.getWidth() / 2; int y = bounds.getY(); g.fillRadialGradient(0xff, 0xff00, bounds.getX(), y, width, height); - g.fillRadialGradient(0xff, 0xff00, bounds.getX() + width, y, width, height,20, 200); + g.fillRadialGradient(0xff, 0xff00, bounds.getX() + width, y, width, height, 20, 200); y += height; - g.fillRectRadialGradient(0xff0000, 0xcccccc, bounds.getX() + width, y, width, height,0.5f, 0.5f, 2); + g.fillRectRadialGradient(0xff0000, 0xcccccc, bounds.getX() + width, y, width, height, 0.5f, 0.5f, 2); g.fillLinearGradient(0xff, 0x999999, bounds.getX(), y, width, height, true); y += height; diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/FillArc.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/FillArc.java index 829f372812..ee915dd397 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/FillArc.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/FillArc.java @@ -10,7 +10,12 @@ protected void drawContent(Graphics g, Rectangle bounds) { g.setColor(0xffffff); for (int iter = 0 ; iter < bounds.getWidth() / 2 ; iter++) { nextColor(g); - g.fillArc(bounds.getX() + iter, bounds.getY() + iter, bounds.getX() + bounds.getWidth() - iter, bounds.getY() + bounds.getHeight() + iter, iter, 180); + int width = bounds.getWidth() - (iter * 2); + int height = bounds.getHeight() - (iter * 2); + if (width <= 0 || height <= 0) { + break; + } + g.fillArc(bounds.getX() + iter, bounds.getY() + iter, width, height, iter, 180); } } diff --git a/scripts/lib/cn1ss.sh b/scripts/lib/cn1ss.sh index d62921b28c..2e43b8bb03 100644 --- a/scripts/lib/cn1ss.sh +++ b/scripts/lib/cn1ss.sh @@ -330,6 +330,12 @@ cn1ss_process_and_report() { # Run ProcessScreenshots local -a compare_args=("--reference-dir" "$ref_dir" "--emit-base64" "--preview-dir" "$preview_dir") + if [ -n "${CN1SS_MAX_CHANNEL_DELTA:-}" ]; then + compare_args+=("--max-channel-delta" "${CN1SS_MAX_CHANNEL_DELTA}") + fi + if [ -n "${CN1SS_MAX_MISMATCH_PERCENT:-}" ]; then + compare_args+=("--max-mismatch-percent" "${CN1SS_MAX_MISMATCH_PERCENT}") + fi for entry in "${actual_entries[@]}"; do compare_args+=("--actual" "$entry") done diff --git a/scripts/run-javase-device-tests.sh b/scripts/run-javase-device-tests.sh index 394236640d..a656b0ed12 100755 --- a/scripts/run-javase-device-tests.sh +++ b/scripts/run-javase-device-tests.sh @@ -239,4 +239,59 @@ SUMMARY_FILE="$ARTIFACTS_DIR/summary.txt" jd_log "Desktop device-runner artifacts stored in $ARTIFACTS_DIR" -exit $SIM_EXIT_CODE +SCREENSHOT_REF_DIR_BASE="${SCREENSHOT_REF_DIR:-$SCRIPT_DIR/android/screenshots}" +SCREENSHOT_REF_DIR="$SCREENSHOT_REF_DIR_BASE" +if [ -n "${CN1SS_ANDROID_BASELINE:-}" ]; then + SCREENSHOT_REF_DIR="$CN1SS_ANDROID_BASELINE" +else + baseline_variant="" + if [ "$JAVA_VERSION_MAJOR" -ge 21 ]; then + baseline_variant="21" + elif [ "$JAVA_VERSION_MAJOR" -ge 17 ]; then + baseline_variant="17" + fi + if [ -n "$baseline_variant" ] && [ -d "$SCREENSHOT_REF_DIR_BASE/$baseline_variant" ]; then + SCREENSHOT_REF_DIR="$SCREENSHOT_REF_DIR_BASE/$baseline_variant" + fi +fi +jd_log "Using screenshot baseline directory: $SCREENSHOT_REF_DIR" + +COMPARE_ENTRIES=() +for test in "${TEST_NAMES[@]}"; do + png_dest="$SCREENSHOT_DIR/${test}.png" + if [ -f "$png_dest" ]; then + COMPARE_ENTRIES+=("${test}=${png_dest}") + fi +done + +COMPARE_JSON="$ARTIFACTS_DIR/screenshot-compare.json" +SUMMARY_FILE="$ARTIFACTS_DIR/screenshot-summary.txt" +COMMENT_FILE="$ARTIFACTS_DIR/screenshot-comment.md" + +export CN1SS_PREVIEW_DIR="$PREVIEW_DIR" +export CN1SS_COMMENT_MARKER="" +export CN1SS_COMMENT_LOG_PREFIX="[run-javase-device-tests]" +export CN1SS_PREVIEW_SUBDIR="javase" +export CN1SS_MAX_CHANNEL_DELTA="${CN1SS_MAX_CHANNEL_DELTA:-4}" +export CN1SS_MAX_MISMATCH_PERCENT="${CN1SS_MAX_MISMATCH_PERCENT:-0.25}" + +if [ "${#COMPARE_ENTRIES[@]}" -gt 0 ]; then + cn1ss_process_and_report \ + "Java SE screenshot updates" \ + "$COMPARE_JSON" \ + "$SUMMARY_FILE" \ + "$COMMENT_FILE" \ + "$SCREENSHOT_REF_DIR" \ + "$PREVIEW_DIR" \ + "$ARTIFACTS_DIR" \ + "${COMPARE_ENTRIES[@]}" + compare_rc=$? +else + compare_rc=0 +fi + +if [ "$SIM_EXIT_CODE" -ne 0 ] && [ "$compare_rc" -eq 0 ]; then + exit "$SIM_EXIT_CODE" +fi + +exit "$compare_rc"