From 7ebec077726d438198bdf599dc8705a6d1d76964 Mon Sep 17 00:00:00 2001 From: droger Date: Fri, 19 Dec 2014 03:19:49 -0800 Subject: Move iOS WebP decoding to a component This CL moves WebP decoding out of the web layer to a component that does not depend on web. This will allow to use the WebP decoding without pulling a dependency on the web layer. BUG=430110 TBR=cbentzel Review URL: https://codereview.chromium.org/814013003 Cr-Commit-Position: refs/heads/master@{#309181} --- components/webp_transcode/DEPS | 10 + components/webp_transcode/OWNERS | 2 + components/webp_transcode/README | 2 + components/webp_transcode/webp_decoder.h | 74 ++++++ components/webp_transcode/webp_decoder.mm | 251 ++++++++++++++++++++ components/webp_transcode/webp_decoder_unittest.mm | 264 +++++++++++++++++++++ 6 files changed, 603 insertions(+) create mode 100644 components/webp_transcode/DEPS create mode 100644 components/webp_transcode/OWNERS create mode 100644 components/webp_transcode/README create mode 100644 components/webp_transcode/webp_decoder.h create mode 100644 components/webp_transcode/webp_decoder.mm create mode 100644 components/webp_transcode/webp_decoder_unittest.mm (limited to 'components/webp_transcode') diff --git a/components/webp_transcode/DEPS b/components/webp_transcode/DEPS new file mode 100644 index 0000000..0e26ac6 --- /dev/null +++ b/components/webp_transcode/DEPS @@ -0,0 +1,10 @@ +include_rules = [ + "+net", + # Only WebP decoding is allowed (no encoding). + "+third_party/libwebp/webp/decode.h", + + # webp_transcode should not depend on //ios for library size reasons. + "-ios", + # webp_transcode is only used by iOS. + "-content", +] diff --git a/components/webp_transcode/OWNERS b/components/webp_transcode/OWNERS new file mode 100644 index 0000000..fa3d0c2 --- /dev/null +++ b/components/webp_transcode/OWNERS @@ -0,0 +1,2 @@ +droger@chromium.org +sdefresne@chromium.org diff --git a/components/webp_transcode/README b/components/webp_transcode/README new file mode 100644 index 0000000..4484825 --- /dev/null +++ b/components/webp_transcode/README @@ -0,0 +1,2 @@ +webp_transcode is used to convert WebP images to other formats supported by the +iOS web views. diff --git a/components/webp_transcode/webp_decoder.h b/components/webp_transcode/webp_decoder.h new file mode 100644 index 0000000..64126c2 --- /dev/null +++ b/components/webp_transcode/webp_decoder.h @@ -0,0 +1,74 @@ +// 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 COMPONENTS_WEBP_TRANSCODE_WEBP_DECODER_H_ +#define COMPONENTS_WEBP_TRANSCODE_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 webp_transcode { + +// 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 webp_transcode + +#endif // COMPONENTS_WEBP_TRANSCODE_WEBP_DECODER_H_ diff --git a/components/webp_transcode/webp_decoder.mm b/components/webp_transcode/webp_decoder.mm new file mode 100644 index 0000000..05c834b --- /dev/null +++ b/components/webp_transcode/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 "components/webp_transcode/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 webp_transcode { + +// 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(), nullptr, + &width, &height, nullptr); + 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 webp_transcode diff --git a/components/webp_transcode/webp_decoder_unittest.mm b/components/webp_transcode/webp_decoder_unittest.mm new file mode 100644 index 0000000..5130ca7 --- /dev/null +++ b/components/webp_transcode/webp_decoder_unittest.mm @@ -0,0 +1,264 @@ +// 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 "components/webp_transcode/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 webp_transcode { +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)); + void OnDataDecoded(NSData* data) override { [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("components/test/data/webp_transcode") + .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, nullptr, false, kCGRenderingIntentDefault)); + break; + case WebpDecoder::PNG: + image.reset(CGImageCreateWithPNGDataProvider( + provider, nullptr, 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 webp_transcode -- cgit v1.1