yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
visual_diff_engine.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <fstream>
6#include <sstream>
7
8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
10
11// Include libpng for PNG encoding/decoding
12extern "C" {
13#include <png.h>
14}
15
16namespace yaze {
17namespace test {
18
19namespace {
20
21// PNG read/write helpers
23 const uint8_t* data;
24 size_t offset;
25 size_t size;
26};
27
28void PngReadCallback(png_structp png_ptr, png_bytep data, png_size_t length) {
29 PngReadContext* ctx = static_cast<PngReadContext*>(png_get_io_ptr(png_ptr));
30 if (ctx->offset + length > ctx->size) {
31 png_error(png_ptr, "Read past end of PNG data");
32 return;
33 }
34 std::memcpy(data, ctx->data + ctx->offset, length);
35 ctx->offset += length;
36}
37
39 std::vector<uint8_t>* output;
40};
41
42void PngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length) {
43 PngWriteContext* ctx = static_cast<PngWriteContext*>(png_get_io_ptr(png_ptr));
44 ctx->output->insert(ctx->output->end(), data, data + length);
45}
46
47void PngFlushCallback(png_structp /*png_ptr*/) {
48 // No-op for memory writes
49}
50
51} // namespace
52
53// ============================================================================
54// VisualDiffResult
55// ============================================================================
56
57std::string VisualDiffResult::Format() const {
58 std::ostringstream ss;
59
60 if (!error_message.empty()) {
61 ss << "Error: " << error_message << "\n";
62 return ss.str();
63 }
64
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)
70 << "\n";
71 ss << " Dimensions: " << width << "x" << height << " (" << total_pixels
72 << " pixels)\n";
73 ss << " Differing Pixels: " << differing_pixels << "\n";
74
75 if (!significant_regions.empty()) {
76 ss << " Significant Regions:\n";
77 for (const auto& region : significant_regions) {
78 ss << " - (" << region.x << ", " << region.y << ") " << region.width
79 << "x" << region.height << " ("
80 << absl::StrFormat("%.1f%%", region.local_diff_pct * 100)
81 << " diff)\n";
82 }
83 }
84
85 if (!diff_description.empty()) {
86 ss << " Description: " << diff_description << "\n";
87 }
88
89 if (!diff_image_png.empty()) {
90 ss << " Diff Image: " << diff_image_png.size() << " bytes (PNG)\n";
91 }
92
93 return ss.str();
94}
95
96std::string VisualDiffResult::ToJson() const {
97 std::ostringstream ss;
98 ss << "{\n";
99 ss << " \"identical\": " << (identical ? "true" : "false") << ",\n";
100 ss << " \"passed\": " << (passed ? "true" : "false") << ",\n";
101 ss << " \"similarity\": " << similarity << ",\n";
102 ss << " \"difference_pct\": " << difference_pct << ",\n";
103 ss << " \"width\": " << width << ",\n";
104 ss << " \"height\": " << height << ",\n";
105 ss << " \"total_pixels\": " << total_pixels << ",\n";
106 ss << " \"differing_pixels\": " << differing_pixels << ",\n";
107 ss << " \"has_diff_image\": " << (!diff_image_png.empty() ? "true" : "false")
108 << ",\n";
109 ss << " \"diff_image_size\": " << diff_image_png.size() << ",\n";
110 ss << " \"significant_regions\": [";
111
112 for (size_t i = 0; i < significant_regions.size(); ++i) {
113 if (i > 0)
114 ss << ", ";
115 const auto& r = significant_regions[i];
116 ss << "{\"x\": " << r.x << ", \"y\": " << r.y << ", \"width\": " << r.width
117 << ", \"height\": " << r.height << ", \"diff_pct\": " << r.local_diff_pct
118 << "}";
119 }
120 ss << "]";
121
122 if (!error_message.empty()) {
123 ss << ",\n \"error\": \"" << error_message << "\"";
124 }
125
126 ss << "\n}";
127 return ss.str();
128}
129
130// ============================================================================
131// VisualDiffEngine
132// ============================================================================
133
135
137 : config_(config) {}
138
139absl::StatusOr<VisualDiffResult> VisualDiffEngine::ComparePngFiles(
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();
144 }
145
146 auto screenshot_b = LoadPng(path_b);
147 if (!screenshot_b.ok()) {
148 return screenshot_b.status();
149 }
150
151 return CompareScreenshots(*screenshot_a, *screenshot_b);
152}
153
154absl::StatusOr<VisualDiffResult> VisualDiffEngine::ComparePngData(
155 const std::vector<uint8_t>& png_a, const std::vector<uint8_t>& png_b) {
156 auto screenshot_a = DecodePng(png_a);
157 if (!screenshot_a.ok()) {
158 return screenshot_a.status();
159 }
160
161 auto screenshot_b = DecodePng(png_b);
162 if (!screenshot_b.ok()) {
163 return screenshot_b.status();
164 }
165
166 return CompareScreenshots(*screenshot_a, *screenshot_b);
167}
168
169absl::StatusOr<VisualDiffResult> VisualDiffEngine::CompareWithReference(
170 const std::vector<uint8_t>& png_data, const std::string& reference_path) {
171 auto screenshot = DecodePng(png_data);
172 if (!screenshot.ok()) {
173 return screenshot.status();
174 }
175
176 auto reference = LoadPng(reference_path);
177 if (!reference.ok()) {
178 return reference.status();
179 }
180
181 return CompareScreenshots(*screenshot, *reference);
182}
183
188
189absl::StatusOr<VisualDiffResult> VisualDiffEngine::CompareRegion(
190 const std::vector<uint8_t>& png_a, const std::vector<uint8_t>& png_b,
191 const ScreenRegion& region) {
192 auto screenshot_a = DecodePng(png_a);
193 if (!screenshot_a.ok()) {
194 return screenshot_a.status();
195 }
196
197 auto screenshot_b = DecodePng(png_b);
198 if (!screenshot_b.ok()) {
199 return screenshot_b.status();
200 }
201
202 return CompareImpl(*screenshot_a, *screenshot_b, region);
203}
204
206 const Screenshot& b,
207 const ScreenRegion& region) {
208 VisualDiffResult result;
209
210 // Validate inputs
211 if (!a.IsValid()) {
212 result.error_message = "First image is invalid";
213 return result;
214 }
215 if (!b.IsValid()) {
216 result.error_message = "Second image is invalid";
217 return result;
218 }
219 if (a.width != b.width || a.height != b.height) {
220 result.error_message =
221 absl::StrFormat("Image dimensions don't match: %dx%d vs %dx%d", a.width,
222 a.height, b.width, b.height);
223 return result;
224 }
225
226 result.width = a.width;
227 result.height = a.height;
228
229 // Run comparison based on configured algorithm
230 switch (config_.algorithm) {
232 result = ComparePixelExact(a, b, region);
233 break;
235 result = CompareSSIM(a, b, region);
236 break;
238 // Fall back to pixel exact for now
239 result = ComparePixelExact(a, b, region);
240 break;
241 }
242
243 // Determine pass/fail
244 result.passed = result.similarity >= config_.tolerance;
245
246 // Generate diff image if requested and there are differences
247 if (config_.generate_diff_image && !result.identical) {
248 switch (config_.diff_style) {
250 result.diff_image_png = GenerateRedHighlightDiff(a, b, result);
251 break;
253 result.diff_image_png = GenerateHeatmapDiff(a, b);
254 break;
256 result.diff_image_png = GenerateSideBySideDiff(a, b, result);
257 break;
258 default:
259 result.diff_image_png = GenerateRedHighlightDiff(a, b, result);
260 break;
261 }
262 }
263
264 // Find significant regions if there are differences
265 if (!result.identical) {
266 result.significant_regions =
269 }
270
271 // Generate description
272 if (result.identical) {
273 result.diff_description = "Images are pixel-perfect identical";
274 } else if (result.passed) {
275 result.diff_description = absl::StrFormat(
276 "Images match within tolerance (%.1f%% similar, %.1f%% threshold)",
277 result.similarity * 100, config_.tolerance * 100);
278 } else {
279 result.diff_description = absl::StrFormat(
280 "Images differ significantly (%.1f%% similar, %.1f%% threshold)",
281 result.similarity * 100, config_.tolerance * 100);
282 }
283
284 return result;
285}
286
288 const Screenshot& a, const Screenshot& b, const ScreenRegion& region) {
289 VisualDiffResult result;
290 result.width = a.width;
291 result.height = a.height;
292
293 // Calculate comparison region
294 int x1 = region.x;
295 int y1 = region.y;
296 int x2 = region.IsFullScreen() ? a.width
297 : std::min(region.x + region.width, a.width);
298 int y2 = region.IsFullScreen() ? a.height
299 : std::min(region.y + region.height, a.height);
300
301 int total = 0;
302 int differing = 0;
303
304 for (int y = y1; y < y2; ++y) {
305 for (int x = x1; x < x2; ++x) {
306 // Skip ignored regions
307 bool ignore = false;
308 for (const auto& ir : config_.ignore_regions) {
309 if (x >= ir.x && x < ir.x + ir.width && y >= ir.y &&
310 y < ir.y + ir.height) {
311 ignore = true;
312 break;
313 }
314 }
315 if (ignore)
316 continue;
317
318 total++;
319 size_t idx = a.GetPixelIndex(x, y);
320 const uint8_t* pa = &a.data[idx];
321 const uint8_t* pb = &b.data[idx];
322
323 if (!ColorsMatch(pa, pb)) {
324 differing++;
325 }
326 }
327 }
328
329 result.total_pixels = total;
330 result.differing_pixels = differing;
331 result.identical = (differing == 0);
332 result.similarity =
333 total > 0 ? 1.0f - (static_cast<float>(differing) / total) : 1.0f;
334 result.difference_pct =
335 total > 0 ? (static_cast<float>(differing) / total) : 0.0f;
336
337 return result;
338}
339
341 const Screenshot& b,
342 const ScreenRegion& region) {
343 VisualDiffResult result = ComparePixelExact(a, b, region);
344
345 // Calculate SSIM for more perceptual comparison
346 float ssim = region.IsFullScreen() ? CalculateSSIM(a, b)
347 : CalculateRegionSSIM(a, b, region);
348
349 // Use SSIM as the similarity metric
350 result.similarity = ssim;
351
352 return result;
353}
354
359
361 const Screenshot& b,
362 const ScreenRegion& region) {
363 // Simplified SSIM calculation
364 // Full SSIM uses sliding windows, but this gives a reasonable approximation
365
366 int x1 = region.x;
367 int y1 = region.y;
368 int x2 = region.IsFullScreen() ? a.width
369 : std::min(region.x + region.width, a.width);
370 int y2 = region.IsFullScreen() ? a.height
371 : std::min(region.y + region.height, a.height);
372
373 double sum_a = 0, sum_b = 0;
374 double sum_aa = 0, sum_bb = 0, sum_ab = 0;
375 int count = 0;
376
377 for (int y = y1; y < y2; ++y) {
378 for (int x = x1; x < x2; ++x) {
379 size_t idx = a.GetPixelIndex(x, y);
380
381 // Convert to grayscale luminance for SSIM
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];
386
387 sum_a += la;
388 sum_b += lb;
389 sum_aa += la * la;
390 sum_bb += lb * lb;
391 sum_ab += la * lb;
392 count++;
393 }
394 }
395
396 if (count == 0)
397 return 1.0f;
398
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);
404
405 // SSIM constants
406 const double C1 = 6.5025; // (0.01 * 255)^2
407 const double C2 = 58.5225; // (0.03 * 255)^2
408
409 double numerator = (2 * mean_a * mean_b + C1) * (2 * cov_ab + C2);
410 double denominator =
411 (mean_a * mean_a + mean_b * mean_b + C1) * (var_a + var_b + C2);
412
413 return static_cast<float>(numerator / denominator);
414}
415
416bool VisualDiffEngine::ColorsMatch(const uint8_t* pixel_a,
417 const uint8_t* pixel_b) const {
418 int threshold = config_.color_threshold;
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;
422}
423
424float VisualDiffEngine::PixelDifference(const uint8_t* pixel_a,
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);
430}
431
433 const Screenshot& a, const Screenshot& b, const VisualDiffResult& result) {
434 // Create diff image: base image A with red overlay on differences
435 Screenshot diff;
436 diff.width = a.width;
437 diff.height = a.height;
438 diff.data.resize(a.data.size());
439
440 for (int y = 0; y < a.height; ++y) {
441 for (int x = 0; x < a.width; ++x) {
442 size_t idx = a.GetPixelIndex(x, y);
443 const uint8_t* pa = &a.data[idx];
444 const uint8_t* pb = &b.data[idx];
445
446 if (ColorsMatch(pa, pb)) {
447 // Same: show original (slightly dimmed)
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;
452 } else {
453 // Different: red highlight
454 diff.data[idx + 0] = 255;
455 diff.data[idx + 1] = 0;
456 diff.data[idx + 2] = 0;
457 diff.data[idx + 3] = 255;
458 }
459 }
460 }
461
462 auto encoded = EncodePng(diff);
463 return encoded.ok() ? *encoded : std::vector<uint8_t>();
464}
465
467 const Screenshot& a, const Screenshot& b) {
468 Screenshot diff;
469 diff.width = a.width;
470 diff.height = a.height;
471 diff.data.resize(a.data.size());
472
473 for (int y = 0; y < a.height; ++y) {
474 for (int x = 0; x < a.width; ++x) {
475 size_t idx = a.GetPixelIndex(x, y);
476 const uint8_t* pa = &a.data[idx];
477 const uint8_t* pb = &b.data[idx];
478
479 float diff_amount = PixelDifference(pa, pb);
480
481 // Map to heatmap color (green -> yellow -> red)
482 uint8_t r, g, b_val;
483 if (diff_amount < 0.5f) {
484 // Green to yellow
485 float t = diff_amount * 2;
486 r = static_cast<uint8_t>(255 * t);
487 g = 255;
488 b_val = 0;
489 } else {
490 // Yellow to red
491 float t = (diff_amount - 0.5f) * 2;
492 r = 255;
493 g = static_cast<uint8_t>(255 * (1 - t));
494 b_val = 0;
495 }
496
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;
501 }
502 }
503
504 auto encoded = EncodePng(diff);
505 return encoded.ok() ? *encoded : std::vector<uint8_t>();
506}
507
509 const Screenshot& a, const Screenshot& b, const VisualDiffResult& result) {
510 // Create side-by-side: A | Diff | B
511 Screenshot combined;
512 combined.width = a.width * 3;
513 combined.height = a.height;
514 combined.data.resize(combined.width * combined.height * 4);
515
516 for (int y = 0; y < a.height; ++y) {
517 for (int x = 0; x < a.width; ++x) {
518 size_t src_idx = a.GetPixelIndex(x, y);
519 const uint8_t* pa = &a.data[src_idx];
520 const uint8_t* pb = &b.data[src_idx];
521
522 // Image A (left)
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;
528
529 // Diff (center)
530 size_t dst_diff = (y * combined.width + x + a.width) * 4;
531 if (ColorsMatch(pa, pb)) {
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;
535 } else {
536 combined.data[dst_diff + 0] = 255;
537 combined.data[dst_diff + 1] = 0;
538 combined.data[dst_diff + 2] = 0;
539 }
540 combined.data[dst_diff + 3] = 255;
541
542 // Image B (right)
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;
548 }
549 }
550
551 auto encoded = EncodePng(combined);
552 return encoded.ok() ? *encoded : std::vector<uint8_t>();
553}
554
555std::vector<VisualDiffResult::DiffRegion>
557 const Screenshot& b, int threshold) {
558 std::vector<VisualDiffResult::DiffRegion> regions;
559
560 // Simple approach: find bounding boxes of contiguous diff regions
561 // More sophisticated: use connected component analysis
562
563 // For now, use a grid-based approach
564 const int grid_size = 32; // 32x32 pixel cells
565
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);
572
573 int diff_count = 0;
574 int total = 0;
575
576 for (int y = y1; y < y2; ++y) {
577 for (int x = x1; x < x2; ++x) {
578 total++;
579 size_t idx = a.GetPixelIndex(x, y);
580 if (!ColorsMatch(&a.data[idx], &b.data[idx])) {
581 diff_count++;
582 }
583 }
584 }
585
586 // If more than 10% of cell is different, report as region
587 float local_diff_pct = static_cast<float>(diff_count) / total;
588 if (local_diff_pct > 0.1f) {
590 region.x = x1;
591 region.y = y1;
592 region.width = x2 - x1;
593 region.height = y2 - y1;
594 region.local_diff_pct = local_diff_pct;
595 regions.push_back(region);
596 }
597 }
598 }
599
600 return regions;
601}
602
604 std::vector<VisualDiffResult::DiffRegion>& regions) {
605 if (regions.size() < 2)
606 return;
607
608 // Simple merge: combine overlapping or adjacent regions
609 bool merged = true;
610 while (merged) {
611 merged = false;
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];
616
617 // Check if regions are close enough to merge
619 bool adjacent =
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);
622
623 if (adjacent) {
624 // Merge into ri
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);
629
630 ri.x = new_x;
631 ri.y = new_y;
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);
635
636 regions.erase(regions.begin() + j);
637 merged = true;
638 break;
639 }
640 }
641 if (merged)
642 break;
643 }
644 }
645}
646
647absl::StatusOr<std::vector<uint8_t>> VisualDiffEngine::GenerateDiffPng(
648 const Screenshot& a, const Screenshot& b) {
650 if (!result.diff_image_png.empty()) {
651 return result.diff_image_png;
652 }
653 return GenerateRedHighlightDiff(a, b, result);
654}
655
657 const std::string& output_path) {
658 if (result.diff_image_png.empty()) {
659 return absl::InvalidArgumentError("No diff image available");
660 }
661
662 std::ofstream file(output_path, std::ios::binary);
663 if (!file) {
664 return absl::InternalError(
665 absl::StrCat("Failed to open file for writing: ", output_path));
666 }
667
668 file.write(reinterpret_cast<const char*>(result.diff_image_png.data()),
669 result.diff_image_png.size());
670
671 return absl::OkStatus();
672}
673
674absl::StatusOr<Screenshot> VisualDiffEngine::DecodePng(
675 const std::vector<uint8_t>& png_data) {
676 if (png_data.size() < 8) {
677 return absl::InvalidArgumentError("PNG data too small");
678 }
679
680 // Check PNG signature
681 if (png_sig_cmp(
682 reinterpret_cast<png_bytep>(const_cast<uint8_t*>(png_data.data())), 0,
683 8) != 0) {
684 return absl::InvalidArgumentError("Invalid PNG signature");
685 }
686
687 png_structp png_ptr =
688 png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
689 if (!png_ptr) {
690 return absl::InternalError("Failed to create PNG read struct");
691 }
692
693 png_infop info_ptr = png_create_info_struct(png_ptr);
694 if (!info_ptr) {
695 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
696 return absl::InternalError("Failed to create PNG info struct");
697 }
698
699 if (setjmp(png_jmpbuf(png_ptr))) {
700 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
701 return absl::InternalError("PNG decoding error");
702 }
703
704 PngReadContext ctx{png_data.data(), 0, png_data.size()};
705 png_set_read_fn(png_ptr, &ctx, PngReadCallback);
706
707 png_read_info(png_ptr, info_ptr);
708
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);
713
714 // Convert to RGBA
715 if (bit_depth == 16)
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);
729
730 png_read_update_info(png_ptr, info_ptr);
731
732 Screenshot result;
733 result.width = static_cast<int>(width);
734 result.height = static_cast<int>(height);
735 result.data.resize(width * height * 4);
736
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;
740 }
741
742 png_read_image(png_ptr, row_pointers.data());
743 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
744
745 return result;
746}
747
748absl::StatusOr<std::vector<uint8_t>> VisualDiffEngine::EncodePng(
749 const Screenshot& screenshot) {
750 if (!screenshot.IsValid()) {
751 return absl::InvalidArgumentError("Invalid screenshot");
752 }
753
754 png_structp png_ptr =
755 png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
756 if (!png_ptr) {
757 return absl::InternalError("Failed to create PNG write struct");
758 }
759
760 png_infop info_ptr = png_create_info_struct(png_ptr);
761 if (!info_ptr) {
762 png_destroy_write_struct(&png_ptr, nullptr);
763 return absl::InternalError("Failed to create PNG info struct");
764 }
765
766 std::vector<uint8_t> output;
767 PngWriteContext ctx{&output};
768
769 if (setjmp(png_jmpbuf(png_ptr))) {
770 png_destroy_write_struct(&png_ptr, &info_ptr);
771 return absl::InternalError("PNG encoding error");
772 }
773
774 png_set_write_fn(png_ptr, &ctx, PngWriteCallback, PngFlushCallback);
775
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);
779
780 png_write_info(png_ptr, info_ptr);
781
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);
786 }
787
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);
791
792 return output;
793}
794
795absl::StatusOr<Screenshot> VisualDiffEngine::LoadPng(const std::string& path) {
796 std::ifstream file(path, std::ios::binary);
797 if (!file) {
798 return absl::NotFoundError(absl::StrCat("File not found: ", path));
799 }
800
801 file.seekg(0, std::ios::end);
802 size_t size = file.tellg();
803 file.seekg(0, std::ios::beg);
804
805 std::vector<uint8_t> data(size);
806 file.read(reinterpret_cast<char*>(data.data()), size);
807
808 auto result = DecodePng(data);
809 if (result.ok()) {
810 result->source = path;
811 }
812 return result;
813}
814
815absl::Status VisualDiffEngine::SavePng(const Screenshot& screenshot,
816 const std::string& path) {
817 auto encoded = EncodePng(screenshot);
818 if (!encoded.ok()) {
819 return encoded.status();
820 }
821
822 std::ofstream file(path, std::ios::binary);
823 if (!file) {
824 return absl::InternalError(
825 absl::StrCat("Failed to open file for writing: ", path));
826 }
827
828 file.write(reinterpret_cast<const char*>(encoded->data()), encoded->size());
829 return absl::OkStatus();
830}
831
832} // namespace test
833} // namespace yaze
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 &region)
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 &region)
float PixelDifference(const uint8_t *pixel_a, const uint8_t *pixel_b) const
VisualDiffResult CompareSSIM(const Screenshot &a, const Screenshot &b, const ScreenRegion &region)
void MergeNearbyRegions(std::vector< VisualDiffResult::DiffRegion > &regions)
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 &region)
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 &region)
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 PngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length)
Region of interest for screenshot comparison.
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
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 Format() const
Format result for human-readable output.
std::vector< DiffRegion > significant_regions