123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979 |
- /*
- * 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 "SDAssociatedObject.h"
- #import "UIImage+Metadata.h"
- #import "SDInternalMacros.h"
- #import "SDGraphicsImageRenderer.h"
- #import "SDInternalMacros.h"
- #import "SDDeviceHelper.h"
- #import <Accelerate/Accelerate.h>
- #define kCGColorSpaceDeviceRGB CFSTR("kCGColorSpaceDeviceRGB")
- #if SD_UIKIT
- static inline UIImage *SDImageDecodeUIKit(UIImage *image) {
- // See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay
- // Need CGImage-based
- if (@available(iOS 15, tvOS 15, *)) {
- UIImage *decodedImage = [image imageByPreparingForDisplay];
- if (decodedImage) {
- SDImageCopyAssociatedObject(image, decodedImage);
- decodedImage.sd_isDecoded = YES;
- return decodedImage;
- }
- }
- return nil;
- }
- static inline UIImage *SDImageDecodeAndScaleDownUIKit(UIImage *image, CGSize destResolution) {
- // See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize
- // Need CGImage-based
- if (@available(iOS 15, tvOS 15, *)) {
- // Calculate thumbnail point size
- CGFloat scale = image.scale ?: 1;
- CGSize thumbnailSize = CGSizeMake(destResolution.width / scale, destResolution.height / scale);
- UIImage *decodedImage = [image imageByPreparingThumbnailOfSize:thumbnailSize];
- if (decodedImage) {
- SDImageCopyAssociatedObject(image, decodedImage);
- decodedImage.sd_isDecoded = YES;
- return decodedImage;
- }
- }
- return nil;
- }
- static inline BOOL SDImageSupportsHardwareHEVCDecoder(void) {
- static dispatch_once_t onceToken;
- static BOOL supportsHardware = NO;
- dispatch_once(&onceToken, ^{
- SEL DeviceInfoSelector = SD_SEL_SPI(deviceInfoForKey:);
- NSString *HEVCDecoder8bitSupported = @"N8lZxRgC7lfdRS3dRLn+Ag";
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
- if ([UIDevice.currentDevice respondsToSelector:DeviceInfoSelector] && [UIDevice.currentDevice performSelector:DeviceInfoSelector withObject:HEVCDecoder8bitSupported]) {
- supportsHardware = YES;
- }
- #pragma clang diagnostic pop
- });
- return supportsHardware;
- }
- #endif
- static UIImage * _Nonnull SDImageGetAlphaDummyImage(void) {
- static dispatch_once_t onceToken;
- static UIImage *dummyImage;
- dispatch_once(&onceToken, ^{
- SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat];
- format.scale = 1;
- format.opaque = NO;
- CGSize size = CGSizeMake(1, 1);
- SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format];
- dummyImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
- CGContextSetFillColorWithColor(context, UIColor.redColor.CGColor);
- CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
- }];
- NSCAssert(dummyImage, @"The sample alpha image (1x1 pixels) returns nil, OS bug ?");
- });
- return dummyImage;
- }
- static UIImage * _Nonnull SDImageGetNonAlphaDummyImage(void) {
- static dispatch_once_t onceToken;
- static UIImage *dummyImage;
- dispatch_once(&onceToken, ^{
- SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat];
- format.scale = 1;
- format.opaque = YES;
- CGSize size = CGSizeMake(1, 1);
- SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format];
- dummyImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
- CGContextSetFillColorWithColor(context, UIColor.redColor.CGColor);
- CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
- }];
- NSCAssert(dummyImage, @"The sample non-alpha image (1x1 pixels) returns nil, OS bug ?");
- });
- return dummyImage;
- }
- static SDImageCoderDecodeSolution kDefaultDecodeSolution = SDImageCoderDecodeSolutionAutomatic;
- static const size_t kBytesPerPixel = 4;
- static const size_t kBitsPerComponent = 8;
- static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
- /*
- * 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.
- */
- #if SD_MAC
- static CGFloat kDestImageLimitBytes = 90.f * kBytesPerMB;
- #elif SD_UIKIT
- static CGFloat kDestImageLimitBytes = 60.f * kBytesPerMB;
- #elif SD_WATCH
- static CGFloat kDestImageLimitBytes = 30.f * kBytesPerMB;
- #endif
- static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
- #if SD_MAC
- @interface SDAnimatedImageRep (Private)
- /// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
- @property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
- @end
- #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 NSTimeInterval 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 += frame.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];
-
- #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++) {
- SDImageFrame *frame = frames[i];
- NSTimeInterval 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 = MAX(frames.firstObject.image.scale, 1);
-
- SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
- NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
- imageRep.size = size;
- imageRep.frames = frames; // Weak assign to avoid effect lazy semantic of NSBitmapImageRep
- animatedImage = [[NSImage alloc] initWithSize:size];
- [animatedImage addRepresentation:imageRep];
- #endif
-
- return animatedImage;
- }
- + (NSArray<SDImageFrame *> *)framesFromAnimatedImage:(UIImage *)animatedImage {
- if (!animatedImage) {
- return nil;
- }
-
- NSMutableArray<SDImageFrame *> *frames;
- NSUInteger frameCount = 0;
-
- #if SD_UIKIT || SD_WATCH
- NSArray<UIImage *> *animatedImages = animatedImage.images;
- frameCount = animatedImages.count;
- if (frameCount == 0) {
- return nil;
- }
- frames = [NSMutableArray arrayWithCapacity:frameCount];
-
- 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 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;
- }
- previousImage = image;
- }];
- // last one
- 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];
- // Check weak assigned frames firstly
- if ([imageRep isKindOfClass:[SDAnimatedImageRep class]]) {
- SDAnimatedImageRep *animatedImageRep = (SDAnimatedImageRep *)imageRep;
- if (animatedImageRep.frames) {
- return animatedImageRep.frames;
- }
- }
-
- NSBitmapImageRep *bitmapImageRep;
- if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
- bitmapImageRep = (NSBitmapImageRep *)imageRep;
- }
- if (!bitmapImageRep) {
- return nil;
- }
- frameCount = [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue];
- if (frameCount == 0) {
- return nil;
- }
- frames = [NSMutableArray arrayWithCapacity:frameCount];
- CGFloat scale = animatedImage.scale;
-
- for (size_t i = 0; i < frameCount; i++) {
- // NSBitmapImageRep need to manually change frame. "Good taste" API
- [bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)];
- NSTimeInterval frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] doubleValue];
- NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
- SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
- [frames addObject:frame];
- }
- #endif
-
- return [frames copy];
- }
- + (CGColorSpaceRef)colorSpaceGetDeviceRGB {
- static CGColorSpaceRef colorSpace;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- #if SD_MAC
- NSScreen *mainScreen = nil;
- if (@available(macOS 10.12, *)) {
- mainScreen = [NSScreen mainScreen];
- } else {
- mainScreen = [NSScreen screens].firstObject;
- }
- colorSpace = mainScreen.colorSpace.CGColorSpace;
- #else
- colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
- #endif
- });
- return colorSpace;
- }
- + (SDImagePixelFormat)preferredPixelFormat:(BOOL)containsAlpha {
- CGImageRef cgImage;
- if (containsAlpha) {
- cgImage = SDImageGetAlphaDummyImage().CGImage;
- } else {
- cgImage = SDImageGetNonAlphaDummyImage().CGImage;
- }
- CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
- size_t bitsPerPixel = 8;
- if (SD_OPTIONS_CONTAINS(bitmapInfo, kCGBitmapFloatComponents)) {
- bitsPerPixel = 16;
- }
- size_t components = 4; // Hardcode now
- // https://github.com/path/FastImageCache#byte-alignment
- // A properly aligned bytes-per-row value must be a multiple of 8 pixels × bytes per pixel.
- size_t alignment = (bitsPerPixel / 8) * components * 8;
- SDImagePixelFormat pixelFormat = {
- .bitmapInfo = bitmapInfo,
- .alignment = alignment
- };
- return pixelFormat;
- }
- + (BOOL)CGImageIsHardwareSupported:(CGImageRef)cgImage {
- BOOL supported = YES;
- // 1. Check byte alignment
- size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
- BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
- SDImagePixelFormat pixelFormat = [self preferredPixelFormat:hasAlpha];
- if (SDByteAlign(bytesPerRow, pixelFormat.alignment) == bytesPerRow) {
- // byte aligned, OK
- supported &= YES;
- } else {
- // not aligned
- supported &= NO;
- }
- if (!supported) return supported;
-
- // 2. Check color space
- CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage);
- CGColorSpaceRef perferredColorSpace = [self colorSpaceGetDeviceRGB];
- if (colorSpace == perferredColorSpace) {
- return supported;
- } else {
- if (@available(iOS 10.0, tvOS 10.0, macOS 10.6, watchOS 3.0, *)) {
- NSString *colorspaceName = (__bridge_transfer NSString *)CGColorSpaceCopyName(colorSpace);
- // Seems sRGB/deviceRGB always supported, P3 not always
- if ([colorspaceName isEqualToString:(__bridge NSString *)kCGColorSpaceDeviceRGB]
- || [colorspaceName isEqualToString:(__bridge NSString *)kCGColorSpaceSRGB]) {
- supported &= YES;
- } else {
- supported &= NO;
- }
- return supported;
- } else {
- // Fallback on earlier versions
- return supported;
- }
- }
- }
- + (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;
- size_t newWidth;
- size_t newHeight;
- switch (orientation) {
- case kCGImagePropertyOrientationLeft:
- case kCGImagePropertyOrientationLeftMirrored:
- case kCGImagePropertyOrientationRight:
- case kCGImagePropertyOrientationRightMirrored: {
- // These orientation should swap width & height
- newWidth = height;
- newHeight = width;
- }
- break;
- default: {
- newWidth = width;
- newHeight = height;
- }
- break;
- }
-
- BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
- // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
- // Check #3330 for more detail about why this bitmap is choosen.
- // From v5.17.0, use runtime detection of bitmap info instead of hardcode.
- CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo;
- CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
- if (!context) {
- return NULL;
- }
-
- // Apply transform
- CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
- CGContextConcatCTM(context, transform);
- CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
- CGImageRef newImageRef = CGBitmapContextCreateImage(context);
- CGContextRelease(context);
-
- return newImageRef;
- }
- + (CGImageRef)CGImageCreateScaled:(CGImageRef)cgImage size:(CGSize)size {
- if (!cgImage) {
- return NULL;
- }
- size_t width = CGImageGetWidth(cgImage);
- size_t height = CGImageGetHeight(cgImage);
- if (width == size.width && height == size.height) {
- CGImageRetain(cgImage);
- return cgImage;
- }
-
- __block vImage_Buffer input_buffer = {}, output_buffer = {};
- @onExit {
- if (input_buffer.data) free(input_buffer.data);
- if (output_buffer.data) free(output_buffer.data);
- };
- BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
- // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
- // Check #3330 for more detail about why this bitmap is choosen.
- // From v5.17.0, use runtime detection of bitmap info instead of hardcode.
- CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo;
- vImage_CGImageFormat format = (vImage_CGImageFormat) {
- .bitsPerComponent = 8,
- .bitsPerPixel = 32,
- .colorSpace = NULL,
- .bitmapInfo = bitmapInfo,
- .version = 0,
- .decode = NULL,
- .renderingIntent = CGImageGetRenderingIntent(cgImage)
- };
-
- vImage_Error a_ret = vImageBuffer_InitWithCGImage(&input_buffer, &format, NULL, cgImage, kvImageNoFlags);
- if (a_ret != kvImageNoError) return NULL;
- output_buffer.width = MAX(size.width, 0);
- output_buffer.height = MAX(size.height, 0);
- output_buffer.rowBytes = SDByteAlign(output_buffer.width * 4, 64);
- output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height);
- if (!output_buffer.data) return NULL;
-
- vImage_Error ret = vImageScale_ARGB8888(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling);
- if (ret != kvImageNoError) return NULL;
-
- CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &format, NULL, NULL, kvImageNoFlags, &ret);
- if (ret != kvImageNoError) {
- CGImageRelease(outputImage);
- return NULL;
- }
-
- return outputImage;
- }
- + (CGSize)scaledSizeWithImageSize:(CGSize)imageSize scaleSize:(CGSize)scaleSize preserveAspectRatio:(BOOL)preserveAspectRatio shouldScaleUp:(BOOL)shouldScaleUp {
- CGFloat width = imageSize.width;
- CGFloat height = imageSize.height;
- CGFloat resultWidth;
- CGFloat resultHeight;
-
- if (width <= 0 || height <= 0 || scaleSize.width <= 0 || scaleSize.height <= 0) {
- // Protect
- resultWidth = width;
- resultHeight = height;
- } else {
- // Scale to fit
- if (preserveAspectRatio) {
- CGFloat pixelRatio = width / height;
- CGFloat scaleRatio = scaleSize.width / scaleSize.height;
- if (pixelRatio > scaleRatio) {
- resultWidth = scaleSize.width;
- resultHeight = ceil(scaleSize.width / pixelRatio);
- } else {
- resultHeight = scaleSize.height;
- resultWidth = ceil(scaleSize.height * pixelRatio);
- }
- } else {
- // Stretch
- resultWidth = scaleSize.width;
- resultHeight = scaleSize.height;
- }
- if (!shouldScaleUp) {
- // Scale down only
- resultWidth = MIN(width, resultWidth);
- resultHeight = MIN(height, resultHeight);
- }
- }
-
- return CGSizeMake(resultWidth, resultHeight);
- }
- + (CGSize)scaledSizeWithImageSize:(CGSize)imageSize limitBytes:(NSUInteger)limitBytes bytesPerPixel:(NSUInteger)bytesPerPixel frameCount:(NSUInteger)frameCount {
- if (CGSizeEqualToSize(imageSize, CGSizeZero)) return CGSizeMake(1, 1);
- NSUInteger totalFramePixelSize = limitBytes / bytesPerPixel / (frameCount ?: 1);
- CGFloat ratio = imageSize.height / imageSize.width;
- CGFloat width = sqrt(totalFramePixelSize / ratio);
- CGFloat height = width * ratio;
- width = MAX(1, floor(width));
- height = MAX(1, floor(height));
- CGSize size = CGSizeMake(width, height);
-
- return size;
- }
- + (UIImage *)decodedImageWithImage:(UIImage *)image {
- return [self decodedImageWithImage:image policy:SDImageForceDecodePolicyAutomatic];
- }
- + (UIImage *)decodedImageWithImage:(UIImage *)image policy:(SDImageForceDecodePolicy)policy {
- if (![self shouldDecodeImage:image policy:policy]) {
- return image;
- }
-
- UIImage *decodedImage;
- SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution;
- #if SD_UIKIT
- if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) {
- // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :(
- SDImageFormat format = image.sd_imageFormat;
- if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) {
- decodedImage = SDImageDecodeUIKit(image);
- } else if (format == SDImageFormatJPEG) {
- decodedImage = SDImageDecodeUIKit(image);
- }
- } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) {
- // Arbitrarily call CMPhoto
- decodedImage = SDImageDecodeUIKit(image);
- }
- if (decodedImage) {
- return decodedImage;
- }
- #endif
-
- CGImageRef imageRef = image.CGImage;
- if (!imageRef) {
- // Only decode for CGImage-based
- return image;
- }
-
- if (decodeSolution == SDImageCoderDecodeSolutionCoreGraphics) {
- CGImageRef decodedImageRef = [self CGImageCreateDecoded:imageRef];
- #if SD_MAC
- decodedImage = [[UIImage alloc] initWithCGImage:decodedImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
- #else
- decodedImage = [[UIImage alloc] initWithCGImage:decodedImageRef scale:image.scale orientation:image.imageOrientation];
- #endif
- CGImageRelease(decodedImageRef);
- } else {
- BOOL hasAlpha = [self CGImageContainsAlpha:imageRef];
- // Prefer to use new Image Renderer to re-draw image, instead of low-level CGBitmapContext and CGContextDrawImage
- // This can keep both OS compatible and don't fight with Apple's performance optimization
- SDGraphicsImageRendererFormat *format = SDGraphicsImageRendererFormat.preferredFormat;
- format.opaque = !hasAlpha;
- format.scale = image.scale;
- CGSize imageSize = image.size;
- SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
- decodedImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
- [image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
- }];
- }
- SDImageCopyAssociatedObject(image, decodedImage);
- decodedImage.sd_isDecoded = YES;
- return decodedImage;
- }
- + (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
- return [self decodedAndScaledDownImageWithImage:image limitBytes:bytes policy:SDImageForceDecodePolicyAutomatic];
- }
- + (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes policy:(SDImageForceDecodePolicy)policy {
- if (![self shouldDecodeImage:image policy:policy]) {
- return image;
- }
-
- CGFloat destTotalPixels;
- CGFloat tileTotalPixels;
- if (bytes == 0) {
- bytes = [self defaultScaleDownLimitBytes];
- }
- bytes = MAX(bytes, kBytesPerPixel);
- destTotalPixels = bytes / kBytesPerPixel;
- tileTotalPixels = destTotalPixels / 3;
-
- CGImageRef sourceImageRef = image.CGImage;
- if (!sourceImageRef) {
- // Only decode for CGImage-based
- return image;
- }
- CGSize sourceResolution = CGSizeZero;
- sourceResolution.width = CGImageGetWidth(sourceImageRef);
- sourceResolution.height = CGImageGetHeight(sourceImageRef);
-
- if (![self shouldScaleDownImagePixelSize:sourceResolution limitBytes:bytes]) {
- return [self decodedImageWithImage:image];
- }
-
- 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 = MAX(1, (int)(sourceResolution.width * imageScale));
- destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale));
-
- UIImage *decodedImage;
- #if SD_UIKIT
- SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution;
- if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) {
- // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :(
- SDImageFormat format = image.sd_imageFormat;
- if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) {
- decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
- } else if (format == SDImageFormatJPEG) {
- decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
- }
- } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) {
- // Arbitrarily call CMPhoto
- decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
- }
- if (decodedImage) {
- return decodedImage;
- }
- #endif
-
- // 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 {
- // device color space
- CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];
- BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef];
-
- // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
- // Check #3330 for more detail about why this bitmap is choosen.
- // From v5.17.0, use runtime detection of bitmap info instead of hardcode.
- CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo;
- CGContextRef 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 bits 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 operation by anchoring 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 = MAX(1, (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 output 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 ) {
- 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 = MIN(0, destTile.origin.y + dify);
- }
- CGContextDrawImage( destContext, destTile, sourceTileImageRef );
- CGImageRelease( sourceTileImageRef );
- }
-
- CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
- CGContextRelease(destContext);
- if (destImageRef == NULL) {
- return image;
- }
- #if SD_MAC
- decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
- #else
- decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
- #endif
- CGImageRelease(destImageRef);
- SDImageCopyAssociatedObject(image, decodedImage);
- decodedImage.sd_isDecoded = YES;
- return decodedImage;
- }
- }
- + (SDImageCoderDecodeSolution)defaultDecodeSolution {
- return kDefaultDecodeSolution;
- }
- + (void)setDefaultDecodeSolution:(SDImageCoderDecodeSolution)defaultDecodeSolution {
- kDefaultDecodeSolution = defaultDecodeSolution;
- }
- + (NSUInteger)defaultScaleDownLimitBytes {
- return kDestImageLimitBytes;
- }
- + (void)setDefaultScaleDownLimitBytes:(NSUInteger)defaultScaleDownLimitBytes {
- if (defaultScaleDownLimitBytes < kBytesPerPixel) {
- return;
- }
- kDestImageLimitBytes = defaultScaleDownLimitBytes;
- }
- #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 Function
- + (BOOL)shouldDecodeImage:(nullable UIImage *)image policy:(SDImageForceDecodePolicy)policy {
- // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
- if (image == nil) {
- return NO;
- }
- // Check policy (never)
- if (policy == SDImageForceDecodePolicyNever) {
- return NO;
- }
- // Avoid extra decode
- if (image.sd_isDecoded) {
- return NO;
- }
- // do not decode animated images
- if (image.sd_isAnimated) {
- return NO;
- }
- // do not decode vector images
- if (image.sd_isVector) {
- return NO;
- }
- // Check policy (always)
- if (policy == SDImageForceDecodePolicyAlways) {
- return YES;
- } else {
- // Check policy (automatic)
- CGImageRef cgImage = image.CGImage;
- if (cgImage) {
- CFStringRef uttype = CGImageGetUTType(cgImage);
- if (uttype) {
- // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier`
- return YES;
- } else {
- // Now, let's check if the CGImage is hardware supported (not byte-aligned will cause extra copy)
- BOOL isSupported = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage];
- return !isSupported;
- }
- }
- }
- return YES;
- }
- + (BOOL)shouldScaleDownImagePixelSize:(CGSize)sourceResolution limitBytes:(NSUInteger)bytes {
- BOOL shouldScaleDown = YES;
-
- CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
- if (sourceTotalPixels <= 0) {
- return NO;
- }
- CGFloat destTotalPixels;
- if (bytes == 0) {
- bytes = [self defaultScaleDownLimitBytes];
- }
- bytes = MAX(bytes, kBytesPerPixel);
- destTotalPixels = bytes / kBytesPerPixel;
- CGFloat imageScale = destTotalPixels / sourceTotalPixels;
- if (imageScale < 1) {
- shouldScaleDown = YES;
- } else {
- shouldScaleDown = NO;
- }
-
- return shouldScaleDown;
- }
- 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
|