From 987f84d0d84d4e59b36ce8532ddcaf174f1346cd Mon Sep 17 00:00:00 2001 From: sdefresne Date: Fri, 12 Dec 2014 06:35:57 -0800 Subject: Add support for decoding WebP images Add a wrapper around libwebp to decompress WebP images and re-encode them to uncompressed TIFF (if small enough) or JPEG/PNG (dependending on image features). BUG=436897 Review URL: https://codereview.chromium.org/771723002 Cr-Commit-Position: refs/heads/master@{#308069} --- ios/ios_tests_unit.gyp | 18 +++ ios/web/DEPS | 1 + ios/web/ios_web.gyp | 16 ++ ios/web/public/webp_decoder.h | 83 ++++++++++ ios/web/public/webp_decoder.mm | 251 ++++++++++++++++++++++++++++++ ios/web/public/webp_decoder_unittest.mm | 263 ++++++++++++++++++++++++++++++++ ios/web/test/data/test.jpg | Bin 0 -> 84820 bytes ios/web/test/data/test.webp | Bin 0 -> 30320 bytes ios/web/test/data/test_alpha.png | Bin 0 -> 175667 bytes ios/web/test/data/test_alpha.webp | Bin 0 -> 92312 bytes ios/web/test/data/test_small.tiff | Bin 0 -> 4306 bytes ios/web/test/data/test_small.webp | Bin 0 -> 724 bytes 12 files changed, 632 insertions(+) create mode 100644 ios/web/public/webp_decoder.h create mode 100644 ios/web/public/webp_decoder.mm create mode 100644 ios/web/public/webp_decoder_unittest.mm create mode 100644 ios/web/test/data/test.jpg create mode 100644 ios/web/test/data/test.webp create mode 100644 ios/web/test/data/test_alpha.png create mode 100644 ios/web/test/data/test_alpha.webp create mode 100644 ios/web/test/data/test_small.tiff create mode 100644 ios/web/test/data/test_small.webp (limited to 'ios') diff --git a/ios/ios_tests_unit.gyp b/ios/ios_tests_unit.gyp index 8b9e164..ed1b9b7 100644 --- a/ios/ios_tests_unit.gyp +++ b/ios/ios_tests_unit.gyp @@ -22,8 +22,26 @@ 'sources': [ 'web/browser_state_unittest.cc', 'web/navigation/navigation_item_impl_unittest.mm', + 'web/public/webp_decoder_unittest.mm', 'web/string_util_unittest.cc', ], + 'actions': [ + { + 'action_name': 'copy_test_data', + 'variables': { + 'test_data_files': [ + 'web/test/data/test.jpg', + 'web/test/data/test.webp', + 'web/test/data/test_alpha.webp', + 'web/test/data/test_alpha.png', + 'web/test/data/test_small.tiff', + 'web/test/data/test_small.webp', + ], + 'test_data_prefix': 'ios', + }, + 'includes': [ '../build/copy_test_data_ios.gypi' ], + }, + ], }, ], } diff --git a/ios/web/DEPS b/ios/web/DEPS index c75a4c1..30f598b 100644 --- a/ios/web/DEPS +++ b/ios/web/DEPS @@ -1,5 +1,6 @@ include_rules = [ "+net", + "+third_party/libwebp/webp", "+ui", ] diff --git a/ios/web/ios_web.gyp b/ios/web/ios_web.gyp index f00800a..276d860 100644 --- a/ios/web/ios_web.gyp +++ b/ios/web/ios_web.gyp @@ -14,6 +14,7 @@ '../..', ], 'dependencies': [ + 'webp_decoder', '../../base/base.gyp:base', '../../content/content.gyp:content_browser', '../../net/net.gyp:net', @@ -68,6 +69,21 @@ ], }, { + 'target_name': 'webp_decoder', + 'type': 'static_library', + 'include_dirs': [ + '../..', + ], + 'dependencies': [ + '../../base/base.gyp:base', + '../../third_party/libwebp/libwebp.gyp:libwebp_dec', + ], + 'sources': [ + 'public/webp_decoder.h', + 'public/webp_decoder.mm', + ], + }, + { 'target_name': 'test_support_ios_web', 'type': 'static_library', 'dependencies': [ diff --git a/ios/web/public/webp_decoder.h b/ios/web/public/webp_decoder.h new file mode 100644 index 0000000..8700718 --- /dev/null +++ b/ios/web/public/webp_decoder.h @@ -0,0 +1,83 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef IOS_WEB_PUBLIC_WEBP_DECODER_H_ +#define IOS_WEB_PUBLIC_WEBP_DECODER_H_ + +#include "base/mac/scoped_nsobject.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "third_party/libwebp/webp/decode.h" + +@class NSData; + +namespace web { + +// Decodes a WebP image into either JPEG, PNG or uncompressed TIFF. +class WebpDecoder : public base::RefCountedThreadSafe { + public: + // Format of the decoded image. + // This enum is used for UMA reporting, keep it in sync with the histogram + // definition. + enum DecodedImageFormat { + JPEG = 1, + PNG, + TIFF, + DECODED_FORMAT_COUNT + }; + + class Delegate : public base::RefCountedThreadSafe { + public: + virtual void OnFinishedDecoding(bool success) = 0; + virtual void SetImageFeatures(size_t total_size, // In bytes. + DecodedImageFormat format) = 0; + virtual void OnDataDecoded(NSData* data) = 0; + + protected: + friend class base::RefCountedThreadSafe; + virtual ~Delegate() {} + }; + + explicit WebpDecoder(WebpDecoder::Delegate* delegate); + + // For tests. + static size_t GetHeaderSize(); + + // Main entry point. + void OnDataReceived(const base::scoped_nsobject& data); + + // Stops the decoding. + void Stop(); + + private: + struct WebPIDecoderDeleter { + inline void operator()(WebPIDecoder* ptr) const { WebPIDelete(ptr); } + }; + + enum State { + READING_FEATURES, + READING_DATA, + DONE + }; + + friend class base::RefCountedThreadSafe; + virtual ~WebpDecoder(); + + // Implements WebP image decoding state machine steps. + void DoReadFeatures(NSData* data); + void DoReadData(NSData* data); + bool DoSendData(); + + scoped_refptr delegate_; + WebPDecoderConfig config_; + WebpDecoder::State state_; + scoped_ptr incremental_decoder_; + base::scoped_nsobject output_buffer_; + base::scoped_nsobject features_; + int has_alpha_; +}; + +} // namespace web + +#endif // IOS_WEB_PUBLIC_WEBP_DECODER_H_ diff --git a/ios/web/public/webp_decoder.mm b/ios/web/public/webp_decoder.mm new file mode 100644 index 0000000..3ac1cda --- /dev/null +++ b/ios/web/public/webp_decoder.mm @@ -0,0 +1,251 @@ +// Copyright 2013 The Chromium 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 "ios/web/public/webp_decoder.h" + +#import +#import + +#include "base/logging.h" +#include "base/metrics/histogram.h" + +namespace { + +const uint8_t kNumIfdEntries = 15; +const unsigned int kExtraDataSize = 16; +// 10b for signature/header + n * 12b entries + 4b for IFD terminator: +const unsigned int kExtraDataOffset = 10 + 12 * kNumIfdEntries + 4; +const unsigned int kHeaderSize = kExtraDataOffset + kExtraDataSize; +const int kRecompressionThreshold = 64 * 64; // Threshold in pixels. +const CGFloat kJpegQuality = 0.85; + +// Adapted from libwebp example dwebp.c. +void PutLE16(uint8_t* const dst, uint32_t value) { + dst[0] = (value >> 0) & 0xff; + dst[1] = (value >> 8) & 0xff; +} + +void PutLE32(uint8_t* const dst, uint32_t value) { + PutLE16(dst + 0, (value >> 0) & 0xffff); + PutLE16(dst + 2, (value >> 16) & 0xffff); +} + +void WriteTiffHeader(uint8_t* dst, + int width, + int height, + int bytes_per_px, + bool has_alpha) { + // For non-alpha case, we omit tag 0x152 (ExtraSamples). + const uint8_t num_ifd_entries = has_alpha ? kNumIfdEntries + : kNumIfdEntries - 1; + uint8_t tiff_header[kHeaderSize] = { + 0x49, 0x49, 0x2a, 0x00, // little endian signature + 8, 0, 0, 0, // offset to the unique IFD that follows + // IFD (offset = 8). Entries must be written in increasing tag order. + num_ifd_entries, 0, // Number of entries in the IFD (12 bytes each). + 0x00, 0x01, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, // 10: Width (TBD) + 0x01, 0x01, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, // 22: Height (TBD) + 0x02, 0x01, 3, 0, bytes_per_px, 0, 0, 0, // 34: BitsPerSample: 8888 + kExtraDataOffset + 0, 0, 0, 0, + 0x03, 0x01, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, // 46: Compression: none + 0x06, 0x01, 3, 0, 1, 0, 0, 0, 2, 0, 0, 0, // 58: Photometric: RGB + 0x11, 0x01, 4, 0, 1, 0, 0, 0, // 70: Strips offset: + kHeaderSize, 0, 0, 0, // data follows header + 0x12, 0x01, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, // 82: Orientation: topleft + 0x15, 0x01, 3, 0, 1, 0, 0, 0, // 94: SamplesPerPixels + bytes_per_px, 0, 0, 0, + 0x16, 0x01, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, // 106: Rows per strip (TBD) + 0x17, 0x01, 4, 0, 1, 0, 0, 0, 0, 0, 0, 0, // 118: StripByteCount (TBD) + 0x1a, 0x01, 5, 0, 1, 0, 0, 0, // 130: X-resolution + kExtraDataOffset + 8, 0, 0, 0, + 0x1b, 0x01, 5, 0, 1, 0, 0, 0, // 142: Y-resolution + kExtraDataOffset + 8, 0, 0, 0, + 0x1c, 0x01, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, // 154: PlanarConfiguration + 0x28, 0x01, 3, 0, 1, 0, 0, 0, 2, 0, 0, 0, // 166: ResolutionUnit (inch) + 0x52, 0x01, 3, 0, 1, 0, 0, 0, 1, 0, 0, 0, // 178: ExtraSamples: rgbA + 0, 0, 0, 0, // 190: IFD terminator + // kExtraDataOffset: + 8, 0, 8, 0, 8, 0, 8, 0, // BitsPerSample + 72, 0, 0, 0, 1, 0, 0, 0 // 72 pixels/inch, for X/Y-resolution + }; + + // Fill placeholders in IFD: + PutLE32(tiff_header + 10 + 8, width); + PutLE32(tiff_header + 22 + 8, height); + PutLE32(tiff_header + 106 + 8, height); + PutLE32(tiff_header + 118 + 8, width * bytes_per_px * height); + if (!has_alpha) + PutLE32(tiff_header + 178, 0); + + memcpy(dst, tiff_header, kHeaderSize); +} + +} // namespace + +namespace web { + +// static +size_t WebpDecoder::GetHeaderSize() { + return kHeaderSize; +} + +WebpDecoder::WebpDecoder(WebpDecoder::Delegate* delegate) + : delegate_(delegate), state_(READING_FEATURES), has_alpha_(0) { + DCHECK(delegate_.get()); + const bool rv = WebPInitDecoderConfig(&config_); + DCHECK(rv); +} + +WebpDecoder::~WebpDecoder() { + WebPFreeDecBuffer(&config_.output); +} + +void WebpDecoder::OnDataReceived(const base::scoped_nsobject& data) { + DCHECK(data); + switch (state_) { + case READING_FEATURES: + DoReadFeatures(data); + break; + case READING_DATA: + DoReadData(data); + break; + case DONE: + DLOG(WARNING) << "Received WebP data but decoding is finished. Ignoring."; + break; + } +} + +void WebpDecoder::Stop() { + if (state_ != DONE) { + state_ = DONE; + DLOG(WARNING) << "Unexpected end of WebP data."; + delegate_->OnFinishedDecoding(false); + } +} + +void WebpDecoder::DoReadFeatures(NSData* data) { + DCHECK_EQ(READING_FEATURES, state_); + DCHECK(data); + if (features_) + [features_ appendData:data]; + else + features_.reset([[NSMutableData alloc] initWithData:data]); + VP8StatusCode status = + WebPGetFeatures(static_cast([features_ bytes]), + [features_ length], &config_.input); + switch (status) { + case VP8_STATUS_OK: { + has_alpha_ = config_.input.has_alpha; + const uint32_t width = config_.input.width; + const uint32_t height = config_.input.height; + const size_t bytes_per_px = has_alpha_ ? 4 : 3; + const int stride = bytes_per_px * width; + const size_t image_data_size = stride * height; + const size_t total_size = image_data_size + kHeaderSize; + // Force pre-multiplied alpha. + config_.output.colorspace = has_alpha_ ? MODE_rgbA : MODE_RGB; + config_.output.u.RGBA.stride = stride; + // Create the output buffer. + config_.output.u.RGBA.size = image_data_size; + uint8_t* dst = static_cast(malloc(total_size)); + if (!dst) { + DLOG(ERROR) << "Could not allocate WebP decoding buffer (size = " + << total_size << ")."; + delegate_->OnFinishedDecoding(false); + state_ = DONE; + break; + } + WriteTiffHeader(dst, width, height, bytes_per_px, has_alpha_); + output_buffer_.reset([[NSData alloc] initWithBytesNoCopy:dst + length:total_size + freeWhenDone:YES]); + config_.output.is_external_memory = 1; + config_.output.u.RGBA.rgba = dst + kHeaderSize; + // Start decoding. + state_ = READING_DATA; + incremental_decoder_.reset(WebPINewDecoder(&config_.output)); + DoReadData(features_); + features_.reset(); + break; + } + case VP8_STATUS_NOT_ENOUGH_DATA: + // Do nothing. + break; + default: + DLOG(ERROR) << "Error in WebP image features."; + delegate_->OnFinishedDecoding(false); + state_ = DONE; + break; + } +} + +void WebpDecoder::DoReadData(NSData* data) { + DCHECK_EQ(READING_DATA, state_); + DCHECK(incremental_decoder_); + DCHECK(data); + VP8StatusCode status = WebPIAppend(incremental_decoder_.get(), + static_cast([data bytes]), + [data length]); + switch (status) { + case VP8_STATUS_SUSPENDED: + // Do nothing: re-compression to JPEG or PNG cannot be done incrementally. + // Wait for the whole image to be decoded. + break; + case VP8_STATUS_OK: { + bool rv = DoSendData(); + DLOG_IF(ERROR, !rv) << "Error in WebP image conversion."; + state_ = DONE; + delegate_->OnFinishedDecoding(rv); + break; + } + default: + DLOG(ERROR) << "Error in WebP image decoding."; + delegate_->OnFinishedDecoding(false); + state_ = DONE; + break; + } +} + +bool WebpDecoder::DoSendData() { + DCHECK_EQ(READING_DATA, state_); + int width, height; + uint8_t* data_ptr = + WebPIDecGetRGB(incremental_decoder_.get(), NULL, &width, &height, NULL); + if (!data_ptr) + return false; + DCHECK_EQ(static_cast([output_buffer_ bytes]) + kHeaderSize, + data_ptr); + base::scoped_nsobject result_data; + // When the WebP image is larger than |kRecompressionThreshold| it is + // compressed to JPEG or PNG. Otherwise, the uncompressed TIFF is used. + DecodedImageFormat format = TIFF; + if (width * height > kRecompressionThreshold) { + base::scoped_nsobject tiff_image( + [[UIImage alloc] initWithData:output_buffer_]); + if (!tiff_image) + return false; + // Compress to PNG if the image is transparent, JPEG otherwise. + // TODO(droger): Use PNG instead of JPEG if the WebP image is lossless. + if (has_alpha_) { + result_data.reset([UIImagePNGRepresentation(tiff_image) retain]); + format = PNG; + } else { + result_data.reset( + [UIImageJPEGRepresentation(tiff_image, kJpegQuality) retain]); + format = JPEG; + } + if (!result_data) + return false; + } else { + result_data.reset([output_buffer_ retain]); + } + UMA_HISTOGRAM_ENUMERATION( + "WebP.DecodedImageFormat", format, DECODED_FORMAT_COUNT); + delegate_->SetImageFeatures([result_data length], format); + delegate_->OnDataDecoded(result_data); + output_buffer_.reset(); + return true; +} + +} // namespace web diff --git a/ios/web/public/webp_decoder_unittest.mm b/ios/web/public/webp_decoder_unittest.mm new file mode 100644 index 0000000..40d20a1 --- /dev/null +++ b/ios/web/public/webp_decoder_unittest.mm @@ -0,0 +1,263 @@ +// Copyright 2012 The Chromium 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 "ios/web/public/webp_decoder.h" + +#import +#import + +#include "base/base_paths.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/mac/scoped_cftyperef.h" +#include "base/mac/scoped_nsobject.h" +#include "base/memory/ref_counted.h" +#include "base/path_service.h" +#include "base/strings/sys_string_conversions.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace web { +namespace { + +class WebpDecoderDelegate : public WebpDecoder::Delegate { + public: + WebpDecoderDelegate() : image_([[NSMutableData alloc] init]) { + } + + NSData* GetImage() const { return image_; } + + // WebpDecoder::Delegate methods. + MOCK_METHOD1(OnFinishedDecoding, void(bool success)); + MOCK_METHOD2(SetImageFeatures, + void(size_t total_size, WebpDecoder::DecodedImageFormat format)); + virtual void OnDataDecoded(NSData* data) { [image_ appendData:data]; } + + private: + virtual ~WebpDecoderDelegate() {} + + base::scoped_nsobject image_; +}; + +class WebpDecoderTest : public testing::Test { + public: + WebpDecoderTest() + : delegate_(new WebpDecoderDelegate), + decoder_(new WebpDecoder(delegate_.get())) {} + + NSData* LoadImage(const base::FilePath& filename) { + base::FilePath path; + PathService::Get(base::DIR_SOURCE_ROOT, &path); + path = path.AppendASCII("ios/web/test/data").Append(filename); + return + [NSData dataWithContentsOfFile:base::SysUTF8ToNSString(path.value())]; + } + + std::vector* DecompressData(NSData* data, + WebpDecoder::DecodedImageFormat format) { + base::ScopedCFTypeRef provider( + CGDataProviderCreateWithCFData((CFDataRef)data)); + base::ScopedCFTypeRef image; + switch (format) { + case WebpDecoder::JPEG: + image.reset(CGImageCreateWithJPEGDataProvider( + provider, NULL, false, kCGRenderingIntentDefault)); + break; + case WebpDecoder::PNG: + image.reset(CGImageCreateWithPNGDataProvider( + provider, NULL, false, kCGRenderingIntentDefault)); + break; + case WebpDecoder::TIFF: + ADD_FAILURE() << "Data already decompressed"; + return nil; + case WebpDecoder::DECODED_FORMAT_COUNT: + ADD_FAILURE() << "Unknown format"; + return nil; + } + size_t width = CGImageGetWidth(image); + size_t height = CGImageGetHeight(image); + base::ScopedCFTypeRef color_space( + CGColorSpaceCreateDeviceRGB()); + size_t bytes_per_pixel = 4; + size_t bytes_per_row = bytes_per_pixel * width; + size_t bits_per_component = 8; + std::vector* result = + new std::vector(width * height * bytes_per_pixel, 0); + base::ScopedCFTypeRef context(CGBitmapContextCreate( + &result->front(), width, height, bits_per_component, bytes_per_row, + color_space, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big)); + CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); + // Check that someting has been written in |result|. + std::vector zeroes(width * height * bytes_per_pixel, 0); + EXPECT_NE(0, memcmp(&result->front(), &zeroes.front(), zeroes.size())) + << "Decompression failed."; + return result; + } + + // Compares data, allowing an averaged absolute difference of 1. + bool CompareUncompressedData(const uint8_t* ptr_1, + const uint8_t* ptr_2, + size_t size) { + uint64_t difference = 0; + for (size_t i = 0; i < size; ++i) { + // Casting to int to avoid overflow. + int error = abs(int(ptr_1[i]) - int(ptr_2[i])); + EXPECT_GE(difference + error, difference) + << "Image difference too big (overflow)."; + difference += error; + } + double average_difference = double(difference) / double(size); + DLOG(INFO) << "Average image difference: " << average_difference; + return average_difference < 1.5; + } + + bool CheckCompressedImagesEqual(NSData* data_1, + NSData* data_2, + WebpDecoder::DecodedImageFormat format) { + scoped_ptr > uncompressed_1( + DecompressData(data_1, format)); + scoped_ptr > uncompressed_2( + DecompressData(data_2, format)); + if (uncompressed_1->size() != uncompressed_2->size()) { + DLOG(ERROR) << "Image sizes don't match"; + return false; + } + return CompareUncompressedData(&uncompressed_1->front(), + &uncompressed_2->front(), + uncompressed_1->size()); + } + + bool CheckTiffImagesEqual(NSData* image_1, NSData* image_2) { + if ([image_1 length] != [image_2 length]) { + DLOG(ERROR) << "Image lengths don't match"; + return false; + } + // Compare headers. + const size_t kHeaderSize = WebpDecoder::GetHeaderSize(); + NSData* header_1 = [image_1 subdataWithRange:NSMakeRange(0, kHeaderSize)]; + NSData* header_2 = [image_2 subdataWithRange:NSMakeRange(0, kHeaderSize)]; + if (!header_1 || !header_2) + return false; + if (![header_1 isEqualToData:header_2]) { + DLOG(ERROR) << "Headers don't match."; + return false; + } + return CompareUncompressedData( + static_cast([image_1 bytes]) + kHeaderSize, + static_cast([image_2 bytes]) + kHeaderSize, + [image_1 length] - kHeaderSize); + } + + protected: + scoped_refptr delegate_; + scoped_refptr decoder_; +}; + +} // namespace + +TEST_F(WebpDecoderTest, DecodeToJpeg) { + // Load a WebP image from disk. + base::scoped_nsobject webp_image( + [LoadImage(base::FilePath("test.webp")) retain]); + ASSERT_TRUE(webp_image != nil); + // Load reference image. + base::scoped_nsobject jpg_image( + [LoadImage(base::FilePath("test.jpg")) retain]); + ASSERT_TRUE(jpg_image != nil); + // Convert to JPEG. + EXPECT_CALL(*delegate_, OnFinishedDecoding(true)).Times(1); + EXPECT_CALL(*delegate_, + SetImageFeatures(testing::_, WebpDecoder::JPEG)).Times(1); + decoder_->OnDataReceived(webp_image); + // Compare to reference image. + EXPECT_TRUE(CheckCompressedImagesEqual(jpg_image, delegate_->GetImage(), + WebpDecoder::JPEG)); +} + +TEST_F(WebpDecoderTest, DecodeToPng) { + // Load a WebP image from disk. + base::scoped_nsobject webp_image( + [LoadImage(base::FilePath("test_alpha.webp")) retain]); + ASSERT_TRUE(webp_image != nil); + // Load reference image. + base::scoped_nsobject png_image( + [LoadImage(base::FilePath("test_alpha.png")) retain]); + ASSERT_TRUE(png_image != nil); + // Convert to PNG. + EXPECT_CALL(*delegate_, OnFinishedDecoding(true)).Times(1); + EXPECT_CALL(*delegate_, + SetImageFeatures(testing::_, WebpDecoder::PNG)).Times(1); + decoder_->OnDataReceived(webp_image); + // Compare to reference image. + EXPECT_TRUE(CheckCompressedImagesEqual(png_image, delegate_->GetImage(), + WebpDecoder::PNG)); +} + +TEST_F(WebpDecoderTest, DecodeToTiff) { + // Load a WebP image from disk. + base::scoped_nsobject webp_image( + [LoadImage(base::FilePath("test_small.webp")) retain]); + ASSERT_TRUE(webp_image != nil); + // Load reference image. + base::scoped_nsobject tiff_image( + [LoadImage(base::FilePath("test_small.tiff")) retain]); + ASSERT_TRUE(tiff_image != nil); + // Convert to TIFF. + EXPECT_CALL(*delegate_, OnFinishedDecoding(true)).Times(1); + EXPECT_CALL(*delegate_, SetImageFeatures([tiff_image length], + WebpDecoder::TIFF)).Times(1); + decoder_->OnDataReceived(webp_image); + // Compare to reference image. + EXPECT_TRUE(CheckTiffImagesEqual(tiff_image, delegate_->GetImage())); +} + +TEST_F(WebpDecoderTest, StreamedDecode) { + // Load a WebP image from disk. + base::scoped_nsobject webp_image( + [LoadImage(base::FilePath("test.webp")) retain]); + ASSERT_TRUE(webp_image != nil); + // Load reference image. + base::scoped_nsobject jpg_image( + [LoadImage(base::FilePath("test.jpg")) retain]); + ASSERT_TRUE(jpg_image != nil); + // Convert to JPEG in chunks. + EXPECT_CALL(*delegate_, OnFinishedDecoding(true)).Times(1); + EXPECT_CALL(*delegate_, + SetImageFeatures(testing::_, WebpDecoder::JPEG)).Times(1); + const size_t kChunkSize = 10; + unsigned int num_chunks = 0; + while ([webp_image length] > kChunkSize) { + base::scoped_nsobject chunk( + [[webp_image subdataWithRange:NSMakeRange(0, kChunkSize)] retain]); + decoder_->OnDataReceived(chunk); + webp_image.reset([[webp_image subdataWithRange:NSMakeRange( + kChunkSize, [webp_image length] - kChunkSize)] retain]); + ++num_chunks; + } + if ([webp_image length] > 0u) { + decoder_->OnDataReceived(webp_image); + ++num_chunks; + } + ASSERT_GT(num_chunks, 3u) << "Not enough chunks"; + // Compare to reference image. + EXPECT_TRUE(CheckCompressedImagesEqual(jpg_image, delegate_->GetImage(), + WebpDecoder::JPEG)); +} + +TEST_F(WebpDecoderTest, InvalidFormat) { + EXPECT_CALL(*delegate_, OnFinishedDecoding(false)).Times(1); + const char dummy_image[] = "(>'-')> <('-'<) ^('-')^ <('-'<) (>'-')>"; + base::scoped_nsobject data( + [[NSData alloc] initWithBytes:dummy_image length:arraysize(dummy_image)]); + decoder_->OnDataReceived(data); + EXPECT_EQ(0u, [delegate_->GetImage() length]); +} + +TEST_F(WebpDecoderTest, DecodeAborted) { + EXPECT_CALL(*delegate_, OnFinishedDecoding(false)).Times(1); + decoder_->Stop(); + EXPECT_EQ(0u, [delegate_->GetImage() length]); +} + +} // namespace web diff --git a/ios/web/test/data/test.jpg b/ios/web/test/data/test.jpg new file mode 100644 index 0000000..182a14a Binary files /dev/null and b/ios/web/test/data/test.jpg differ diff --git a/ios/web/test/data/test.webp b/ios/web/test/data/test.webp new file mode 100644 index 0000000..122741b Binary files /dev/null and b/ios/web/test/data/test.webp differ diff --git a/ios/web/test/data/test_alpha.png b/ios/web/test/data/test_alpha.png new file mode 100644 index 0000000..2de1a48 Binary files /dev/null and b/ios/web/test/data/test_alpha.png differ diff --git a/ios/web/test/data/test_alpha.webp b/ios/web/test/data/test_alpha.webp new file mode 100644 index 0000000..ba39447 Binary files /dev/null and b/ios/web/test/data/test_alpha.webp differ diff --git a/ios/web/test/data/test_small.tiff b/ios/web/test/data/test_small.tiff new file mode 100644 index 0000000..8f1986d Binary files /dev/null and b/ios/web/test/data/test_small.tiff differ diff --git a/ios/web/test/data/test_small.webp b/ios/web/test/data/test_small.webp new file mode 100644 index 0000000..5447184 Binary files /dev/null and b/ios/web/test/data/test_small.webp differ -- cgit v1.1