Extended/TIPXWebPCodec.m (659 lines of code) (raw):

// // TIPXWebPCodec.m // TwitterImagePipeline // // Created on 11/9/16. // Copyright © 2020 Twitter. All rights reserved. // #pragma mark imports #import <Accelerate/Accelerate.h> #import <TwitterImagePipeline/TwitterImagePipeline.h> #import "TIPXUtils.h" #import "TIPXWebPCodec.h" #pragma mark WebP includes // TODO: remove this pushed diagnostic "ignored" when the modular closure of libwebp framework headers is completely fixed #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #if TARGET_OS_MACCATALYST #import <webp/webp.h> #if TIPX_WEBP_ANIMATION_DECODING_ENABLED #define WEBP_HAS_DEMUX 1 #endif #else #import <WebP/decode.h> #import <WebP/encode.h> #if TIPX_WEBP_ANIMATION_DECODING_ENABLED #import <WebPDemux/demux.h> #define WEBP_HAS_DEMUX 1 #endif #endif #pragma clang diagnostic pop NS_ASSUME_NONNULL_BEGIN #pragma mark - Declarations static UIImage * __nullable TIPXWebPRenderImage(NSData *dataBuffer, CGSize sourceDimensions, CGSize targetDimensions, UIViewContentMode targetContentMode, CGRect framingRect, CGContextRef __nullable canvas); static UIImage * __nullable TIPXWebPConstructImage(CGDataProviderRef dataProvider, const size_t width, const size_t height, const size_t bytesPerPixel, const size_t componentsPerPixel); static BOOL TIPXWebPPictureImport(WebPPicture *picture, CGImageRef imageRef); static BOOL TIPXWebPCreateRGBADataForImage(CGImageRef sourceImage, vImage_Buffer *convertedImageBuffer); @interface TIPXWebPDecoderContext : NSObject <TIPImageDecoderContext> @property (nonatomic, readonly) NSData *tip_data; @property (nonatomic, readonly) CGSize tip_dimensions; @property (nonatomic, readonly) BOOL tip_hasAlpha; @property (nonatomic, readonly) NSUInteger tip_frameCount; @property (nonatomic, readonly) BOOL tip_isAnimated; - (instancetype)initWithExpectedContentLength:(NSUInteger)length buffer:(NSMutableData *)buffer; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @property (tipx_nonatomic_direct, readonly) NSUInteger expectedContentLength; - (TIPImageDecoderAppendResult)append:(NSData *)data TIPX_OBJC_DIRECT; - (nullable TIPImageContainer *)renderImage:(TIPImageDecoderRenderMode)renderMode targetDimensions:(CGSize)targetDimensions targetContentMode:(UIViewContentMode)targetContentMode TIPX_OBJC_DIRECT; - (TIPImageDecoderAppendResult)finalizeDecoding TIPX_OBJC_DIRECT; @end @interface TIPXWebPDecoder : NSObject <TIPImageDecoder> @end @interface TIPXWebPEncoder : NSObject <TIPImageEncoder> @end #pragma mark - Implementations @implementation TIPXWebPCodec - (instancetype)init { // Shouldn't be called, but will permit in case of type erasure return [self initWithPreferredCodec:nil]; } - (instancetype)initWithPreferredCodec:(nullable id<TIPImageCodec>)preferredCodec { if (self = [super init]) { _tip_decoder = preferredCodec.tip_decoder ?: [[TIPXWebPDecoder alloc] init]; _tip_encoder = preferredCodec.tip_encoder ?: [[TIPXWebPEncoder alloc] init]; } return self; } + (BOOL)hasAnimationDecoding { #if WEBP_HAS_DEMUX return YES; #else return NO; #endif } @end @implementation TIPXWebPDecoder - (TIPImageDecoderDetectionResult)tip_detectDecodableData:(NSData *)data isCompleteData:(BOOL)complete earlyGuessImageType:(nullable NSString *)imageType { // RIFF layout is: // Offset tag // 0...3 "RIFF" 4-byte tag // 4...7 size of image data (including metadata) starting at offset 8 // 8...11 "WEBP" our form-type signature if (data.length >= 12) { const Byte *bytes = data.bytes; if (0 == memcmp(bytes, "RIFF", 4)) { bytes += 8; if (0 == memcmp(bytes, "WEBP", 4)) { return TIPImageDecoderDetectionResultMatch; } } return TIPImageDecoderDetectionResultNoMatch; } return TIPImageDecoderDetectionResultNeedMoreData; } - (id<TIPImageDecoderContext>)tip_initiateDecoding:(nullable id __unused)config expectedDataLength:(NSUInteger)expectedDataLength buffer:(nullable NSMutableData *)buffer { return [[TIPXWebPDecoderContext alloc] initWithExpectedContentLength:expectedDataLength buffer:buffer]; } - (TIPImageDecoderAppendResult)tip_append:(TIPXWebPDecoderContext *)context data:(NSData *)data { return [context append:data]; } - (nullable TIPImageContainer *)tip_renderImage:(TIPXWebPDecoderContext *)context renderMode:(TIPImageDecoderRenderMode)renderMode targetDimensions:(CGSize)targetDimensions targetContentMode:(UIViewContentMode)targetContentMode { return [context renderImage:renderMode targetDimensions:targetDimensions targetContentMode:targetContentMode]; } - (TIPImageDecoderAppendResult)tip_finalizeDecoding:(TIPXWebPDecoderContext *)context { return [context finalizeDecoding]; } @end @implementation TIPXWebPEncoder - (nullable NSData *)tip_writeDataWithImage:(TIPImageContainer *)imageContainer encodingOptions:(TIPImageEncodingOptions)encodingOptions suggestedQuality:(float)quality error:(out NSError * __nullable __autoreleasing * __nullable)error { __block WebPPicture *pictureRef = NULL; __block TIPErrorCode errorCode = TIPErrorCodeUnknown; __block NSData *outputData = nil; tipx_defer(^{ if (pictureRef) { WebPPictureFree(pictureRef); } if (error && !outputData) { *error = [NSError errorWithDomain:TIPErrorDomain code:errorCode userInfo:nil]; } }); UIImage *image = imageContainer.image; if (imageContainer.animated) { // TODO: supported animated image = image.images.firstObject; } CGImageRef imageRef = image.CGImage; if (!imageRef) { errorCode = TIPErrorCodeMissingCGImage; return nil; } WebPConfig config; if (!WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, quality * 100.f)) { return nil; } config.lossless = (quality == 1.f) ? 1 : 0; if (!WebPValidateConfig(&config)) { return nil; } WebPPicture pictureStruct; if (!WebPPictureInit(&pictureStruct)) { return nil; } pictureRef = &pictureStruct; const size_t width = CGImageGetWidth(imageRef); const size_t height = CGImageGetHeight(imageRef); pictureRef->width = (int)width; pictureRef->height = (int)height; if (!TIPXWebPPictureImport(pictureRef, imageRef)) { return nil; } WebPMemoryWriter *writerRef = (WebPMemoryWriter *)WebPMalloc(sizeof(WebPMemoryWriter)); WebPMemoryWriterInit(writerRef); dispatch_block_t writerDeallocBlock = ^{ WebPMemoryWriterClear(writerRef); WebPFree(writerRef); }; pictureRef->writer = WebPMemoryWrite; pictureRef->custom_ptr = writerRef; if (!WebPEncode(&config, pictureRef)) { writerDeallocBlock(); return nil; } outputData = [[NSData alloc] initWithBytesNoCopy:writerRef->mem length:writerRef->size deallocator:^(void *bytes, NSUInteger length) { writerDeallocBlock(); }]; return outputData; } @end @implementation TIPXWebPDecoderContext { struct { BOOL didEncounterFailure:1; BOOL didLoadHeaders:1; BOOL didLoadFrame:1; BOOL didComplete:1; BOOL isAnimated:1; BOOL isCachedImageFirstFrame:1; } _flags; NSMutableData *_dataBuffer; TIPImageContainer *_cachedImageContainer; } @synthesize tip_data = _dataBuffer; - (BOOL)tip_isAnimated { return _flags.isAnimated; } - (nullable id)tip_config { return nil; } - (instancetype)initWithExpectedContentLength:(NSUInteger)length buffer:(NSMutableData *)buffer { if (self = [super init]) { _expectedContentLength = length; if (buffer) { _dataBuffer = buffer; } else if (length > 0) { _dataBuffer = [NSMutableData dataWithCapacity:length]; } else { _dataBuffer = [NSMutableData data]; } } return self; } - (void)dealloc { [self _cleanup]; } - (TIPImageDecoderAppendResult)append:(NSData *)data { if (_flags.didComplete) { return TIPImageDecoderAppendResultDidCompleteLoading; } if (_flags.didEncounterFailure) { return TIPImageDecoderAppendResultDidProgress; } TIPImageDecoderAppendResult result = TIPImageDecoderAppendResultDidProgress; // append & update if (data) { [_dataBuffer appendData:data]; if (!_flags.didLoadHeaders) { WebPBitstreamFeatures features; if (VP8_STATUS_OK == WebPGetFeatures(_dataBuffer.bytes, _dataBuffer.length, &features)) { _tip_dimensions = CGSizeMake(features.width, features.height); _tip_hasAlpha = !!features.has_alpha; if (!features.has_animation) { _tip_frameCount = 1; } else { #if WEBP_HAS_DEMUX _flags.isAnimated = 1; // WebP animations do not have a header indicating the number of frames, // will have to decode entire file to get number of frames. // Set number of frames to 2 for now and update along the way. _tip_frameCount = 2; #else _flags.didEncounterFailure = 1; return result; #endif } result = TIPImageDecoderAppendResultDidLoadHeaders; _flags.didLoadHeaders = 1; } } } #if WEBP_HAS_DEMUX if (_flags.didLoadHeaders && !_flags.didLoadFrame && _flags.isAnimated) { WebPData webpData = (WebPData){ .bytes = _dataBuffer.bytes, .size = _dataBuffer.length }; WebPDemuxState state = WEBP_DEMUX_PARSING_HEADER; WebPDemuxer* demuxer = WebPDemuxPartial(&webpData, &state); tipx_defer(^{ WebPDemuxDelete(demuxer); }); if (demuxer && state >= WEBP_DEMUX_PARSED_HEADER) { if (WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT) > 1) { _flags.didLoadFrame = 1; result = TIPImageDecoderAppendResultDidLoadFrame; } } } #endif return result; } - (nullable TIPImageContainer *)renderImage:(TIPImageDecoderRenderMode)renderMode targetDimensions:(CGSize)targetDimensions targetContentMode:(UIViewContentMode)targetContentMode { @autoreleasepool { #if WEBP_HAS_DEMUX if (_flags.isAnimated) { return [self _renderAnimatedImage:renderMode targetDimensions:targetDimensions targetContentMode:targetContentMode]; } #endif return [self _renderStaticImage:renderMode targetDimensions:targetDimensions targetContentMode:targetContentMode]; } } - (nullable TIPImageContainer *)_renderStaticImage:(TIPImageDecoderRenderMode)renderMode targetDimensions:(CGSize)targetDimensions targetContentMode:(UIViewContentMode)targetContentMode TIPX_OBJC_DIRECT { if (!_flags.didComplete) { return nil; } if (!_cachedImageContainer && !_flags.didEncounterFailure) { UIImage *image = TIPXWebPRenderImage(_dataBuffer, _tip_dimensions, targetDimensions, targetContentMode, CGRectZero, NULL); if (image) { _cachedImageContainer = [[TIPImageContainer alloc] initWithImage:image]; [self _cleanup]; } else { _flags.didEncounterFailure = 1; } } return _cachedImageContainer; } #if WEBP_HAS_DEMUX - (nullable TIPImageContainer *)_renderAnimatedImage:(TIPImageDecoderRenderMode)renderMode targetDimensions:(CGSize)targetDimensions targetContentMode:(UIViewContentMode)targetContentMode TIPX_OBJC_DIRECT { BOOL justFirstFrame = NO; if (!_flags.didComplete) { if (TIPImageDecoderRenderModeCompleteImage == renderMode) { return nil; } if (!_flags.didLoadFrame) { return nil; } if (_flags.isCachedImageFirstFrame && _cachedImageContainer) { return _cachedImageContainer; } justFirstFrame = YES; // <-- we only need the first frame as a preview } if (!justFirstFrame) { if (_cachedImageContainer && !_flags.isCachedImageFirstFrame) { return _cachedImageContainer; } } // Create our demuxer (defer delete it) WebPData data = (WebPData){ .bytes = _dataBuffer.bytes, .size = _dataBuffer.length }; WebPDemuxState state = WEBP_DEMUX_PARSING_HEADER; WebPDemuxer* demuxer = WebPDemuxPartial(&data, &state); tipx_defer(^{ WebPDemuxDelete(demuxer); }); if (state < WEBP_DEMUX_PARSED_HEADER) { return nil; } // Allocate the WebPIterator (and defer free it) WebPIterator* iter = (WebPIterator*)WebPMalloc(sizeof(WebPIterator)); memset(iter, 0, sizeof(WebPIterator)); tipx_defer(^{ WebPFree(iter); }); // Populate the WebPIterator with the first frame (1 based index, 0 index is the last frame) if (!WebPDemuxGetFrame(demuxer, 1, iter)) { // Failed to get the frame, nothing populated on the iterator return nil; } // Iterator was populated with bookkeeping/allocations for the first frame, defer releasing those references tipx_defer(^{ WebPDemuxReleaseIterator(iter); }); // Go through the animation and pull out the frames (stopping after the first frame if `justFirstFrame` is `YES`) NSTimeInterval totalDuration = 0; const NSUInteger loopCount = (NSUInteger)WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT); #if DEBUG const CGSize canvasDimensions = CGSizeMake(WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH), WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT)); NSCParameterAssert(canvasDimensions.width == _tip_dimensions.width); NSCParameterAssert(canvasDimensions.height == _tip_dimensions.height); #endif const CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); TIPXDeferRelease(colorSpace); CGContextRef canvas = CGBitmapContextCreate(NULL /*data*/, (size_t)_tip_dimensions.width, (size_t)_tip_dimensions.height, 8, 4 * (size_t)_tip_dimensions.width, colorSpace, bitmapInfo); TIPXDeferRelease(canvas); CGContextClearRect(canvas, (CGRect){ .origin = CGPointZero, .size = _tip_dimensions }); NSMutableArray<UIImage *> *frames = [[NSMutableArray alloc] initWithCapacity:(NSUInteger)iter->num_frames]; NSMutableArray<NSNumber *> *frameDurations = [[NSMutableArray alloc] initWithCapacity:(NSUInteger)iter->num_frames]; do { NSData *fragment = [[NSData alloc] initWithBytesNoCopy:(void*)iter->fragment.bytes length:iter->fragment.size freeWhenDone:NO]; const CGRect framingRect = CGRectMake(iter->x_offset, iter->y_offset, iter->width, iter->height); // CGBitmapContext is bottem-left aligned instead of top-left aligned, so adjust the origin for that use case const CGRect canvasFramingRect = CGRectMake(framingRect.origin.x, _tip_dimensions.height - framingRect.size.height - framingRect.origin.y, framingRect.size.width, framingRect.size.height); const BOOL isSizedToCanvas = CGSizeEqualToSize(framingRect.size, _tip_dimensions) && CGPointEqualToPoint(framingRect.origin, CGPointZero); if (iter->blend_method == WEBP_MUX_NO_BLEND) { // clear the area we are about to draw to CGContextClearRect(canvas, canvasFramingRect); } UIImage *frame = TIPXWebPRenderImage(fragment, _tip_dimensions, (justFirstFrame && isSizedToCanvas) ? targetDimensions : CGSizeZero, targetContentMode, framingRect, canvas); if (iter->dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { // clear the area we just finished drawing to CGContextClearRect(canvas, canvasFramingRect); } if (!frame) { return nil; } const NSTimeInterval duration = (NSTimeInterval)iter->duration / 1000.; [frames addObject:frame]; [frameDurations addObject:@(duration)]; totalDuration += duration; } while (!justFirstFrame && WebPDemuxNextFrame(iter)); if (justFirstFrame) { // Static image (a preview of the full animation) _cachedImageContainer = [[TIPImageContainer alloc] initWithImage:frames.firstObject]; } else { // Full animation UIImage *image = [UIImage animatedImageWithImages:frames duration:totalDuration]; _cachedImageContainer = [[TIPImageContainer alloc] initWithAnimatedImage:image loopCount:loopCount frameDurations:frameDurations]; } if (_cachedImageContainer) { _flags.isCachedImageFirstFrame = !!justFirstFrame; } return _cachedImageContainer; } #endif - (TIPImageDecoderAppendResult)finalizeDecoding { if (_flags.didEncounterFailure) { return TIPImageDecoderAppendResultDidCompleteLoading; } if (!_flags.didLoadHeaders) { return TIPImageDecoderAppendResultDidProgress; } #if WEBP_HAS_DEMUX if (_flags.isAnimated) { WebPData data = (WebPData){ .bytes = _dataBuffer.bytes, .size = _dataBuffer.length }; WebPDemuxState state = WEBP_DEMUX_PARSING_HEADER; WebPDemuxer* demuxer = WebPDemuxPartial(&data, &state); tipx_defer(^{ WebPDemuxDelete(demuxer); }); if (!demuxer || WEBP_DEMUX_DONE != state) { _flags.didEncounterFailure = 1; return TIPImageDecoderAppendResultDidCompleteLoading; } // Update the frame count to the full count (was set to "2" as a placeholder) _tip_frameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT); } #endif _flags.didComplete = 1; return TIPImageDecoderAppendResultDidCompleteLoading; } #pragma mark Private - (void)_cleanup TIPX_OBJC_DIRECT { // clean up any temporary state before decoding to a TIPImageContainer } static UIImage *TIPXWebPRenderImage(NSData *dataBuffer, CGSize sourceDimensions, CGSize targetDimensions, UIViewContentMode targetContentMode, CGRect framingRect, CGContextRef __nullable canvas) { __block WebPDecoderConfig* config = (WebPDecoderConfig*)WebPMalloc(sizeof(WebPDecoderConfig)); tipx_defer(^{ if (config) { WebPFree(config); } }); if (!WebPInitDecoderConfig(config)) { return nil; } BOOL isAligned = canvas != NULL; // having a canvas indicates an animation, always treat fragment as being "aligned" if (!isAligned) { // is aligned if the origin is offset isAligned = !CGPointEqualToPoint(framingRect.origin, CGPointZero); if (!isAligned) { // is aligned if the size is not the canvas size (nor zero size) isAligned = !CGSizeEqualToSize(framingRect.size, sourceDimensions) && !CGSizeEqualToSize(framingRect.size, CGSizeZero); } } if (!isAligned) { const CGSize scaledDimensions = TIPDimensionsScaledToTargetSizing(sourceDimensions, targetDimensions, targetContentMode); if (!CGSizeEqualToSize(scaledDimensions, sourceDimensions)) { config->options.scaled_width = (int)scaledDimensions.width; config->options.scaled_height = (int)scaledDimensions.height; config->options.use_scaling = 1; // should we stop fancy upscaling? config.options.no_fancy_upsampling = 1; } framingRect.origin = CGPointZero; framingRect.size = scaledDimensions; } // Set the output colorspace as RGB (TODO: there might be a device optimization using BGRA or ABGR...) config->output.colorspace = MODE_RGBA; if (VP8_STATUS_OK != WebPDecode(dataBuffer.bytes, dataBuffer.length, config)) { return nil; } static const size_t bitsPerComponent = 8; static const size_t bitsPerPixel = 32; static const size_t bytesPerPixel = bitsPerPixel / 8; static const size_t componentsPerPixel = bitsPerPixel / bitsPerComponent; WebPDecoderConfig* configLongLived = config; os_compiler_barrier(); config = nil; // clear the ref to avoid being free on scope exit NSData *data = [[NSData alloc] initWithBytesNoCopy:configLongLived->output.u.RGBA.rgba length:configLongLived->output.u.RGBA.size deallocator:^(void * _Nonnull bytes, NSUInteger length) { WebPFreeDecBuffer(&configLongLived->output); WebPFree(configLongLived); }]; CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)data); TIPXDeferRelease(provider); if (!provider) { return nil; } UIImage *image = TIPXWebPConstructImage(provider, (size_t)configLongLived->output.width, (size_t)configLongLived->output.height, componentsPerPixel, bytesPerPixel); if (image && isAligned) { if (canvas) { framingRect.origin.y = sourceDimensions.height - framingRect.size.height - framingRect.origin.y; CGContextDrawImage(canvas, framingRect, image.CGImage); CGImageRef imageRef = CGBitmapContextCreateImage(canvas); TIPXDeferRelease(imageRef); image = [UIImage imageWithCGImage:imageRef]; } else { UIGraphicsBeginImageContextWithOptions(sourceDimensions, !configLongLived->input.has_alpha, 1.0); tipx_defer(^{ UIGraphicsEndImageContext(); }); CGContextClearRect(UIGraphicsGetCurrentContext(), (CGRect){ .origin = CGPointZero, .size = sourceDimensions }); [image drawInRect:framingRect]; image = UIGraphicsGetImageFromCurrentImageContext(); } } return image; } @end static UIImage *TIPXWebPConstructImage(CGDataProviderRef dataProvider, const size_t width, const size_t height, const size_t bytesPerPixel, const size_t componentsPerPixel) { CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); TIPXDeferRelease(colorSpace); if (!colorSpace) { return nil; } const CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaLast; const CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; const BOOL shouldInterpolate = YES; const size_t bitsPerComponent = 8; const size_t bitsPerPixel = bitsPerComponent * componentsPerPixel; CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerPixel * width, colorSpace, bitmapInfo, dataProvider, NULL /* CGFloat *decode */, shouldInterpolate, renderingIntent); TIPXDeferRelease(imageRef); return [UIImage imageWithCGImage:imageRef]; } static BOOL TIPXWebPPictureImport(WebPPicture *picture, CGImageRef imageRef) { __block vImage_Buffer convertedImageBuffer = {}; tipx_defer(^{ if (convertedImageBuffer.data) { free(convertedImageBuffer.data); } }); if (!TIPXWebPCreateRGBADataForImage(imageRef, &convertedImageBuffer)) { return NO; } return 0 != WebPPictureImportRGBA(picture, convertedImageBuffer.data, (int)convertedImageBuffer.rowBytes); } static BOOL TIPXWebPCreateRGBADataForImage(CGImageRef sourceImage, vImage_Buffer *convertedImageBuffer) { if (!convertedImageBuffer) { return NO; } vImage_CGImageFormat sourceImageFormat = { .bitsPerComponent = (uint32_t)CGImageGetBitsPerComponent(sourceImage), .bitsPerPixel = (uint32_t)CGImageGetBitsPerPixel(sourceImage), .colorSpace = CGImageGetColorSpace(sourceImage), .bitmapInfo = CGImageGetBitmapInfo(sourceImage) }; CGColorSpaceRef deviceRGBColorSpace = CGColorSpaceCreateDeviceRGB(); TIPXDeferRelease(deviceRGBColorSpace); vImage_CGImageFormat convertedImageFormat = { .bitsPerComponent = 8, .bitsPerPixel = 32, .colorSpace = deviceRGBColorSpace, .bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaLast }; vImage_Error errorCode = kvImageNoError; __block vImage_Buffer sourceImageBuffer = {}; tipx_defer(^{ if (sourceImageBuffer.data) { free(sourceImageBuffer.data); } }); errorCode = vImageBuffer_InitWithCGImage(&sourceImageBuffer, &sourceImageFormat, NULL /*backgroundColor*/, sourceImage, kvImageNoFlags); if (errorCode != kvImageNoError) { return NO; } errorCode = vImageBuffer_Init(convertedImageBuffer, sourceImageBuffer.height, sourceImageBuffer.width, convertedImageFormat.bitsPerPixel, kvImageNoFlags); if (errorCode != kvImageNoError) { return NO; } vImageConverterRef imageConverter = vImageConverter_CreateWithCGImageFormat(&sourceImageFormat, &convertedImageFormat, NULL /*backgroundColor*/, kvImageNoFlags, NULL /*error out*/); if (!imageConverter) { return NO; } tipx_defer(^{ vImageConverter_Release(imageConverter); }); errorCode = vImageConvert_AnyToAny(imageConverter, &sourceImageBuffer, convertedImageBuffer, NULL /*tempBuffer*/, kvImageNoFlags); if (errorCode != kvImageNoError) { return NO; } return YES; } NS_ASSUME_NONNULL_END