| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "screenshot_helper.h" |
| |
| #include <fuchsia/io/cpp/fidl.h> |
| #include <lib/zx/vmar.h> |
| #include <png.h> |
| #include <zircon/status.h> |
| |
| #include <cstdint> |
| #include <cstdio> |
| #include <fstream> |
| #include <iostream> |
| #include <iterator> |
| #include <memory> |
| #include <unordered_map> |
| #include <vector> |
| |
| #include "src/ui/scenic/lib/utils/pixel.h" |
| |
| namespace ui_testing { |
| namespace { |
| |
| constexpr uint64_t kBytesPerPixel = 4; |
| constexpr uint8_t kPNGHeaderBytes = 8; |
| |
| // Needed for |png_set_read_fn| so that libpng can read from a zx::vmo in a stream-like fashion. |
| struct libpng_vmo { |
| zx::vmo* vmo; |
| size_t offset; |
| }; |
| |
| } // namespace |
| |
| Screenshot::Screenshot(const zx::vmo& screenshot_vmo, uint64_t width, uint64_t height, int rotation) |
| : width_(width), height_(height) { |
| if (rotation == 90 || rotation == 270) { |
| std::swap(width_, height_); |
| } |
| |
| // Populate |screenshot_| from |screenshot_vmo|. |
| uint64_t vmo_size; |
| screenshot_vmo.get_prop_content_size(&vmo_size); |
| |
| FX_CHECK(vmo_size == kBytesPerPixel * width_ * height_); |
| |
| uint8_t* vmo_host = nullptr; |
| auto status = zx::vmar::root_self()->map(ZX_VM_PERM_READ, /*vmar_offset*/ 0, screenshot_vmo, |
| /*vmo_offset*/ 0, vmo_size, |
| reinterpret_cast<uintptr_t*>(&vmo_host)); |
| |
| FX_CHECK(status == ZX_OK); |
| |
| ExtractScreenshotFromVMO(vmo_host); |
| |
| // Unmap the pointer. |
| uintptr_t address = reinterpret_cast<uintptr_t>(vmo_host); |
| status = zx::vmar::root_self()->unmap(address, vmo_size); |
| FX_CHECK(status == ZX_OK); |
| } |
| |
| Screenshot::Screenshot() : width_(0), height_(0) {} |
| |
| Screenshot::Screenshot(const zx::vmo& png_vmo) { |
| zx::vmo vmo_copy; |
| FX_CHECK(png_vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &vmo_copy) == ZX_OK); |
| ExtractScreenshotFromPngVMO(vmo_copy); |
| } |
| |
| Pixel Screenshot::GetPixelAt(uint64_t x, uint64_t y) const { |
| FX_CHECK(x >= 0 && x < width_ && y >= 0 && y < height_) << "Index out of bounds"; |
| return screenshot_[y][x]; |
| } |
| |
| std::map<Pixel, uint32_t> Screenshot::Histogram() const { |
| std::map<Pixel, uint32_t> histogram; |
| FX_CHECK(screenshot_.size() == height_ && screenshot_[0].size() == width_); |
| |
| for (size_t i = 0; i < height_; i++) { |
| for (size_t j = 0; j < width_; j++) { |
| histogram[screenshot_[i][j]]++; |
| } |
| } |
| |
| return histogram; |
| } |
| |
| float Screenshot::ComputeSimilarity(const Screenshot& other) const { |
| if (width() != other.width() || height() != other.height()) |
| return 0; |
| |
| uint64_t num_matching_pixels = 0; |
| for (uint64_t x = 0; x < width(); ++x) { |
| for (uint64_t y = 0; y < height(); ++y) { |
| if (GetPixelAt(x, y) == other.GetPixelAt(x, y)) |
| ++num_matching_pixels; |
| } |
| } |
| return 100.f * static_cast<float>(num_matching_pixels) / static_cast<float>((width() * height())); |
| } |
| |
| float Screenshot::ComputeHistogramSimilarity(const Screenshot& other) const { |
| if (width() != other.width() || height() != other.height()) |
| return 0; |
| |
| auto histogram = this->Histogram(); |
| auto other_histogram = other.Histogram(); |
| |
| uint64_t num_matching_pixels = 0; |
| |
| for (auto it = histogram.begin(); it != histogram.end(); ++it) { |
| if (other_histogram.find(it->first) != other_histogram.end()) { |
| num_matching_pixels += std::min(it->second, other_histogram[it->first]); |
| } |
| } |
| return 100.f * static_cast<float>(num_matching_pixels) / static_cast<float>((width() * height())); |
| } |
| |
| bool Screenshot::DumpToCustomArtifacts(const std::string& filename) const { |
| const std::string file_path = "/custom_artifacts/" + filename; |
| std::ofstream file(file_path); |
| if (!file.is_open()) { |
| FX_LOGS(ERROR) |
| << "Artifact cannot be opened. Follow https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#custom-artifacts."; |
| } else { |
| for (size_t y = 0; y < height(); ++y) { |
| for (size_t x = 0; x < width(); ++x) { |
| const auto pixel = GetPixelAt(x, y); |
| file << pixel.blue << pixel.green << pixel.red << pixel.alpha; |
| } |
| } |
| file.close(); |
| FX_LOGS(INFO) << "Screenshot artifact dumped."; |
| } |
| return true; |
| } |
| |
| bool Screenshot::DumpPngToCustomArtifacts(const std::string& filename) const { |
| const std::string file_path = "/custom_artifacts/" + filename; |
| FILE* fp = fopen(file_path.c_str(), "wb"); |
| if (!fp) { |
| FX_LOGS(ERROR) |
| << file_path |
| << " cannot be written. Follow https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#custom-artifacts."; |
| return false; |
| } |
| |
| png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr /* error_ptr */, |
| nullptr /* error_fn */, nullptr /* warn_fn */); |
| FX_CHECK(png); |
| png_infop png_info = png_create_info_struct(png); |
| FX_CHECK(png_info); |
| |
| // This is libpng's syntax for setting up the error handler |
| // by using a jump address(!). All calls to libpng must reside in |
| // this function. |
| if (setjmp(png_jmpbuf(png))) { |
| FX_LOGS(ERROR) << "Something went wrong in libpng during write"; |
| fclose(fp); |
| png_destroy_write_struct(&png, &png_info); |
| return false; |
| } |
| |
| png_init_io(png, fp); |
| |
| // Set the headers: output is 8-bit depth, RGBA format. |
| png_set_IHDR(png, png_info, (uint32_t)width_, (uint32_t)height_, 8 /* bit depth */, |
| PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, |
| PNG_FILTER_TYPE_DEFAULT); |
| png_write_info(png, png_info); |
| |
| // libpng needs pointers to each row of pixels. |
| std::vector<const uint8_t*> row_ptrs(height_); |
| for (size_t y = 0; y < height_; ++y) { |
| row_ptrs[y] = reinterpret_cast<const uint8_t*>(screenshot_[y].data()); |
| } |
| |
| png_set_rows(png, png_info, const_cast<uint8_t**>(row_ptrs.data())); |
| // PNG expected RGBA, but screenshot_ is BGRA, so use PNG_TRANSFORM_BGR |
| png_write_png(png, png_info, PNG_TRANSFORM_BGR, nullptr /* unused */); |
| |
| fclose(fp); |
| png_destroy_write_struct(&png, &png_info); |
| |
| FX_LOGS(INFO) << "Screenshot saved to " << file_path; |
| return true; |
| } |
| |
| bool Screenshot::LoadFromPng(const std::string& png_filename) { |
| FILE* fp = fopen(png_filename.c_str(), "rb"); |
| if (!fp) { |
| FX_LOGS(FATAL) << " cannot read " << png_filename; |
| return false; |
| } |
| |
| png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr /* error_ptr */, |
| nullptr /* error_fn */, nullptr /* warn_fn */); |
| FX_CHECK(png); |
| png_infop png_info = png_create_info_struct(png); |
| FX_CHECK(png_info); |
| |
| // This is libpng's syntax for setting up the error handler |
| // by using a jump address(!). All calls to libpng must reside in |
| // this function. |
| if (setjmp(png_jmpbuf(png))) { |
| FX_LOGS(ERROR) << "Something went wrong in libpng during read"; |
| fclose(fp); |
| png_destroy_read_struct(&png, &png_info, nullptr /* end_info_ptr_ptr */); |
| return false; |
| } |
| |
| png_init_io(png, fp); |
| |
| // Read header info |
| png_read_info(png, png_info); |
| |
| width_ = png_get_image_width(png, png_info); |
| height_ = png_get_image_height(png, png_info); |
| |
| auto color_type = png_get_color_type(png, png_info); |
| auto bit_depth = png_get_bit_depth(png, png_info); |
| FX_CHECK(color_type == PNG_COLOR_TYPE_RGBA) << "Only RGBA png format is supported"; |
| FX_CHECK(bit_depth == 8) << "Only 8 bit png format is supported"; |
| |
| // Initialize screenshot memory and pass row ptrs to libpng. |
| screenshot_.clear(); |
| std::vector<uint8_t*> row_ptrs(height_); |
| for (size_t y = 0; y < height_; ++y) { |
| // Initialize row memory with empty pixels. |
| screenshot_.emplace_back(width_, Pixel(0, 0, 0, 0)); |
| row_ptrs[y] = reinterpret_cast<uint8_t*>(screenshot_[y].data()); |
| } |
| png_set_bgr(png); |
| png_read_image(png, reinterpret_cast<uint8_t**>(row_ptrs.data())); |
| |
| fclose(fp); |
| png_destroy_read_struct(&png, &png_info, nullptr /* end_info_ptr_ptr */); |
| |
| FX_LOGS(INFO) << "Screenshot loaded from " << png_filename; |
| |
| return true; |
| } |
| |
| std::vector<std::pair<uint32_t, utils::Pixel>> Screenshot::LogHistogramTopPixels( |
| int num_top_pixels) const { |
| auto histogram = Histogram(); |
| std::vector<std::pair<uint32_t, utils::Pixel>> vec; |
| std::transform( |
| histogram.begin(), histogram.end(), std::inserter(vec, vec.begin()), |
| [](const std::pair<utils::Pixel, uint32_t> p) { return std::make_pair(p.second, p.first); }); |
| std::stable_sort(vec.begin(), vec.end(), |
| [](const auto& a, const auto& b) { return a.first > b.first; }); |
| |
| std::vector<std::pair<uint32_t, utils::Pixel>> top; |
| std::copy(vec.begin(), vec.begin() + std::min<ptrdiff_t>(vec.size(), num_top_pixels), |
| std::back_inserter(top)); |
| |
| std::cout << "Histogram top:" << std::endl; |
| for (const auto& elems : top) { |
| std::cout << "{ " << elems.second << " value: " << elems.first << " }" << std::endl; |
| } |
| std::cout << "--------------" << std::endl; |
| return top; |
| } |
| |
| void Screenshot::ExtractScreenshotFromVMO(uint8_t* screenshot_vmo) { |
| FX_CHECK(screenshot_vmo); |
| |
| for (size_t i = 0; i < height_; i++) { |
| // The head index of the ith row in the screenshot is |i* width_* KbytesPerPixel|. |
| screenshot_.push_back(GetPixelsInRow(screenshot_vmo, i)); |
| } |
| } |
| |
| std::vector<Pixel> Screenshot::GetPixelsInRow(uint8_t* screenshot_vmo, size_t row_index) { |
| std::vector<Pixel> row; |
| |
| for (size_t col_idx = 0; col_idx < static_cast<size_t>(width_ * kBytesPerPixel); |
| col_idx += kBytesPerPixel) { |
| // Each row in the screenshot has |kBytesPerPixel * width_| elements. Therefore in order to |
| // reach the first pixel of the |row_index| row, we have to jump |row_index * width_ * |
| // kBytesPerPixel| positions. |
| auto pixel_start_index = row_index * width_ * kBytesPerPixel; |
| |
| // Every |kBytesPerPixel| bytes represents the BGRA values of a pixel. Skip |kBytesPerPixel| |
| // bytes |
| // to get to the BGRA values of the next pixel. Each row in a screenshot has |kBytesPerPixel * |
| // width_| bytes of data. |
| // Example:- |
| // auto data = TakeScreenshot(); |
| // data[0-3] -> RGBA of pixel 0. |
| // data[4-7] -> RGBA pf pixel 1. |
| row.emplace_back(screenshot_vmo[pixel_start_index + col_idx], |
| screenshot_vmo[pixel_start_index + col_idx + 1], |
| screenshot_vmo[pixel_start_index + col_idx + 2], |
| screenshot_vmo[pixel_start_index + col_idx + 3]); |
| } |
| |
| return row; |
| } |
| |
| // Decode PNG-encoded VMO back to raw format and populate screenshot_ with raw pixels. |
| // Done this way for testing purposes so we can compare accuracy of screenshots taken in PNG format. |
| void Screenshot::ExtractScreenshotFromPngVMO(zx::vmo& png_vmo) { |
| png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); |
| FX_DCHECK(png) << "png_create_read_struct failed"; |
| |
| png_infop info = png_create_info_struct(png); |
| FX_DCHECK(info) << "png_create_info_struct failed"; |
| |
| // Tell libpng how to read from a zx::vmo in a stream-like fashion. |
| libpng_vmo read_fn_vmo = {.vmo = &png_vmo, .offset = 0u}; |
| png_set_read_fn(png, &read_fn_vmo, |
| [](png_structp png_ptr, png_bytep out_bytes, size_t length) -> void { |
| // Read |length| bytes into |out_bytes| from the VMO. |
| libpng_vmo* vmo = reinterpret_cast<libpng_vmo*>(png_get_io_ptr(png_ptr)); |
| FX_CHECK(vmo->vmo->read(out_bytes, vmo->offset, length) == ZX_OK); |
| vmo->offset += length; |
| }); |
| |
| png_read_info(png, info); |
| |
| width_ = png_get_image_width(png, info); |
| height_ = png_get_image_height(png, info); |
| |
| uint32_t color_type = png_get_color_type(png, info); |
| uint32_t bit_depth = png_get_bit_depth(png, info); |
| |
| // Only works with 4 bytes (32-bits) per pixel. |
| FX_CHECK(color_type == PNG_COLOR_TYPE_RGBA) << "currently only supports RGBA"; |
| FX_CHECK(bit_depth == 8) << "currently only supports 8-bit channel"; |
| |
| int64_t row_bytes = png_get_rowbytes(png, info); |
| int64_t expected_row_bytes = kBytesPerPixel * width_; // We assume each pixel is 4 bytes. |
| FX_DCHECK(row_bytes == expected_row_bytes) |
| << "unexpected row_bytes: " << row_bytes << " expect: 4 * " << width_; |
| |
| const uint64_t bytesPerRow = png_get_rowbytes(png, info); |
| std::vector<uint8_t> rowData(bytesPerRow); |
| |
| // Read one row at a time. For some reason, this is necessary instead of |png_read_image()|. Maybe |
| // because we're reading from memory instead of a file? |
| for (uint32_t rowIdx = 0; rowIdx < height_; ++rowIdx) { |
| png_read_row(png, static_cast<png_bytep>(rowData.data()), nullptr); |
| |
| uint32_t byteIndex = 0; |
| std::vector<Pixel> row; |
| for (uint32_t colIdx = 0; colIdx < width_; ++colIdx) { |
| const uint8_t red = rowData[byteIndex++]; |
| const uint8_t green = rowData[byteIndex++]; |
| const uint8_t blue = rowData[byteIndex++]; |
| const uint8_t alpha = rowData[byteIndex++]; |
| |
| row.emplace_back(blue, green, red, alpha); |
| } |
| screenshot_.push_back(row); |
| } |
| png_destroy_read_struct(&png, &info, nullptr); |
| } |
| |
| } // namespace ui_testing |