blob: 73c6a10d679ab560c86926ef804fa47335c1aab4 [file] [log] [blame]
// 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