| // 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) |