8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
31 png_error(png_ptr,
"Read past end of PNG data");
34 std::memcpy(data, ctx->
data + ctx->
offset, length);
44 ctx->
output->insert(ctx->
output->end(), data, data + length);
58 std::ostringstream ss;
65 ss <<
"Visual Comparison Result:\n";
66 ss <<
" Identical: " << (
identical ?
"Yes" :
"No") <<
"\n";
67 ss <<
" Passed: " << (
passed ?
"Yes" :
"No") <<
"\n";
68 ss <<
" Similarity: " << absl::StrFormat(
"%.2f%%",
similarity * 100) <<
"\n";
69 ss <<
" Difference: " << absl::StrFormat(
"%.2f%%",
difference_pct * 100)
76 ss <<
" Significant Regions:\n";
78 ss <<
" - (" << region.x <<
", " << region.y <<
") " << region.width
79 <<
"x" << region.height <<
" ("
80 << absl::StrFormat(
"%.1f%%", region.local_diff_pct * 100)
90 ss <<
" Diff Image: " <<
diff_image_png.size() <<
" bytes (PNG)\n";
97 std::ostringstream ss;
99 ss <<
" \"identical\": " << (
identical ?
"true" :
"false") <<
",\n";
100 ss <<
" \"passed\": " << (
passed ?
"true" :
"false") <<
",\n";
101 ss <<
" \"similarity\": " <<
similarity <<
",\n";
103 ss <<
" \"width\": " <<
width <<
",\n";
104 ss <<
" \"height\": " <<
height <<
",\n";
107 ss <<
" \"has_diff_image\": " << (!
diff_image_png.empty() ?
"true" :
"false")
110 ss <<
" \"significant_regions\": [";
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 absl::StrFormat(
"Image dimensions don't match: %dx%d vs %dx%d", a.
width,
274 }
else if (result.
passed) {
276 "Images match within tolerance (%.1f%% similar, %.1f%% threshold)",
280 "Images differ significantly (%.1f%% similar, %.1f%% threshold)",
304 for (
int y = y1; y < y2; ++y) {
305 for (
int x = x1; x < x2; ++x) {
309 if (x >= ir.x && x < ir.x + ir.width && y >= ir.y &&
310 y < ir.y + ir.height) {
320 const uint8_t* pa = &a.
data[idx];
321 const uint8_t* pb = &b.
data[idx];
333 total > 0 ? 1.0f - (
static_cast<float>(differing) / total) : 1.0f;
335 total > 0 ? (
static_cast<float>(differing) / total) : 0.0f;
373 double sum_a = 0, sum_b = 0;
374 double sum_aa = 0, sum_bb = 0, sum_ab = 0;
377 for (
int y = y1; y < y2; ++y) {
378 for (
int x = x1; x < x2; ++x) {
382 double la = 0.299 * a.
data[idx] + 0.587 * a.
data[idx + 1] +
383 0.114 * a.
data[idx + 2];
384 double lb = 0.299 * b.
data[idx] + 0.587 * b.
data[idx + 1] +
385 0.114 * b.
data[idx + 2];
399 double mean_a = sum_a / count;
400 double mean_b = sum_b / count;
401 double var_a = (sum_aa / count) - (mean_a * mean_a);
402 double var_b = (sum_bb / count) - (mean_b * mean_b);
403 double cov_ab = (sum_ab / count) - (mean_a * mean_b);
406 const double C1 = 6.5025;
407 const double C2 = 58.5225;
409 double numerator = (2 * mean_a * mean_b + C1) * (2 * cov_ab + C2);
411 (mean_a * mean_a + mean_b * mean_b + C1) * (var_a + var_b + C2);
413 return static_cast<float>(numerator / denominator);
417 const uint8_t* pixel_b)
const {
419 return std::abs(pixel_a[0] - pixel_b[0]) <= threshold &&
420 std::abs(pixel_a[1] - pixel_b[1]) <= threshold &&
421 std::abs(pixel_a[2] - pixel_b[2]) <= threshold;
425 const uint8_t* pixel_b)
const {
426 int diff_r = std::abs(pixel_a[0] - pixel_b[0]);
427 int diff_g = std::abs(pixel_a[1] - pixel_b[1]);
428 int diff_b = std::abs(pixel_a[2] - pixel_b[2]);
429 return (diff_r + diff_g + diff_b) / (255.0f * 3.0f);
440 for (
int y = 0; y < a.
height; ++y) {
441 for (
int x = 0; x < a.
width; ++x) {
443 const uint8_t* pa = &a.
data[idx];
444 const uint8_t* pb = &b.
data[idx];
448 diff.
data[idx + 0] = pa[0] / 2;
449 diff.
data[idx + 1] = pa[1] / 2;
450 diff.
data[idx + 2] = pa[2] / 2;
451 diff.
data[idx + 3] = 255;
454 diff.
data[idx + 0] = 255;
455 diff.
data[idx + 1] = 0;
456 diff.
data[idx + 2] = 0;
457 diff.
data[idx + 3] = 255;
463 return encoded.ok() ? *encoded : std::vector<uint8_t>();
473 for (
int y = 0; y < a.
height; ++y) {
474 for (
int x = 0; x < a.
width; ++x) {
476 const uint8_t* pa = &a.
data[idx];
477 const uint8_t* pb = &b.
data[idx];
483 if (diff_amount < 0.5f) {
485 float t = diff_amount * 2;
486 r =
static_cast<uint8_t
>(255 * t);
491 float t = (diff_amount - 0.5f) * 2;
493 g =
static_cast<uint8_t
>(255 * (1 - t));
497 diff.
data[idx + 0] = r;
498 diff.
data[idx + 1] = g;
499 diff.
data[idx + 2] = b_val;
500 diff.
data[idx + 3] = 255;
505 return encoded.ok() ? *encoded : std::vector<uint8_t>();
516 for (
int y = 0; y < a.
height; ++y) {
517 for (
int x = 0; x < a.
width; ++x) {
519 const uint8_t* pa = &a.
data[src_idx];
520 const uint8_t* pb = &b.
data[src_idx];
523 size_t dst_a = (y * combined.
width + x) * 4;
524 combined.
data[dst_a + 0] = pa[0];
525 combined.
data[dst_a + 1] = pa[1];
526 combined.
data[dst_a + 2] = pa[2];
527 combined.
data[dst_a + 3] = 255;
530 size_t dst_diff = (y * combined.
width + x + a.
width) * 4;
532 combined.
data[dst_diff + 0] = pa[0] / 2;
533 combined.
data[dst_diff + 1] = pa[1] / 2;
534 combined.
data[dst_diff + 2] = pa[2] / 2;
536 combined.
data[dst_diff + 0] = 255;
537 combined.
data[dst_diff + 1] = 0;
538 combined.
data[dst_diff + 2] = 0;
540 combined.
data[dst_diff + 3] = 255;
543 size_t dst_b = (y * combined.
width + x + a.
width * 2) * 4;
544 combined.
data[dst_b + 0] = pb[0];
545 combined.
data[dst_b + 1] = pb[1];
546 combined.
data[dst_b + 2] = pb[2];
547 combined.
data[dst_b + 3] = 255;
552 return encoded.ok() ? *encoded : std::vector<uint8_t>();
555std::vector<VisualDiffResult::DiffRegion>
558 std::vector<VisualDiffResult::DiffRegion> regions;
564 const int grid_size = 32;
566 for (
int gy = 0; gy < (a.
height + grid_size - 1) / grid_size; ++gy) {
567 for (
int gx = 0; gx < (a.
width + grid_size - 1) / grid_size; ++gx) {
568 int x1 = gx * grid_size;
569 int y1 = gy * grid_size;
570 int x2 = std::min(x1 + grid_size, a.
width);
571 int y2 = std::min(y1 + grid_size, a.
height);
576 for (
int y = y1; y < y2; ++y) {
577 for (
int x = x1; x < x2; ++x) {
587 float local_diff_pct =
static_cast<float>(diff_count) / total;
588 if (local_diff_pct > 0.1f) {
592 region.
width = x2 - x1;
595 regions.push_back(region);
604 std::vector<VisualDiffResult::DiffRegion>& regions) {
605 if (regions.size() < 2)
612 for (
size_t i = 0; i < regions.size(); ++i) {
613 for (
size_t j = i + 1; j < regions.size(); ++j) {
614 auto& ri = regions[i];
615 auto& rj = regions[j];
620 (ri.x - gap <= rj.x + rj.width && ri.x + ri.width + gap >= rj.x &&
621 ri.y - gap <= rj.y + rj.height && ri.y + ri.height + gap >= rj.y);
625 int new_x = std::min(ri.x, rj.x);
626 int new_y = std::min(ri.y, rj.y);
627 int new_x2 = std::max(ri.x + ri.width, rj.x + rj.width);
628 int new_y2 = std::max(ri.y + ri.height, rj.y + rj.height);
632 ri.width = new_x2 - new_x;
633 ri.height = new_y2 - new_y;
634 ri.local_diff_pct = std::max(ri.local_diff_pct, rj.local_diff_pct);
636 regions.erase(regions.begin() + j);
657 const std::string& output_path) {
659 return absl::InvalidArgumentError(
"No diff image available");
662 std::ofstream file(output_path, std::ios::binary);
664 return absl::InternalError(
665 absl::StrCat(
"Failed to open file for writing: ", output_path));
668 file.write(
reinterpret_cast<const char*
>(result.
diff_image_png.data()),
671 return absl::OkStatus();
675 const std::vector<uint8_t>& png_data) {
676 if (png_data.size() < 8) {
677 return absl::InvalidArgumentError(
"PNG data too small");
682 reinterpret_cast<png_bytep
>(
const_cast<uint8_t*
>(png_data.data())), 0,
684 return absl::InvalidArgumentError(
"Invalid PNG signature");
687 png_structp png_ptr =
688 png_create_read_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
690 return absl::InternalError(
"Failed to create PNG read struct");
693 png_infop info_ptr = png_create_info_struct(png_ptr);
695 png_destroy_read_struct(&png_ptr,
nullptr,
nullptr);
696 return absl::InternalError(
"Failed to create PNG info struct");
699 if (setjmp(png_jmpbuf(png_ptr))) {
700 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
701 return absl::InternalError(
"PNG decoding error");
704 PngReadContext ctx{png_data.data(), 0, png_data.size()};
705 png_set_read_fn(png_ptr, &ctx, PngReadCallback);
707 png_read_info(png_ptr, info_ptr);
709 png_uint_32 width = png_get_image_width(png_ptr, info_ptr);
710 png_uint_32 height = png_get_image_height(png_ptr, info_ptr);
711 png_byte color_type = png_get_color_type(png_ptr, info_ptr);
712 png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
716 png_set_strip_16(png_ptr);
717 if (color_type == PNG_COLOR_TYPE_PALETTE)
718 png_set_palette_to_rgb(png_ptr);
719 if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
720 png_set_expand_gray_1_2_4_to_8(png_ptr);
721 if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
722 png_set_tRNS_to_alpha(png_ptr);
723 if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY ||
724 color_type == PNG_COLOR_TYPE_PALETTE)
725 png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
726 if (color_type == PNG_COLOR_TYPE_GRAY ||
727 color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
728 png_set_gray_to_rgb(png_ptr);
730 png_read_update_info(png_ptr, info_ptr);
733 result.
width =
static_cast<int>(width);
734 result.
height =
static_cast<int>(height);
735 result.
data.resize(width * height * 4);
737 std::vector<png_bytep> row_pointers(height);
738 for (png_uint_32 y = 0; y < height; ++y) {
739 row_pointers[y] = result.
data.data() + y * width * 4;
742 png_read_image(png_ptr, row_pointers.data());
743 png_destroy_read_struct(&png_ptr, &info_ptr,
nullptr);
751 return absl::InvalidArgumentError(
"Invalid screenshot");
754 png_structp png_ptr =
755 png_create_write_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
757 return absl::InternalError(
"Failed to create PNG write struct");
760 png_infop info_ptr = png_create_info_struct(png_ptr);
762 png_destroy_write_struct(&png_ptr,
nullptr);
763 return absl::InternalError(
"Failed to create PNG info struct");
766 std::vector<uint8_t> output;
767 PngWriteContext ctx{&output};
769 if (setjmp(png_jmpbuf(png_ptr))) {
770 png_destroy_write_struct(&png_ptr, &info_ptr);
771 return absl::InternalError(
"PNG encoding error");
774 png_set_write_fn(png_ptr, &ctx, PngWriteCallback, PngFlushCallback);
776 png_set_IHDR(png_ptr, info_ptr, screenshot.
width, screenshot.
height, 8,
777 PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
778 PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
780 png_write_info(png_ptr, info_ptr);
782 std::vector<png_bytep> row_pointers(screenshot.
height);
783 for (
int y = 0; y < screenshot.
height; ++y) {
784 row_pointers[y] =
const_cast<png_bytep
>(screenshot.
data.data() +
785 y * screenshot.
width * 4);
788 png_write_image(png_ptr, row_pointers.data());
789 png_write_end(png_ptr,
nullptr);
790 png_destroy_write_struct(&png_ptr, &info_ptr);
796 std::ifstream file(path, std::ios::binary);
798 return absl::NotFoundError(absl::StrCat(
"File not found: ", path));
801 file.seekg(0, std::ios::end);
802 size_t size = file.tellg();
803 file.seekg(0, std::ios::beg);
805 std::vector<uint8_t> data(size);
806 file.read(
reinterpret_cast<char*
>(data.data()), size);
810 result->source = path;
816 const std::string& path) {
819 return encoded.status();
822 std::ofstream file(path, std::ios::binary);
824 return absl::InternalError(
825 absl::StrCat(
"Failed to open file for writing: ", path));
828 file.write(
reinterpret_cast<const char*
>(encoded->data()), encoded->size());
829 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