| /* |
| * Copyright 2014 Google Inc. |
| * |
| * 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. |
| */ |
| |
| // Author: Victor Chudnovsky |
| |
| #include <cstddef> |
| #include <memory> |
| #include <vector> |
| |
| #include "pagespeed/kernel/base/gtest.h" |
| #include "pagespeed/kernel/base/message_handler.h" |
| #include "pagespeed/kernel/base/mock_message_handler.h" |
| #include "pagespeed/kernel/base/null_mutex.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/image/frame_interface_optimizer.h" |
| #include "pagespeed/kernel/image/image_frame_interface.h" |
| #include "pagespeed/kernel/image/image_util.h" |
| #include "pagespeed/kernel/image/test_utils.h" |
| namespace { |
| |
| using net_instaweb::MessageHandler; |
| using net_instaweb::MockMessageHandler; |
| using net_instaweb::NullMutex; |
| using pagespeed::image_compression::FrameSpec; |
| using pagespeed::image_compression::GRAY_8; |
| using pagespeed::image_compression::ImageSpec; |
| using pagespeed::image_compression::MultipleFramePaddingReader; |
| using pagespeed::image_compression::MultipleFrameReader; |
| using pagespeed::image_compression::PixelFormat; |
| using pagespeed::image_compression::PixelRgbaChannels; |
| using pagespeed::image_compression::PixelRgbaChannelsToString; |
| using pagespeed::image_compression::RgbaChannels; |
| using pagespeed::image_compression::RgbaToPackedRgba; |
| using pagespeed::image_compression::RGB_888; |
| using pagespeed::image_compression::RGBA_8888; |
| using pagespeed::image_compression::RGBA_ALPHA; |
| using pagespeed::image_compression::RGBA_BLUE; |
| using pagespeed::image_compression::RGBA_GREEN; |
| using pagespeed::image_compression::RGBA_NUM_CHANNELS; |
| using pagespeed::image_compression::RGBA_RED; |
| using pagespeed::image_compression::ScanlineStatus; |
| using pagespeed::image_compression::SCANLINE_STATUS_INVOCATION_ERROR; |
| using pagespeed::image_compression::SCANLINE_STATUS_SUCCESS; |
| using pagespeed::image_compression::SCANLINE_UNKNOWN; |
| using pagespeed::image_compression::kAlphaTransparent; |
| using pagespeed::image_compression::size_px; |
| |
| // Fake reader class that synthesizes a series of frames whose specs are |
| // given in the constructor argument. The color of the frame is the |
| // bit-wise inverse of the image background color. |
| class FakeReader : public MultipleFrameReader { |
| public: |
| FakeReader(const ImageSpec& image_spec, |
| const std::vector<FrameSpec>& frames, |
| MessageHandler* handler) : |
| MultipleFrameReader(handler), |
| image_spec_(image_spec), |
| frames_(frames), |
| state_(UNINITIALIZED) { |
| Reset(); |
| } |
| |
| virtual ~FakeReader() {} |
| |
| virtual ScanlineStatus Reset() { |
| current_frame_ = 0; |
| next_frame_ = 0; |
| current_scanline_ = 0; |
| scanline_.reset(); |
| state_ = INITIALIZED; |
| return ScanlineStatus(SCANLINE_STATUS_SUCCESS); |
| } |
| |
| virtual ScanlineStatus Initialize() { |
| return Reset(); |
| } |
| |
| virtual bool HasMoreFrames() const { |
| return (next_frame_ < frames_.size()); |
| } |
| |
| virtual bool HasMoreScanlines() const { |
| return (current_scanline_ < frames_[current_frame_].height); |
| } |
| |
| static void GetForegroundColor(const PixelRgbaChannels bg_color, |
| PixelRgbaChannels fg_color) { |
| for (int channel = 0; |
| channel < static_cast<int>(RGBA_NUM_CHANNELS); |
| ++channel) { |
| fg_color[channel] = ~bg_color[channel]; |
| } |
| } |
| |
| virtual ScanlineStatus PrepareNextFrame() { |
| if ((state_ < INITIALIZED) || !HasMoreFrames()) { |
| return ScanlineStatus(SCANLINE_STATUS_INVOCATION_ERROR, |
| SCANLINE_UNKNOWN, |
| "FakeReader::PrepareNextFrame called unexpectedly"); |
| } |
| |
| current_frame_ = next_frame_; |
| ++next_frame_; |
| current_scanline_ = 0; |
| FrameSpec frame = frames_[current_frame_]; |
| size_t bytes_per_pixel = GetBytesPerPixel(frame.pixel_format); |
| |
| scanline_.reset(new uint8_t[frame.width * bytes_per_pixel]); |
| PixelRgbaChannels foreground_color; |
| GetForegroundColor(image_spec_.bg_color, foreground_color); |
| for (int i = 0; i < frame.width; ++i) { |
| memcpy(&scanline_[i * bytes_per_pixel], |
| foreground_color, |
| bytes_per_pixel); |
| } |
| |
| state_ = FRAME_PREPARED; |
| return ScanlineStatus(SCANLINE_STATUS_SUCCESS); |
| } |
| |
| virtual ScanlineStatus ReadNextScanline(const void** out_scanline_bytes) { |
| if (((state_ != FRAME_PREPARED) && (state_ != SCANLINE_READ)) || |
| (!HasMoreScanlines())) { |
| return ScanlineStatus(SCANLINE_STATUS_INVOCATION_ERROR, |
| SCANLINE_UNKNOWN, |
| "FakeReader::ReadNextScanline called unexpectedly"); |
| } |
| |
| *out_scanline_bytes = scanline_.get(); |
| ++current_scanline_; |
| state_ = SCANLINE_READ; |
| return ScanlineStatus(SCANLINE_STATUS_SUCCESS); |
| } |
| |
| virtual ScanlineStatus GetFrameSpec(FrameSpec* frame_spec) const { |
| *frame_spec = frames_[current_frame_]; |
| return ScanlineStatus(SCANLINE_STATUS_SUCCESS); |
| } |
| |
| virtual ScanlineStatus GetImageSpec(ImageSpec* image_spec) const { |
| *image_spec = image_spec_; |
| return ScanlineStatus(SCANLINE_STATUS_SUCCESS); |
| } |
| |
| private: |
| enum State { |
| UNINITIALIZED = 0, |
| INITIALIZED, |
| FRAME_PREPARED, |
| SCANLINE_READ |
| }; |
| ImageSpec image_spec_; |
| std::vector<FrameSpec> frames_; |
| |
| size_px current_frame_; |
| size_px next_frame_; |
| size_px current_scanline_; |
| net_instaweb::scoped_array<uint8_t> scanline_; |
| |
| State state_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FakeReader); |
| }; |
| |
| // Verifies that the pixels in the positions [start,end) all have the |
| // value 'color' by comparing as many bytes as appropriate for the |
| // given pixel format. |
| void VerifyPixels(const void* const scanline, size_px start, size_px end, |
| const PixelRgbaChannels color, PixelFormat format) { |
| size_t bytes_per_pixel = GetBytesPerPixel(format); |
| for (size_px idx = start; idx < end; ++idx) { |
| // We ASSERT rather than EXPECT here because, in case of failure, |
| // we don't want a log message for every single pixel. |
| ASSERT_EQ(0, |
| memcmp( |
| static_cast<const uint8_t*>(scanline) + idx * bytes_per_pixel, |
| color, |
| bytes_per_pixel)) |
| << "[" << start << "," << end << "](bpp:" << bytes_per_pixel << ") " |
| << "got: " |
| << PixelRgbaChannelsToString(static_cast<const uint8_t *>(scanline) + |
| idx * bytes_per_pixel) |
| << "want: " << PixelRgbaChannelsToString(color); |
| } |
| } |
| |
| class MultipleFramePaddingReaderTest : public testing::Test { |
| public: |
| MultipleFramePaddingReaderTest() |
| : message_handler_(new NullMutex) { |
| } |
| |
| protected: |
| // Tests that an image with 'image_spec' and a series of frames |
| // described by 'all_frames' are properly padded by |
| // MultipleFramePaddingReader. This method is invoked by several |
| // test cases below. |
| void TestAllFramesPadded(const ImageSpec& image_spec, |
| const std::vector<FrameSpec>& all_frames) { |
| static const PixelRgbaChannels kTransparent = {0, 0, 0, kAlphaTransparent}; |
| |
| net_instaweb::scoped_ptr<MultipleFrameReader> padder( |
| new MultipleFramePaddingReader( |
| new FakeReader(image_spec, all_frames, &message_handler_))); |
| |
| PixelRgbaChannels fg_color; |
| FakeReader::GetForegroundColor(image_spec.bg_color, fg_color); |
| |
| FrameSpec frame_orig; // FrameSpec specified in all_frames |
| FrameSpec frame_spec; // FrameSpec returned from MultipleFramePaddingReader |
| ScanlineStatus status; |
| EXPECT_TRUE(padder->Initialize(NULL, 0, &status)) << status.ToString(); |
| for (size_px frame_idx = 0; frame_idx < all_frames.size(); ++frame_idx) { |
| ASSERT_TRUE(padder->HasMoreFrames()); |
| ASSERT_TRUE(padder->PrepareNextFrame(&status)) << status.ToString(); |
| |
| FrameSpec frame_orig = all_frames[frame_idx]; |
| |
| EXPECT_TRUE(padder->GetFrameSpec(&frame_spec, &status)) |
| << status.ToString(); |
| EXPECT_EQ(image_spec.width, frame_spec.width); |
| EXPECT_EQ(image_spec.height, frame_spec.height); |
| EXPECT_EQ(0, frame_spec.top); |
| EXPECT_EQ(0, frame_spec.left); |
| EXPECT_EQ(frame_orig.pixel_format, frame_spec.pixel_format); |
| |
| for (size_px line_idx = 0; line_idx < image_spec.height; ++line_idx) { |
| ASSERT_TRUE(padder->HasMoreScanlines()); |
| const void* scanline = NULL; |
| EXPECT_TRUE(padder->ReadNextScanline(&scanline, &status)) |
| << status.ToString(); |
| EXPECT_FALSE(NULL == scanline); |
| |
| size_px foreground_start = 0; |
| size_px foreground_end = 0; |
| |
| if ((line_idx >=frame_orig.top) && |
| (line_idx < (frame_orig.top + frame_orig.height))) { |
| foreground_start = image_spec.TruncateXIndex(frame_orig.left); |
| foreground_end = image_spec.TruncateXIndex(frame_orig.left + |
| frame_orig.width); |
| } |
| |
| VerifyPixels(scanline, 0, foreground_start, |
| (image_spec.use_bg_color ? |
| image_spec.bg_color : kTransparent), |
| frame_spec.pixel_format); |
| VerifyPixels(scanline, foreground_start, foreground_end, |
| fg_color, frame_spec.pixel_format); |
| VerifyPixels(scanline, foreground_end, image_spec.width, |
| (image_spec.use_bg_color ? |
| image_spec.bg_color : kTransparent), |
| frame_spec.pixel_format); |
| } |
| EXPECT_FALSE(padder->HasMoreScanlines()); |
| } |
| EXPECT_FALSE(padder->HasMoreFrames()); |
| } |
| |
| // Tests that, for a given format, we properly pad each frame in an |
| // image. The frames tested have several positions and sizes. |
| void TestReaderPadsAllFrames(PixelFormat pixel_format, bool use_bg_color) { |
| ImageSpec image_spec; |
| FrameSpec frame_spec; |
| PixelRgbaChannels bg_color_rgba; |
| std::vector<FrameSpec> all_frames; |
| |
| bg_color_rgba[RGBA_RED] = 5; |
| bg_color_rgba[RGBA_GREEN] = 15; |
| bg_color_rgba[RGBA_BLUE] = 25; |
| bg_color_rgba[RGBA_ALPHA] = 35; |
| |
| image_spec.width = 100; |
| image_spec.height = 100; |
| image_spec.num_frames = 1; |
| image_spec.use_bg_color = use_bg_color; |
| memcpy(image_spec.bg_color, bg_color_rgba, sizeof(bg_color_rgba)); |
| |
| frame_spec.width = 20; |
| frame_spec.height = 30; |
| frame_spec.top = 10; |
| frame_spec.left = 15; |
| frame_spec.pixel_format = pixel_format; |
| all_frames.push_back(frame_spec); |
| |
| frame_spec.width = 35; |
| frame_spec.height = 17; |
| frame_spec.top = 51; |
| frame_spec.left = 14; |
| frame_spec.pixel_format = pixel_format; |
| all_frames.push_back(frame_spec); |
| |
| // Frame coincides with image. |
| frame_spec.width = 100; |
| frame_spec.height = 100; |
| frame_spec.top = 0; |
| frame_spec.left = 0; |
| frame_spec.pixel_format = pixel_format; |
| all_frames.push_back(frame_spec); |
| |
| // Frame offset and falls off image. |
| frame_spec.width = 100; |
| frame_spec.height = 100; |
| frame_spec.top = 10; |
| frame_spec.left = 10; |
| frame_spec.pixel_format = pixel_format; |
| all_frames.push_back(frame_spec); |
| |
| // Frame larger than image. |
| frame_spec.width = 200; |
| frame_spec.height = 200; |
| frame_spec.top = 0; |
| frame_spec.left = 0; |
| frame_spec.pixel_format = pixel_format; |
| all_frames.push_back(frame_spec); |
| |
| TestAllFramesPadded(image_spec, all_frames); |
| } |
| |
| MockMessageHandler message_handler_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MultipleFramePaddingReaderTest); |
| }; |
| |
| |
| TEST_F(MultipleFramePaddingReaderTest, ReaderPadsRGBA_8888) { |
| TestReaderPadsAllFrames(RGBA_8888, true); |
| TestReaderPadsAllFrames(RGBA_8888, false); |
| } |
| |
| TEST_F(MultipleFramePaddingReaderTest, ReaderPadsRGB_888) { |
| TestReaderPadsAllFrames(RGB_888, true); |
| TestReaderPadsAllFrames(RGB_888, false); |
| } |
| |
| TEST_F(MultipleFramePaddingReaderTest, ReaderPadsGRAY_8) { |
| TestReaderPadsAllFrames(GRAY_8, true); |
| TestReaderPadsAllFrames(GRAY_8, false); |
| } |
| |
| } // namespace |