TwitterImagePipeline/TIPImageTypes.m (372 lines of code) (raw):
//
// TIPImageTypes.m
// TwitterImagePipeline
//
// Created on 8/1/16.
// Copyright © 2020 Twitter. All rights reserved.
//
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import "TIP_Project.h"
#import "TIPImageCodecCatalogue.h"
#import "TIPImageTypes.h"
NS_ASSUME_NONNULL_BEGIN
static const UInt8 kBMP1MagicNumbers[] = { 0x42, 0x4D };
static const UInt8 kJPEGMagicNumbers[] = { 0xFF, 0xD8, 0xFF };
static const UInt8 kGIFMagicNumbers[] = { 0x47, 0x49, 0x46 };
static const UInt8 kPNGMagicNumbers[] = { 0x89, 0x50, 0x4E, 0x47,
0x0D, 0x0A, 0x1A, 0x0A };
static const UInt8 kWEBPMagicNumbers[] = { 'R', 'I', 'F', 'F',
'\0', '\0', '\0', '\0',
'W', 'E', 'B', 'P' };
#define BIGGER(x, y) ((x) > (y) ? (x) : (y))
// statically assign the largest magic number at compile time
const NSUInteger TIPMagicNumbersForImageTypeMaximumLength =
(NSUInteger)BIGGER(sizeof(kPNGMagicNumbers),
BIGGER(sizeof(kGIFMagicNumbers),
BIGGER(sizeof(kJPEGMagicNumbers),
BIGGER(sizeof(kBMP1MagicNumbers),
sizeof(kWEBPMagicNumbers)))));
#define MAGIC_NUMBERS_ARE_EQUAL(bytes, len, magicNumber) \
( (len >= sizeof( magicNumber )) && (memcmp(bytes, magicNumber, sizeof( magicNumber )) == 0) )
#define TIPWorkAroundCoreGraphicsUTTypeLoadBug() \
do { \
/** \
Annoying bug in Apple's CoreGraphics will FAIL to load certain image formats (like RAW) \
until the identifiers list has been hydrated. \
Call `TIPReadableImageTypes` to force that hydration. \
Doesn't seem to affect iOS 8, but definitely affects iOS 9+. \
NOTE: This will trigger XPC on first access via `CGImageSourceCopyTypeIdentifiers(...)` \
*/ \
(void)TIPReadableImageTypes(); \
} while (0)
#pragma mark - Image Types
NSString * const TIPImageTypeJPEG = @"public.jpeg";
NSString * const TIPImageTypeJPEG2000 = @"public.jpeg-2000";
NSString * const TIPImageTypePNG = @"public.png";
NSString * const TIPImageTypeGIF = @"com.compuserve.gif";
NSString * const TIPImageTypeTIFF = @"public.tiff";
NSString * const TIPImageTypeBMP = @"com.microsoft.bmp";
NSString * const TIPImageTypeTARGA = @"com.truevision.tga-image";
NSString * const TIPImageTypePICT = @"com.apple.pict";
NSString * const TIPImageTypeQTIF = @"com.apple.quicktime-image";
NSString * const TIPImageTypeICNS = @"com.apple.icns";
NSString * const TIPImageTypeICO = @"com.microsoft.ico";
NSString * const TIPImageTypeRAW = @"public.camera-raw-image";
NSString * const TIPImageTypeHEIC = @"public.heic";
NSString * const TIPImageTypeAVCI = @"public.avci";
NSString * const TIPImageTypeWEBP = @"org.webmproject.webp";
#pragma mark - Static Functions
static NSSet<NSString *> *TIPReadableImageTypes()
{
static NSSet<NSString *> *sReadableImageTypes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CFArrayRef typeIds = CGImageSourceCopyTypeIdentifiers();
TIPDeferRelease(typeIds);
sReadableImageTypes = [NSSet setWithArray:(__bridge NSArray *)typeIds];
});
return sReadableImageTypes;
}
static NSSet<NSString *> *TIPWriteableImageTypes()
{
static NSSet<NSString *> *sWriteableImageTypes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CFArrayRef typeIds = CGImageDestinationCopyTypeIdentifiers();
TIPDeferRelease(typeIds);
NSMutableSet<NSString *> *set = [NSMutableSet setWithArray:(__bridge NSArray *)typeIds];
// forcibly remove formats that are too restrictive for write support
[set removeObject:TIPImageTypeICO];
[set removeObject:TIPImageTypeICNS];
sWriteableImageTypes = [set copy];
});
return sWriteableImageTypes;
}
#pragma mark - Functions
NSSet<NSString *> * TIPDetectableImageTypesViaMagicNumbers(void)
{
static NSSet<NSString *> *sTypes = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sTypes = [NSSet setWithObjects: TIPImageTypeBMP,
TIPImageTypeJPEG,
TIPImageTypeGIF,
TIPImageTypePNG,
TIPImageTypeWEBP,
nil];
});
return sTypes;
}
NSString * __nullable TIPDetectImageTypeViaMagicNumbers(NSData *dataObj)
{
CFDataRef data = (__bridge CFDataRef)dataObj;
const size_t length = (data) ? (size_t)CFDataGetLength(data) : 0;
const UInt8 *bytes = (data) ? CFDataGetBytePtr(data) : NULL;
if (MAGIC_NUMBERS_ARE_EQUAL(bytes, length, kJPEGMagicNumbers)) {
// kUTTypeJPEG;
return TIPImageTypeJPEG;
}
if (MAGIC_NUMBERS_ARE_EQUAL(bytes, length, kPNGMagicNumbers)) {
// kUTTypePNG;
return TIPImageTypePNG;
}
if (MAGIC_NUMBERS_ARE_EQUAL(bytes, length, kBMP1MagicNumbers)) {
// kUTTypeBMP;
return TIPImageTypeBMP;
}
if (MAGIC_NUMBERS_ARE_EQUAL(bytes, length, kGIFMagicNumbers)) {
// kUTTypeGIF;
return TIPImageTypeGIF;
}
if (length >= 12) {
// WebP has 2 magic numbers flanking real data,
// so we need to split the check.
if (0 == memcmp(bytes, kWEBPMagicNumbers, 4)) {
if (0 == memcmp(bytes + 8, kWEBPMagicNumbers + 8, 4)) {
// kUTTypeWEBP
return TIPImageTypeWEBP;
}
}
}
return nil;
}
static BOOL TIPImageTypeHasProgressiveVariant(NSString * __nullable type)
{
const BOOL hasProgressiveVariant = [type isEqualToString:TIPImageTypeJPEG]
|| [type isEqualToString:TIPImageTypeJPEG2000]
|| [type isEqualToString:TIPImageTypePNG];
return hasProgressiveVariant;
}
BOOL TIPImageTypeSupportsLossyQuality(NSString * __nullable type)
{
const BOOL hasLossy = [type isEqualToString:TIPImageTypeJPEG]
|| [type isEqualToString:TIPImageTypeJPEG2000]
|| [type isEqualToString:TIPImageTypeHEIC]
|| [type isEqualToString:TIPImageTypeAVCI];
// though GIF can be munged many ways to change the quality,
// we'll keep it simple and not treat GIF as supporting lossy quality
return hasLossy;
}
BOOL TIPImageTypeSupportsIndexedPalette(NSString * __nullable type)
{
const BOOL supportsPalette = [type isEqualToString:TIPImageTypePNG]
|| [type isEqualToString:TIPImageTypeGIF];
// GIF actually _only_ supports indexed palette encoding.
return supportsPalette;
}
NSString * __nullable TIPImageTypeToUTType(NSString * __nullable type)
{
TIPWorkAroundCoreGraphicsUTTypeLoadBug();
if (UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeImage)) {
return type;
}
return nil;
}
NSString * __nullable TIPImageTypeFromUTType(NSString * __nullable utType)
{
CFStringRef imageType = (__bridge CFStringRef)utType;
// We check RAW first since RAW camera images can be detected as both RAW and TIFF and we'll bias to RAW
const BOOL isTypeRawImage = (BOOL)UTTypeConformsTo(imageType, kUTTypeRawImage) || [utType isEqualToString:TIPImageTypeRAW];
if (isTypeRawImage) {
return TIPImageTypeRAW;
} else if (UTTypeConformsTo(imageType, kUTTypeJPEG)) {
return TIPImageTypeJPEG;
} else if (UTTypeConformsTo(imageType, kUTTypePNG)) {
return TIPImageTypePNG;
} else if (UTTypeConformsTo(imageType, kUTTypeJPEG2000)) {
return TIPImageTypeJPEG2000;
} else if (UTTypeConformsTo(imageType, kUTTypeGIF)) {
return TIPImageTypeGIF;
} else if (UTTypeConformsTo(imageType, kUTTypeTIFF)) {
return TIPImageTypeTIFF;
} else if (UTTypeConformsTo(imageType, kUTTypeBMP)) {
return TIPImageTypeBMP;
} else if (UTTypeConformsTo(imageType, CFSTR("com.truevision.tga-image"))) {
return TIPImageTypeTARGA;
} else if (UTTypeConformsTo(imageType, kUTTypePICT)) {
return TIPImageTypePICT;
} else if (UTTypeConformsTo(imageType, kUTTypeICO)) {
return TIPImageTypeICO;
} else if (UTTypeConformsTo(imageType, kUTTypeAppleICNS)) {
return TIPImageTypeICNS;
} else if (UTTypeConformsTo(imageType, kUTTypeQuickTimeImage)) {
return TIPImageTypeQTIF;
} else if (UTTypeConformsTo(imageType, CFSTR("public.heic"))) {
return TIPImageTypeHEIC;
} else if (UTTypeConformsTo(imageType, CFSTR("public.avci"))) {
return TIPImageTypeAVCI;
} else if (UTTypeConformsTo(imageType, CFSTR("org.webmproject.webp"))) {
return TIPImageTypeWEBP;
}
return nil;
}
NSString * __nullable TIPFileExtensionFromUTType(NSString * __nullable utType)
{
if (!utType) {
return nil;
}
return (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)utType,
kUTTagClassFilenameExtension));
}
NSString * __nullable TIPFileExtensionToUTType(NSString * __nullable fileExtension, BOOL mustBeImageUTType)
{
if (!fileExtension) {
return nil;
}
return (NSString *)CFBridgingRelease(UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
(__bridge CFStringRef)fileExtension,
(mustBeImageUTType) ? kUTTypeImage : NULL));
}
BOOL TIPImageTypeCanReadWithImageIO(NSString * __nullable imageType)
{
return (imageType) ? [TIPReadableImageTypes() containsObject:imageType] : NO;
}
BOOL TIPImageTypeCanWriteWithImageIO(NSString * __nullable imageType)
{
return (imageType) ? [TIPWriteableImageTypes() containsObject:imageType] : NO;
}
TIPRecommendedImageTypeOptions TIPRecommendedImageTypeOptionsFromEncodingOptions(TIPImageEncodingOptions encodingOptions, float quality)
{
TIPRecommendedImageTypeOptions options = 0;
if (quality < 1.f) {
options |= TIPRecommendedImageTypePermitLossy;
}
if (TIP_BITMASK_HAS_SUBSET_FLAGS(encodingOptions, TIPImageEncodingProgressive)) {
options |= TIPRecommendedImageTypePreferProgressive;
}
if (TIP_BITMASK_HAS_SUBSET_FLAGS(encodingOptions, TIPImageEncodingNoAlpha)) {
options |= TIPRecommendedImageTypeAssumeNoAlpha;
}
return options;
}
#pragma mark Inspection stuff
static NSString * __nullable _DetectImageTypeFromImageSource(CGImageSourceRef imageSourceRef,
TIPImageEncodingOptions * __nullable optionsOut,
NSUInteger * __nullable animationFrameCountOut)
{
NSString *type = nil;
TIPImageEncodingOptions optionsRead = TIPImageEncodingNoOptions;
NSUInteger animationFrameCount = 1;
TIPWorkAroundCoreGraphicsUTTypeLoadBug();
if (imageSourceRef != NULL) {
// Read type
CFStringRef imageTypeStringRef = CGImageSourceGetType(imageSourceRef);
if (imageTypeStringRef != NULL) {
type = TIPImageTypeFromUTType((__bridge NSString *)imageTypeStringRef);
}
#if DEBUG && TEST_CODE
// Test image construction
for (NSUInteger i = 0; i < CGImageSourceGetCount(imageSourceRef); i++) {
CGImageRef dbgImageRef = CGImageSourceCreateImageAtIndex(imageSourceRef, i, NULL);
TIPDeferRelease(dbgImageRef);
UIImage *dbgImage = [UIImage imageWithCGImage:dbgImageRef];
if (!CGSizeEqualToSize(CGSizeZero, dbgImage.size)) {
dbgImage = nil;
}
}
#endif
if (optionsOut) {
// Options are desired, check for progressive
if (TIPImageTypeHasProgressiveVariant(type)) {
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSourceRef, 0, NULL);
TIPDeferRelease(imageProperties);
if (imageProperties != NULL) {
if ([(NSNumber *)CFDictionaryGetValue(imageProperties, kCGImagePropertyIsIndexed) boolValue]) {
optionsRead |= TIPImageEncodingIndexedColorPalette;
}
BOOL progressive = NO;
if ([type isEqualToString:TIPImageTypeJPEG]) {
CFDictionaryRef jfifProperties = CFDictionaryGetValue(imageProperties, kCGImagePropertyJFIFDictionary);
if (jfifProperties) {
CFBooleanRef isProgressiveBool = CFDictionaryGetValue(jfifProperties, kCGImagePropertyJFIFIsProgressive) ?: kCFBooleanFalse;
progressive = !!CFBooleanGetValue(isProgressiveBool);
}
} else if ([type isEqualToString:TIPImageTypePNG]) {
CFDictionaryRef pngProperties = CFDictionaryGetValue(imageProperties, kCGImagePropertyPNGDictionary);
if (pngProperties) {
CFTypeRef interlaceType = CFDictionaryGetValue(pngProperties, kCGImagePropertyPNGInterlaceType) ?: NULL;
NSNumber *interlaceTypeNumber = (__bridge NSNumber *)interlaceType;
if ([interlaceTypeNumber unsignedIntegerValue] == 1 /* Adam7 Interlaced Encoding */) {
progressive = YES;
}
}
} else if (TIP_BITMASK_EXCLUDES_FLAGS(optionsRead, TIPImageEncodingIndexedColorPalette) && [type isEqualToString:TIPImageTypeGIF]) {
optionsRead |= TIPImageEncodingIndexedColorPalette;
}
if (progressive) {
optionsRead |= TIPImageEncodingProgressive;
}
}
}
}
if (animationFrameCountOut) {
// Read image count (if potentially animated)
if ([[TIPImageCodecCatalogue sharedInstance] codecWithImageTypeSupportsAnimation:type]) {
animationFrameCount = (NSUInteger)MAX((size_t)1, CGImageSourceGetCount(imageSourceRef));
}
}
}
if (optionsOut) {
*optionsOut = optionsRead;
}
if (animationFrameCountOut) {
*animationFrameCountOut = animationFrameCount;
}
return type;
}
NSString * __nullable TIPDetectImageTypeFromFile(NSURL *filePath,
TIPImageEncodingOptions * __nullable optionsOut,
NSUInteger * __nullable animationFrameCountOut)
{
if (filePath.isFileURL) {
NSDictionary *options = @{ (NSString *)kCGImageSourceShouldCache : @NO };
CGImageSourceRef imageSourceRef = CGImageSourceCreateWithURL((CFURLRef)filePath, (CFDictionaryRef)options);
TIPDeferRelease(imageSourceRef);
if (imageSourceRef != NULL) {
return _DetectImageTypeFromImageSource(imageSourceRef, optionsOut, animationFrameCountOut);
}
}
if (optionsOut) {
*optionsOut = TIPImageEncodingNoOptions;
}
if (animationFrameCountOut) {
*animationFrameCountOut = 1;
}
// Couldn't detect, try magic numbers
if (filePath.isFileURL) {
FILE *file = fopen(filePath.path.UTF8String, "r");
if (file) {
tip_defer(^{ fclose(file); });
char buffer[TIPMagicNumbersForImageTypeMaximumLength] = { 0 };
fread(buffer, 1, TIPMagicNumbersForImageTypeMaximumLength, file);
NSData *data = [NSData dataWithBytesNoCopy:buffer length:TIPMagicNumbersForImageTypeMaximumLength freeWhenDone:NO];
return TIPDetectImageTypeViaMagicNumbers(data);
}
}
return nil;
}
NSString * __nullable TIPDetectImageType(NSData *data,
TIPImageEncodingOptions * __nullable optionsOut,
NSUInteger * __nullable animationFrameCountOut,
BOOL hasCompleteImageData)
{
NSDictionary *options = @{ (NSString *)kCGImageSourceShouldCache : @NO };
CGImageSourceRef imageSourceRef = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)options);
TIPDeferRelease(imageSourceRef);
if (imageSourceRef != NULL && data != nil) {
CGImageSourceUpdateData(imageSourceRef, (__bridge CFDataRef)data, hasCompleteImageData);
return _DetectImageTypeFromImageSource(imageSourceRef, optionsOut, animationFrameCountOut);
}
if (optionsOut) {
*optionsOut = TIPImageEncodingNoOptions;
}
if (animationFrameCountOut) {
*animationFrameCountOut = 1;
}
// Couldn't detect, try magic numbers
return TIPDetectImageTypeViaMagicNumbers(data);
}
NSUInteger TIPImageDetectProgressiveScanCount(NSData *data)
{
size_t byteIndex = 0;
size_t length = (size_t)data.length;
const UInt8 *bytes = (const UInt8 *)data.bytes;
if (length <= 10) {
return 0;
}
if (!MAGIC_NUMBERS_ARE_EQUAL(bytes, length, kJPEGMagicNumbers)) {
return 0;
}
byteIndex += sizeof(kJPEGMagicNumbers);
for (; byteIndex < length; byteIndex++) {
if (bytes[byteIndex] == 0xFF) {
byteIndex++;
if (bytes[byteIndex] == 0xC0) {
return 0; // not progressive
} else if (bytes[byteIndex] == 0xC2) {
byteIndex++;
break;
}
}
}
NSUInteger count = 0;
for (; byteIndex < length; byteIndex++) {
if (bytes[byteIndex] == 0xFF) {
byteIndex++;
if (bytes[byteIndex] == 0xDA) {
// start of new scan
count++;
}
}
}
return count;
}
NS_ASSUME_NONNULL_END