blob: 19466078e0ac8f950f8740c53815016d43d2dbe6 [file] [log] [blame]
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "starboard/nplb/blitter_pixel_tests/image.h"
#include <math.h>
#include <vector>
#include "starboard/blitter.h"
#include "starboard/file.h"
#include "starboard/log.h"
#include "third_party/libpng/png.h"
#if SB_HAS(BLITTER)
namespace starboard {
namespace nplb {
namespace blitter_pixel_tests {
Image::Image(SbBlitterSurface surface) {
SbBlitterSurfaceInfo surface_info;
SB_CHECK(SbBlitterGetSurfaceInfo(surface, &surface_info));
width_ = surface_info.width;
height_ = surface_info.height;
pixel_data_ = new uint8_t[width_ * height_ * 4];
SB_CHECK(SbBlitterDownloadSurfacePixels(
surface, kSbBlitterPixelDataFormatRGBA8, width_ * 4, pixel_data_));
}
Image::Image(uint8_t* passed_pixel_data, int width, int height) {
pixel_data_ = passed_pixel_data;
width_ = width;
height_ = height;
}
namespace {
// Helper function for reading PNG files.
void PNGReadPlatformFile(png_structp png,
png_bytep buffer,
png_size_t buffer_size) {
// Casting between two pointer types.
SbFile in_file = reinterpret_cast<SbFile>(png_get_io_ptr(png));
int bytes_to_read = buffer_size;
char* current_read_pos = reinterpret_cast<char*>(buffer);
while (bytes_to_read > 0) {
int bytes_read = SbFileRead(in_file, current_read_pos, bytes_to_read);
SB_CHECK(bytes_read > 0);
bytes_to_read -= bytes_read;
current_read_pos += bytes_read;
}
}
} // namespace
Image::Image(const std::string& png_path) {
// Much of this PNG loading code is based on a section from the libpng manual:
// http://www.libpng.org/pub/png/libpng-1.2.5-manual.html#section-3
SbFile in_file = OpenFileForReading(png_path);
if (!SbFileIsValid(in_file)) {
SB_DLOG(ERROR) << "Error opening file for reading: " << png_path;
SB_NOTREACHED();
}
uint8_t header[8];
int bytes_to_read = sizeof(header);
char* current_read_pos = reinterpret_cast<char*>(header);
while (bytes_to_read > 0) {
int bytes_read = SbFileRead(in_file, current_read_pos, bytes_to_read);
SB_CHECK(bytes_read > 0);
bytes_to_read -= bytes_read;
current_read_pos += bytes_read;
}
SB_CHECK(!png_sig_cmp(header, 0, 8)) << "Invalid PNG header.";
// Set up a libpng context for reading images.
png_structp png =
png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
SB_CHECK(png);
// Create a structure to contain metadata about the image.
png_infop png_metadata = png_create_info_struct(png);
SB_DCHECK(png_metadata);
// libpng expects to longjump to png->jmpbuf if it encounters an error.
// According to longjmp's documentation, this implies that stack unwinding
// will occur in that case, though C++ objects with non-trivial destructors
// will not be called. This is fine though, since we abort upon errors here.
// If alternative behavior is desired, custom error and warning handler
// functions can be passed into the png_create_read_struct() call above.
if (setjmp(png->jmpbuf)) {
SB_NOTREACHED() << "libpng encountered an error during encoding.";
}
// Set up for file i/o.
png_set_read_fn(png, reinterpret_cast<void*>(in_file), &PNGReadPlatformFile);
// Tell png we already read 8 bytes.
png_set_sig_bytes(png, 8);
// Read the image info.
png_read_info(png, png_metadata);
// Transform PNGs into a canonical RGBA form, in order to simplify the process
// of reading png files of varying formats. Of course, if we would like to
// maintain data in formats other than RGBA, this logic should be adjusted.
{
if (png_get_bit_depth(png, png_metadata) == 16) {
png_set_strip_16(png);
}
png_byte color = png_get_color_type(png, png_metadata);
if (color == PNG_COLOR_TYPE_GRAY &&
png_get_bit_depth(png, png_metadata) < 8) {
png_set_expand_gray_1_2_4_to_8(png);
}
// Convert from grayscale or palette to color.
if (!(color & PNG_COLOR_MASK_COLOR)) {
png_set_gray_to_rgb(png);
} else if (color == PNG_COLOR_TYPE_PALETTE) {
png_set_palette_to_rgb(png);
}
// Add an alpha channel if missing.
if (!(color & PNG_COLOR_MASK_ALPHA)) {
png_set_add_alpha(png, 0xff /* opaque */, PNG_FILLER_AFTER);
}
}
// End transformations. Get the updated info, and then verify.
png_read_update_info(png, png_metadata);
SB_DCHECK(png_get_color_type(png, png_metadata) == PNG_COLOR_TYPE_RGBA);
SB_DCHECK(png_get_bit_depth(png, png_metadata) == 8);
width_ = png_get_image_width(png, png_metadata);
height_ = png_get_image_height(png, png_metadata);
int pitch_in_bytes = width_ * 4;
pixel_data_ = new uint8_t[height_ * pitch_in_bytes];
std::vector<png_bytep> rows(height_);
for (int i = 0; i < height_; ++i) {
rows[i] = pixel_data_ + i * pitch_in_bytes;
}
png_read_image(png, &rows[0]);
// Time to clean up. First create a structure to read image metadata (like
// comments) from the end of the png file, then read the remaining data in
// the png file, and then finally release our context and close the file.
png_infop end = png_create_info_struct(png);
SB_DCHECK(end);
// Read the end data in the png file.
png_read_end(png, end);
// Release our png reading context and associated info structs.
png_destroy_read_struct(&png, &png_metadata, &end);
SB_CHECK(SbFileClose(in_file));
}
Image::~Image() {
delete[] pixel_data_;
}
bool Image::CanOpenFile(const std::string& path) {
SbFile in_file = OpenFileForReading(path);
if (!SbFileIsValid(in_file)) {
return false;
}
SB_CHECK(SbFileClose(in_file));
return true;
}
Image Image::Diff(const Image& other,
int pixel_test_value_fuzz,
bool* is_same) const {
SB_DCHECK(pixel_test_value_fuzz >= 0);
// Image dimensions involved in the diff must match each other.
if (width_ != other.width_ || height_ != other.height_) {
SB_LOG(ERROR) << "Test images have different dimensions.";
SB_NOTREACHED();
}
*is_same = true;
int num_pixels = width_ * height_;
int num_bytes = num_pixels * 4;
// Setup our destination diff image data that will eventually be returned.
uint8_t* diff_pixel_data = new uint8_t[num_bytes];
for (int i = 0; i < num_pixels; ++i) {
int byte_offset = i * 4;
bool components_differ = false;
// Iterate through each color component and for each one test it to see if
// it differs by more than |pixel_test_value_fuzz|.
for (int c = 0; c < 4; ++c) {
int index = byte_offset + c;
int diff = pixel_data_[index] - other.pixel_data_[index];
components_differ |= (diff < 0 ? -diff > pixel_test_value_fuzz
: diff > pixel_test_value_fuzz);
}
// Mark the corresponding pixel in the diff image appropriately, depending
// on the results of the test.
if (components_differ) {
// Mark differing pixels as opaque red.
diff_pixel_data[byte_offset + 0] = 255;
diff_pixel_data[byte_offset + 1] = 0;
diff_pixel_data[byte_offset + 2] = 0;
diff_pixel_data[byte_offset + 3] = 255;
*is_same = false;
} else {
// Mark matching pixels as transparent white.
diff_pixel_data[byte_offset + 0] = 255;
diff_pixel_data[byte_offset + 1] = 255;
diff_pixel_data[byte_offset + 2] = 255;
diff_pixel_data[byte_offset + 3] = 0;
}
}
return Image(diff_pixel_data, width_, height_);
}
Image Image::GaussianBlur(float sigma) const {
SB_DCHECK(sigma >= 0);
// While it is typical to set the window size to 3 times the standard
// deviation because most of the Gaussian function fits within that range,
// we use 2 here to increase performance. We thus contain 95% of the
// function's mass versus 99.7% if we used 3 times.
int kernel_radius = static_cast<int>(sigma * 2.0f);
int kernel_size = 1 + 2 * kernel_radius;
int kernel_center = kernel_radius;
// Allocate space for our small Gaussian kernel image.
float* kernel = new float[kernel_size * kernel_size];
// Compute the kernel image. Go through each pixel of the kernel image and
// calculate the value of the Gaussian function with |sigma| at that point.
// We assume that the center of the image is located at
// (|kernel_center|, |kernel_center|).
float kernel_total = 0.0f;
for (int y = 0; y < kernel_size; ++y) {
for (int x = 0; x < kernel_size; ++x) {
int diff_x = x - kernel_center;
int diff_y = y - kernel_center;
float distance_sq = diff_x * diff_x + diff_y * diff_y;
int kernel_index = y * kernel_size + x;
float kernel_value =
(sigma == 0 ? 1 : exp(-distance_sq / (2 * sigma * sigma)));
kernel[kernel_index] = kernel_value;
kernel_total += kernel_value;
}
}
// Normalize the function so that its volume is 1.
for (int i = 0; i < kernel_size * kernel_size; ++i) {
kernel[i] /= kernel_total;
}
// Allocate pixel data for our blurred results image.
uint8_t* blur_pixel_data = new uint8_t[width_ * height_ * 4];
// Setup some constants that will be accessed from the tight loop coming up.
const uint8_t* pixel_data_end = pixel_data_ + width_ * height_ * 4;
const int skip_row_bytes = width_ * 4 - kernel_size * 4;
// Now convolve our Gaussian kernel over the |pixel_data_| and put the results
// into |blur_pixel_data|.
uint8_t* cur_dest = blur_pixel_data;
for (int y = 0; y < height_; ++y) {
for (int x = 0; x < width_; ++x) {
// We keep intermediate convolution results as a float to maintain
// precision until we're done accumulating values from the convolution
// window.
float cur_pixel[4];
for (int i = 0; i < 4; ++i) {
cur_pixel[i] = 0.0f;
}
// Setup pointers into the kernel image and the source image that we will
// increment as we visit each pixel of the kernel image.
float* kernel_value = kernel;
int source_start_y = y - kernel_center;
int source_start_x = x - kernel_center;
const uint8_t* cur_src =
pixel_data_ + (source_start_y * width_ + source_start_x) * 4;
// Iterate over the kernel image.
for (int k_y = 0; k_y < kernel_size; ++k_y, cur_src += skip_row_bytes) {
for (int k_x = 0; k_x < kernel_size;
++k_x, cur_src += 4, ++kernel_value) {
// Do not accumulate anything for source pixels that are outside of
// the source image. This implies that the edges of the destination
// image will always have some transparency.
if ((cur_src < pixel_data_) | (cur_src >= pixel_data_end)) {
continue;
}
// Accumulate the destination value.
for (int i = 0; i < 4; ++i) {
cur_pixel[i] += *kernel_value * cur_src[i];
}
}
}
// Finally save the computed value to the destination image memory.
for (int i = 0; i < 4; ++i) {
cur_dest[i] = static_cast<uint8_t>(
(cur_pixel[i] > 255.0f ? 255.0f : cur_pixel[i]) + 0.5f);
}
cur_dest += 4;
}
}
delete[] kernel;
return Image(blur_pixel_data, width_, height_);
}
namespace {
// Write PNG data to a vector to simplify memory management.
typedef std::vector<png_byte> PNGByteVector;
void PNGWriteFunction(png_structp png_ptr, png_bytep data, png_size_t length) {
PNGByteVector* out_buffer =
reinterpret_cast<PNGByteVector*>(png_get_io_ptr(png_ptr));
// Append the data to the array using pointers to the beginning and end of the
// buffer as the first and last iterators.
out_buffer->insert(out_buffer->end(), data, data + length);
}
} // namespace
void Image::WriteToPNG(const std::string& png_path) const {
// Initialize png library and headers for writing.
png_structp png =
png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
SB_DCHECK(png);
png_infop info = png_create_info_struct(png);
SB_DCHECK(info);
// if error encountered png will call longjmp(), so we set up a setjmp() here
// with a failed assert to indicate an error in one of the png functions.
if (setjmp(png->jmpbuf)) {
png_destroy_write_struct(&png, &info);
SB_NOTREACHED() << "libpng encountered an error during encoding.";
}
// Structure into which png data will be written.
PNGByteVector png_buffer;
// Set the write callback. Don't set the flush function, since there's no
// need for buffered IO when writing to memory.
png_set_write_fn(png, &png_buffer, &PNGWriteFunction, NULL);
// Stuff and then write png header.
png_set_IHDR(png, info, width_, height_,
8, // 8 bits per color channel.
PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
// Write image bytes, row by row.
png_bytep row = static_cast<png_bytep>(pixel_data_);
for (int i = 0; i < height_; ++i) {
png_write_row(png, row);
row += width_ * 4;
}
png_write_end(png, NULL);
png_destroy_write_struct(&png, &info);
size_t num_bytes = png_buffer.size() * sizeof(PNGByteVector::value_type);
SbFile out_file = SbFileOpen(png_path.c_str(),
kSbFileWrite | kSbFileCreateAlways, NULL, NULL);
if (!SbFileIsValid(out_file)) {
SB_DLOG(ERROR) << "Error opening file for writing: " << png_path;
SB_NOTREACHED();
}
int bytes_remaining = num_bytes;
char* data_pointer = reinterpret_cast<char*>(&(png_buffer[0]));
while (bytes_remaining > 0) {
int bytes_written = SbFileWrite(out_file, data_pointer, bytes_remaining);
if (bytes_written == -1) {
SB_LOG(ERROR) << "Error writing encoded image data to PNG file: "
<< png_path;
SB_NOTREACHED();
}
bytes_remaining -= bytes_written;
data_pointer += bytes_written;
SB_DCHECK(bytes_remaining >= 0);
}
SB_CHECK(SbFileClose(out_file));
}
SbFile Image::OpenFileForReading(const std::string& path) {
return SbFileOpen(path.c_str(), kSbFileRead | kSbFileOpenOnly, NULL, NULL);
}
} // namespace blitter_pixel_tests
} // namespace nplb
} // namespace starboard
#endif // SB_HAS(BLITTER)