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..72fc4c6f 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.class 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