8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
32 png_error(png_ptr,
"Read past end of PNG data");
35 std::memcpy(data, ctx->
data + ctx->
offset, length);
46 ctx->
output->insert(ctx->
output->end(), data, data + length);
60 std::ostringstream ss;
67 ss <<
"Visual Comparison Result:\n";
68 ss <<
" Identical: " << (
identical ?
"Yes" :
"No") <<
"\n";
69 ss <<
" Passed: " << (
passed ?
"Yes" :
"No") <<
"\n";
70 ss <<
" Similarity: " << absl::StrFormat(
"%.2f%%",
similarity * 100) <<
"\n";
71 ss <<
" Difference: " << absl::StrFormat(
"%.2f%%",
difference_pct * 100)
78 ss <<
" Significant Regions:\n";
80 ss <<
" - (" << region.x <<
", " << region.y <<
") " << region.width
81 <<
"x" << region.height <<
" ("
82 << absl::StrFormat(
"%.1f%%", region.local_diff_pct * 100) <<
" diff)\n";
91 ss <<
" Diff Image: " <<
diff_image_png.size() <<
" bytes (PNG)\n";
98 std::ostringstream ss;
100 ss <<
" \"identical\": " << (
identical ?
"true" :
"false") <<
",\n";
101 ss <<
" \"passed\": " << (
passed ?
"true" :
"false") <<
",\n";
102 ss <<
" \"similarity\": " <<
similarity <<
",\n";
104 ss <<
" \"width\": " <<
width <<
",\n";
105 ss <<
" \"height\": " <<
height <<
",\n";
108 ss <<
" \"has_diff_image\": " << (!
diff_image_png.empty() ?
"true" :
"false")
111 ss <<
" \"significant_regions\": [";
114 if (i > 0) ss <<
", ";
116 ss <<
"{\"x\": " << r.x <<
", \"y\": " << r.y <<
", \"width\": " << r.width
117 <<
", \"height\": " << r.height <<
", \"diff_pct\": " << r.local_diff_pct
140 const std::string& path_a,
const std::string& path_b) {
141 auto screenshot_a =
LoadPng(path_a);
142 if (!screenshot_a.ok()) {
143 return screenshot_a.status();
146 auto screenshot_b =
LoadPng(path_b);
147 if (!screenshot_b.ok()) {
148 return screenshot_b.status();
155 const std::vector<uint8_t>& png_a,
const std::vector<uint8_t>& png_b) {
157 if (!screenshot_a.ok()) {
158 return screenshot_a.status();
162 if (!screenshot_b.ok()) {
163 return screenshot_b.status();
170 const std::vector<uint8_t>& png_data,
const std::string& reference_path) {
172 if (!screenshot.ok()) {
173 return screenshot.status();
176 auto reference =
LoadPng(reference_path);
177 if (!reference.ok()) {
178 return reference.status();
190 const std::vector<uint8_t>& png_a,
const std::vector<uint8_t>& png_b,
193 if (!screenshot_a.ok()) {
194 return screenshot_a.status();
198 if (!screenshot_b.ok()) {
199 return screenshot_b.status();
202 return CompareImpl(*screenshot_a, *screenshot_b, region);
221 "Image dimensions don't match: %dx%d vs %dx%d", a.
width, a.
height,
274 }
else if (result.
passed) {
276 "Images match within tolerance (%.1f%% similar, %.1f%% threshold)",
280 "Images differ significantly (%.1f%% similar, %.1f%% threshold)",
303 for (
int y = y1; y < y2; ++y) {
304 for (
int x = x1; x < x2; ++x) {
308 if (x >= ir.x && x < ir.x + ir.width && y >= ir.y &&
309 y < ir.y + ir.height) {
314 if (ignore)
continue;
318 const uint8_t* pa = &a.
data[idx];
319 const uint8_t* pb = &b.
data[idx];
330 result.
similarity = total > 0 ? 1.0f - (
static_cast<float>(differing) / total) : 1.0f;
331 result.
difference_pct = total > 0 ? (
static_cast<float>(differing) / total) : 0.0f;
366 double sum_a = 0, sum_b = 0;
367 double sum_aa = 0, sum_bb = 0, sum_ab = 0;
370 for (
int y = y1; y < y2; ++y) {
371 for (
int x = x1; x < x2; ++x) {
375 double la = 0.299 * a.
data[idx] + 0.587 * a.
data[idx + 1] +
376 0.114 * a.
data[idx + 2];
377 double lb = 0.299 * b.
data[idx] + 0.587 * b.
data[idx + 1] +
378 0.114 * b.
data[idx + 2];
389 if (count == 0)
return 1.0f;
391 double mean_a = sum_a / count;
392 double mean_b = sum_b / count;
393 double var_a = (sum_aa / count) - (mean_a * mean_a);
394 double var_b = (sum_bb / count) - (mean_b * mean_b);
395 double cov_ab = (sum_ab / count) - (mean_a * mean_b);
398 const double C1 = 6.5025;
399 const double C2 = 58.5225;
401 double numerator = (2 * mean_a * mean_b + C1) * (2 * cov_ab + C2);
403 (mean_a * mean_a + mean_b * mean_b + C1) * (var_a + var_b + C2);
405 return static_cast<float>(numerator / denominator);
409 const uint8_t* pixel_b)
const {
411 return std::abs(pixel_a[0] - pixel_b[0]) <= threshold &&
412 std::abs(pixel_a[1] - pixel_b[1]) <= threshold &&
413 std::abs(pixel_a[2] - pixel_b[2]) <= threshold;
417 const uint8_t* pixel_b)
const {
418 int diff_r = std::abs(pixel_a[0] - pixel_b[0]);
419 int diff_g = std::abs(pixel_a[1] - pixel_b[1]);
420 int diff_b = std::abs(pixel_a[2] - pixel_b[2]);
421 return (diff_r + diff_g + diff_b) / (255.0f * 3.0f);
432 for (
int y = 0; y < a.
height; ++y) {
433 for (
int x = 0; x < a.
width; ++x) {
435 const uint8_t* pa = &a.
data[idx];
436 const uint8_t* pb = &b.
data[idx];
440 diff.
data[idx + 0] = pa[0] / 2;
441 diff.
data[idx + 1] = pa[1] / 2;
442 diff.
data[idx + 2] = pa[2] / 2;
443 diff.
data[idx + 3] = 255;
446 diff.
data[idx + 0] = 255;
447 diff.
data[idx + 1] = 0;
448 diff.
data[idx + 2] = 0;
449 diff.
data[idx + 3] = 255;
455 return encoded.ok() ? *encoded : std::vector<uint8_t>();
465 for (
int y = 0; y < a.
height; ++y) {
466 for (
int x = 0; x < a.
width; ++x) {
468 const uint8_t* pa = &a.
data[idx];
469 const uint8_t* pb = &b.
data[idx];
475 if (diff_amount < 0.5f) {
477 float t = diff_amount * 2;
478 r =
static_cast<uint8_t
>(255 * t);
483 float t = (diff_amount - 0.5f) * 2;
485 g =
static_cast<uint8_t
>(255 * (1 - t));
489 diff.
data[idx + 0] = r;
490 diff.
data[idx + 1] = g;
491 diff.
data[idx + 2] = b_val;
492 diff.
data[idx + 3] = 255;
497 return encoded.ok() ? *encoded : std::vector<uint8_t>();
508 for (
int y = 0; y < a.
height; ++y) {
509 for (
int x = 0; x < a.
width; ++x) {
511 const uint8_t* pa = &a.
data[src_idx];
512 const uint8_t* pb = &b.
data[src_idx];
515 size_t dst_a = (y * combined.
width + x) * 4;
516 combined.
data[dst_a + 0] = pa[0];
517 combined.
data[dst_a + 1] = pa[1];
518 combined.
data[dst_a + 2] = pa[2];
519 combined.
data[dst_a + 3] = 255;
522 size_t dst_diff = (y * combined.
width + x + a.
width) * 4;
524 combined.
data[dst_diff + 0] = pa[0] / 2;
525 combined.
data[dst_diff + 1] = pa[1] / 2;
526 combined.
data[dst_diff + 2] = pa[2] / 2;
528 combined.
data[dst_diff + 0] = 255;
529 combined.
data[dst_diff + 1] = 0;
530 combined.
data[dst_diff + 2] = 0;
532 combined.
data[dst_diff + 3] = 255;
535 size_t dst_b = (y * combined.
width + x + a.
width * 2) * 4;
536 combined.
data[dst_b + 0] = pb[0];
537 combined.
data[dst_b + 1] = pb[1];
538 combined.
data[dst_b + 2] = pb[2];
539 combined.
data[dst_b + 3] = 255;
544 return encoded.ok() ? *encoded : std::vector<uint8_t>();
549 std::vector<VisualDiffResult::DiffRegion> regions;
555 const int grid_size = 32;
557 for (
int gy = 0; gy < (a.
height + grid_size - 1) / grid_size; ++gy) {
558 for (
int gx = 0; gx < (a.
width + grid_size - 1) / grid_size; ++gx) {
559 int x1 = gx * grid_size;
560 int y1 = gy * grid_size;
561 int x2 = std::min(x1 + grid_size, a.
width);
562 int y2 = std::min(y1 + grid_size, a.
height);
567 for (
int y = y1; y < y2; ++y) {
568 for (
int x = x1; x < x2; ++x) {
578 float local_diff_pct =
static_cast<float>(diff_count) / total;
579 if (local_diff_pct > 0.1f) {
583 region.
width = x2 - x1;
586 regions.push_back(region);
595 std::vector<VisualDiffResult::DiffRegion>& regions) {
596 if (regions.size() < 2)
return;
602 for (
size_t i = 0; i < regions.size(); ++i) {
603 for (
size_t j = i + 1; j < regions.size(); ++j) {
604 auto& ri = regions[i];
605 auto& rj = regions[j];
610 (ri.x - gap <= rj.x + rj.width && ri.x + ri.width + gap >= rj.x &&
611 ri.y - gap <= rj.y + rj.height && ri.y + ri.height + gap >= rj.y);
615 int new_x = std::min(ri.x, rj.x);
616 int new_y = std::min(ri.y, rj.y);
617 int new_x2 = std::max(ri.x + ri.width, rj.x + rj.width);
618 int new_y2 = std::max(ri.y + ri.height, rj.y + rj.height);
622 ri.width = new_x2 - new_x;
623 ri.height = new_y2 - new_y;
624 ri.local_diff_pct = std::max(ri.local_diff_pct, rj.local_diff_pct);
626 regions.erase(regions.begin() + j);
646 const std::string& output_path) {
648 return absl::InvalidArgumentError(
"No diff image available");
651 std::ofstream file(output_path, std::ios::binary);
653 return absl::InternalError(
654 absl::StrCat(
"Failed to open file for writing: ", output_path));
657 file.write(
reinterpret_cast<const char*
>(result.
diff_image_png.data()),
660 return absl::OkStatus();
664 const std::vector<uint8_t>& png_data) {
665 if (png_data.size() < 8) {
666 return absl::InvalidArgumentError(
"PNG data too small");
670 if (png_sig_cmp(
reinterpret_cast<png_bytep
>(
671 const_cast<uint8_t*
>(png_data.data())),
673 return absl::InvalidArgumentError(
"Invalid PNG signature");
676 png_structp png_ptr =
677 png_create_read_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
679 return absl::InternalError(
"Failed to create PNG read struct");
682 png_infop info_ptr = png_create_info_struct(png_ptr);
684 png_destroy_read_struct(&png_ptr,
nullptr,
nullptr);
685 return absl::InternalError(
"Failed to create PNG info struct");
688 if (setjmp(png_jmpbuf(png_ptr))) {
689 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
690 return absl::InternalError(
"PNG decoding error");
693 PngReadContext ctx{png_data.data(), 0, png_data.size()};
694 png_set_read_fn(png_ptr, &ctx, PngReadCallback);
696 png_read_info(png_ptr, info_ptr);
698 png_uint_32 width = png_get_image_width(png_ptr, info_ptr);
699 png_uint_32 height = png_get_image_height(png_ptr, info_ptr);
700 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
701 png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
704 if (bit_depth == 16) png_set_strip_16(png_ptr);
705 if (color_type == PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png_ptr);
706 if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
707 png_set_expand_gray_1_2_4_to_8(png_ptr);
708 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
709 png_set_tRNS_to_alpha(png_ptr);
710 if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY ||
711 color_type == PNG_COLOR_TYPE_PALETTE)
712 png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
713 if (color_type == PNG_COLOR_TYPE_GRAY ||
714 color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
715 png_set_gray_to_rgb(png_ptr);
717 png_read_update_info(png_ptr, info_ptr);
720 result.
width =
static_cast<int>(width);
721 result.
height =
static_cast<int>(height);
722 result.
data.resize(width * height * 4);
724 std::vector<png_bytep> row_pointers(height);
725 for (png_uint_32 y = 0; y < height; ++y) {
726 row_pointers[y] = result.
data.data() + y * width * 4;
729 png_read_image(png_ptr, row_pointers.data());
730 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
738 return absl::InvalidArgumentError(
"Invalid screenshot");
741 png_structp png_ptr =
742 png_create_write_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
744 return absl::InternalError(
"Failed to create PNG write struct");
747 png_infop info_ptr = png_create_info_struct(png_ptr);
749 png_destroy_write_struct(&png_ptr,
nullptr);
750 return absl::InternalError(
"Failed to create PNG info struct");
753 std::vector<uint8_t> output;
754 PngWriteContext ctx{&output};
756 if (setjmp(png_jmpbuf(png_ptr))) {
757 png_destroy_write_struct(&png_ptr, &info_ptr);
758 return absl::InternalError(
"PNG encoding error");
761 png_set_write_fn(png_ptr, &ctx, PngWriteCallback, PngFlushCallback);
763 png_set_IHDR(png_ptr, info_ptr, screenshot.
width, screenshot.
height, 8,
764 PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
765 PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
767 png_write_info(png_ptr, info_ptr);
769 std::vector<png_bytep> row_pointers(screenshot.
height);
770 for (
int y = 0; y < screenshot.
height; ++y) {
772 const_cast<png_bytep
>(screenshot.
data.data() + y * screenshot.
width * 4);
775 png_write_image(png_ptr, row_pointers.data());
776 png_write_end(png_ptr,
nullptr);
777 png_destroy_write_struct(&png_ptr, &info_ptr);
783 std::ifstream file(path, std::ios::binary);
785 return absl::NotFoundError(absl::StrCat(
"File not found: ", path));
788 file.seekg(0, std::ios::end);
789 size_t size = file.tellg();
790 file.seekg(0, std::ios::beg);
792 std::vector<uint8_t> data(size);
793 file.read(
reinterpret_cast<char*
>(data.data()), size);
797 result->source = path;
803 const std::string& path) {
806 return encoded.status();
809 std::ofstream file(path, std::ios::binary);
811 return absl::InternalError(
812 absl::StrCat(
"Failed to open file for writing: ", path));
815 file.write(
reinterpret_cast<const char*
>(encoded->data()), encoded->size());
816 return absl::OkStatus();
bool ColorsMatch(const uint8_t *pixel_a, const uint8_t *pixel_b) const
static absl::StatusOr< Screenshot > DecodePng(const std::vector< uint8_t > &png_data)
Decode PNG data to Screenshot.
absl::StatusOr< VisualDiffResult > CompareWithReference(const std::vector< uint8_t > &png_data, const std::string &reference_path)
Compare PNG data against a reference file.
std::vector< uint8_t > GenerateRedHighlightDiff(const Screenshot &a, const Screenshot &b, const VisualDiffResult &result)
absl::StatusOr< VisualDiffResult > CompareRegion(const std::vector< uint8_t > &png_a, const std::vector< uint8_t > &png_b, const ScreenRegion ®ion)
Compare a specific region of two images.
static float CalculateSSIM(const Screenshot &a, const Screenshot &b)
Calculate Structural Similarity Index.
VisualDiffResult ComparePixelExact(const Screenshot &a, const Screenshot &b, const ScreenRegion ®ion)
float PixelDifference(const uint8_t *pixel_a, const uint8_t *pixel_b) const
VisualDiffResult CompareSSIM(const Screenshot &a, const Screenshot &b, const ScreenRegion ®ion)
void MergeNearbyRegions(std::vector< VisualDiffResult::DiffRegion > ®ions)
std::vector< uint8_t > GenerateHeatmapDiff(const Screenshot &a, const Screenshot &b)
static absl::StatusOr< Screenshot > LoadPng(const std::string &path)
Load PNG from file.
absl::StatusOr< VisualDiffResult > ComparePngData(const std::vector< uint8_t > &png_a, const std::vector< uint8_t > &png_b)
Compare two PNG images from raw data.
static absl::Status SavePng(const Screenshot &screenshot, const std::string &path)
Save screenshot to PNG file.
absl::StatusOr< VisualDiffResult > ComparePngFiles(const std::string &path_a, const std::string &path_b)
Compare two PNG files.
VisualDiffResult CompareImpl(const Screenshot &a, const Screenshot &b, const ScreenRegion ®ion)
static absl::StatusOr< std::vector< uint8_t > > EncodePng(const Screenshot &screenshot)
Encode Screenshot to PNG.
VisualDiffResult CompareScreenshots(const Screenshot &a, const Screenshot &b)
Compare two Screenshot objects directly.
std::vector< uint8_t > GenerateSideBySideDiff(const Screenshot &a, const Screenshot &b, const VisualDiffResult &result)
absl::StatusOr< std::vector< uint8_t > > GenerateDiffPng(const Screenshot &a, const Screenshot &b)
Generate a diff image highlighting differences.
std::vector< VisualDiffResult::DiffRegion > FindSignificantRegions(const Screenshot &a, const Screenshot &b, int threshold)
static float CalculateRegionSSIM(const Screenshot &a, const Screenshot &b, const ScreenRegion ®ion)
Calculate SSIM for a specific region.
absl::Status SaveDiffImage(const VisualDiffResult &result, const std::string &output_path)
Save diff image to file.
void PngReadCallback(png_structp png_ptr, png_bytep data, png_size_t length)
void PngFlushCallback(png_structp)
void PngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length)
Region of interest for screenshot comparison.
bool IsFullScreen() const
static ScreenRegion FullScreen()
Screenshot data container.
std::vector< uint8_t > data
size_t GetPixelIndex(int x, int y) const
Configuration for visual diff engine.
std::vector< ScreenRegion > ignore_regions
int region_merge_distance
Result of visual comparison with diff image.
std::string ToJson() const
Serialize to JSON for MCP tool output.
std::vector< uint8_t > diff_image_png
std::string diff_description
std::string error_message
std::string Format() const
Format result for human-readable output.
std::vector< DiffRegion > significant_regions
std::vector< uint8_t > * output