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 =
30 static_cast<PngReadContext*>(png_get_io_ptr(png_ptr));
31 if (ctx->offset + length > ctx->size) {
32 png_error(png_ptr, "Read past end of PNG data");
33 return;
34 }
35 std::memcpy(data, ctx->data + ctx->offset, length);
36 ctx->offset += length;
37}
38
40 std::vector<uint8_t>* output;
41};
42
43void PngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length) {
44 PngWriteContext* ctx =
45 static_cast<PngWriteContext*>(png_get_io_ptr(png_ptr));
46 ctx->output->insert(ctx->output->end(), data, data + length);
47}
48
49void PngFlushCallback(png_structp /*png_ptr*/) {
50 // No-op for memory writes
51}
52
53} // namespace
54
55// ============================================================================
56// VisualDiffResult
57// ============================================================================
58
59std::string VisualDiffResult::Format() const {
60 std::ostringstream ss;
61
62 if (!error_message.empty()) {
63 ss << "Error: " << error_message << "\n";
64 return ss.str();
65 }
66
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)
72 << "\n";
73 ss << " Dimensions: " << width << "x" << height << " (" << total_pixels
74 << " pixels)\n";
75 ss << " Differing Pixels: " << differing_pixels << "\n";
76
77 if (!significant_regions.empty()) {
78 ss << " Significant Regions:\n";
79 for (const auto& region : significant_regions) {
80 ss << " - (" << region.x << ", " << region.y << ") " << region.width
81 << "x" << region.height << " ("
82 << absl::StrFormat("%.1f%%", region.local_diff_pct * 100) << " diff)\n";
83 }
84 }
85
86 if (!diff_description.empty()) {
87 ss << " Description: " << diff_description << "\n";
88 }
89
90 if (!diff_image_png.empty()) {
91 ss << " Diff Image: " << diff_image_png.size() << " bytes (PNG)\n";
92 }
93
94 return ss.str();
95}
96
97std::string VisualDiffResult::ToJson() const {
98 std::ostringstream ss;
99 ss << "{\n";
100 ss << " \"identical\": " << (identical ? "true" : "false") << ",\n";
101 ss << " \"passed\": " << (passed ? "true" : "false") << ",\n";
102 ss << " \"similarity\": " << similarity << ",\n";
103 ss << " \"difference_pct\": " << difference_pct << ",\n";
104 ss << " \"width\": " << width << ",\n";
105 ss << " \"height\": " << height << ",\n";
106 ss << " \"total_pixels\": " << total_pixels << ",\n";
107 ss << " \"differing_pixels\": " << differing_pixels << ",\n";
108 ss << " \"has_diff_image\": " << (!diff_image_png.empty() ? "true" : "false")
109 << ",\n";
110 ss << " \"diff_image_size\": " << diff_image_png.size() << ",\n";
111 ss << " \"significant_regions\": [";
112
113 for (size_t i = 0; i < significant_regions.size(); ++i) {
114 if (i > 0) 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 = absl::StrFormat(
221 "Image dimensions don't match: %dx%d vs %dx%d", a.width, a.height,
222 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& b,
289 const ScreenRegion& region) {
290 VisualDiffResult result;
291 result.width = a.width;
292 result.height = a.height;
293
294 // Calculate comparison region
295 int x1 = region.x;
296 int y1 = region.y;
297 int x2 = region.IsFullScreen() ? a.width : std::min(region.x + region.width, a.width);
298 int y2 = region.IsFullScreen() ? a.height : std::min(region.y + region.height, a.height);
299
300 int total = 0;
301 int differing = 0;
302
303 for (int y = y1; y < y2; ++y) {
304 for (int x = x1; x < x2; ++x) {
305 // Skip ignored regions
306 bool ignore = false;
307 for (const auto& ir : config_.ignore_regions) {
308 if (x >= ir.x && x < ir.x + ir.width && y >= ir.y &&
309 y < ir.y + ir.height) {
310 ignore = true;
311 break;
312 }
313 }
314 if (ignore) continue;
315
316 total++;
317 size_t idx = a.GetPixelIndex(x, y);
318 const uint8_t* pa = &a.data[idx];
319 const uint8_t* pb = &b.data[idx];
320
321 if (!ColorsMatch(pa, pb)) {
322 differing++;
323 }
324 }
325 }
326
327 result.total_pixels = total;
328 result.differing_pixels = differing;
329 result.identical = (differing == 0);
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;
332
333 return result;
334}
335
337 const Screenshot& b,
338 const ScreenRegion& region) {
339 VisualDiffResult result = ComparePixelExact(a, b, region);
340
341 // Calculate SSIM for more perceptual comparison
342 float ssim = region.IsFullScreen() ? CalculateSSIM(a, b)
343 : CalculateRegionSSIM(a, b, region);
344
345 // Use SSIM as the similarity metric
346 result.similarity = ssim;
347
348 return result;
349}
350
354
356 const Screenshot& b,
357 const ScreenRegion& region) {
358 // Simplified SSIM calculation
359 // Full SSIM uses sliding windows, but this gives a reasonable approximation
360
361 int x1 = region.x;
362 int y1 = region.y;
363 int x2 = region.IsFullScreen() ? a.width : std::min(region.x + region.width, a.width);
364 int y2 = region.IsFullScreen() ? a.height : std::min(region.y + region.height, a.height);
365
366 double sum_a = 0, sum_b = 0;
367 double sum_aa = 0, sum_bb = 0, sum_ab = 0;
368 int count = 0;
369
370 for (int y = y1; y < y2; ++y) {
371 for (int x = x1; x < x2; ++x) {
372 size_t idx = a.GetPixelIndex(x, y);
373
374 // Convert to grayscale luminance for SSIM
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];
379
380 sum_a += la;
381 sum_b += lb;
382 sum_aa += la * la;
383 sum_bb += lb * lb;
384 sum_ab += la * lb;
385 count++;
386 }
387 }
388
389 if (count == 0) return 1.0f;
390
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);
396
397 // SSIM constants
398 const double C1 = 6.5025; // (0.01 * 255)^2
399 const double C2 = 58.5225; // (0.03 * 255)^2
400
401 double numerator = (2 * mean_a * mean_b + C1) * (2 * cov_ab + C2);
402 double denominator =
403 (mean_a * mean_a + mean_b * mean_b + C1) * (var_a + var_b + C2);
404
405 return static_cast<float>(numerator / denominator);
406}
407
408bool VisualDiffEngine::ColorsMatch(const uint8_t* pixel_a,
409 const uint8_t* pixel_b) const {
410 int threshold = config_.color_threshold;
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;
414}
415
416float VisualDiffEngine::PixelDifference(const uint8_t* pixel_a,
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);
422}
423
425 const Screenshot& a, const Screenshot& b, const VisualDiffResult& result) {
426 // Create diff image: base image A with red overlay on differences
427 Screenshot diff;
428 diff.width = a.width;
429 diff.height = a.height;
430 diff.data.resize(a.data.size());
431
432 for (int y = 0; y < a.height; ++y) {
433 for (int x = 0; x < a.width; ++x) {
434 size_t idx = a.GetPixelIndex(x, y);
435 const uint8_t* pa = &a.data[idx];
436 const uint8_t* pb = &b.data[idx];
437
438 if (ColorsMatch(pa, pb)) {
439 // Same: show original (slightly dimmed)
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;
444 } else {
445 // Different: red highlight
446 diff.data[idx + 0] = 255;
447 diff.data[idx + 1] = 0;
448 diff.data[idx + 2] = 0;
449 diff.data[idx + 3] = 255;
450 }
451 }
452 }
453
454 auto encoded = EncodePng(diff);
455 return encoded.ok() ? *encoded : std::vector<uint8_t>();
456}
457
459 const Screenshot& b) {
460 Screenshot diff;
461 diff.width = a.width;
462 diff.height = a.height;
463 diff.data.resize(a.data.size());
464
465 for (int y = 0; y < a.height; ++y) {
466 for (int x = 0; x < a.width; ++x) {
467 size_t idx = a.GetPixelIndex(x, y);
468 const uint8_t* pa = &a.data[idx];
469 const uint8_t* pb = &b.data[idx];
470
471 float diff_amount = PixelDifference(pa, pb);
472
473 // Map to heatmap color (green -> yellow -> red)
474 uint8_t r, g, b_val;
475 if (diff_amount < 0.5f) {
476 // Green to yellow
477 float t = diff_amount * 2;
478 r = static_cast<uint8_t>(255 * t);
479 g = 255;
480 b_val = 0;
481 } else {
482 // Yellow to red
483 float t = (diff_amount - 0.5f) * 2;
484 r = 255;
485 g = static_cast<uint8_t>(255 * (1 - t));
486 b_val = 0;
487 }
488
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;
493 }
494 }
495
496 auto encoded = EncodePng(diff);
497 return encoded.ok() ? *encoded : std::vector<uint8_t>();
498}
499
501 const Screenshot& a, const Screenshot& b, const VisualDiffResult& result) {
502 // Create side-by-side: A | Diff | B
503 Screenshot combined;
504 combined.width = a.width * 3;
505 combined.height = a.height;
506 combined.data.resize(combined.width * combined.height * 4);
507
508 for (int y = 0; y < a.height; ++y) {
509 for (int x = 0; x < a.width; ++x) {
510 size_t src_idx = a.GetPixelIndex(x, y);
511 const uint8_t* pa = &a.data[src_idx];
512 const uint8_t* pb = &b.data[src_idx];
513
514 // Image A (left)
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;
520
521 // Diff (center)
522 size_t dst_diff = (y * combined.width + x + a.width) * 4;
523 if (ColorsMatch(pa, pb)) {
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;
527 } else {
528 combined.data[dst_diff + 0] = 255;
529 combined.data[dst_diff + 1] = 0;
530 combined.data[dst_diff + 2] = 0;
531 }
532 combined.data[dst_diff + 3] = 255;
533
534 // Image B (right)
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;
540 }
541 }
542
543 auto encoded = EncodePng(combined);
544 return encoded.ok() ? *encoded : std::vector<uint8_t>();
545}
546
547std::vector<VisualDiffResult::DiffRegion> VisualDiffEngine::FindSignificantRegions(
548 const Screenshot& a, const Screenshot& b, int threshold) {
549 std::vector<VisualDiffResult::DiffRegion> regions;
550
551 // Simple approach: find bounding boxes of contiguous diff regions
552 // More sophisticated: use connected component analysis
553
554 // For now, use a grid-based approach
555 const int grid_size = 32; // 32x32 pixel cells
556
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);
563
564 int diff_count = 0;
565 int total = 0;
566
567 for (int y = y1; y < y2; ++y) {
568 for (int x = x1; x < x2; ++x) {
569 total++;
570 size_t idx = a.GetPixelIndex(x, y);
571 if (!ColorsMatch(&a.data[idx], &b.data[idx])) {
572 diff_count++;
573 }
574 }
575 }
576
577 // If more than 10% of cell is different, report as region
578 float local_diff_pct = static_cast<float>(diff_count) / total;
579 if (local_diff_pct > 0.1f) {
581 region.x = x1;
582 region.y = y1;
583 region.width = x2 - x1;
584 region.height = y2 - y1;
585 region.local_diff_pct = local_diff_pct;
586 regions.push_back(region);
587 }
588 }
589 }
590
591 return regions;
592}
593
595 std::vector<VisualDiffResult::DiffRegion>& regions) {
596 if (regions.size() < 2) return;
597
598 // Simple merge: combine overlapping or adjacent regions
599 bool merged = true;
600 while (merged) {
601 merged = false;
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];
606
607 // Check if regions are close enough to merge
609 bool adjacent =
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);
612
613 if (adjacent) {
614 // Merge into ri
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);
619
620 ri.x = new_x;
621 ri.y = new_y;
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);
625
626 regions.erase(regions.begin() + j);
627 merged = true;
628 break;
629 }
630 }
631 if (merged) break;
632 }
633 }
634}
635
636absl::StatusOr<std::vector<uint8_t>> VisualDiffEngine::GenerateDiffPng(
637 const Screenshot& a, const Screenshot& b) {
639 if (!result.diff_image_png.empty()) {
640 return result.diff_image_png;
641 }
642 return GenerateRedHighlightDiff(a, b, result);
643}
644
646 const std::string& output_path) {
647 if (result.diff_image_png.empty()) {
648 return absl::InvalidArgumentError("No diff image available");
649 }
650
651 std::ofstream file(output_path, std::ios::binary);
652 if (!file) {
653 return absl::InternalError(
654 absl::StrCat("Failed to open file for writing: ", output_path));
655 }
656
657 file.write(reinterpret_cast<const char*>(result.diff_image_png.data()),
658 result.diff_image_png.size());
659
660 return absl::OkStatus();
661}
662
663absl::StatusOr<Screenshot> VisualDiffEngine::DecodePng(
664 const std::vector<uint8_t>& png_data) {
665 if (png_data.size() < 8) {
666 return absl::InvalidArgumentError("PNG data too small");
667 }
668
669 // Check PNG signature
670 if (png_sig_cmp(reinterpret_cast<png_bytep>(
671 const_cast<uint8_t*>(png_data.data())),
672 0, 8) != 0) {
673 return absl::InvalidArgumentError("Invalid PNG signature");
674 }
675
676 png_structp png_ptr =
677 png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
678 if (!png_ptr) {
679 return absl::InternalError("Failed to create PNG read struct");
680 }
681
682 png_infop info_ptr = png_create_info_struct(png_ptr);
683 if (!info_ptr) {
684 png_destroy_read_struct(&png_ptr, nullptr, nullptr);
685 return absl::InternalError("Failed to create PNG info struct");
686 }
687
688 if (setjmp(png_jmpbuf(png_ptr))) {
689 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
690 return absl::InternalError("PNG decoding error");
691 }
692
693 PngReadContext ctx{png_data.data(), 0, png_data.size()};
694 png_set_read_fn(png_ptr, &ctx, PngReadCallback);
695
696 png_read_info(png_ptr, info_ptr);
697
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);
702
703 // Convert to RGBA
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);
716
717 png_read_update_info(png_ptr, info_ptr);
718
719 Screenshot result;
720 result.width = static_cast<int>(width);
721 result.height = static_cast<int>(height);
722 result.data.resize(width * height * 4);
723
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;
727 }
728
729 png_read_image(png_ptr, row_pointers.data());
730 png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
731
732 return result;
733}
734
735absl::StatusOr<std::vector<uint8_t>> VisualDiffEngine::EncodePng(
736 const Screenshot& screenshot) {
737 if (!screenshot.IsValid()) {
738 return absl::InvalidArgumentError("Invalid screenshot");
739 }
740
741 png_structp png_ptr =
742 png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
743 if (!png_ptr) {
744 return absl::InternalError("Failed to create PNG write struct");
745 }
746
747 png_infop info_ptr = png_create_info_struct(png_ptr);
748 if (!info_ptr) {
749 png_destroy_write_struct(&png_ptr, nullptr);
750 return absl::InternalError("Failed to create PNG info struct");
751 }
752
753 std::vector<uint8_t> output;
754 PngWriteContext ctx{&output};
755
756 if (setjmp(png_jmpbuf(png_ptr))) {
757 png_destroy_write_struct(&png_ptr, &info_ptr);
758 return absl::InternalError("PNG encoding error");
759 }
760
761 png_set_write_fn(png_ptr, &ctx, PngWriteCallback, PngFlushCallback);
762
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);
766
767 png_write_info(png_ptr, info_ptr);
768
769 std::vector<png_bytep> row_pointers(screenshot.height);
770 for (int y = 0; y < screenshot.height; ++y) {
771 row_pointers[y] =
772 const_cast<png_bytep>(screenshot.data.data() + y * screenshot.width * 4);
773 }
774
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);
778
779 return output;
780}
781
782absl::StatusOr<Screenshot> VisualDiffEngine::LoadPng(const std::string& path) {
783 std::ifstream file(path, std::ios::binary);
784 if (!file) {
785 return absl::NotFoundError(absl::StrCat("File not found: ", path));
786 }
787
788 file.seekg(0, std::ios::end);
789 size_t size = file.tellg();
790 file.seekg(0, std::ios::beg);
791
792 std::vector<uint8_t> data(size);
793 file.read(reinterpret_cast<char*>(data.data()), size);
794
795 auto result = DecodePng(data);
796 if (result.ok()) {
797 result->source = path;
798 }
799 return result;
800}
801
802absl::Status VisualDiffEngine::SavePng(const Screenshot& screenshot,
803 const std::string& path) {
804 auto encoded = EncodePng(screenshot);
805 if (!encoded.ok()) {
806 return encoded.status();
807 }
808
809 std::ofstream file(path, std::ios::binary);
810 if (!file) {
811 return absl::InternalError(
812 absl::StrCat("Failed to open file for writing: ", path));
813 }
814
815 file.write(reinterpret_cast<const char*>(encoded->data()), encoded->size());
816 return absl::OkStatus();
817}
818
819} // namespace test
820} // 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