Skip to content

feat: add captureCoords option to retrieve x,y diff information #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.yaml linguist-detectable=false
.ci/* linguist-vendored
.ci/* linguist-vendored
test/coords-fixture.json linguist-generated
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export type ODiffOptions = Partial<{
antialiasing: boolean;
/** If `true` reason: "pixel-diff" output will contain the set of line indexes containing different pixels */
captureDiffLines: boolean;
/** If `true` the diff result will include coordinates of all different pixels, grouped by x coordinate */
captureDiffCoords: boolean;
/** If `true` odiff will use less memory but will be slower with larger images */
reduceRamUsage: boolean;
/** An array of regions to ignore in the diff. */
Expand Down Expand Up @@ -142,6 +144,8 @@ declare function compare(
diffPercentage: number;
/** Individual line indexes containing different pixels. Guaranteed to be ordered and distinct. */
diffLines?: number[];
/** Array of [x, y[]] pairs where x is the x coordinate and y[] is an array of y coordinates that changed at that x position */
diffCoords?: [number, ...Array<[number, number]][];
}
| {
match: false;
Expand Down
8 changes: 4 additions & 4 deletions bin/Main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type 'output diffResult = { exitCode : int; diff : 'output option }
(* Arguments must remain positional for the cmd parser lib that we use *)
let main img1Path img2Path diffPath threshold outputDiffMask failOnLayoutChange
diffColorHex toEmitStdoutParsableString antialiasing ignoreRegions diffLines
disableGcOptimizations =
captureDiffCoords disableGcOptimizations =
(*
Increase amount of allowed overhead to reduce amount of GC work and cycles.
we target 1-2 minor collections per run which is the best tradeoff between
Expand Down Expand Up @@ -47,7 +47,7 @@ let main img1Path img2Path diffPath threshold outputDiffMask failOnLayoutChange
let img2 = IO2.loadImage img2Path in
let { diff; exitCode } =
Diff.diff img1 img2 ~outputDiffMask ~threshold ~failOnLayoutChange
~antialiasing ~ignoreRegions ~diffLines
~antialiasing ~ignoreRegions ~diffLines ~captureDiffCoords
~diffPixel:
(match Color.ofHexString diffColorHex with
| Some c -> c
Expand All @@ -56,10 +56,10 @@ let main img1Path img2Path diffPath threshold outputDiffMask failOnLayoutChange
|> Print.printDiffResult toEmitStdoutParsableString
|> function
| Layout -> { diff = None; exitCode = 21 }
| Pixel (diffOutput, diffCount, stdoutParsableString, _) when diffCount = 0
| Pixel (diffOutput, diffCount, stdoutParsableString, _, _) when diffCount = 0
->
{ exitCode = 0; diff = Some diffOutput }
| Pixel (diffOutput, diffCount, diffPercentage, _) ->
| Pixel (diffOutput, diffCount, diffPercentage, _, _) ->
diffPath |> Option.iter (IO1.saveImage diffOutput);
{ exitCode = 22; diff = Some diffOutput }
in
Expand Down
9 changes: 8 additions & 1 deletion bin/ODiffBin.ml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ let diffLines =
"With this flag enabled, output result in case of different images \
will output lines for all the different pixels"

let captureDiffCoords =
value & flag
& info [ "capture-diff-coords" ]
~doc:
"With this flag enabled, the diff result will include coordinates of \
all different pixels, grouped by x coordinate"

let disableGcOptimizations =
value & flag
& info [ "reduce-ram-usage" ]
Expand All @@ -76,7 +83,7 @@ let ignoreRegions =
let cmd =
const Main.main $ base $ comp $ diffPath $ threshold $ diffMask $ failOnLayout
$ diffColor $ parsableOutput $ antialiasing $ ignoreRegions $ diffLines
$ disableGcOptimizations
$ captureDiffCoords $ disableGcOptimizations

let version =
match Build_info.V1.version () with
Expand Down
29 changes: 21 additions & 8 deletions bin/Print.ml
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,36 @@ let printDiffResult makeParsableOutput result =
| Layout, false ->
Format.printf "%s%sFailure!%s Images have different layout.\n" red bold
reset
| Pixel (_output, diffCount, diffPercentage, stack), true
when not (Stack.is_empty stack) ->
| Pixel (_output, diffCount, diffPercentage, stack, coords), true
when not (Stack.is_empty stack) || coords <> [] ->
let lines_str =
if Stack.is_empty stack then ";" else
";" ^ (stack |> Stack.fold (fun acc line -> (line |> Int.to_string) ^ "," ^ acc) "")
in
let coords_str =
if coords = [] then "" else
let coord_to_str (y, ranges) =
let ranges_str = ranges |> List.map (fun (start, end_) ->
Printf.sprintf "%d-%d" start end_) |> String.concat ","
in
Printf.sprintf "%d:%s" y ranges_str
in
";" ^ String.concat "|" (List.map coord_to_str coords)
in
Int.to_string diffCount ^ ";"
^ Float.to_string diffPercentage
^ ";"
^ (stack
|> Stack.fold (fun acc line -> (line |> Int.to_string) ^ "," ^ acc) "")
^ lines_str
^ coords_str
|> print_endline
| Pixel (_output, diffCount, diffPercentage, _), true ->
| Pixel (_output, diffCount, diffPercentage, _, _), true ->
Int.to_string diffCount ^ ";" ^ Float.to_string diffPercentage
|> print_endline
| Pixel (_output, diffCount, _percentage, _lines), false when diffCount == 0
| Pixel (_output, diffCount, _percentage, _lines, _), false when diffCount == 0
->
Format.printf
"%s%sSuccess!%s Images are equal.\n%sNo diff output created.%s\n" green
bold reset dim reset
| Pixel (_output, diffCount, diffPercentage, _lines), false ->
| Pixel (_output, diffCount, diffPercentage, _lines, _), false ->
Format.printf
"%s%sFailure!%s Images are different.\n\
Different pixels: %s%s%i (%f%%)%s\n"
Expand Down
4 changes: 4 additions & 0 deletions npm_package/odiff.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type ODiffOptions = Partial<{
antialiasing: boolean;
/** If `true` reason: "pixel-diff" output will contain the set of line indexes containing different pixels */
captureDiffLines: boolean;
/** If `true` the diff result will include coordinates of all different pixels, grouped by x coordinate */
captureDiffCoords: boolean;
/** If `true` odiff will use less memory but will be slower with larger images */
reduceRamUsage: boolean;
/** An array of regions to ignore in the diff. */
Expand Down Expand Up @@ -41,6 +43,8 @@ declare function compare(
diffPercentage: number;
/** Individual line indexes containing different pixels. Guaranteed to be ordered and distinct. */
diffLines?: number[];
/** Array of [y, ranges[]] pairs where y is the y coordinate and ranges[] is an array of [startX, endX] pairs representing continuous ranges of x coordinates */
diffCoords?: [number, ...Array<[number, number]>][];
}
| {
match: false;
Expand Down
26 changes: 25 additions & 1 deletion npm_package/odiff.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ function optionsToArgs(options) {
setFlag("reduce-ram-usage", value);
break;

case "captureDiffCoords":
setFlag("capture-diff-coords", value);
break;

case "ignoreRegions": {
const regions = value
.map(
Expand All @@ -70,7 +74,7 @@ function optionsToArgs(options) {
return argArray;
}

/** @type {(stdout: string) => Partial<{ diffCount: number, diffPercentage: number, diffLines: number[] }>} */
/** @type {(stdout: string) => Partial<{ diffCount: number, diffPercentage: number, diffLines: number[], diffCoords: Array<[number, ...Array<[number, number]>]> }>} */
function parsePixelDiffStdout(stdout) {
try {
const parts = stdout.split(";");
Expand All @@ -94,6 +98,26 @@ function parsePixelDiffStdout(stdout) {
return isNaN(parsedInt) ? [] : parsedInt;
}),
};
} else if (parts.length >= 3) {
const [diffCount, diffPercentage, linesPart, coordsPart] = parts;

return {
diffCount: parseInt(diffCount),
diffPercentage: parseFloat(diffPercentage),
diffLines: linesPart.split(",").flatMap((line) => {
let parsedInt = parseInt(line);
return isNaN(parsedInt) ? [] : parsedInt;
}),
diffCoords: coordsPart.split("|").map((coord) => {
const [y, ranges] = coord.split(":");
/** @type {[number, number][]} */
const parsedRanges = ranges.split(",").map((range) => {
const [start, end] = range.split("-");
return [parseInt(start), parseInt(end)];
});
return [parseInt(y), ...parsedRanges];
}),
};
} else {
throw new Error(`Weird pixel diff stdout: ${stdout}`);
}
Expand Down
63 changes: 52 additions & 11 deletions src/Diff.ml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ let redPixel = Int32.of_int 4278190335
(* Decimal representation of the RGBA in32 pixel green pixel *)
let maxYIQPossibleDelta = 35215.

type 'a diffVariant = Layout | Pixel of ('a * int * float * int Stack.t)
type 'a diffVariant = Layout | Pixel of ('a * int * float * int Stack.t * (int * (int * int) list) list)

let unrollIgnoreRegions width list =
list
Expand All @@ -29,7 +29,7 @@ module MakeDiff (IO1 : ImageIO.ImageIO) (IO2 : ImageIO.ImageIO) = struct

let compare (base : IO1.t ImageIO.img) (comp : IO2.t ImageIO.img)
?(antialiasing = false) ?(outputDiffMask = false) ?(diffLines = false)
?diffPixel ?(threshold = 0.1) ?ignoreRegions ?(captureDiff = true) () =
?diffPixel ?(threshold = 0.1) ?ignoreRegions ?(captureDiff = true) ?(captureDiffCoords = false) () =
let maxDelta = maxYIQPossibleDelta *. (threshold ** 2.) in
let diffPixel = match diffPixel with Some x -> x | None -> redPixel in
let diffOutput =
Expand All @@ -44,10 +44,40 @@ module MakeDiff (IO1 : ImageIO.ImageIO) (IO2 : ImageIO.ImageIO) = struct

let diffCount = ref 0 in
let diffLinesStack = Stack.create () in
let diffCoords = ref [] in
let currentRanges = ref [] in
let lastY = ref (-1) in
let lastX = ref (-1) in
let currentRange = ref None in

let countDifference x y =
incr diffCount;
diffOutput |> Option.iter (IO1.setImgColor ~x ~y diffPixel);

if captureDiffCoords then (
if !lastY <> y then (
!currentRange |> Option.iter (fun (start, _) ->
currentRanges := (start, !lastX) :: !currentRanges);
if !currentRanges <> [] then (
diffCoords := (!lastY, List.rev !currentRanges) :: !diffCoords;
currentRanges := []
);
lastY := y;
lastX := -1;
currentRange := None
);

if !lastX = -1 || x <> !lastX + 1 then (
!currentRange |> Option.iter (fun (start, _) ->
currentRanges := (start, !lastX) :: !currentRanges);
currentRange := Some (x, x)
) else (
!currentRange |> Option.iter (fun (start, _) ->
currentRange := Some (start, x))
);
lastX := x
);

if
diffLines
&& (diffLinesStack |> Stack.is_empty || diffLinesStack |> Stack.top < y)
Expand Down Expand Up @@ -109,33 +139,44 @@ module MakeDiff (IO1 : ImageIO.ImageIO) (IO2 : ImageIO.ImageIO) = struct
100.0 *. Float.of_int !diffCount
/. (Float.of_int base.width *. Float.of_int base.height)
in
(diffOutput, !diffCount, diffPercentage, diffLinesStack)

if captureDiffCoords then (
if !currentRange <> None then (
currentRanges := (Option.get !currentRange) :: !currentRanges;
currentRange := None
);
if !currentRanges <> [] then
diffCoords := (!lastY, List.rev !currentRanges) :: !diffCoords
);

let diffCoords = List.rev !diffCoords in
(diffOutput, !diffCount, diffPercentage, diffLinesStack, diffCoords)

let diff (base : IO1.t ImageIO.img) (comp : IO2.t ImageIO.img) ~outputDiffMask
?(threshold = 0.1) ~diffPixel ?(failOnLayoutChange = true)
?(antialiasing = false) ?(diffLines = false) ?ignoreRegions () =
?(antialiasing = false) ?(diffLines = false) ?ignoreRegions ?(captureDiffCoords = false) () =
if
failOnLayoutChange = true
&& (base.width <> comp.width || base.height <> comp.height)
then Layout
else
let diffOutput, diffCount, diffPercentage, diffLinesStack =
let diffOutput, diffCount, diffPercentage, diffLinesStack, diffCoords =
compare base comp ~threshold ~diffPixel ~outputDiffMask ~antialiasing
~diffLines ?ignoreRegions ~captureDiff:true ()
~diffLines ?ignoreRegions ~captureDiff:true ~captureDiffCoords ()
in
Pixel (Option.get diffOutput, diffCount, diffPercentage, diffLinesStack)
Pixel (Option.get diffOutput, diffCount, diffPercentage, diffLinesStack, diffCoords)

let diffWithoutOutput (base : IO1.t ImageIO.img) (comp : IO2.t ImageIO.img)
?(threshold = 0.1) ?(failOnLayoutChange = true) ?(antialiasing = false)
?(diffLines = false) ?ignoreRegions () =
?(diffLines = false) ?ignoreRegions ?(captureDiffCoords = false) () =
if
failOnLayoutChange = true
&& (base.width <> comp.width || base.height <> comp.height)
then Layout
else
let diffResult =
let diffOutput, diffCount, diffPercentage, diffLinesStack, diffCoords =
compare base comp ~threshold ~outputDiffMask:false ~antialiasing
~diffLines ?ignoreRegions ~captureDiff:false ()
~diffLines ?ignoreRegions ~captureDiff:false ~captureDiffCoords ()
in
Pixel diffResult
Pixel (diffOutput, diffCount, diffPercentage, diffLinesStack, diffCoords)
end
14 changes: 7 additions & 7 deletions test/Test_Core.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ let test_antialiasing () =
Sys.getcwd () |> print_endline;
let img1 = Png.IO.loadImage "test-images/aa/antialiasing-on.png" in
let img2 = Png.IO.loadImage "test-images/aa/antialiasing-off.png" in
let _, diffPixels, diffPercentage, _ =
let _, diffPixels, diffPercentage, _, _ =
PNG_Diff.compare img1 img2 ~outputDiffMask:false ~antialiasing:true ()
in
check int "diffPixels" 46 diffPixels;
Expand All @@ -14,7 +14,7 @@ let test_antialiasing () =
let test_different_sized_aa_images () =
let img1 = Png.IO.loadImage "test-images/aa/antialiasing-on.png" in
let img2 = Png.IO.loadImage "test-images/aa/antialiasing-off-small.png" in
let _, diffPixels, diffPercentage, _ =
let _, diffPixels, diffPercentage, _, _ =
PNG_Diff.compare img1 img2 ~outputDiffMask:true ~antialiasing:true ()
in
check int "diffPixels" 417 diffPixels;
Expand All @@ -23,7 +23,7 @@ let test_different_sized_aa_images () =
let test_threshold () =
let img1 = Png.IO.loadImage "test-images/png/orange.png" in
let img2 = Png.IO.loadImage "test-images/png/orange_changed.png" in
let _, diffPixels, diffPercentage, _ =
let _, diffPixels, diffPercentage, _, _ =
PNG_Diff.compare img1 img2 ~threshold:0.5 ()
in
check int "diffPixels" 25 diffPixels;
Expand All @@ -32,7 +32,7 @@ let test_threshold () =
let test_ignore_regions () =
let img1 = Png.IO.loadImage "test-images/png/orange.png" in
let img2 = Png.IO.loadImage "test-images/png/orange_changed.png" in
let _diffOutput, diffPixels, diffPercentage, _ =
let _diffOutput, diffPixels, diffPercentage, _, _ =
PNG_Diff.compare img1 img2
~ignoreRegions:[ ((150, 30), (310, 105)); ((20, 175), (105, 200)) ]
()
Expand All @@ -43,15 +43,15 @@ let test_ignore_regions () =
let test_diff_color () =
let img1 = Png.IO.loadImage "test-images/png/orange.png" in
let img2 = Png.IO.loadImage "test-images/png/orange_changed.png" in
let diffOutput, _, _, _ =
let diffOutput, _, _, _, _ =
PNG_Diff.compare img1 img2
~diffPixel:(Int32.of_int 4278255360 (*int32 representation of #00ff00*))
()
in
check bool "diffOutput" (Option.is_some diffOutput) true;
let diffOutput = Option.get diffOutput in
let originalDiff = Png.IO.loadImage "test-images/png/orange_diff_green.png" in
let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _ =
let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _, _ =
PNG_Diff.compare originalDiff diffOutput ()
in
check bool "diffMaskOfDiff" (Option.is_some diffMaskOfDiff) true;
Expand Down Expand Up @@ -81,7 +81,7 @@ let test_different_layouts () =
Sys.getcwd () |> print_endline;
let img1 = Png.IO.loadImage "test-images/png/white4x4.png" in
let img2 = Png.IO.loadImage "test-images/png/purple8x8.png" in
let _, diffPixels, diffPercentage, _ =
let _, diffPixels, diffPercentage, _, _ =
PNG_Diff.compare img1 img2 ~outputDiffMask:false ~antialiasing:false ()
in
check int "diffPixels" 16 diffPixels;
Expand Down
10 changes: 5 additions & 5 deletions test/Test_IO_BMP.ml
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,28 @@ let load_png_image path =
let test_finds_difference_between_images () =
let img1 = load_image "test-images/bmp/clouds.bmp" in
let img2 = load_image "test-images/bmp/clouds-2.bmp" in
let _, diffPixels, diffPercentage, _ = Diff.compare img1 img2 () in
let _, diffPixels, diffPercentage, _, _ = Diff.compare img1 img2 () in
check int "diffPixels" 191 diffPixels;
check (float 0.001) "diffPercentage" 0.076 diffPercentage

let test_diff_mask_no_mask_equal () =
let img1 = load_image "test-images/bmp/clouds.bmp" in
let img2 = load_image "test-images/bmp/clouds-2.bmp" in
let _, diffPixels, diffPercentage, _ = Diff.compare img1 img2 ~outputDiffMask:false () in
let _, diffPixels, diffPercentage, _, _ = Diff.compare img1 img2 ~outputDiffMask:false () in
let img1 = load_image "test-images/bmp/clouds.bmp" in
let img2 = load_image "test-images/bmp/clouds-2.bmp" in
let _, diffPixelsMask, diffPercentageMask, _ = Diff.compare img1 img2 ~outputDiffMask:true () in
let _, diffPixelsMask, diffPercentageMask, _, _ = Diff.compare img1 img2 ~outputDiffMask:true () in
check int "diffPixels" diffPixels diffPixelsMask;
check (float 0.001) "diffPercentage" diffPercentage diffPercentageMask

let test_creates_correct_diff_output_image () =
let img1 = load_image "test-images/bmp/clouds.bmp" in
let img2 = load_image "test-images/bmp/clouds-2.bmp" in
let diffOutput, _, _, _ = Diff.compare img1 img2 () in
let diffOutput, _, _, _, _ = Diff.compare img1 img2 () in
check bool "diffOutput" (Option.is_some diffOutput) true;
let diffOutput = Option.get diffOutput in
let originalDiff = load_png_image "test-images/bmp/clouds-diff.png" in
let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _ = Output_Diff.compare originalDiff diffOutput () in
let diffMaskOfDiff, diffOfDiffPixels, diffOfDiffPercentage, _, _ = Output_Diff.compare originalDiff diffOutput () in
check bool "diffMaskOfDiff" (Option.is_some diffMaskOfDiff) true;
let diffMaskOfDiff = Option.get diffMaskOfDiff in
if diffOfDiffPixels > 0 then (
Expand Down
Loading
Loading