From f4706453d6de61b2b397e1fee26d517cd9f4e6b3 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 10 Nov 2022 23:47:34 +0800 Subject: [PATCH 1/2] Use CoreGraphics to decode PDF instead of ImageIO to solve iOS 16's issue We no longer use ImageIO to decode PDF, seems they don't maintain that kSDCGImageSourceRasterizationDPI Copy the code from SDWebImagePDFCoder, need to update --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 8 +- SDWebImage/Core/SDImageIOCoder.m | 108 +++++++++++++++-------- Tests/Tests/SDImageCoderTests.m | 33 +++++-- 3 files changed, 103 insertions(+), 46 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 893eb350..56fbf22c 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -18,8 +18,6 @@ #import #import -// Specify DPI for vector format in CGImageSource, like PDF -static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizationDPI"; // Specify File Size for lossy format encoding, like JPEG static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize"; @@ -568,6 +566,12 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) { return nil; } NSMutableDictionary *properties = [NSMutableDictionary dictionary]; +#if SD_UIKIT || SD_WATCH + CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation]; +#else + CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp; +#endif + properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation); // Encoding Options double compressionQuality = 1; if (options[SDImageCoderEncodeCompressionQuality]) { diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index 489a2d18..3d22404f 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -10,13 +10,12 @@ #import "SDImageCoderHelper.h" #import "NSImage+Compatibility.h" #import "UIImage+Metadata.h" +#import "SDImageGraphics.h" #import "SDImageIOAnimatedCoderInternal.h" #import #import -// Specify DPI for vector format in CGImageSource, like PDF -static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizationDPI"; // Specify File Size for lossy format encoding, like JPEG static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize"; @@ -57,29 +56,65 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination return coder; } -#pragma mark - Utils -+ (CGRect)boxRectFromPDFFData:(nonnull NSData *)data { +#pragma mark - Bitmap PDF representation +- (UIImage *)createBitmapPDFWithData:(nonnull NSData *)data pageNumber:(NSUInteger)pageNumber targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio { + NSParameterAssert(data); + UIImage *image; + CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data); if (!provider) { - return CGRectZero; + return nil; } CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider); CGDataProviderRelease(provider); if (!document) { - return CGRectZero; + return nil; } // `CGPDFDocumentGetPage` page number is 1-indexed. - CGPDFPageRef page = CGPDFDocumentGetPage(document, 1); + CGPDFPageRef page = CGPDFDocumentGetPage(document, pageNumber + 1); if (!page) { CGPDFDocumentRelease(document); - return CGRectZero; + return nil; } - CGRect boxRect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox); + CGPDFBox box = kCGPDFMediaBox; + CGRect rect = CGPDFPageGetBoxRect(page, box); + CGRect targetRect = rect; + if (!CGSizeEqualToSize(targetSize, CGSizeZero)) { + targetRect = CGRectMake(0, 0, targetSize.width, targetSize.height); + } + + CGFloat xRatio = targetRect.size.width / rect.size.width; + CGFloat yRatio = targetRect.size.height / rect.size.height; + CGFloat xScale = preserveAspectRatio ? MIN(xRatio, yRatio) : xRatio; + CGFloat yScale = preserveAspectRatio ? MIN(xRatio, yRatio) : yRatio; + + // `CGPDFPageGetDrawingTransform` will only scale down, but not scale up, so we need calculate the actual scale again + CGRect drawRect = CGRectMake( 0, 0, targetRect.size.width / xScale, targetRect.size.height / yScale); + CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScale, yScale); + CGAffineTransform transform = CGPDFPageGetDrawingTransform(page, box, drawRect, 0, preserveAspectRatio); + + SDGraphicsBeginImageContextWithOptions(targetRect.size, NO, 0); + CGContextRef context = SDGraphicsGetCurrentContext(); + +#if SD_UIKIT || SD_WATCH + // Core Graphics coordinate system use the bottom-left, UIKit use the flipped one + CGContextTranslateCTM(context, 0, targetRect.size.height); + CGContextScaleCTM(context, 1, -1); +#endif + + CGContextConcatCTM(context, scaleTransform); + CGContextConcatCTM(context, transform); + + CGContextDrawPDFPage(context, page); + + image = SDGraphicsGetImageFromCurrentImageContext(); + SDGraphicsEndImageContext(); + CGPDFDocumentRelease(document); - return boxRect; + return image; } #pragma mark - Decode @@ -113,6 +148,31 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination preserveAspectRatio = preserveAspectRatioValue.boolValue; } + // Check vector format + if ([NSData sd_imageFormatForImageData:data] == SDImageFormatPDF) { + // History before iOS 16, ImageIO can decode PDF with rasterization size, but can't ever :( + // So, use CoreGraphics to decode PDF (copy code from SDWebImagePDFCoder, may do refactor in the future) + UIImage *image; + NSUInteger pageNumber = 0; // Still use first page, may added options is user want +#if SD_MAC + // If don't use thumbnail, prefers the built-in generation of vector image + // macOS's `NSImage` supports PDF built-in rendering + if (thumbnailSize.width == 0 || thumbnailSize.height == 0) { + NSPDFImageRep *imageRep = [[NSPDFImageRep alloc] initWithData:data]; + if (imageRep) { + imageRep.currentPage = pageNumber; + image = [[NSImage alloc] initWithSize:imageRep.size]; + [image addRepresentation:imageRep]; + image.sd_imageFormat = SDImageFormatPDF; + return image; + } + } +#endif + image = [self createBitmapPDFWithData:data pageNumber:pageNumber targetSize:thumbnailSize preserveAspectRatio:preserveAspectRatio]; + image.sd_imageFormat = SDImageFormatPDF; + return image; + } + BOOL lazyDecode = YES; // Defaults YES for static image coder NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; if (lazyDecodeValue != nil) { @@ -150,35 +210,9 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination CFStringRef uttype = CGImageSourceGetType(source); SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype]; - // Check vector format - NSDictionary *decodingOptions = nil; - if (imageFormat == SDImageFormatPDF) { - // Use 72 DPI (1:1 inch to pixel) by default, matching Apple's PDFKit behavior - NSUInteger rasterizationDPI = 72; - CGFloat maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height); - if (maxPixelSize > 0) { - // Calculate DPI based on PDF box and pixel size - CGRect boxRect = [self.class boxRectFromPDFFData:data]; - CGFloat maxBoxSize = MAX(boxRect.size.width, boxRect.size.height); - if (maxBoxSize > 0) { - rasterizationDPI = rasterizationDPI * (maxPixelSize / maxBoxSize); - } - } - decodingOptions = @{ - // This option will cause ImageIO return the pixel size from `CGImageSourceCopyProperties` - // If not provided, it always return 0 size - kSDCGImageSourceRasterizationDPI : @(rasterizationDPI), - }; - // Already calculated DPI, avoid re-calculation based on thumbnail information - preserveAspectRatio = YES; - thumbnailSize = CGSizeZero; - } - UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:decodingOptions]; + UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:nil]; CFRelease(source); - if (!image) { - return nil; - } image.sd_imageFormat = imageFormat; return image; diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 510529bd..566f333b 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -11,12 +11,6 @@ #import "UIColor+SDHexString.h" #import -@interface SDImageIOCoder () - -+ (CGRect)boxRectFromPDFFData:(nonnull NSData *)data; - -@end - @interface SDWebImageDecoderTests : SDTestCase @end @@ -484,7 +478,7 @@ withLocalImageURL:(NSURL *)imageUrl expect(pixelHeight).beGreaterThan(0); // check vector format should use 72 DPI if (isVector) { - CGRect boxRect = [SDImageIOCoder boxRectFromPDFFData:inputImageData]; + CGRect boxRect = [self boxRectFromPDFData:inputImageData]; expect(boxRect.size.width).beGreaterThan(0); expect(boxRect.size.height).beGreaterThan(0); // Since 72 DPI is 1:1 from inch size to pixel size @@ -564,4 +558,29 @@ withLocalImageURL:(NSURL *)imageUrl return thumbnailImages; } +#pragma mark - Utils +- (CGRect)boxRectFromPDFData:(nonnull NSData *)data { + CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data); + if (!provider) { + return CGRectZero; + } + CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider); + CGDataProviderRelease(provider); + if (!document) { + return CGRectZero; + } + + // `CGPDFDocumentGetPage` page number is 1-indexed. + CGPDFPageRef page = CGPDFDocumentGetPage(document, 1); + if (!page) { + CGPDFDocumentRelease(document); + return CGRectZero; + } + + CGRect boxRect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox); + CGPDFDocumentRelease(document); + + return boxRect; +} + @end From 30f165abd5444cf83a8e30f239139c820dd926e6 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 11 Nov 2022 00:07:32 +0800 Subject: [PATCH 2/2] Change from instance method to class method --- SDWebImage/Core/SDImageIOCoder.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index 3d22404f..72fc4c6f 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -57,7 +57,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination } #pragma mark - Bitmap PDF representation -- (UIImage *)createBitmapPDFWithData:(nonnull NSData *)data pageNumber:(NSUInteger)pageNumber targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio { ++ (UIImage *)createBitmapPDFWithData:(nonnull NSData *)data pageNumber:(NSUInteger)pageNumber targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio { NSParameterAssert(data); UIImage *image; @@ -168,7 +168,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination } } #endif - image = [self createBitmapPDFWithData:data pageNumber:pageNumber targetSize:thumbnailSize preserveAspectRatio:preserveAspectRatio]; + image = [self.class createBitmapPDFWithData:data pageNumber:pageNumber targetSize:thumbnailSize preserveAspectRatio:preserveAspectRatio]; image.sd_imageFormat = SDImageFormatPDF; return image; }