629 lines
24 KiB
Objective-C
629 lines
24 KiB
Objective-C
/*
|
|
* 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 "SDImageCoderHelper.h"
|
|
#import "SDImageFrame.h"
|
|
#import "NSImage+Compatibility.h"
|
|
#import "NSData+ImageContentType.h"
|
|
#import "SDAnimatedImageRep.h"
|
|
#import "UIImage+ForceDecode.h"
|
|
#import "UIImage+Metadata.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.f;
|
|
|
|
/*
|
|
* 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.f;
|
|
|
|
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 SDImageCoderHelper
|
|
|
|
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
|
|
NSUInteger frameCount = frames.count;
|
|
if (frameCount == 0) {
|
|
return nil;
|
|
}
|
|
|
|
UIImage *animatedImage;
|
|
|
|
#if SD_UIKIT || SD_WATCH
|
|
NSUInteger durations[frameCount];
|
|
for (size_t i = 0; i < frameCount; i++) {
|
|
durations[i] = frames[i].duration * 1000;
|
|
}
|
|
NSUInteger const gcd = gcdArray(frameCount, durations);
|
|
__block NSUInteger totalDuration = 0;
|
|
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
|
|
[frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
UIImage *image = frame.image;
|
|
NSUInteger duration = frame.duration * 1000;
|
|
totalDuration += duration;
|
|
NSUInteger repeatCount;
|
|
if (gcd) {
|
|
repeatCount = duration / gcd;
|
|
} else {
|
|
repeatCount = 1;
|
|
}
|
|
for (size_t i = 0; i < repeatCount; ++i) {
|
|
[animatedImages addObject:image];
|
|
}
|
|
}];
|
|
|
|
animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];
|
|
|
|
#else
|
|
|
|
NSMutableData *imageData = [NSMutableData data];
|
|
CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:SDImageFormatGIF];
|
|
// Create an image destination. GIF does not support EXIF image orientation
|
|
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frameCount, NULL);
|
|
if (!imageDestination) {
|
|
// Handle failure.
|
|
return nil;
|
|
}
|
|
|
|
for (size_t i = 0; i < frameCount; i++) {
|
|
@autoreleasepool {
|
|
SDImageFrame *frame = frames[i];
|
|
float frameDuration = frame.duration;
|
|
CGImageRef frameImageRef = frame.image.CGImage;
|
|
NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
|
|
CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
|
|
}
|
|
}
|
|
// Finalize the destination.
|
|
if (CGImageDestinationFinalize(imageDestination) == NO) {
|
|
// Handle failure.
|
|
CFRelease(imageDestination);
|
|
return nil;
|
|
}
|
|
CFRelease(imageDestination);
|
|
CGFloat scale = frames.firstObject.image.scale;
|
|
if (scale < 1) {
|
|
scale = 1;
|
|
}
|
|
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
|
|
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
|
|
imageRep.size = size;
|
|
animatedImage = [[NSImage alloc] initWithSize:size];
|
|
[animatedImage addRepresentation:imageRep];
|
|
#endif
|
|
|
|
return animatedImage;
|
|
}
|
|
|
|
+ (NSArray<SDImageFrame *> *)framesFromAnimatedImage:(UIImage *)animatedImage {
|
|
if (!animatedImage) {
|
|
return nil;
|
|
}
|
|
|
|
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
|
|
NSUInteger frameCount = 0;
|
|
|
|
#if SD_UIKIT || SD_WATCH
|
|
NSArray<UIImage *> *animatedImages = animatedImage.images;
|
|
frameCount = animatedImages.count;
|
|
if (frameCount == 0) {
|
|
return nil;
|
|
}
|
|
|
|
NSTimeInterval avgDuration = animatedImage.duration / frameCount;
|
|
if (avgDuration == 0) {
|
|
avgDuration = 0.1; // if it's a animated image but no duration, set it to default 100ms (this do not have that 10ms limit like GIF or WebP to allow custom coder provide the limit)
|
|
}
|
|
|
|
__block NSUInteger index = 0;
|
|
__block NSUInteger repeatCount = 1;
|
|
__block UIImage *previousImage = animatedImages.firstObject;
|
|
[animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
// ignore first
|
|
if (idx == 0) {
|
|
return;
|
|
}
|
|
if ([image isEqual:previousImage]) {
|
|
repeatCount++;
|
|
} else {
|
|
SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
|
|
[frames addObject:frame];
|
|
repeatCount = 1;
|
|
index++;
|
|
}
|
|
previousImage = image;
|
|
// last one
|
|
if (idx == frameCount - 1) {
|
|
SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
|
|
[frames addObject:frame];
|
|
}
|
|
}];
|
|
|
|
#else
|
|
|
|
NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height);
|
|
NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil];
|
|
NSBitmapImageRep *bitmapImageRep;
|
|
if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
|
|
bitmapImageRep = (NSBitmapImageRep *)imageRep;
|
|
}
|
|
if (!bitmapImageRep) {
|
|
return nil;
|
|
}
|
|
frameCount = [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue];
|
|
if (frameCount == 0) {
|
|
return nil;
|
|
}
|
|
CGFloat scale = animatedImage.scale;
|
|
|
|
for (size_t i = 0; i < frameCount; i++) {
|
|
@autoreleasepool {
|
|
// NSBitmapImageRep need to manually change frame. "Good taste" API
|
|
[bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)];
|
|
float frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] floatValue];
|
|
NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
|
|
SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
|
|
[frames addObject:frame];
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return frames;
|
|
}
|
|
|
|
+ (CGColorSpaceRef)colorSpaceGetDeviceRGB {
|
|
#if SD_MAC
|
|
CGColorSpaceRef screenColorSpace = NSScreen.mainScreen.colorSpace.CGColorSpace;
|
|
if (screenColorSpace) {
|
|
return screenColorSpace;
|
|
}
|
|
#endif
|
|
static CGColorSpaceRef colorSpace;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
#if SD_UIKIT
|
|
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
|
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
} else {
|
|
colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
}
|
|
#else
|
|
colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
#endif
|
|
});
|
|
return colorSpace;
|
|
}
|
|
|
|
+ (BOOL)CGImageContainsAlpha:(CGImageRef)cgImage {
|
|
if (!cgImage) {
|
|
return NO;
|
|
}
|
|
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
|
|
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
|
|
alphaInfo == kCGImageAlphaNoneSkipFirst ||
|
|
alphaInfo == kCGImageAlphaNoneSkipLast);
|
|
return hasAlpha;
|
|
}
|
|
|
|
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
|
|
return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
|
|
}
|
|
|
|
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
|
|
if (!cgImage) {
|
|
return NULL;
|
|
}
|
|
size_t width = CGImageGetWidth(cgImage);
|
|
size_t height = CGImageGetHeight(cgImage);
|
|
if (width == 0 || height == 0) return NULL;
|
|
BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
|
|
// iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
|
|
// Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage`
|
|
// But since our build-in coders use this bitmapInfo, this can have a little performance benefit
|
|
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
|
|
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
|
|
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
|
|
if (!context) {
|
|
return NULL;
|
|
}
|
|
|
|
// Apply transform
|
|
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(width, height));
|
|
CGRect rect;
|
|
switch (orientation) {
|
|
case kCGImagePropertyOrientationLeft:
|
|
case kCGImagePropertyOrientationLeftMirrored:
|
|
case kCGImagePropertyOrientationRight:
|
|
case kCGImagePropertyOrientationRightMirrored: {
|
|
// These orientation should swap width & height
|
|
rect = CGRectMake(0, 0, height, width);
|
|
}
|
|
break;
|
|
default: {
|
|
rect = CGRectMake(0, 0, width, height);
|
|
}
|
|
break;
|
|
}
|
|
CGContextConcatCTM(context, transform);
|
|
CGContextDrawImage(context, rect, cgImage);
|
|
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
|
|
CGContextRelease(context);
|
|
|
|
return newImageRef;
|
|
}
|
|
|
|
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
|
|
#if SD_MAC
|
|
return image;
|
|
#else
|
|
if (![self shouldDecodeImage:image]) {
|
|
return image;
|
|
}
|
|
|
|
CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
|
|
if (!imageRef) {
|
|
return image;
|
|
}
|
|
UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
|
|
CGImageRelease(imageRef);
|
|
decodedImage.sd_isDecoded = YES;
|
|
decodedImage.sd_imageFormat = image.sd_imageFormat;
|
|
return decodedImage;
|
|
#endif
|
|
}
|
|
|
|
+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
|
|
#if SD_MAC
|
|
return image;
|
|
#else
|
|
if (![self shouldDecodeImage:image]) {
|
|
return image;
|
|
}
|
|
|
|
if (![self shouldScaleDownImage:image limitBytes:bytes]) {
|
|
return [self decodedImageWithImage:image];
|
|
}
|
|
|
|
CGFloat destTotalPixels;
|
|
CGFloat tileTotalPixels;
|
|
if (bytes > 0) {
|
|
destTotalPixels = bytes / kBytesPerPixel;
|
|
tileTotalPixels = destTotalPixels / 3;
|
|
} else {
|
|
destTotalPixels = kDestTotalPixels;
|
|
tileTotalPixels = kTileTotalPixels;
|
|
}
|
|
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);
|
|
CGFloat 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.
|
|
CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
|
|
CGSize destResolution = CGSizeZero;
|
|
destResolution.width = (int)(sourceResolution.width * imageScale);
|
|
destResolution.height = (int)(sourceResolution.height * imageScale);
|
|
|
|
// device color space
|
|
CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];
|
|
BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef];
|
|
// iOS display alpha info (BGRA8888/BGRX8888)
|
|
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
|
|
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
|
|
|
|
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
|
|
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipFirst
|
|
// to create bitmap graphics contexts without alpha info.
|
|
destContext = CGBitmapContextCreate(NULL,
|
|
destResolution.width,
|
|
destResolution.height,
|
|
kBitsPerComponent,
|
|
0,
|
|
colorspaceRef,
|
|
bitmapInfo);
|
|
|
|
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)(tileTotalPixels / 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 alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
|
|
CGImageRelease(destImageRef);
|
|
if (destImage == nil) {
|
|
return image;
|
|
}
|
|
destImage.sd_isDecoded = YES;
|
|
destImage.sd_imageFormat = image.sd_imageFormat;
|
|
return destImage;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if SD_UIKIT || SD_WATCH
|
|
// Convert an EXIF image orientation to an iOS one.
|
|
+ (UIImageOrientation)imageOrientationFromEXIFOrientation:(CGImagePropertyOrientation)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.
|
|
+ (CGImagePropertyOrientation)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation {
|
|
CGImagePropertyOrientation 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
|
|
|
|
#pragma mark - Helper Fuction
|
|
#if SD_UIKIT || SD_WATCH
|
|
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
|
|
// Avoid extra decode
|
|
if (image.sd_isDecoded) {
|
|
return NO;
|
|
}
|
|
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
|
|
if (image == nil) {
|
|
return NO;
|
|
}
|
|
// do not decode animated images
|
|
if (image.images != nil) {
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
|
|
BOOL shouldScaleDown = YES;
|
|
|
|
CGImageRef sourceImageRef = image.CGImage;
|
|
CGSize sourceResolution = CGSizeZero;
|
|
sourceResolution.width = CGImageGetWidth(sourceImageRef);
|
|
sourceResolution.height = CGImageGetHeight(sourceImageRef);
|
|
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
|
|
if (sourceTotalPixels <= 0) {
|
|
return NO;
|
|
}
|
|
CGFloat destTotalPixels;
|
|
if (bytes > 0) {
|
|
destTotalPixels = bytes / kBytesPerPixel;
|
|
} else {
|
|
destTotalPixels = kDestTotalPixels;
|
|
}
|
|
if (destTotalPixels <= kPixelsPerMB) {
|
|
// Too small to scale down
|
|
return NO;
|
|
}
|
|
float imageScale = destTotalPixels / sourceTotalPixels;
|
|
if (imageScale < 1) {
|
|
shouldScaleDown = YES;
|
|
} else {
|
|
shouldScaleDown = NO;
|
|
}
|
|
|
|
return shouldScaleDown;
|
|
}
|
|
#endif
|
|
|
|
static inline CGAffineTransform SDCGContextTransformFromOrientation(CGImagePropertyOrientation orientation, CGSize size) {
|
|
// Inspiration from @libfeihu
|
|
// We need to calculate the proper transformation to make the image upright.
|
|
// We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
|
|
CGAffineTransform transform = CGAffineTransformIdentity;
|
|
|
|
switch (orientation) {
|
|
case kCGImagePropertyOrientationDown:
|
|
case kCGImagePropertyOrientationDownMirrored:
|
|
transform = CGAffineTransformTranslate(transform, size.width, size.height);
|
|
transform = CGAffineTransformRotate(transform, M_PI);
|
|
break;
|
|
|
|
case kCGImagePropertyOrientationLeft:
|
|
case kCGImagePropertyOrientationLeftMirrored:
|
|
transform = CGAffineTransformTranslate(transform, size.width, 0);
|
|
transform = CGAffineTransformRotate(transform, M_PI_2);
|
|
break;
|
|
|
|
case kCGImagePropertyOrientationRight:
|
|
case kCGImagePropertyOrientationRightMirrored:
|
|
transform = CGAffineTransformTranslate(transform, 0, size.height);
|
|
transform = CGAffineTransformRotate(transform, -M_PI_2);
|
|
break;
|
|
case kCGImagePropertyOrientationUp:
|
|
case kCGImagePropertyOrientationUpMirrored:
|
|
break;
|
|
}
|
|
|
|
switch (orientation) {
|
|
case kCGImagePropertyOrientationUpMirrored:
|
|
case kCGImagePropertyOrientationDownMirrored:
|
|
transform = CGAffineTransformTranslate(transform, size.width, 0);
|
|
transform = CGAffineTransformScale(transform, -1, 1);
|
|
break;
|
|
|
|
case kCGImagePropertyOrientationLeftMirrored:
|
|
case kCGImagePropertyOrientationRightMirrored:
|
|
transform = CGAffineTransformTranslate(transform, size.height, 0);
|
|
transform = CGAffineTransformScale(transform, -1, 1);
|
|
break;
|
|
case kCGImagePropertyOrientationUp:
|
|
case kCGImagePropertyOrientationDown:
|
|
case kCGImagePropertyOrientationLeft:
|
|
case kCGImagePropertyOrientationRight:
|
|
break;
|
|
}
|
|
|
|
return transform;
|
|
}
|
|
|
|
#if SD_UIKIT || SD_WATCH
|
|
static NSUInteger gcd(NSUInteger a, NSUInteger b) {
|
|
NSUInteger c;
|
|
while (a != 0) {
|
|
c = a;
|
|
a = b % a;
|
|
b = c;
|
|
}
|
|
return b;
|
|
}
|
|
|
|
static NSUInteger gcdArray(size_t const count, NSUInteger const * const values) {
|
|
if (count == 0) {
|
|
return 0;
|
|
}
|
|
NSUInteger result = values[0];
|
|
for (size_t i = 1; i < count; ++i) {
|
|
result = gcd(values[i], result);
|
|
}
|
|
return result;
|
|
}
|
|
#endif
|
|
|
|
@end
|