基於 NSData 的圖片壓縮

yang152412發表於2019-02-27

當前網上找到的圖片壓縮方法大部分都是基於 UIImage 的,但是如果要支援 gif 的話,那麼從相簿讀出來就得是 NSData,就必須要基於 NSData 來做壓縮了。

主要用到了 <ImageIO><CoreGraphics> 庫。
步驟就是

1. `CGImageSource` 從 data讀出 `CGImageSourceRef` 物件;
2.  `CGImageSourceRef` 中讀取 `CGImageRef`;
3. 經過 旋轉、調整大小、壓縮質量;
4. 然後用 `CGImageDestination` 重新寫入新的`data`。
複製程式碼

一、 靜態圖片,並轉為 jpg

1、通過 CGImageSourceRef 讀取資料

//通過CFData讀取檔案的資料
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
    //獲取檔案的幀數
    size_t count = CGImageSourceGetCount(source);
    if (count == 0) {
        CFRelease(source);
        return nil;
    }
複製程式碼

2、 讀取 每一幀的屬性,一般 source 的 count 都是1,GifLive Photo的都是大於1的。

// 影像屬性
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil);
    //影像的旋轉方向
    CGImagePropertyOrientation orientation = (uint32_t)[(__bridge NSNumber *)CFDictionaryGetValue(cfFrameProperties, kCGImagePropertyOrientation) integerValue];
    if (orientation == 0) { // 方向丟失
        orientation = kCGImagePropertyOrientationUp;
    }
複製程式碼

3、讀取到 CGImageRef

imageRef = CGImageSourceCreateImageAtIndex(source, 0, NULL);
複製程式碼

4、處理 大小,旋轉

#pragma mark  調整大小,自動旋轉,減少分開的計算而已
CGImageRef DDCreateResizeAndUpOrientationImageRef(CGImageRef imageRef, CGSize targetSize, CGImagePropertyOrientation currentOrientation)
{
    size_t width = targetSize.width;
    size_t height = targetSize.height;
    // orientation 如果是左右的話,那麼 size 要反一下,否則 寬高不對。
    switch (currentOrientation) {
        case kCGImagePropertyOrientationLeft:
        case kCGImagePropertyOrientationLeftMirrored:
        case kCGImagePropertyOrientationRight:
        case kCGImagePropertyOrientationRightMirrored:
        {
            size_t temp = width;
            width = height;
            height = temp;
        }
            break;
        default:
            break;
    }
    
    CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
    if((bitmapInfo == kCGImageAlphaLast) || (bitmapInfo == kCGImageAlphaNone))
        bitmapInfo = (CGBitmapInfo)kCGImageAlphaNoneSkipLast;
    size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
//    size_t bytesPerRow = width * 4;
    size_t bytesPerRow = 0;
    // create context
    CGContextRef context = CGBitmapContextCreate(NULL,
                                                 width,
                                                 height,
                                                 bitsPerComponent,
                                                 bytesPerRow,
                                                 space,
                                                 bitmapInfo);
    CGColorSpaceRelease(space); // release space
    if (!context) {
        return nil;
    }
    CGContextSetInterpolationQuality(context, kCGInterpolationMedium);
    if (currentOrientation != kCGImagePropertyOrientationUp) {
        // 需要旋轉 圖片
        CGAffineTransform transform = TransformForOrientation(currentOrientation, CGSizeMake(width, height));
        CGContextConcatCTM(context, transform);
        
        CGRect transposedRect = CGRectMake(0, 0, height, width);
        CGContextDrawImage(context, transposedRect, imageRef);
    } else {
        CGRect newRect = CGRectIntegral(CGRectMake(0, 0, width, height));
        CGContextDrawImage(context, newRect, imageRef);
    }
    
    // get new imageRef
    CGImageRef decoded = CGBitmapContextCreateImage(context);
    CFRelease(context);
    if (!decoded) {
        return nil;
    }
    return decoded;
}
複製程式碼

5、調整質量

通過 CGImageDestinationRef 寫入 data,設定 kCGImageDestinationLossyCompressionQuality 引數來調整質量

引數主要是前面讀取的CGImageRef的屬性,這裡因為不想重新讀,所以就當引數傳遞了。

在 這裡之前,需要 重新設定 圖片的 orientation,否則結果 orientation 會丟失,造成旋轉失效

// 修改引數 orientation,如果上面修改了 orientation,則這裡也必須修改,否則會丟失新旋轉。
    CFMutableDictionaryRef mutDicRef = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, cfFrameProperties);
    CGFloat width = newSize.width;
    CGFloat height = newSize.height;
    CFNumberRef widthNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &width);
    CFNumberRef heightNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &height);
    CFDictionarySetValue(mutDicRef, kCGImagePropertyPixelWidth, widthNum);
    CFDictionarySetValue(mutDicRef, kCGImagePropertyPixelHeight, heightNum);
    
    CGImagePropertyOrientation newOrientation = kCGImagePropertyOrientationUp;
    CFNumberRef newOrientationNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &newOrientation);
    CFDictionarySetValue(mutDicRef, kCGImagePropertyOrientation, newOrientationNum);
複製程式碼
+ (NSData *)getCompressImageDataWIthImageRef:(CGImageRef)imageRef uttype:(CFStringRef)uttype properties:(CFDictionaryRef)properties isToJPG:(BOOL)isToJPG toMaxBytes:(NSUInteger)maxBytes
{
    // 轉為 jpg,並壓縮質量
    CFStringRef type = nil;
    if (isToJPG) {
        type = kUTTypeJPEG;
    } else if (uttype) {
        type = CFStringCreateCopy(kCFAllocatorDefault, uttype);
    } else {
        if (@available(iOS 9.0, *)) {
            type = CGImageGetUTType(imageRef);
        }
    }
    BOOL success = NO;
    NSMutableData *mutableData = nil;
    do {
        mutableData = [NSMutableData data];
        // destination 的 type 是確定最終 image 的型別。
        CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)mutableData, type, 1, NULL);
        CFMutableDictionaryRef mutDicRef = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, properties);
        float c = 0.5;
        CFNumberRef compressionQuality = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &c);
        CFDictionarySetValue(mutDicRef, kCGImageDestinationLossyCompressionQuality, compressionQuality);
        CGImageDestinationAddImage(destination, imageRef, mutDicRef);
        success = CGImageDestinationFinalize(destination);
        
        CFRelease(destination);
        CFRelease(mutDicRef);
        CFRelease(compressionQuality);
        
        if (!success) {
            NSDictionary *userInfo = @{
                                       NSLocalizedDescriptionKey: NSLocalizedString(@"Could not finalize image destination", nil)
                                       };
            NSLog(@"resizePNGImageDataToSize %@",userInfo);
            break;
        }
    } while (mutableData.length > maxBytes);
    
    CFRelease(type);
    if (!success) {
        return nil;
    }
    return mutableData;
}
複製程式碼

6、完整程式碼

#pragma mark - 壓縮 靜態圖,並轉為 JPG
+ (NSData *)thumbnailImageData:(NSData *)imageData fitSize:(CGSize)targetSize maxBytes:(double)maxBytes minBytes:(double)minBytes toJPG:(BOOL)isToJPG
{
    if (targetSize.width == 0 || targetSize.height == 0) {
        return imageData;
    }
    
    // < 30k 也算了,不轉了
    if (imageData.length < minBytes) {
        return imageData;
    }
    CGSize size = [UIImage sizeFromImageData:imageData];
    // size 不超,bytes 也不超,也不轉了
    if (size.width <= targetSize.width && size.height <= targetSize.height &&
        imageData.length <= maxBytes) {
        return imageData;
    }
    //通過CFData讀取檔案的資料
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
    //獲取檔案的幀數
    size_t count = CGImageSourceGetCount(source);
    if (count == 0) {
        CFRelease(source);
        return nil;
    }
    
    // 計算 size
    CGSize newSize = [self fitJPGImageSize:size toSize:targetSize];
    
    // 影像屬性
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil);
    //影像的旋轉方向
    CGImagePropertyOrientation orientation = (uint32_t)[(__bridge NSNumber *)CFDictionaryGetValue(cfFrameProperties, kCGImagePropertyOrientation) integerValue];
    if (orientation == 0) { // 方向丟失
        orientation = kCGImagePropertyOrientationUp;
    }
    // 建立第一個 imageRef
    CGImageRef imageRef = NULL;
    imageRef = CGImageSourceCreateImageAtIndex(source, 0, NULL);
    if (!imageRef) {
        CFRelease(source);
        return nil;
    }
    // 1. 旋轉
    // 2. 調整大小
    if (!CGSizeEqualToSize(newSize, size)) { // 需要壓縮大小 或者 需要旋轉,進行重繪
        CGImageRef resizeImageRef = DDCreateResizeAndUpOrientationImageRef(imageRef, newSize, orientation);
        if (resizeImageRef) {
            
            CGImageRelease(imageRef);
            imageRef = NULL;
            imageRef = CGImageCreateCopy(resizeImageRef);
            CGImageRelease(resizeImageRef);
        }
    } else if (orientation != kCGImagePropertyOrientationUp){
        // 旋轉
        CGImageRef rotateRef = DDCreateRotateImageRef(imageRef, orientation);
        if (rotateRef) {
            CGImageRelease(imageRef);
            imageRef = CGImageCreateCopy(rotateRef);
            CGImageRelease(rotateRef);
        }
    }
    // 3. 轉為 jpg,並壓縮質量
    CFStringRef type = CGImageSourceGetType(source);
    if (isToJPG) {
        type = kUTTypeJPEG;
    }
    
    // 修改引數 orientation,如果上面修改了 orientation,則這裡也必須修改,否則會丟失新旋轉。
    CFMutableDictionaryRef mutDicRef = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, cfFrameProperties);
    CGFloat width = newSize.width;
    CGFloat height = newSize.height;
    CFNumberRef widthNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &width);
    CFNumberRef heightNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &height);
    CFDictionarySetValue(mutDicRef, kCGImagePropertyPixelWidth, widthNum);
    CFDictionarySetValue(mutDicRef, kCGImagePropertyPixelHeight, heightNum);
    
    CGImagePropertyOrientation newOrientation = kCGImagePropertyOrientationUp;
    CFNumberRef newOrientationNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &newOrientation);
    CFDictionarySetValue(mutDicRef, kCGImagePropertyOrientation, newOrientationNum);
    
    
    NSData *mutableData = [self getCompressImageDataWIthImageRef:imageRef uttype:type properties:mutDicRef isToJPG:isToJPG toMaxBytes:maxBytes];
    
    CFRelease(cfFrameProperties);
    CGImageRelease(imageRef);
    CFRelease(type);
    CFRelease(source);
    
    return mutableData;
}

複製程式碼

二、 gif

gif 的處理類似,主要就是迴圈遍歷 CGImageSourceRef 裡的 CGImageRef,但是gif 每一幀的圖片可能不一樣,所以壓縮質量不太好處理,只處理了畫素大小。

+ (NSData *)thumbnailGIFImageData:(NSData *)gifData fitSize:(CGSize)targetSize maxBytes:(double)maxBytes
{
    if (targetSize.width == 0 || targetSize.height == 0) {
        return gifData;
    }
    // resize new size
    CGSize size = [UIImage sizeFromImageData:gifData];
    
    if (size.width <= targetSize.width && size.height <= targetSize.height && gifData.length <= maxBytes) {
        return gifData;
    }
    // 計算新的大小
    CGSize newSize = [NSData fitGifImageSize:size toSize:targetSize];
    return [NSData resizeGifImageData:gifData toSize:newSize];
    
}
/// gif 每一幀的圖片大小應該稍微有點區別,不太一樣,所以只調整 size,不壓縮質量
+ (NSData *)resizeGifImageData:(NSData *)gifData toSize:(CGSize)targetSize
{
    if (targetSize.width == 0 || targetSize.height == 0) {
        return gifData;
    }
    //通過CFData讀取gif檔案的資料
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)gifData, NULL);
    //獲取gif檔案的幀數
    size_t count = CGImageSourceGetCount(source);
    //設定gif播放的時間
    NSTimeInterval duration = 0.0f;
    
    CFDictionaryRef imageProperties = CGImageSourceCopyProperties(source, nil);
    NSLog(@"
 CGImageSourceCopyProperties %@ 
",imageProperties);
    
    // gif data properties
    NSMutableData *mutableData = [NSMutableData data];
    CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)mutableData, kUTTypeGIF, count, NULL);
    
    CFDictionaryRef gifProperties;
    BOOL result = CFDictionaryGetValueIfPresent(imageProperties, kCGImagePropertyGIFDictionary, (const void **)&gifProperties);
    if (result) {
        CGImageDestinationSetProperties(destination, gifProperties);
    }
    CFRelease(imageProperties);
    for (size_t i = 0; i < count; i++) {
        //獲取gif指定幀的畫素點陣圖
        CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
        if (!imageRef) {
            continue;
        }
        //獲取每張圖的播放時間
        float frameDuration = [NSData frameDurationAtIndex:i source:source];
        duration += frameDuration;
        
        //
        CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, i, nil);
        //影像的旋轉方向
        CGImagePropertyOrientation orientation = (uint32_t)[(__bridge NSNumber *)CFDictionaryGetValue(cfFrameProperties, kCGImagePropertyOrientation) integerValue];
        if (orientation == 0) { // 方向丟失
            orientation = kCGImagePropertyOrientationUp;
        }
        
        CGImageRef newImageRef = DDCreateResizeAndUpOrientationImageRef(imageRef, targetSize, orientation);
        
        // add quality
        CFMutableDictionaryRef mutDicRef = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, cfFrameProperties);
        
        float c = 0.5;
        CFNumberRef compressionQuality = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &c);
        CFDictionarySetValue(mutDicRef, kCGImageDestinationLossyCompressionQuality, compressionQuality);
        // orientation 必須修改,否則會丟失 旋轉。
        CGImagePropertyOrientation newOrientation = kCGImagePropertyOrientationUp;
        CFNumberRef newOrientationNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &newOrientation);
        CFDictionarySetValue(mutDicRef, kCGImagePropertyOrientation, newOrientationNum);
        
        // modify size
        CGFloat width = targetSize.width;
        CGFloat height = targetSize.height;
        CFNumberRef widthNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &width);
        CFNumberRef heightNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &height);
        CFDictionarySetValue(mutDicRef, kCGImagePropertyPixelWidth, widthNum);
        CFDictionarySetValue(mutDicRef, kCGImagePropertyPixelHeight, heightNum);
        
        NSLog(@" mutDicRef 
 %@ 
",mutDicRef);
        CGImageDestinationAddImage(destination, newImageRef, mutDicRef);
        CFRelease(cfFrameProperties);
        CGImageRelease(newImageRef);
        CGImageRelease(imageRef);
        CFRelease(mutDicRef);
        CFRelease(compressionQuality);
        newImageRef = NULL;
        imageRef = NULL;
        mutDicRef = NULL;
    }
    
    
    BOOL success = CGImageDestinationFinalize(destination);
    CFRelease(destination);
    
    if (!success) {
        NSDictionary *userInfo = @{
                     NSLocalizedDescriptionKey: NSLocalizedString(@"Could not finalize image destination", nil)
                     };
        NSLog(@"resizeGifImageDataToSize %@",userInfo);
        CFRelease(source);
        return nil;
    }
    CFRelease(source);
    if (mutableData.length > gifData.length) {
        return gifData;
    }
    return [NSData dataWithData:mutableData];
}

#pragma mark gif 每一幀的 播放時間
+ (float)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
    float frameDuration = 0.1f;
    //獲取這一幀圖片的屬性字典
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
    NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
    //獲取gif屬性字典
    NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
    //獲取這一幀持續的時間
    NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
    if (delayTimeUnclampedProp) {
        frameDuration = [delayTimeUnclampedProp floatValue];
    }
    else {
        NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
        if (delayTimeProp) {
            frameDuration = [delayTimeProp floatValue];
        }
    }
    //如果幀數小於0.1,則指定為0.1
    if (frameDuration < 0.011f) {
        frameDuration = 0.100f;
    }
    CFRelease(cfFrameProperties);
    return frameDuration;
}
複製程式碼

最後

介紹一個 gif 處理的庫,用 c++ 寫的,處理 gif 質量比較好,

gifsicle

ImageOption 裡面處理 gif 用的就是 gifsicle

參考連結

1、www.jianshu.com/p/154b938d2…

2、blog.csdn.net/xcysuccess3…

3、www.jianshu.com/p/ea0274a33…

4、www.jianshu.com/p/2345afeab…

5、www.aliyun.com/jiaocheng/3…

6、www.jianshu.com/p/af34e85b9…

7、blog.csdn.net/Philm_iOS/a…

8、www.cnblogs.com/silence-cnb…

9、segmentfault.com/a/119000000…

相關文章