Feature: refactor decoding code and provide decoding plugin

- open the decoding/encoding procedures to the users
- switch from hardcoded decoding/encoding to pluginable decoders/encoders (builtin + user created)
- `SDWebImageCodersManager` is a singleton holding an array of `SDImageCoder` (protocol). Even though a singleton is not necesarily a good pattern, in this case it eliminates dealing with passing this array around
- uses a priority queue behind scenes, which means the latest added coders have priority.
- the priority is crucial when encoding/decoding something, we go through the list and ask each coder if they can handle the current data (see `canDecodeFromData:`, `canEncodeToFormat:`, `canIncrementallyDecodeFromData:`)
- each coder must conform to this protocol `SDImageCoder` describing all the required behavior for a coder
- we provide 3 built-in coders: `SDWebImageImageIOCoder` (for JPEG, PNG, TIFF), `SDWebImageGIFCoder` (for GIF), `SDWebImageWebPCoder` (for WebP and animated WebP)
- the user of SDWebImage can create custom coders by conforming to `SDImageCoder` and adding the coders to `SDWebImageCodersManager`. See `addCoder:` or `removeCoder:` or `coders` getter to get the array
- in order to preserve backwards compatibility, the UIImage categories were preserved, calling the new coders APIs described above
This commit is contained in:
DreamPiggy 2017-10-16 19:52:18 +03:00 committed by Bogdan Poplauschi
parent c1df782869
commit a0879fc049
38 changed files with 2742 additions and 1006 deletions

View File

@ -28,7 +28,7 @@ Pod::Spec.new do |s|
s.subspec 'Core' do |core|
core.source_files = 'SDWebImage/{NS,SD,UI}*.{h,m}'
core.exclude_files = 'SDWebImage/UIImage+WebP.{h,m}'
core.exclude_files = 'SDWebImage/UIImage+WebP.{h,m}', 'SDWebImage/SDWebImageWebPCoder.{h,m}'
core.tvos.exclude_files = 'SDWebImage/MKAnnotationView+WebCache.*'
end
@ -52,7 +52,7 @@ Pod::Spec.new do |s|
end
s.subspec 'WebP' do |webp|
webp.source_files = 'SDWebImage/UIImage+WebP.{h,m}'
webp.source_files = 'SDWebImage/UIImage+WebP.{h,m}', 'SDWebImage/SDWebImageWebPCoder.{h,m}'
webp.xcconfig = {
'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SD_WEBP=1',
'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src'

File diff suppressed because it is too large Load Diff

View File

@ -30,4 +30,12 @@ typedef NS_ENUM(NSInteger, SDImageFormat) {
*/
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data;
/**
Convert SDImageFormat to UTType
@param format Format as SDImageFormat
@return The UTType as CFStringRef
*/
+ (nonnull CFStringRef)sd_UTTypeFromSDImageFormat:(SDImageFormat)format;
@end

View File

@ -8,7 +8,11 @@
*/
#import "NSData+ImageContentType.h"
#if SD_MAC
#import <CoreServices/CoreServices.h>
#else
#import <MobileCoreServices/MobileCoreServices.h>
#endif
@implementation NSData (ImageContentType)
@ -43,4 +47,27 @@
return SDImageFormatUndefined;
}
+ (nonnull CFStringRef)sd_UTTypeFromSDImageFormat:(SDImageFormat)format {
CFStringRef UTType;
switch (format) {
case SDImageFormatJPEG:
UTType = kUTTypeJPEG;
break;
case SDImageFormatPNG:
UTType = kUTTypePNG;
break;
case SDImageFormatGIF:
UTType = kUTTypeGIF;
break;
case SDImageFormatTIFF:
UTType = kUTTypeTIFF;
break;
default:
// default is kUTTypePNG
UTType = kUTTypePNG;
break;
}
return UTType;
}
@end

View File

@ -7,12 +7,9 @@
*/
#import "SDImageCache.h"
#import "SDWebImageDecoder.h"
#import "UIImage+MultiFormat.h"
#import <CommonCrypto/CommonDigest.h>
#import "UIImage+GIF.h"
#import "NSData+ImageContentType.h"
#import "NSImage+WebCache.h"
#import "SDWebImageCodersManager.h"
// See https://github.com/rs/SDWebImage/pull/1141 for discussion
@interface AutoPurgeCache : NSCache
@ -220,9 +217,9 @@ FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
data = [image sd_imageDataAsFormat:imageFormatFromData];
}
// If we do not have any data to detect image format, use PNG format
data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
}
[self storeImageDataToDisk:data forKey:key];
}
@ -345,16 +342,10 @@ FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
image = [self scaledImageForKey:key image:image];
#ifdef SD_WEBP
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
if (imageFormat == SDImageFormatWebP) {
return image;
}
#endif
if (self.config.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
return image;
} else {
@ -378,7 +369,7 @@ FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if ([image isGIF]) {
if (image.images) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {

View File

@ -0,0 +1,115 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <Foundation/Foundation.h>
#import "SDWebImageCompat.h"
#import "NSData+ImageContentType.h"
/**
A Boolean value indicating whether to scale down large images during decompressing. (NSNumber)
*/
FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageCoderScaleDownLargeImagesKey;
/**
Return the shared device-dependent RGB color space created with CGColorSpaceCreateDeviceRGB.
@return The device-dependent RGB color space
*/
CG_EXTERN CGColorSpaceRef _Nonnull SDCGColorSpaceGetDeviceRGB(void);
/**
Check whether CGImageRef contains alpha channel.
@param imageRef The CGImageRef
@return Return YES if CGImageRef contains alpha channel, otherwise return NO
*/
CG_EXTERN BOOL SDCGImageRefContainsAlpha(_Nullable CGImageRef imageRef);
/**
This is the image coder protocol to provide custom image decoding/encoding.
@note Pay attention that these methods are not called from main queue.
*/
@protocol SDWebImageCoder <NSObject>
#pragma mark - Decoding
/**
Returns YES if this coder can decode some data. Otherwise, the data should be passed to another coder.
@param data The image data so we can look at it
@return YES if this coder can decode the data, NO otherwise
*/
- (BOOL)canDecodeFromData:(nullable NSData *)data;
/**
Decode the image data to image.
@param data The image data to be decoded
@return The decoded image from data
*/
- (nullable UIImage *)decodedImageWithData:(nullable NSData *)data;
/**
Decompress the image with original image and image data.
@param image The original image to be decompressed
@param data The pointer to original image data. The pointer itself is nonnull but image data can be null. This data will set to cache if needed. If you do not need to modify data at the sametime, ignore this param.
@param optionsDict A dictionary containing any decompressing options. Pass {SDWebImageCoderScaleDownLargeImagesKey: @(YES)} to scale down large images
@return The decompressed image
*/
- (nullable UIImage *)decompressedImageWithImage:(nullable UIImage *)image
data:(NSData * _Nullable * _Nonnull)data
options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict;
#pragma mark - Encoding
/**
Returns YES if this coder can encode some image. Otherwise, it should be passed to another coder.
@param format The image format
@return YES if this coder can encode the image, NO otherwise
*/
- (BOOL)canEncodeToFormat:(SDImageFormat)format;
/**
Encode the image to image data.
@param image The image to be encoded
@param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible
@return The encoded image data
*/
- (nullable NSData *)encodedDataWithImage:(nullable UIImage *)image format:(SDImageFormat)format;
@end
/**
This is the image coder protocol to provide custom progressive image decoding.
@note Pay attention that these methods are not called from main queue.
*/
@protocol SDWebImageProgressiveCoder <SDWebImageCoder>
/**
Returns YES if this coder can incremental decode some data. Otherwise, it should be passed to another coder.
@param data The image data so we can look at it
@return YES if this coder can decode the data, NO otherwise
*/
- (BOOL)canIncrementallyDecodeFromData:(nullable NSData *)data;
/**
Incremental decode the image data to image.
@param data The image data has been downloaded so far
@param finished Whether the download has finished
@warning because incremental decoding need to keep the decoded context, we will alloc a new instance with the same class for each download operation to avoid conflicts
@return The decoded image from data
*/
- (nullable UIImage *)incrementallyDecodedImageWithData:(nullable NSData *)data finished:(BOOL)finished;
@end

View File

@ -0,0 +1,31 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDWebImageCoder.h"
NSString * const SDWebImageCoderScaleDownLargeImagesKey = @"scaleDownLargeImages";
CGColorSpaceRef SDCGColorSpaceGetDeviceRGB(void) {
static CGColorSpaceRef colorSpace;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
colorSpace = CGColorSpaceCreateDeviceRGB();
});
return colorSpace;
}
BOOL SDCGImageRefContainsAlpha(CGImageRef imageRef) {
if (!imageRef) {
return NO;
}
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
return hasAlpha;
}

View File

@ -0,0 +1,58 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <Foundation/Foundation.h>
#import "SDWebImageCoder.h"
/**
Global object holding the array of coders, so that we avoid passing them from object to object.
Uses a priority queue behind scenes, which means the latest added coders have priority.
This is done so when encoding/decoding something, we go through the list and ask each coder if they can handle the current data.
That way, users can add their custom coders while preserving our existing prebuilt ones
Note: the `coders` getter will return the coders in their reversed order
Example:
- by default we internally set coders = `IOCoder`, `GIFCoder`, `WebPCoder`
- calling `coders` will return `@[WebPCoder, GIFCoder, IOCoder]`
- call `[addCoder:[MyCrazyCoder new]]`
- calling `coders` now returns `@[MyCrazyCoder, WebPCoder, GIFCoder, IOCoder]`
Coders
------
A coder must conform to the `SDWebImageCoder` protocol or even to `SDWebImageProgressiveCoder` if it supports progressive decoding
Conformance is important because that way, they will implement `canDecodeFromData` or `canEncodeToFormat`
Those methods are called on each coder in the array (using the priority order) until one of them returns YES.
That means that coder can decode that data / encode to that format
*/
@interface SDWebImageCodersManager : NSObject<SDWebImageCoder>
/**
Shared reusable instance
*/
+ (nonnull instancetype)sharedInstance;
/**
All coders in coders manager. The coders array is a priority queue, which means the later added coder will have the highest priority
*/
@property (nonatomic, strong, readwrite, nullable) NSArray<SDWebImageCoder>* coders;
/**
Add a new coder to the end of coders array. Which has the highest priority.
@param coder coder
*/
- (void)addCoder:(nonnull id<SDWebImageCoder>)coder;
/**
Remove a coder in the coders array.
@param coder coder
*/
- (void)removeCoder:(nonnull id<SDWebImageCoder>)coder;
@end

View File

@ -0,0 +1,137 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDWebImageCodersManager.h"
#import "SDWebImageImageIOCoder.h"
#import "SDWebImageGIFCoder.h"
#ifdef SD_WEBP
#import "SDWebImageWebPCoder.h"
#endif
@interface SDWebImageCodersManager ()
@property (nonatomic, strong, nonnull) NSMutableArray<SDWebImageCoder>* mutableCoders;
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t mutableCodersAccessQueue;
@end
@implementation SDWebImageCodersManager
+ (nonnull instancetype)sharedInstance {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (instancetype)init {
if (self = [super init]) {
// initialize with default coders
_mutableCoders = [@[[SDWebImageImageIOCoder sharedCoder], [SDWebImageGIFCoder sharedCoder]] mutableCopy];
#ifdef SD_WEBP
[_mutableCoders addObject:[SDWebImageWebPCoder sharedCoder]];
#endif
_mutableCodersAccessQueue = dispatch_queue_create("com.hackemist.SDWebImageCodersManager", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)dealloc {
SDDispatchQueueRelease(_mutableCodersAccessQueue);
}
#pragma mark - Coder IO operations
- (void)addCoder:(nonnull id<SDWebImageCoder>)coder {
if ([coder conformsToProtocol:@protocol(SDWebImageCoder)]) {
dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{
[self.mutableCoders addObject:coder];
});
}
}
- (void)removeCoder:(nonnull id<SDWebImageCoder>)coder {
dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{
[self.mutableCoders removeObject:coder];
});
}
- (NSArray<SDWebImageCoder> *)coders {
__block NSArray<SDWebImageCoder> *sortedCoders = nil;
dispatch_sync(self.mutableCodersAccessQueue, ^{
sortedCoders = (NSArray<SDWebImageCoder> *)[[[self.mutableCoders copy] reverseObjectEnumerator] allObjects];
});
return sortedCoders;
}
- (void)setCoders:(NSArray<SDWebImageCoder> *)coders {
dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{
self.mutableCoders = [coders mutableCopy];
});
}
#pragma mark - SDWebImageCoder
- (BOOL)canDecodeFromData:(NSData *)data {
for (id<SDWebImageCoder> coder in self.coders) {
if ([coder canDecodeFromData:data]) {
return YES;
}
}
return NO;
}
- (BOOL)canEncodeToFormat:(SDImageFormat)format {
for (id<SDWebImageCoder> coder in self.coders) {
if ([coder canEncodeToFormat:format]) {
return YES;
}
}
return NO;
}
- (UIImage *)decodedImageWithData:(NSData *)data {
if (!data) {
return nil;
}
for (id<SDWebImageCoder> coder in self.coders) {
if ([coder canDecodeFromData:data]) {
return [coder decodedImageWithData:data];
}
}
return nil;
}
- (UIImage *)decompressedImageWithImage:(UIImage *)image
data:(NSData *__autoreleasing _Nullable *)data
options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
if (!image) {
return nil;
}
for (id<SDWebImageCoder> coder in self.coders) {
if ([coder canDecodeFromData:*data]) {
return [coder decompressedImageWithImage:image data:data options:optionsDict];
}
}
return nil;
}
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
if (!image) {
return nil;
}
for (id<SDWebImageCoder> coder in self.coders) {
if ([coder canEncodeToFormat:format]) {
return [coder encodedDataWithImage:image format:format];
}
}
return nil;
}
@end

View File

@ -7,8 +7,7 @@
*/
#import "SDWebImageCompat.h"
#import "objc/runtime.h"
#import "UIImage+MultiFormat.h"
#if !__has_feature(objc_arc)
#error SDWebImage is ARC only. Either turn on ARC for the project or use -fobjc-arc flag
@ -30,16 +29,9 @@ inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullabl
}
UIImage *animatedImage = [UIImage animatedImageWithImages:scaledImages duration:image.duration];
#ifdef SD_WEBP
if (animatedImage) {
SEL sd_webpLoopCount = NSSelectorFromString(@"sd_webpLoopCount");
NSNumber *value = objc_getAssociatedObject(image, sd_webpLoopCount);
NSInteger loopCount = value.integerValue;
if (loopCount) {
objc_setAssociatedObject(animatedImage, sd_webpLoopCount, @(loopCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
animatedImage.sd_imageLoopCount = image.sd_imageLoopCount;
}
#endif
return animatedImage;
} else {
#if SD_WATCH

View File

@ -1,272 +0,0 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
* (c) james <https://github.com/mystcolor>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDWebImageDecoder.h"
@implementation UIImage (ForceDecode)
#if SD_UIKIT || SD_WATCH
static const size_t kBytesPerPixel = 4;
static const size_t kBitsPerComponent = 8;
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
if (![UIImage shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
size_t bytesPerRow = kBytesPerPixel * width;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
/*
* Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 60.
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
*/
static const CGFloat kDestImageSizeMB = 60.0f;
/*
* Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 20.
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
*/
static const CGFloat kSourceImageTileSizeMB = 20.0f;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
if (![UIImage shouldDecodeImage:image]) {
return image;
}
if (![UIImage shouldScaleDownImage:image]) {
return [UIImage decodedImageWithImage:image];
}
CGContextRef destContext;
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool {
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
float imageScale = kDestTotalPixels / sourceTotalPixels;
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
// current color space
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
size_t bytesPerRow = kBytesPerPixel * destResolution.width;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
destContext = CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (destContext == NULL) {
return image;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
return destImage;
}
}
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// do not decode animated images
if (image.images != nil) {
return NO;
}
CGImageRef imageRef = image.CGImage;
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
// do not decode images with alpha
if (anyAlpha) {
return NO;
}
return YES;
}
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
BOOL shouldScaleDown = YES;
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
float imageScale = kDestTotalPixels / sourceTotalPixels;
if (imageScale < 1) {
shouldScaleDown = YES;
} else {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace) {
colorspaceRef = CGColorSpaceCreateDeviceRGB();
CFAutorelease(colorspaceRef);
}
return colorspaceRef;
}
#elif SD_MAC
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
return image;
}
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
return image;
}
#endif
@end

View File

@ -111,13 +111,11 @@ typedef SDHTTPHeadersDictionary * _Nullable (^SDWebImageDownloaderHeadersFilterB
*/
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
/**
* The timeout value (in seconds) for the download operation. Default: 15.0.
*/
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
/**
* The configuration in use by the internal NSURLSession.
* Mutating this object directly has no effect.

View File

@ -7,11 +7,9 @@
*/
#import "SDWebImageDownloaderOperation.h"
#import "SDWebImageDecoder.h"
#import "UIImage+MultiFormat.h"
#import <ImageIO/ImageIO.h>
#import "SDWebImageManager.h"
#import "NSImage+WebCache.h"
#import "SDWebImageCodersManager.h"
NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
@ -46,15 +44,11 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
#endif
@property (strong, nonatomic, nullable) id<SDWebImageProgressiveCoder> progressiveCoder;
@end
@implementation SDWebImageDownloaderOperation {
size_t _width, _height;
#if SD_UIKIT || SD_WATCH
UIImageOrientation _orientation;
#endif
CGImageSourceRef _imageSource;
}
@implementation SDWebImageDownloaderOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
@ -82,10 +76,6 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
- (void)dealloc {
SDDispatchQueueRelease(_barrierQueue);
if (_imageSource) {
CFRelease(_imageSource);
_imageSource = NULL;
}
}
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
@ -329,9 +319,6 @@ didReceiveResponse:(NSURLResponse *)response
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
// The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
// Thanks to the author @Nyx0uf
// Get the image data
NSData *imageData = [self.imageData copy];
// Get the total bytes downloaded
@ -339,83 +326,25 @@ didReceiveResponse:(NSURLResponse *)response
// Get the finish status
BOOL finished = (totalSize >= self.expectedSize);
if (!_imageSource) {
_imageSource = CGImageSourceCreateIncremental(NULL);
}
// Update the data source, we must pass ALL the data, not just the new bytes
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)imageData, finished);
if (_width + _height == 0) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = -1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
CFRelease(properties);
// When we draw to Core Graphics, we lose orientation information,
// which means the image below born of initWithCGIImage will be
// oriented incorrectly sometimes. (Unlike the image born of initWithData
// in didCompleteWithError.) So save it here and pass it on later.
#if SD_UIKIT || SD_WATCH
_orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
#endif
if (!self.progressiveCoder) {
// We need to create a new instance for progressive decoding to avoid conflicts
for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] &&
[((id<SDWebImageProgressiveCoder>)coder) canIncrementallyDecodeFromData:imageData]) {
self.progressiveCoder = [[[coder class] alloc] init];
}
}
}
if (_width + _height > 0 && !finished) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished];
if (image) {
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
#if SD_UIKIT || SD_WATCH
// Workaround for iOS anamorphic image
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, _width, _height, 8, _width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = _width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
if (partialImageRef) {
#if SD_UIKIT || SD_WATCH
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:_orientation];
#elif SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
CGImageRelease(partialImageRef);
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
if (finished) {
if (_imageSource) {
CFRelease(_imageSource);
_imageSource = NULL;
}
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
@ -463,7 +392,6 @@ didReceiveResponse:(NSURLResponse *)response
*/
NSData *imageData = [self.imageData copy];
if (imageData) {
UIImage *image = [UIImage sd_imageWithData:imageData];
/** if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`,
* then we should check if the cached data is equal to image data
*/
@ -471,6 +399,7 @@ didReceiveResponse:(NSURLResponse *)response
// call completion block with nil
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
} else {
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
@ -489,14 +418,8 @@ didReceiveResponse:(NSURLResponse *)response
if (shouldDecode) {
if (self.shouldDecompressImages) {
if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
image = [UIImage decodedAndScaledDownImageWithImage:image];
imageData = UIImagePNGRepresentation(image);
#endif
} else {
image = [UIImage decodedImageWithImage:image];
}
BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages;
image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}
}
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
@ -544,32 +467,6 @@ didReceiveResponse:(NSURLResponse *)response
}
#pragma mark Helper methods
#if SD_UIKIT || SD_WATCH
+ (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value {
switch (value) {
case 1:
return UIImageOrientationUp;
case 3:
return UIImageOrientationDown;
case 8:
return UIImageOrientationLeft;
case 6:
return UIImageOrientationRight;
case 2:
return UIImageOrientationUpMirrored;
case 4:
return UIImageOrientationDownMirrored;
case 5:
return UIImageOrientationLeftMirrored;
case 7:
return UIImageOrientationRightMirrored;
default:
return UIImageOrientationUp;
}
}
#endif
- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
return SDScaledImageForKey(key, image);
}

View File

@ -0,0 +1,19 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <Foundation/Foundation.h>
#import "SDWebImageCoder.h"
/**
Built in coder that supports GIF
*/
@interface SDWebImageGIFCoder : NSObject <SDWebImageCoder>
+ (nonnull instancetype)sharedCoder;
@end

View File

@ -0,0 +1,117 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDWebImageGIFCoder.h"
#import "NSImage+WebCache.h"
#import <ImageIO/ImageIO.h>
#import "NSData+ImageContentType.h"
@implementation SDWebImageGIFCoder
+ (instancetype)sharedCoder {
static SDWebImageGIFCoder *coder;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
coder = [[SDWebImageGIFCoder alloc] init];
});
return coder;
}
#pragma mark - Decode
- (BOOL)canDecodeFromData:(nullable NSData *)data {
return ([NSData sd_imageFormatForImageData:data] == SDImageFormatGIF);
}
- (UIImage *)decodedImageWithData:(NSData *)data {
if (!data) {
return nil;
}
#if SD_MAC
return [[UIImage alloc] initWithData:data];
#else
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
UIImage *staticImage;
if (count <= 1) {
staticImage = [[UIImage alloc] initWithData:data];
} else {
// we will only retrieve the 1st frame. the full GIF support is available via the FLAnimatedImageView category.
// this here is only code to allow drawing animated images as static ones
#if SD_WATCH
CGFloat scale = 1;
scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
CGFloat scale = 1;
scale = [UIScreen mainScreen].scale;
#endif
CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCH
UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];
staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#endif
CGImageRelease(CGImage);
}
CFRelease(source);
return staticImage;
#endif
}
- (UIImage *)decompressedImageWithImage:(UIImage *)image
data:(NSData *__autoreleasing _Nullable *)data
options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
// GIF do not decompress
return image;
}
#pragma mark - Encode
- (BOOL)canEncodeToFormat:(SDImageFormat)format {
return (format == SDImageFormatGIF);
}
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
if (!image) {
return nil;
}
if (format != SDImageFormatGIF) {
return nil;
}
NSMutableData *imageData = [NSMutableData data];
CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:format];
// Create an image destination.
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
if (!imageDestination) {
// Handle failure.
return nil;
}
// Add your image to the destination.
CGImageDestinationAddImage(imageDestination, image.CGImage, nil);
// Finalize the destination.
if (CGImageDestinationFinalize(imageDestination) == NO) {
// Handle failure.
imageData = nil;
}
CFRelease(imageDestination);
return [imageData copy];
}
@end

View File

@ -0,0 +1,19 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <Foundation/Foundation.h>
#import "SDWebImageCoder.h"
/**
Built in coder that supports PNG, JPEG, TIFF, includes support for progressive decoding
*/
@interface SDWebImageImageIOCoder : NSObject <SDWebImageProgressiveCoder>
+ (nonnull instancetype)sharedCoder;
@end

View File

@ -0,0 +1,579 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDWebImageImageIOCoder.h"
#import "NSImage+WebCache.h"
#import <ImageIO/ImageIO.h>
#import "NSData+ImageContentType.h"
#if SD_UIKIT || SD_WATCH
static const size_t kBytesPerPixel = 4;
static const size_t kBitsPerComponent = 8;
/*
* Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 60.
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
*/
static const CGFloat kDestImageSizeMB = 60.0f;
/*
* Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 20.
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
*/
static const CGFloat kSourceImageTileSizeMB = 20.0f;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
#endif
@implementation SDWebImageImageIOCoder {
size_t _width, _height;
#if SD_UIKIT || SD_WATCH
UIImageOrientation _orientation;
#endif
CGImageSourceRef _imageSource;
}
- (void)dealloc {
if (_imageSource) {
CFRelease(_imageSource);
_imageSource = NULL;
}
}
+ (instancetype)sharedCoder {
static SDWebImageImageIOCoder *coder;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
coder = [[SDWebImageImageIOCoder alloc] init];
});
return coder;
}
#pragma mark - Decode
- (BOOL)canDecodeFromData:(nullable NSData *)data {
switch ([NSData sd_imageFormatForImageData:data]) {
// Do not support GIF and WebP decoding
case SDImageFormatGIF:
case SDImageFormatWebP:
return NO;
default:
return YES;
}
}
- (BOOL)canIncrementallyDecodeFromData:(NSData *)data {
switch ([NSData sd_imageFormatForImageData:data]) {
// Support static GIF progressive decoding
case SDImageFormatWebP:
return NO;
default:
return YES;
}
}
- (UIImage *)decodedImageWithData:(NSData *)data {
if (!data) {
return nil;
}
UIImage *image = [[UIImage alloc] initWithData:data];
#if SD_UIKIT || SD_WATCH
UIImageOrientation orientation = [[self class] sd_imageOrientationFromImageData:data];
if (orientation != UIImageOrientationUp) {
image = [UIImage imageWithCGImage:image.CGImage
scale:image.scale
orientation:orientation];
}
#endif
return image;
}
- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
if (!_imageSource) {
_imageSource = CGImageSourceCreateIncremental(NULL);
}
UIImage *image;
// The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
// Thanks to the author @Nyx0uf
// Update the data source, we must pass ALL the data, not just the new bytes
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
if (_width + _height == 0) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = 1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
CFRelease(properties);
// When we draw to Core Graphics, we lose orientation information,
// which means the image below born of initWithCGIImage will be
// oriented incorrectly sometimes. (Unlike the image born of initWithData
// in didCompleteWithError.) So save it here and pass it on later.
#if SD_UIKIT || SD_WATCH
_orientation = [[self class] sd_imageOrientationFromEXIFOrientation:orientationValue];
#endif
}
}
if (_width + _height > 0) {
// Create the image
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
#if SD_UIKIT || SD_WATCH
// Workaround for iOS anamorphic image
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = SDCGColorSpaceGetDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, _width, _height, 8, _width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = _width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
if (partialImageRef) {
#if SD_UIKIT || SD_WATCH
image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:_orientation];
#elif SD_MAC
image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
CGImageRelease(partialImageRef);
}
}
if (finished) {
if (_imageSource) {
CFRelease(_imageSource);
_imageSource = NULL;
}
}
return image;
}
- (UIImage *)decompressedImageWithImage:(UIImage *)image
data:(NSData *__autoreleasing _Nullable *)data
options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
#if SD_MAC
return image;
#endif
#if SD_UIKIT || SD_WATCH
BOOL shouldScaleDown = NO;
if (optionsDict != nil) {
NSNumber *scaleDownLargeImagesOption = nil;
if ([optionsDict[SDWebImageCoderScaleDownLargeImagesKey] isKindOfClass:[NSNumber class]]) {
scaleDownLargeImagesOption = (NSNumber *)optionsDict[SDWebImageCoderScaleDownLargeImagesKey];
}
if (scaleDownLargeImagesOption != nil) {
shouldScaleDown = [scaleDownLargeImagesOption boolValue];
}
}
if (!shouldScaleDown) {
return [self sd_decompressedImageWithImage:image];
} else {
return [self sd_decompressedAndScaledDownImageWithImage:image];
}
#endif
}
#if SD_UIKIT || SD_WATCH
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
if (![[self class] shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:imageRef];
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
size_t bytesPerRow = kBytesPerPixel * width;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
- (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image {
if (![[self class] shouldDecodeImage:image]) {
return image;
}
if (![[self class] shouldScaleDownImage:image]) {
return [self sd_decompressedImageWithImage:image];
}
CGContextRef destContext;
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool {
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
float imageScale = kDestTotalPixels / sourceTotalPixels;
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
// current color space
CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:sourceImageRef];
size_t bytesPerRow = kBytesPerPixel * destResolution.width;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
destContext = CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (destContext == NULL) {
return image;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
return destImage;
}
}
#endif
#pragma mark - Encode
- (BOOL)canEncodeToFormat:(SDImageFormat)format {
switch (format) {
// Do not support GIF and WebP encoding
case SDImageFormatGIF:
case SDImageFormatWebP:
return NO;
default:
return YES;
}
}
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
if (!image) {
return nil;
}
if (format == SDImageFormatUndefined) {
BOOL hasAlpha = SDCGImageRefContainsAlpha(image.CGImage);
if (hasAlpha) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
}
NSMutableData *imageData = [NSMutableData data];
CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:format];
// Create an image destination.
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
if (!imageDestination) {
// Handle failure.
return nil;
}
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
#if SD_UIKIT
NSInteger exifOrientation = [[self class] sd_exifOrientationFromImageOrientation:image.imageOrientation];
[properties setValue:@(exifOrientation) forKey:(__bridge_transfer NSString *)kCGImagePropertyOrientation];
#endif
// Add your image to the destination.
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
// Finalize the destination.
if (CGImageDestinationFinalize(imageDestination) == NO) {
// Handle failure.
imageData = nil;
}
CFRelease(imageDestination);
return [imageData copy];
}
#pragma mark - Helper
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// do not decode animated images
if (image.images != nil) {
return NO;
}
CGImageRef imageRef = image.CGImage;
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// do not decode images with alpha
if (hasAlpha) {
return NO;
}
return YES;
}
#if SD_UIKIT || SD_WATCH
#pragma mark EXIF orientation tag converter
+ (UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData {
UIImageOrientation result = UIImageOrientationUp;
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (imageSource) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
CFTypeRef val;
NSInteger exifOrientation;
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) {
CFNumberGetValue(val, kCFNumberNSIntegerType, &exifOrientation);
result = [self sd_imageOrientationFromEXIFOrientation:exifOrientation];
} // else - if it's not set it remains at up
CFRelease((CFTypeRef) properties);
} else {
//NSLog(@"NO PROPERTIES, FAIL");
}
CFRelease(imageSource);
}
return result;
}
// Convert an EXIF image orientation to an iOS one.
+ (UIImageOrientation)sd_imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation {
UIImageOrientation imageOrientation = UIImageOrientationUp;
switch (exifOrientation) {
case kCGImagePropertyOrientationUp:
imageOrientation = UIImageOrientationUp;
break;
case kCGImagePropertyOrientationDown:
imageOrientation = UIImageOrientationDown;
break;
case kCGImagePropertyOrientationLeft:
imageOrientation = UIImageOrientationLeft;
break;
case kCGImagePropertyOrientationRight:
imageOrientation = UIImageOrientationRight;
break;
case kCGImagePropertyOrientationUpMirrored:
imageOrientation = UIImageOrientationUpMirrored;
break;
case kCGImagePropertyOrientationDownMirrored:
imageOrientation = UIImageOrientationDownMirrored;
break;
case kCGImagePropertyOrientationLeftMirrored:
imageOrientation = UIImageOrientationLeftMirrored;
break;
case kCGImagePropertyOrientationRightMirrored:
imageOrientation = UIImageOrientationRightMirrored;
break;
default:
break;
}
return imageOrientation;
}
// Convert an iOS orientation to an EXIF image orientation.
+ (NSInteger)sd_exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation {
NSInteger exifOrientation = kCGImagePropertyOrientationUp;
switch (imageOrientation) {
case UIImageOrientationUp:
exifOrientation = kCGImagePropertyOrientationUp;
break;
case UIImageOrientationDown:
exifOrientation = kCGImagePropertyOrientationDown;
break;
case UIImageOrientationLeft:
exifOrientation = kCGImagePropertyOrientationLeft;
break;
case UIImageOrientationRight:
exifOrientation = kCGImagePropertyOrientationRight;
break;
case UIImageOrientationUpMirrored:
exifOrientation = kCGImagePropertyOrientationUpMirrored;
break;
case UIImageOrientationDownMirrored:
exifOrientation = kCGImagePropertyOrientationDownMirrored;
break;
case UIImageOrientationLeftMirrored:
exifOrientation = kCGImagePropertyOrientationLeftMirrored;
break;
case UIImageOrientationRightMirrored:
exifOrientation = kCGImagePropertyOrientationRightMirrored;
break;
default:
break;
}
return exifOrientation;
}
#endif
#if SD_UIKIT || SD_WATCH
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
BOOL shouldScaleDown = YES;
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
float imageScale = kDestTotalPixels / sourceTotalPixels;
if (imageScale < 1) {
shouldScaleDown = YES;
} else {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace) {
colorspaceRef = SDCGColorSpaceGetDeviceRGB();
}
return colorspaceRef;
}
#endif
@end

View File

@ -7,8 +7,8 @@
*/
#import "SDWebImageManager.h"
#import <objc/message.h>
#import "NSImage+WebCache.h"
#import <objc/message.h>
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

View File

@ -0,0 +1,23 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#ifdef SD_WEBP
#import <Foundation/Foundation.h>
#import "SDWebImageCoder.h"
/**
Built in coder that supports WebP and animated WebP
*/
@interface SDWebImageWebPCoder : NSObject <SDWebImageProgressiveCoder>
+ (nonnull instancetype)sharedCoder;
@end
#endif

View File

@ -0,0 +1,561 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#ifdef SD_WEBP
#import "SDWebImageWebPCoder.h"
#import "NSImage+WebCache.h"
#import "UIImage+MultiFormat.h"
#import "NSData+ImageContentType.h"
#import <ImageIO/ImageIO.h>
#if __has_include(<webp/decode.h>) && __has_include(<webp/encode.h>) && __has_include(<webp/demux.h>) && __has_include(<webp/mux.h>)
#import <webp/decode.h>
#import <webp/encode.h>
#import <webp/demux.h>
#import <webp/mux.h>
#else
#import "webp/decode.h"
#import "webp/encode.h"
#import "webp/demux.h"
#import "webp/mux.h"
#endif
@implementation SDWebImageWebPCoder {
WebPIDecoder *_idec;
}
- (void)dealloc {
if (_idec) {
WebPIDelete(_idec);
_idec = NULL;
}
}
+ (instancetype)sharedCoder {
static SDWebImageWebPCoder *coder;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
coder = [[SDWebImageWebPCoder alloc] init];
});
return coder;
}
#pragma mark - Decode
- (BOOL)canDecodeFromData:(nullable NSData *)data {
return ([NSData sd_imageFormatForImageData:data] == SDImageFormatWebP);
}
- (BOOL)canIncrementallyDecodeFromData:(NSData *)data {
return ([NSData sd_imageFormatForImageData:data] == SDImageFormatWebP);
}
- (UIImage *)decodedImageWithData:(NSData *)data {
if (!data) {
return nil;
}
WebPData webpData;
WebPDataInit(&webpData);
webpData.bytes = data.bytes;
webpData.size = data.length;
WebPDemuxer *demuxer = WebPDemux(&webpData);
if (!demuxer) {
return nil;
}
uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS);
#if SD_UIKIT || SD_WATCH
int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT);
int frameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT);
#endif
int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH);
int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
CGBitmapInfo bitmapInfo;
if (!(flags & ALPHA_FLAG)) {
bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
} else {
bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
}
CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!canvas) {
WebPDemuxDelete(demuxer);
return nil;
}
if (!(flags & ANIMATION_FLAG)) {
// for static single webp image
UIImage *staticImage = [self sd_rawWebpImageWithData:webpData];
if (staticImage) {
// draw on CGBitmapContext can reduce memory usage
CGImageRef imageRef = staticImage.CGImage;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
CGContextDrawImage(canvas, CGRectMake(0, 0, width, height), imageRef);
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
#if SD_UIKIT || SD_WATCH
staticImage = [[UIImage alloc] initWithCGImage:newImageRef];
#else
staticImage = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
#endif
CGImageRelease(newImageRef);
}
WebPDemuxDelete(demuxer);
CGContextRelease(canvas);
return staticImage;
}
// for animated webp image
WebPIterator iter;
if (!WebPDemuxGetFrame(demuxer, 1, &iter)) {
WebPDemuxReleaseIterator(&iter);
WebPDemuxDelete(demuxer);
CGContextRelease(canvas);
return nil;
}
NSMutableArray<UIImage *> *images = [NSMutableArray array];
#if SD_UIKIT || SD_WATCH
NSTimeInterval totalDuration = 0;
int durations[frameCount];
#endif
do {
@autoreleasepool {
UIImage *image;
if (iter.blend_method == WEBP_MUX_BLEND) {
image = [self sd_blendWebpImageWithCanvas:canvas iterator:iter];
} else {
image = [self sd_nonblendWebpImageWithCanvas:canvas iterator:iter];
}
if (!image) {
continue;
}
[images addObject:image];
#if SD_MAC
break;
#else
int duration = iter.duration;
if (duration <= 10) {
// WebP standard says 0 duration is used for canvas updating but not showing image, but actually Chrome and other implementations set it to 100ms if duration is lower or equal than 10ms
// Some animated WebP images also created without duration, we should keep compatibility
duration = 100;
}
totalDuration += duration;
size_t count = images.count;
durations[count - 1] = duration;
#endif
}
} while (WebPDemuxNextFrame(&iter));
WebPDemuxReleaseIterator(&iter);
WebPDemuxDelete(demuxer);
CGContextRelease(canvas);
UIImage *finalImage = nil;
#if SD_UIKIT || SD_WATCH
NSArray<UIImage *> *animatedImages = [self sd_animatedImagesWithImages:images durations:durations totalDuration:totalDuration];
finalImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.0];
if (finalImage) {
finalImage.sd_imageLoopCount = loopCount;
}
#elif SD_MAC
finalImage = images.firstObject;
#endif
return finalImage;
}
- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
if (!_idec) {
// Progressive images need transparent, so always use premultiplied RGBA
_idec = WebPINewRGB(MODE_rgbA, NULL, 0, 0);
if (!_idec) {
return nil;
}
}
UIImage *image;
VP8StatusCode status = WebPIUpdate(_idec, data.bytes, data.length);
if (status != VP8_STATUS_OK && status != VP8_STATUS_SUSPENDED) {
return nil;
}
int width;
int height;
uint8_t *rgba = WebPIDecGetRGB(_idec, NULL, (int *)&width, (int *)&height, NULL);
if (width + height > 0) {
// Construct a UIImage from the decoded RGBA value array
CGDataProviderRef provider =
CGDataProviderCreateWithData(NULL, rgba, 0, NULL);
CGColorSpaceRef colorSpaceRef = SDCGColorSpaceGetDeviceRGB();
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
size_t components = 4;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
CGDataProviderRelease(provider);
if (!imageRef) {
return nil;
}
CGContextRef canvas = CGBitmapContextCreate(NULL, width, height, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!canvas) {
CGImageRelease(imageRef);
return nil;
}
CGContextDrawImage(canvas, CGRectMake(0, 0, width, height), imageRef);
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
CGImageRelease(imageRef);
if (!newImageRef) {
CGContextRelease(canvas);
return nil;
}
#if SD_UIKIT || SD_WATCH
image = [[UIImage alloc] initWithCGImage:newImageRef];
#else
image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
#endif
CGImageRelease(newImageRef);
CGContextRelease(canvas);
}
if (finished) {
if (_idec) {
WebPIDelete(_idec);
_idec = NULL;
}
}
return image;
}
- (UIImage *)decompressedImageWithImage:(UIImage *)image
data:(NSData *__autoreleasing _Nullable *)data
options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
// WebP do not decompress
return image;
}
- (nullable UIImage *)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter {
UIImage *image = [self sd_rawWebpImageWithData:iter.fragment];
if (!image) {
return nil;
}
size_t canvasWidth = CGBitmapContextGetWidth(canvas);
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
CGSize size = CGSizeMake(canvasWidth, canvasHeight);
CGFloat tmpX = iter.x_offset;
CGFloat tmpY = size.height - iter.height - iter.y_offset;
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
CGContextDrawImage(canvas, imageRect, image.CGImage);
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
#if SD_UIKIT || SD_WATCH
image = [UIImage imageWithCGImage:newImageRef];
#elif SD_MAC
image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
#endif
CGImageRelease(newImageRef);
if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
CGContextClearRect(canvas, imageRect);
}
return image;
}
- (nullable UIImage *)sd_nonblendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter {
UIImage *image = [self sd_rawWebpImageWithData:iter.fragment];
if (!image) {
return nil;
}
size_t canvasWidth = CGBitmapContextGetWidth(canvas);
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
CGSize size = CGSizeMake(canvasWidth, canvasHeight);
CGFloat tmpX = iter.x_offset;
CGFloat tmpY = size.height - iter.height - iter.y_offset;
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
CGContextClearRect(canvas, imageRect);
CGContextDrawImage(canvas, imageRect, image.CGImage);
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
#if SD_UIKIT || SD_WATCH
image = [UIImage imageWithCGImage:newImageRef];
#elif SD_MAC
image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
#endif
CGImageRelease(newImageRef);
if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
CGContextClearRect(canvas, imageRect);
}
return image;
}
- (nullable UIImage *)sd_rawWebpImageWithData:(WebPData)webpData {
WebPDecoderConfig config;
if (!WebPInitDecoderConfig(&config)) {
return nil;
}
if (WebPGetFeatures(webpData.bytes, webpData.size, &config.input) != VP8_STATUS_OK) {
return nil;
}
config.output.colorspace = config.input.has_alpha ? MODE_rgbA : MODE_RGB;
config.options.use_threads = 1;
// Decode the WebP image data into a RGBA value array
if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) {
return nil;
}
int width = config.input.width;
int height = config.input.height;
if (config.options.use_scaling) {
width = config.options.scaled_width;
height = config.options.scaled_height;
}
// Construct a UIImage from the decoded RGBA value array
CGDataProviderRef provider =
CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData);
CGColorSpaceRef colorSpaceRef = SDCGColorSpaceGetDeviceRGB();
CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
size_t components = config.input.has_alpha ? 4 : 3;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
CGDataProviderRelease(provider);
#if SD_UIKIT || SD_WATCH
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef size:NSZeroSize];
#endif
CGImageRelease(imageRef);
return image;
}
#pragma mark - Encode
- (BOOL)canEncodeToFormat:(SDImageFormat)format {
return (format == SDImageFormatWebP);
}
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
if (!image) {
return nil;
}
NSData *data;
#if SD_UIKIT || SD_WATCH
if (!image.images) {
#endif
// for static single webp image
data = [self sd_encodedWebpDataWithImage:image];
#if SD_UIKIT || SD_WATCH
} else {
// for animated webp image
int durations[image.images.count];
NSArray<UIImage *> *images = [self sd_imagesFromAnimatedImages:image.images totalDuration:image.duration durations:durations];
WebPMux *mux = WebPMuxNew();
if (!mux) {
return nil;
}
for (NSUInteger i = 0; i < images.count; i++) {
NSData *webpData = [self sd_encodedWebpDataWithImage:images[i]];
int duration = durations[i];
WebPMuxFrameInfo frame = { .bitstream.bytes = webpData.bytes,
.bitstream.size = webpData.length,
.duration = duration,
.id = WEBP_CHUNK_ANMF,
.dispose_method = WEBP_MUX_DISPOSE_BACKGROUND, // each frame will clear canvas
.blend_method = WEBP_MUX_NO_BLEND
};
if (WebPMuxPushFrame(mux, &frame, 0) != WEBP_MUX_OK) {
WebPMuxDelete(mux);
return nil;
}
}
int loopCount = (int)image.sd_imageLoopCount;
WebPMuxAnimParams params = { .bgcolor = 0,
.loop_count = loopCount
};
if (WebPMuxSetAnimationParams(mux, &params) != WEBP_MUX_OK) {
WebPMuxDelete(mux);
return nil;
}
WebPData outputData;
WebPMuxError error = WebPMuxAssemble(mux, &outputData);
WebPMuxDelete(mux);
if (error != WEBP_MUX_OK) {
return nil;
}
data = [NSData dataWithBytes:outputData.bytes length:outputData.size];
WebPDataClear(&outputData);
}
#endif
return data;
}
- (nullable NSData *)sd_encodedWebpDataWithImage:(nullable UIImage *)image {
if (!image) {
return nil;
}
NSData *webpData;
CGImageRef imageRef = image.CGImage;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == 0 || width > WEBP_MAX_DIMENSION) {
return nil;
}
if (height == 0 || height > WEBP_MAX_DIMENSION) {
return nil;
}
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
CFDataRef dataRef = CGDataProviderCopyData(dataProvider);
uint8_t *rgba = (uint8_t *)CFDataGetBytePtr(dataRef);
uint8_t *data = NULL;
float quality = 100.0;
size_t size = WebPEncodeRGBA(rgba, (int)width, (int)height, (int)bytesPerRow, quality, &data);
CFRelease(dataRef);
rgba = NULL;
if (size) {
// success
webpData = [NSData dataWithBytes:data length:size];
}
if (data) {
WebPFree(data);
}
return webpData;
}
- (NSArray<UIImage *> *)sd_animatedImagesWithImages:(NSArray<UIImage *> *)images durations:(int const * const)durations totalDuration:(NSTimeInterval)totalDuration
{
// [UIImage animatedImageWithImages:duration:] only use the average duration for per frame
// divide the total duration to implement per frame duration for animated WebP
NSUInteger count = images.count;
if (!count) {
return nil;
}
if (count == 1) {
return images;
}
int const gcd = gcdArray(count, durations);
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:count];
[images enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
int duration = durations[idx];
int repeatCount;
if (gcd) {
repeatCount = duration / gcd;
} else {
repeatCount = 1;
}
for (int i = 0; i < repeatCount; ++i) {
[animatedImages addObject:image];
}
}];
return animatedImages;
}
- (NSArray<UIImage *> *)sd_imagesFromAnimatedImages:(NSArray<UIImage *> *)animatedImages totalDuration:(NSTimeInterval)totalDuration durations:(int * const)durations {
// This is the reversed procedure to sd_animatedImagesWithImages:durations:totalDuration
// To avoid precision loss, convert from s to ms during this method
NSUInteger count = animatedImages.count;
if (!count) {
return nil;
}
if (count == 1) {
durations[0] = totalDuration * 1000; // s -> ms
}
int const duration = totalDuration * 1000 / count;
__block NSUInteger index = 0;
__block int repeatCount = 1;
__block UIImage *previousImage = animatedImages.firstObject;
NSMutableArray<UIImage *> *images = [NSMutableArray array];
[animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
// ignore first
if (idx == 0) {
return;
}
if ([image isEqual:previousImage]) {
repeatCount++;
} else {
[images addObject:previousImage];
durations[index] = duration * repeatCount;
repeatCount = 1;
index++;
}
previousImage = image;
// last one
if (idx == count - 1) {
[images addObject:previousImage];
durations[index] = duration * repeatCount;
}
}];
return images;
}
static void FreeImageData(void *info, const void *data, size_t size) {
free((void *)data);
}
static int gcdArray(size_t const count, int const * const values) {
int result = values[0];
for (size_t i = 1; i < count; ++i) {
result = gcd(values[i], result);
}
return result;
}
static int gcd(int a,int b) {
int c;
while (a != 0) {
c = a;
a = b % a;
b = c;
}
return b;
}
@end
#endif

View File

@ -1,13 +1,11 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
* (c) james <https://github.com/mystcolor>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <Foundation/Foundation.h>
#import "SDWebImageCompat.h"
@interface UIImage (ForceDecode)

View File

@ -0,0 +1,30 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "UIImage+ForceDecode.h"
#import "SDWebImageCodersManager.h"
@implementation UIImage (ForceDecode)
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
if (!image) {
return nil;
}
NSData *tempData;
return [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&tempData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
}
+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image {
if (!image) {
return nil;
}
NSData *tempData;
return [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&tempData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(YES)}];
}
@end

View File

@ -8,8 +8,7 @@
*/
#import "UIImage+GIF.h"
#import <ImageIO/ImageIO.h>
#import "objc/runtime.h"
#import "SDWebImageGIFCoder.h"
#import "NSImage+WebCache.h"
@implementation UIImage (GIF)
@ -18,42 +17,7 @@
if (!data) {
return nil;
}
#if SD_MAC
return [[UIImage alloc] initWithData:data];
#else
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
UIImage *staticImage;
if (count <= 1) {
staticImage = [[UIImage alloc] initWithData:data];
} else {
// we will only retrieve the 1st frame. the full GIF support is available via the FLAnimatedImageView category.
// this here is only code to allow drawing animated images as static ones
#if SD_WATCH
CGFloat scale = 1;
scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
CGFloat scale = 1;
scale = [UIScreen mainScreen].scale;
#endif
CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCH
UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];
staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#endif
CGImageRelease(CGImage);
}
CFRelease(source);
return staticImage;
#endif
return [[SDWebImageGIFCoder sharedCoder] decodedImageWithData:data];
}
- (BOOL)isGIF {

View File

@ -11,6 +11,15 @@
@interface UIImage (MultiFormat)
/**
* For static image format, this value is always 0.
* For animated image format, 0 means infinite looping.
* Note that because of the limitations of categories this property can get out of sync
* if you create another instance with CGImage or other methods.
*/
@property (nonatomic, assign) NSUInteger sd_imageLoopCount;
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data;
- (nullable NSData *)sd_imageData;
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat;

View File

@ -7,115 +7,30 @@
*/
#import "UIImage+MultiFormat.h"
#import "UIImage+GIF.h"
#import "NSData+ImageContentType.h"
#import <ImageIO/ImageIO.h>
#ifdef SD_WEBP
#import "UIImage+WebP.h"
#endif
#import "objc/runtime.h"
#import "SDWebImageCodersManager.h"
@implementation UIImage (MultiFormat)
- (NSUInteger)sd_imageLoopCount {
NSUInteger imageLoopCount = 0;
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_imageLoopCount));
if ([value isKindOfClass:[NSNumber class]]) {
imageLoopCount = value.unsignedIntegerValue;
}
return imageLoopCount;
}
- (void)setSd_imageLoopCount:(NSUInteger)sd_imageLoopCount {
NSNumber *value = @(sd_imageLoopCount);
objc_setAssociatedObject(self, @selector(sd_imageLoopCount), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
if (!data) {
return nil;
}
UIImage *image;
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
if (imageFormat == SDImageFormatGIF) {
image = [UIImage sd_animatedGIFWithData:data];
}
#ifdef SD_WEBP
else if (imageFormat == SDImageFormatWebP)
{
image = [UIImage sd_imageWithWebPData:data];
}
#endif
else {
image = [[UIImage alloc] initWithData:data];
#if SD_UIKIT || SD_WATCH
UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
if (orientation != UIImageOrientationUp) {
image = [UIImage imageWithCGImage:image.CGImage
scale:image.scale
orientation:orientation];
}
#endif
}
return image;
return [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
}
#if SD_UIKIT || SD_WATCH
+(UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData {
UIImageOrientation result = UIImageOrientationUp;
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (imageSource) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
CFTypeRef val;
int exifOrientation;
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) {
CFNumberGetValue(val, kCFNumberIntType, &exifOrientation);
result = [self sd_exifOrientationToiOSOrientation:exifOrientation];
} // else - if it's not set it remains at up
CFRelease((CFTypeRef) properties);
} else {
//NSLog(@"NO PROPERTIES, FAIL");
}
CFRelease(imageSource);
}
return result;
}
#pragma mark EXIF orientation tag converter
// Convert an EXIF image orientation to an iOS one.
// reference see here: http://sylvana.net/jpegcrop/exif_orientation.html
+ (UIImageOrientation) sd_exifOrientationToiOSOrientation:(int)exifOrientation {
UIImageOrientation orientation = UIImageOrientationUp;
switch (exifOrientation) {
case 1:
orientation = UIImageOrientationUp;
break;
case 3:
orientation = UIImageOrientationDown;
break;
case 8:
orientation = UIImageOrientationLeft;
break;
case 6:
orientation = UIImageOrientationRight;
break;
case 2:
orientation = UIImageOrientationUpMirrored;
break;
case 4:
orientation = UIImageOrientationDownMirrored;
break;
case 5:
orientation = UIImageOrientationLeftMirrored;
break;
case 7:
orientation = UIImageOrientationRightMirrored;
break;
default:
break;
}
return orientation;
}
#endif
- (nullable NSData *)sd_imageData {
return [self sd_imageDataAsFormat:SDImageFormatUndefined];
}
@ -123,36 +38,7 @@
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
NSData *imageData = nil;
if (self) {
#if SD_UIKIT || SD_WATCH
int alphaInfo = CGImageGetAlphaInfo(self.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL usePNG = hasAlpha;
// the imageFormat param has priority here. But if the format is undefined, we relly on the alpha channel
if (imageFormat != SDImageFormatUndefined) {
usePNG = (imageFormat == SDImageFormatPNG);
}
if (usePNG) {
imageData = UIImagePNGRepresentation(self);
} else {
imageData = UIImageJPEGRepresentation(self, (CGFloat)1.0);
}
#else
NSBitmapImageFileType imageFileType = NSJPEGFileType;
if (imageFormat == SDImageFormatGIF) {
imageFileType = NSGIFFileType;
} else if (imageFormat == SDImageFormatPNG) {
imageFileType = NSPNGFileType;
}
imageData = [NSBitmapImageRep representationOfImageRepsInArray:self.representations
usingType:imageFileType
properties:@{}];
#endif
imageData = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:self format:imageFormat];
}
return imageData;
}

View File

@ -19,8 +19,9 @@
* Note that because of the limitations of categories this property can get out of sync
* if you create another instance with CGImage or other methods.
* @return WebP image loop count
* @deprecated use `sd_imageLoopCount` instead.
*/
- (NSInteger)sd_webpLoopCount;
- (NSInteger)sd_webpLoopCount __deprecated_msg("Method deprecated. Use `sd_imageLoopCount` in `UIImage+MultiFormat.h`");
+ (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data;

View File

@ -9,318 +9,20 @@
#ifdef SD_WEBP
#import "UIImage+WebP.h"
#import "NSImage+WebCache.h"
#if __has_include(<webp/decode.h>) && __has_include(<webp/mux_types.h>) && __has_include(<webp/demux.h>)
#import <webp/decode.h>
#import <webp/mux_types.h>
#import <webp/demux.h>
#else
#import "webp/decode.h"
#import "webp/mux_types.h"
#import "webp/demux.h"
#endif
#import "objc/runtime.h"
// Callback for CGDataProviderRelease
static void FreeImageData(void *info, const void *data, size_t size) {
free((void *)data);
}
#import "SDWebImageWebPCoder.h"
#import "UIImage+MultiFormat.h"
@implementation UIImage (WebP)
- (NSInteger)sd_webpLoopCount
{
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_webpLoopCount));
return value.integerValue;
- (NSInteger)sd_webpLoopCount {
return self.sd_imageLoopCount;
}
+ (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data {
if (!data) {
return nil;
}
WebPData webpData;
WebPDataInit(&webpData);
webpData.bytes = data.bytes;
webpData.size = data.length;
WebPDemuxer *demuxer = WebPDemux(&webpData);
if (!demuxer) {
return nil;
}
uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS);
#if SD_UIKIT || SD_WATCH
int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT);
int frameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT);
#endif
int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH);
int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
CGBitmapInfo bitmapInfo;
if (!(flags & ALPHA_FLAG)) {
bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
} else {
bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
}
CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!canvas) {
WebPDemuxDelete(demuxer);
return nil;
}
if (!(flags & ANIMATION_FLAG)) {
// for static single webp image
UIImage *staticImage = [self sd_rawWebpImageWithData:webpData];
if (staticImage) {
// draw on CGBitmapContext can reduce memory usage
CGImageRef imageRef = staticImage.CGImage;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
CGContextDrawImage(canvas, CGRectMake(0, 0, width, height), imageRef);
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
#if SD_UIKIT || SD_WATCH
staticImage = [[UIImage alloc] initWithCGImage:newImageRef];
#else
staticImage = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
#endif
CGImageRelease(newImageRef);
}
WebPDemuxDelete(demuxer);
CGContextRelease(canvas);
return staticImage;
}
// for animated webp image
WebPIterator iter;
if (!WebPDemuxGetFrame(demuxer, 1, &iter)) {
WebPDemuxReleaseIterator(&iter);
WebPDemuxDelete(demuxer);
CGContextRelease(canvas);
return nil;
}
NSMutableArray<UIImage *> *images = [NSMutableArray array];
#if SD_UIKIT || SD_WATCH
NSTimeInterval totalDuration = 0;
int durations[frameCount];
#endif
do {
@autoreleasepool {
UIImage *image;
if (iter.blend_method == WEBP_MUX_BLEND) {
image = [self sd_blendWebpImageWithCanvas:canvas iterator:iter];
} else {
image = [self sd_nonblendWebpImageWithCanvas:canvas iterator:iter];
}
if (!image) {
continue;
}
[images addObject:image];
#if SD_MAC
break;
#else
int duration = iter.duration;
if (duration <= 10) {
// WebP standard says 0 duration is used for canvas updating but not showing image, but actually Chrome and other implementations set it to 100ms if duration is lower or equal than 10ms
// Some animated WebP images also created without duration, we should keep compatibility
duration = 100;
}
totalDuration += duration;
size_t count = images.count;
durations[count - 1] = duration;
#endif
}
} while (WebPDemuxNextFrame(&iter));
WebPDemuxReleaseIterator(&iter);
WebPDemuxDelete(demuxer);
CGContextRelease(canvas);
UIImage *finalImage = nil;
#if SD_UIKIT || SD_WATCH
NSArray<UIImage *> *animatedImages = [self sd_animatedImagesWithImages:images durations:durations totalDuration:totalDuration];
finalImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.0];
if (finalImage) {
objc_setAssociatedObject(finalImage, @selector(sd_webpLoopCount), @(loopCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#elif SD_MAC
finalImage = images.firstObject;
#endif
return finalImage;
}
+ (nullable UIImage *)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter {
UIImage *image = [self sd_rawWebpImageWithData:iter.fragment];
if (!image) {
return nil;
}
size_t canvasWidth = CGBitmapContextGetWidth(canvas);
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
CGSize size = CGSizeMake(canvasWidth, canvasHeight);
CGFloat tmpX = iter.x_offset;
CGFloat tmpY = size.height - iter.height - iter.y_offset;
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
CGContextDrawImage(canvas, imageRect, image.CGImage);
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
#if SD_UIKIT || SD_WATCH
image = [UIImage imageWithCGImage:newImageRef];
#elif SD_MAC
image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
#endif
CGImageRelease(newImageRef);
if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
CGContextClearRect(canvas, imageRect);
}
return image;
}
+ (nullable UIImage *)sd_nonblendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter {
UIImage *image = [self sd_rawWebpImageWithData:iter.fragment];
if (!image) {
return nil;
}
size_t canvasWidth = CGBitmapContextGetWidth(canvas);
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
CGSize size = CGSizeMake(canvasWidth, canvasHeight);
CGFloat tmpX = iter.x_offset;
CGFloat tmpY = size.height - iter.height - iter.y_offset;
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
CGContextClearRect(canvas, imageRect);
CGContextDrawImage(canvas, imageRect, image.CGImage);
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
#if SD_UIKIT || SD_WATCH
image = [UIImage imageWithCGImage:newImageRef];
#elif SD_MAC
image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
#endif
CGImageRelease(newImageRef);
if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
CGContextClearRect(canvas, imageRect);
}
return image;
}
+ (nullable UIImage *)sd_rawWebpImageWithData:(WebPData)webpData {
WebPDecoderConfig config;
if (!WebPInitDecoderConfig(&config)) {
return nil;
}
if (WebPGetFeatures(webpData.bytes, webpData.size, &config.input) != VP8_STATUS_OK) {
return nil;
}
config.output.colorspace = config.input.has_alpha ? MODE_rgbA : MODE_RGB;
config.options.use_threads = 1;
// Decode the WebP image data into a RGBA value array
if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) {
return nil;
}
int width = config.input.width;
int height = config.input.height;
if (config.options.use_scaling) {
width = config.options.scaled_width;
height = config.options.scaled_height;
}
// Construct a UIImage from the decoded RGBA value array
CGDataProviderRef provider =
CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData);
CGColorSpaceRef colorSpaceRef = SDCGColorSpaceGetDeviceRGB();
CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
size_t components = config.input.has_alpha ? 4 : 3;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
CGDataProviderRelease(provider);
#if SD_UIKIT || SD_WATCH
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef size:NSZeroSize];
#endif
CGImageRelease(imageRef);
return image;
}
+ (NSArray<UIImage *> *)sd_animatedImagesWithImages:(NSArray<UIImage *> *)images durations:(int const * const)durations totalDuration:(NSTimeInterval)totalDuration
{
// [UIImage animatedImageWithImages:duration:] only use the average duration for per frame
// divide the total duration to implement per frame duration for animated WebP
NSUInteger count = images.count;
if (!count) {
return nil;
}
if (count == 1) {
return images;
}
int const gcd = gcdArray(count, durations);
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:count];
[images enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
int duration = durations[idx];
int repeatCount;
if (gcd) {
repeatCount = duration / gcd;
} else {
repeatCount = 1;
}
for (int i = 0; i < repeatCount; ++i) {
[animatedImages addObject:image];
}
}];
return animatedImages;
}
static CGColorSpaceRef SDCGColorSpaceGetDeviceRGB() {
static CGColorSpaceRef space;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
space = CGColorSpaceCreateDeviceRGB();
});
return space;
}
static int gcdArray(size_t const count, int const * const values) {
int result = values[0];
for (size_t i = 1; i < count; ++i) {
result = gcd(values[i], result);
}
return result;
}
static int gcd(int a,int b) {
int c;
while (a != 0) {
c = a;
a = b % a;
b = c;
}
return b;
return [[SDWebImageWebPCoder sharedCoder] decodedImageWithData:data];
}
@end

View File

@ -9,6 +9,9 @@
/* Begin PBXBuildFile section */
1E3C51E919B46E370092B5E6 /* SDWebImageDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E3C51E819B46E370092B5E6 /* SDWebImageDownloaderTests.m */; };
2D7AF0601F329763000083C2 /* SDTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D7AF05F1F329763000083C2 /* SDTestCase.m */; };
321259EC1F39E3240096FE0E /* TestImageStatic.webp in Resources */ = {isa = PBXBuildFile; fileRef = 321259EB1F39E3240096FE0E /* TestImageStatic.webp */; };
321259EE1F39E4110096FE0E /* TestImageAnimated.webp in Resources */ = {isa = PBXBuildFile; fileRef = 321259ED1F39E4110096FE0E /* TestImageAnimated.webp */; };
32E6F0321F3A1B4700A945E6 /* SDWebImageTestDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 32E6F0311F3A1B4700A945E6 /* SDWebImageTestDecoder.m */; };
433BBBB51D7EF5C00086B6E9 /* SDWebImageDecoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 433BBBB41D7EF5C00086B6E9 /* SDWebImageDecoderTests.m */; };
433BBBB71D7EF8200086B6E9 /* TestImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 433BBBB61D7EF8200086B6E9 /* TestImage.gif */; };
433BBBB91D7EF8260086B6E9 /* TestImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 433BBBB81D7EF8260086B6E9 /* TestImage.png */; };
@ -24,7 +27,6 @@
DA248D61195472AA00390AB0 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DA248D5F195472AA00390AB0 /* InfoPlist.strings */; };
DA248D69195475D800390AB0 /* SDImageCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DA248D68195475D800390AB0 /* SDImageCacheTests.m */; };
DA248D6B195476AC00390AB0 /* SDWebImageManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DA248D6A195476AC00390AB0 /* SDWebImageManagerTests.m */; };
DA91BEBC19795BC9006F2536 /* UIImageMultiFormatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DA91BEBB19795BC9006F2536 /* UIImageMultiFormatTests.m */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -32,6 +34,10 @@
1E3C51E819B46E370092B5E6 /* SDWebImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDWebImageDownloaderTests.m; sourceTree = "<group>"; };
2D7AF05E1F329763000083C2 /* SDTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDTestCase.h; sourceTree = "<group>"; };
2D7AF05F1F329763000083C2 /* SDTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDTestCase.m; sourceTree = "<group>"; };
321259EB1F39E3240096FE0E /* TestImageStatic.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageStatic.webp; sourceTree = "<group>"; };
321259ED1F39E4110096FE0E /* TestImageAnimated.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageAnimated.webp; sourceTree = "<group>"; };
32E6F0301F3A1B4700A945E6 /* SDWebImageTestDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDWebImageTestDecoder.h; sourceTree = "<group>"; };
32E6F0311F3A1B4700A945E6 /* SDWebImageTestDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDWebImageTestDecoder.m; sourceTree = "<group>"; };
433BBBB41D7EF5C00086B6E9 /* SDWebImageDecoderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDWebImageDecoderTests.m; sourceTree = "<group>"; };
433BBBB61D7EF8200086B6E9 /* TestImage.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = TestImage.gif; sourceTree = "<group>"; };
433BBBB81D7EF8260086B6E9 /* TestImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TestImage.png; sourceTree = "<group>"; };
@ -51,7 +57,6 @@
DA248D64195472AA00390AB0 /* Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Prefix.pch"; sourceTree = "<group>"; };
DA248D68195475D800390AB0 /* SDImageCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDImageCacheTests.m; sourceTree = "<group>"; };
DA248D6A195476AC00390AB0 /* SDWebImageManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDWebImageManagerTests.m; sourceTree = "<group>"; };
DA91BEBB19795BC9006F2536 /* UIImageMultiFormatTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIImageMultiFormatTests.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -115,16 +120,19 @@
5F7F38AC1AE2A77A00B0E330 /* TestImage.jpg */,
43828A441DA67F9900000E62 /* TestImageLarge.jpg */,
433BBBB81D7EF8260086B6E9 /* TestImage.png */,
321259ED1F39E4110096FE0E /* TestImageAnimated.webp */,
321259EB1F39E3240096FE0E /* TestImageStatic.webp */,
DA248D5D195472AA00390AB0 /* Supporting Files */,
DA248D68195475D800390AB0 /* SDImageCacheTests.m */,
DA248D6A195476AC00390AB0 /* SDWebImageManagerTests.m */,
DA91BEBB19795BC9006F2536 /* UIImageMultiFormatTests.m */,
1E3C51E819B46E370092B5E6 /* SDWebImageDownloaderTests.m */,
433BBBB41D7EF5C00086B6E9 /* SDWebImageDecoderTests.m */,
4369C1D01D97F80F007E863A /* SDWebImagePrefetcherTests.m */,
4369C2731D9804B1007E863A /* SDCategoriesTests.m */,
2D7AF05E1F329763000083C2 /* SDTestCase.h */,
2D7AF05F1F329763000083C2 /* SDTestCase.m */,
32E6F0301F3A1B4700A945E6 /* SDWebImageTestDecoder.h */,
32E6F0311F3A1B4700A945E6 /* SDWebImageTestDecoder.m */,
);
path = Tests;
sourceTree = "<group>";
@ -192,9 +200,11 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
321259EE1F39E4110096FE0E /* TestImageAnimated.webp in Resources */,
5F7F38AD1AE2A77A00B0E330 /* TestImage.jpg in Resources */,
43828A451DA67F9900000E62 /* TestImageLarge.jpg in Resources */,
433BBBB71D7EF8200086B6E9 /* TestImage.gif in Resources */,
321259EC1F39E3240096FE0E /* TestImageStatic.webp in Resources */,
DA248D61195472AA00390AB0 /* InfoPlist.strings in Resources */,
433BBBB91D7EF8260086B6E9 /* TestImage.png in Resources */,
433BBBBB1D7EFA8B0086B6E9 /* MonochromeTestImage.jpg in Resources */,
@ -210,13 +220,16 @@
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Tests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
85E5D3885A03BFC23B050908 /* [CP] Copy Pods Resources */ = {
@ -240,9 +253,18 @@
files = (
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-Tests/Pods-Tests-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/Expecta/Expecta.framework",
"${BUILT_PRODUCTS_DIR}/FLAnimatedImage/FLAnimatedImage.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Expecta.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FLAnimatedImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@ -256,6 +278,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
32E6F0321F3A1B4700A945E6 /* SDWebImageTestDecoder.m in Sources */,
1E3C51E919B46E370092B5E6 /* SDWebImageDownloaderTests.m in Sources */,
4369C2741D9804B1007E863A /* SDCategoriesTests.m in Sources */,
2D7AF0601F329763000083C2 /* SDTestCase.m in Sources */,
@ -263,7 +286,6 @@
DA248D69195475D800390AB0 /* SDImageCacheTests.m in Sources */,
DA248D6B195476AC00390AB0 /* SDWebImageManagerTests.m in Sources */,
433BBBB51D7EF5C00086B6E9 /* SDWebImageDecoderTests.m in Sources */,
DA91BEBC19795BC9006F2536 /* UIImageMultiFormatTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -125,7 +125,7 @@
XCTestExpectation *expectation = [self expectationWithDescription:@"FLAnimatedImageView setImageWithURL"];
FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init];
NSURL *originalImageURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif"];
NSURL *originalImageURL = [NSURL URLWithString:@"https://www.interntheory.com/img/loading-small.gif"];
[imageView sd_setImageWithURL:originalImageURL
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {

View File

@ -8,6 +8,8 @@
#import "SDTestCase.h"
#import <SDWebImage/SDImageCache.h>
#import <SDWebImage/SDWebImageCodersManager.h>
#import "SDWebImageTestDecoder.h"
NSString *kImageTestKey = @"TestImageKey.jpg";
@ -190,24 +192,86 @@ NSString *kImageTestKey = @"TestImageKey.jpg";
// TODO -- Testing image data insertion
- (void)test40InsertionOfImageData {
XCTestExpectation *expectation = [self expectationWithDescription:@"Insertion of image data works"];
NSData *imageData = [NSData dataWithContentsOfFile:[self testImagePath]];
UIImage *image = [UIImage imageWithContentsOfFile:[self testImagePath]];
NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
[self.sharedImageCache storeImageDataToDisk:imageData forKey:kImageTestKey];
UIImage *storedImageFromMemory = [self.sharedImageCache imageFromMemoryCacheForKey:kImageTestKey];
expect(storedImageFromMemory).to.equal(nil);
NSString *cachePath = [self.sharedImageCache defaultCachePathForKey:kImageTestKey];
NSData *storedImageData = [NSData dataWithContentsOfFile:cachePath];
expect([storedImageData isEqualToData:imageData]).will.beTruthy;
UIImage *cachedImage = [UIImage imageWithContentsOfFile:cachePath];
NSData *storedImageData = UIImageJPEGRepresentation(cachedImage, 1.0);
expect(storedImageData.length).to.beGreaterThan(0);
expect(cachedImage.size).to.equal(image.size);
// can't directly compare image and cachedImage because apparently there are some slight differences, even though the image is the same
__block int blocksCalled = 0;
[self.sharedImageCache diskImageExistsWithKey:kImageTestKey completion:^(BOOL isInCache) {
expect(isInCache).to.equal(YES);
blocksCalled += 1;
if (blocksCalled == 2) {
[expectation fulfill];
}
}];
[self.sharedImageCache calculateSizeWithCompletionBlock:^(NSUInteger fileCount, NSUInteger totalSize) {
expect(fileCount).to.beLessThan(100);
blocksCalled += 1;
if (blocksCalled == 2) {
[expectation fulfill];
}
}];
[self waitForExpectationsWithCommonTimeout];
}
- (void)test41ThatCustomDecoderWorksForImageCache {
XCTestExpectation *expectation = [self expectationWithDescription:@"Custom decoder for SDImageCache not works"];
SDImageCache *cache = [[SDImageCache alloc] initWithNamespace:@"TestDecode"];
SDWebImageTestDecoder *testDecoder = [[SDWebImageTestDecoder alloc] init];
[[SDWebImageCodersManager sharedInstance] addCoder:testDecoder];
NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:testImagePath];
NSString *key = @"TestPNGImageEncodedToDataAndRetrieveToJPEG";
[cache storeImage:image imageData:nil forKey:key toDisk:YES completion:^{
[cache clearMemory];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
SEL diskImageDataBySearchingAllPathsForKey = @selector(diskImageDataBySearchingAllPathsForKey:);
#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
NSData *data = [cache performSelector:diskImageDataBySearchingAllPathsForKey withObject:key];
#pragma clang diagnostic pop
NSString *str1 = @"TestEncode";
NSString *str2 = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (![str1 isEqualToString:str2]) {
XCTFail(@"Custom decoder not work for SDImageCache, check -[SDWebImageTestDecoder encodedDataWithImage:format:]");
}
UIImage *diskCacheImage = [cache imageFromDiskCacheForKey:key];
// Decoded result is JPEG
NSString * decodedImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
UIImage *testJPEGImage = [UIImage imageWithContentsOfFile:decodedImagePath];
NSData *data1 = UIImagePNGRepresentation(testJPEGImage);
NSData *data2 = UIImagePNGRepresentation(diskCacheImage);
if (![data1 isEqualToData:data2]) {
XCTFail(@"Custom decoder not work for SDImageCache, check -[SDWebImageTestDecoder decodedImageWithData:]");
}
[[SDWebImageCodersManager sharedInstance] removeCoder:testDecoder];
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
}
#pragma mark Helper methods

View File

@ -8,7 +8,9 @@
*/
#import "SDTestCase.h"
#import <SDWebImage/SDWebImageDecoder.h>
#import <SDWebImage/SDWebImageImageIOCoder.h>
#import <SDWebImage/SDWebImageWebPCoder.h>
#import <SDWebImage/UIImage+ForceDecode.h>
@interface SDWebImageDecoderTests : SDTestCase
@ -78,4 +80,48 @@
expect(decodedImage.size.height).to.equal(image.size.height);
}
- (void)test08ImageOrientationFromImageDataWithInvalidData {
// sync download image
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
SEL selector = @selector(sd_imageOrientationFromImageData:);
#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
UIImageOrientation orientation = (UIImageOrientation)[[SDWebImageImageIOCoder class] performSelector:selector withObject:nil];
#pragma clang diagnostic pop
expect(orientation).to.equal(UIImageOrientationUp);
}
- (void)test09ThatStaticWebPCoderWorks {
NSURL *staticWebPURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageStatic" withExtension:@"webp"];
NSData *staticWebPData = [NSData dataWithContentsOfURL:staticWebPURL];
UIImage *staticWebPImage = [[SDWebImageWebPCoder sharedCoder] decodedImageWithData:staticWebPData];
expect(staticWebPImage).toNot.beNil();
NSData *outputData = [[SDWebImageWebPCoder sharedCoder] encodedDataWithImage:staticWebPImage format:SDImageFormatWebP];
expect(outputData).toNot.beNil();
}
- (void)test10ThatAnimatedWebPCoderWorks {
NSURL *animatedWebPURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"webp"];
NSData *animatedWebPData = [NSData dataWithContentsOfURL:animatedWebPURL];
UIImage *animatedWebPImage = [[SDWebImageWebPCoder sharedCoder] decodedImageWithData:animatedWebPData];
expect(animatedWebPImage).toNot.beNil();
expect(animatedWebPImage.images.count).to.beGreaterThan(0);
CGSize imageSize = animatedWebPImage.size;
CGFloat imageScale = animatedWebPImage.scale;
[animatedWebPImage.images enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
CGSize size = image.size;
CGFloat scale = image.scale;
expect(imageSize.width).to.equal(size.width);
expect(imageSize.height).to.equal(size.height);
expect(imageScale).to.equal(scale);
}];
NSData *outputData = [[SDWebImageWebPCoder sharedCoder] encodedDataWithImage:animatedWebPImage format:SDImageFormatWebP];
expect(outputData).toNot.beNil();
}
@end

View File

@ -10,6 +10,8 @@
#import "SDTestCase.h"
#import <SDWebImage/SDWebImageDownloader.h>
#import <SDWebImage/SDWebImageDownloaderOperation.h>
#import <SDWebImage/SDWebImageCodersManager.h>
#import "SDWebImageTestDecoder.h"
/**
* Category for SDWebImageDownloader so we can access the operationClass
@ -256,6 +258,21 @@
[self waitForExpectationsWithCommonTimeout];
}
- (void)test16ThatProgressiveWebPWorks {
XCTestExpectation *expectation = [self expectationWithDescription:@"Progressive WebP download"];
NSURL *imageURL = [NSURL URLWithString:@"http://www.ioncannon.net/wp-content/uploads/2011/06/test9.webp"];
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imageURL options:SDWebImageDownloaderProgressiveDownload progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
if (image && data && !error && finished) {
[expectation fulfill];
} else if (finished) {
XCTFail(@"Something went wrong");
} else {
// progressive updates
}
}];
[self waitForExpectationsWithCommonTimeout];
}
/**
* Per #883 - Fix multiple requests for same image and then canceling one
* Old SDWebImage (3.x) could not handle correctly multiple requests for the same image + cancel
@ -333,4 +350,33 @@
[self waitForExpectationsWithCommonTimeout];
}
- (void)test22ThatCustomDecoderWorksForImageDownload {
XCTestExpectation *expectation = [self expectationWithDescription:@"Custom decoder for SDWebImageDownloader not works"];
SDWebImageDownloader *downloader = [[SDWebImageDownloader alloc] init];
SDWebImageTestDecoder *testDecoder = [[SDWebImageTestDecoder alloc] init];
[[SDWebImageCodersManager sharedInstance] addCoder:testDecoder];
NSURL * testImageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"png"];
// Decoded result is JPEG
NSString *testJPEGImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
UIImage *testJPEGImage = [UIImage imageWithContentsOfFile:testJPEGImagePath];
[downloader downloadImageWithURL:testImageURL options:0 progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
NSData *data1 = UIImagePNGRepresentation(testJPEGImage);
NSData *data2 = UIImagePNGRepresentation(image);
if (![data1 isEqualToData:data2]) {
XCTFail(@"The image data is not equal to cutom decoder, check -[SDWebImageTestDecoder decodedImageWithData:]");
}
NSString *str1 = @"TestDecompress";
NSString *str2 = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (![str1 isEqualToString:str2]) {
XCTFail(@"The image data is not modified by the custom decoder, check -[SDWebImageTestDecoder decompressedImageWithImage:data:options:]");
}
[[SDWebImageCodersManager sharedInstance] removeCoder:testDecoder];
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
}
@end

View File

@ -0,0 +1,15 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
* (c) Matt Galloway
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <Foundation/Foundation.h>
#import <SDWebImage/SDWebImageCoder.h>
@interface SDWebImageTestDecoder : NSObject <SDWebImageCoder>
@end

View File

@ -0,0 +1,50 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
* (c) Matt Galloway
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDWebImageTestDecoder.h"
@implementation SDWebImageTestDecoder
- (BOOL)canDecodeFromData:(nullable NSData *)data {
return YES;
}
- (BOOL)canEncodeToFormat:(SDImageFormat)format {
return YES;
}
- (UIImage *)decodedImageWithData:(NSData *)data {
NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:testImagePath];
return image;
}
- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"gif"];
UIImage *image = [UIImage imageWithContentsOfFile:testImagePath];
return image;
}
- (UIImage *)decompressedImageWithImage:(UIImage *)image
data:(NSData *__autoreleasing _Nullable *)data
options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
NSString *testString = @"TestDecompress";
NSData *testData = [testString dataUsingEncoding:NSUTF8StringEncoding];
*data = testData;
return image;
}
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
NSString *testString = @"TestEncode";
NSData *data = [testString dataUsingEncoding:NSUTF8StringEncoding];
return data;
}
@end

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,56 +0,0 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDTestCase.h"
#import <SDWebImage/UIImage+MultiFormat.h>
@interface UIImageMultiFormatTests : SDTestCase
@end
@implementation UIImageMultiFormatTests
- (void)test01ImageOrientationFromImageDataWithInvalidData {
// sync download image
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
SEL selector = @selector(sd_imageOrientationFromImageData:);
#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
UIImageOrientation orientation = (UIImageOrientation)[[UIImage class] performSelector:selector withObject:nil];
#pragma clang diagnostic pop
expect(orientation).to.equal(UIImageOrientationUp);
}
- (void)test02AnimatedWebPImageArrayWithEqualSizeAndScale {
NSURL *webpURL = [NSURL URLWithString:@"https://isparta.github.io/compare-webp/image/gif_webp/webp/2.webp"];
NSData *data = [NSData dataWithContentsOfURL:webpURL];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
SEL selector = @selector(sd_imageWithWebPData:);
#pragma clang diagnostic pop
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
UIImage *animatedImage = [[UIImage class] performSelector:selector withObject:data];
#pragma clang diagnostic pop
CGSize imageSize = animatedImage.size;
CGFloat imageScale = animatedImage.scale;
[animatedImage.images enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
CGSize size = image.size;
CGFloat scale = image.scale;
expect(imageSize.width).to.equal(size.width);
expect(imageSize.height).to.equal(size.height);
expect(imageScale).to.equal(scale);
}];
}
@end

View File

@ -39,9 +39,14 @@ FOUNDATION_EXPORT const unsigned char WebImageVersionString[];
#import <SDWebImage/MKAnnotationView+WebCache.h>
#endif
#import <SDWebImage/SDWebImageDecoder.h>
#import <SDWebImage/SDWebImageCodersManager.h>
#import <SDWebImage/SDWebImageCoder.h>
#import <SDWebImage/SDWebImageWebPCoder.h>
#import <SDWebImage/SDWebImageGIFCoder.h>
#import <SDWebImage/SDWebImageImageIOCoder.h>
#import <SDWebImage/UIImage+WebP.h>
#import <SDWebImage/UIImage+GIF.h>
#import <SDWebImage/UIImage+ForceDecode.h>
#import <SDWebImage/NSData+ImageContentType.h>
#if SD_MAC