Applying Built-in Filters of CIImage on GIF Images

GIFs are gaining popularity because, like memes, they’re useful for communicating jokes, emotions, and ideas. These are animated images but aren’t really videos. GIF files can hold multiple pictures at once, and people realized that these pictures could load sequentially. The format supports up to 8 bits per pixel for each image, allowing a single image to reference its own palette of up to 256 different colors chosen from the 24-bit RGB color space. It also supports animations and allows a separate palette of up to 256 colors for each frame. These palette limitations make GIF less suitable for reproducing color photographs and other images with color gradients, but it is well-suited for simpler images such as graphics or logos with solid areas of color. Unlike video, the GIF file format does not support audio.

Core Image is a powerful framework that lets you easily apply filters to images. It can use either the CPU or GPU to process the image data speedily. You typically use CIImage objects in conjunction with other Core Image classes such as CIFilter, CIContext, CIColor, and CIVector. CIImage object has image data associated with it, it is not an image. Lets dig deep into how can we open, access, and even create a GIF using  CIImage framework.

Getting Started

Before starting we expect that you are familiar with Objective-C which is used as primary language. This code can be converted into Swift code easily by the one who has enough knowledge of Swift.

GIF filters process will involve following steps which we will go through one by one:

  • XCode Project Creation

  • Render or Play GIF image

  • Extract GIF images

  • Apply effects on extracted images

  • Merge to create final GIF

1) XCode Project Creation

Open up XCode and create a single view new project “GifHandler”. User Storyboard to design application UI and select Objective-C for code syntax.

2) Render or Play GIF Animation

Proceeding further, we need to select GIF image to play its animation. It can be from any source like Photo library. Add a UILabel and UIButton on “Select GIF Scene”. This UIButton will trigger action to open device photo library using SDK framework. In ViewController.m add IBAction

- (IBAction) showImagePickerForLibrary:(id)sender

and connect it with UIButton just created.

We will present UIImagePickerController to get GIF image from library. Once, photo picker is launched, select desired GIF image and continue.Implement Photo Picker delegate method imagePickerController:(UIImagePickerController*)pickerdidFinishPickingMediaWithInfo:

[imageAssetrequestContentEditingInputWithOptions:optionscompletionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
            BOOL bFileExist = false;
            NSURL *sourceFileURL = contentEditingInput.fullSizeImageURL;
            if (sourceFileURL)
bFileExist = [[NSFileManagerdefaultManager] fileExistsAtPath:sourceFileURL.path];
            if (bFileExist)
            {
                [self proceedToSaveAndDisplayGif:sourceFileURL];
            }
            else
            {
                // Proceed to get it from request image data
                [self performSelectorInBackground:@selector(getCanvaseImageFromPHAssetIfURLNotFound:) withObject:imageAsset];
            }
        }];

Photo picker provides PHAsset object. We need to get image data from it. Either we get physical source url using “requestContentEditingInputWithOptions” method of phasset or we get it from “PHImageManagerrequestImageDataForAsset” method. We will then save GIF image to our processing directory.

Now, render the saved GIF image at our processing directory. As GIF is a sequence of images so from iOS 13.0,SDK is providing CGAnimateImageAtURLWithBlock method to animate GIF images. Callback of method will keep on assigning sequence of images to pDisplayImageView until it is stopped.

- (void) displayGifImageAtPath:(NSString*)sourceFilePath
{
    self->sourceFilePath = sourceFilePath;
NSLog(@"SourcePath: %@", self->sourceFilePath);
    if (@available(iOS 13.0, *)) {
newImageAnimator = [[ImageAnimatoralloc] init];
        [newImageAnimatoranimateImageAtURL:[NSURL fileURLWithPath:sourceFilePath] onAnimate:^(UIImage * _Nullable img, NSError * _Nullable error) {
            if (!self->newImageAnimator.stopPlayback)
                self->pDisplayImageView.image = img;
        }];
    }
    else
    {
pDisplayImageView.image = [UIImageanimatedImageWithAnimatedGIFURL:[NSURL fileURLWithPath:sourceFilePath]];
    }
}

Create a new Objective-C file “ImageAnimator” using class “NSObject” and implement CGAnimateImageAtURLWithBlockmethod in it. Now create its object in displayGifImageAtPath method and start animating GIF. The animation can be stopped by setting the Boolean parameter of the block to false.

[newImageAnimatorsetStopPlayback:YES];
- (void)animateImageAtURL:(NSURL *)urlonAnimate:(onAnimate)animationBlock
{
//    __weak typeof(self) weakSelf = self;
NSDictionary *options = [self animationOptionsDictionary];
    if (@available(iOS 13.0, *)) {
CGAnimateImageAtURLWithBlock((CFURLRef)url, (CFDictionaryRef)options, ^(size_t index, CGImageRef  _Nonnull image, bool * _Nonnull stop) {
            *stop = self.stopPlayback;
animationBlock([UIImageimageWithCGImage:image], nil /* report any relevant OSStatus if needed*/);
        });
    } else {
        // Fallback on earlier versions
    }
}
3) Extract GIF Images

As GIF is sequence of images so we will be now extracting all images from it. Add a new file view controller and give it name ApplyEffectsViewController to project. We will do GIF images extraction and effects apply on ApplyEffectsViewController.

Add UIButton with title “Next” in Select GIF scene and attach it to action to move to ApplyEffectsViewController and pass source path of GIF to it.

ApplyEffectsViewController *UIVC = [storyboard instantiateViewControllerWithIdentifier:@"ApplyEffectsViewController"];
UIVC.sourceFilePath = self->sourceFilePath;
[self.navigationControllerpushViewController:UIVCanimated:true];

In viewDidlLoad of ApplyEffectsViewController.m, initiate arraygifMergingPathsBeforeMerge.

gifMergingPathsBeforeMerge= [NSMutableArray new];
gifMergingPathsBeforeMerge = [self animatedSequenceOfImagesOfGIFImageURL:gifURL];

This array will hold extracted image paths of GIF in our local directory. iOS SDK provides a method CGImageSourceCreateImageAtIndexavailable in Image I/O framework. It creates a CGImage object for the image data associated with the specified index in an image source.

NSData *imageData = [NSDatadataWithContentsOfURL:gifURL];
CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageData, nil);
size_timagesCount = CGImageSourceGetCount(source);

Get number of images available in GIF image and then loop on all available images.

for (size_t i = 0; i < imagesCount; i++) { CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, i, nil); NSString *writePath = [NSString stringWithFormat:@"%@-%lu.jpg", pathDirectory, i + 1]; BOOL isWritten = [self CGImageWriteToFile:cgImage andPathString:writePath]; int delay = delayCentisecondsForGifImageAtIndex(source, i); if (isWritten) { [images addObject:@{@"path": writePath, @"duration": [NSNumber numberWithInt:delay]}]; } }

For each CGImageRef obtained from CGImageSourceCreateImageAtIndex, we will write this image ref to local directory. If you are confident the memory isn’t a problem, you can skip this

CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypeJPEG, 1, NULL);
CGImageDestinationAddImage(destination, image, nil);
CGImageDestinationFinalize(destination);
CFRelease(destination);

GIF animation delay time is one of the important values to save. It is the amount of time, in seconds, to wait before displaying the next image in an animated sequence.From image source ref, extract its properties.And get GIF property dictionary kCGImagePropertyGIFDictionary value and extract delay time value from it. We need to extract this value in seconds for each indexed image.

CFDictionaryRef const gifProperties = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary);
NSNumber *number = fromCFCFDictionaryGetValue(gifProperties, kCGImagePropertyGIFUnclampedDelayTime);
delayCentiseconds = (int)lrint([number doubleValue] * 100);

Now after saving image and getting delay time value, we append these values as dictionary in array. This array will be assigned to gifMergingPathsBeforeMerge array.

[images addObject:@{@"path":writePath, @"duration":[NSNumbernumberWithInt:delay]}];

Once array is filled with extracted images dictionary value now we are ready to use this array to apply effects

4) Apply effects on extracted images

Before applying any effect/filter, we need to add UIImageView to scene controller to see how our applied effect looks.

Tap on storyboard and add UIImageView to Apply Effects Scene. Instead of displaying all GIF images, we show first index of extracted images.

There are many CIImage filters that can be applied but in this blog we will apply some filters. Add UIButtons to scence controller and attach to respective actions.We will discuss Sepia filter to explain things.

Applying any effect/filter on GIF image means applying it on all sequence of images. So what we need to do isto loop on all extracted images and apply filter on each individual image and saving to another local directory to avoid over writing of source images. gifMergingPathsAfterMergewill hold saved image paths.

gifMergingPathsAfterMerge = [NSMutableArray new];
NSDictionary *tDict = gifMergingPathsBeforeMerge[gifMergingCurrentIndex];
NSURL *writtenPath = [self mergeEffectsInSourceImageFromURL:[NSURL fileURLWithPath:tDict[@"path"]]];

Filters are to be applied on CIImage so first we need to create it from path

Directly loading CIImage from URL may contain invalid orientation,that’s why we need to change it to UIImage and then CIImage. Also Correct the orientation of image if required.

UIImage *imageObjectBeforeMerge = [[UIImagealloc] initWithContentsOfFile:fromURL.path];
CIImage *ciImageToMerge = [[CIImagealloc] initWithImage:imageObjectBeforeMerge];
ciImageToMerge= [self sepiaFilterImage:ciImageToMerge withIntensity:0.9];

Once CIImage object is available apply sepia filter on it.

CIFilter* sepiaFilter = [CIFilterfilterWithName:@"CISepiaTone"];
[sepiaFiltersetValue:inputImageforKey:kCIInputImageKey];
[sepiaFiltersetValue:@(intensity) forKey:kCIInputIntensityKey];
return sepiaFilter.outputImage;

Output image CIFilter is a ciimage so we assign it our ciimage object “ciImageToMerge”.Now let’s save this ciimage object. We create CIContext  and using its write representation method to save image.

BOOL bRes = [context writeJPEGRepresentationOfImage:ciImageToMergetoURL:urlToWritecolorSpace:CGColorSpaceCreateDeviceRGB() options:compressOptions error:&jpgError];

Once image is saved, we update our local array with saved path.

NSString *duration = tDict[@"duration"] ?tDict[@"duration"] : @"0";
[gifMergingPathsAfterMergeaddObject:@{@"path":writtenPath.path, @"delay":duration}];

When filter is applied on all extracted images, we proceed to create final merged GIF.

5) Merge to create final GIF

Up till now we have applied sepia filter on all sequence images of source GIF and have populated gifMergingPathsAfterMergearray with saved paths. Now we proceed to create one complete GIF from these paths.

First we need to create destination image ref

CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)fileURL, kUTTypeGIF, kFrameCount, NULL);

And set its properties

CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)fileProperties);

Loop on all gifMergingPathsAfterMerge objects and extract path and delay count values and add in destination image

for (NSUIntegeri = 0; i < kFrameCount; i++) {
NSLog(@"%@",imgArray[i]);
NSDictionary *tempDict = imgArray[i];
            int delay = [tempDict[@"delay"] intValue];
NSString *path = tempDict[@"path"];
            float deleyInCenti = (float)delay/100.0;
NSDictionary *frameProperties = @{
                                              (__bridge id)kCGImagePropertyGIFDictionary: @{
                                                      (__bridge id)kCGImagePropertyGIFDelayTime: [NSNumbernumberWithFloat:deleyInCenti] // a float (not double!) in seconds, rounded to centiseconds in the GIF data
                                                      }
                                              };
UIImage *image = [UIImageimageWithContentsOfFile:path];
CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
    }

Finalize method will create a single GIF image at destination path. It writes image data and properties to the data, URL, or data consumer associated with the image destination.
For user to preview, show the 1st image.

NSDictionary *tDict = imgArray[0];
NSString *sourceImagePath = tDict[@"path"];
self->pImageView.image = [UIImageimageWithContentsOfFile:displayImagePath];

Final GIF with applied filter is ready on destination path. We can now save it our photo galley as well. Add Export to Library UIButton to scene and in its action use PhotoLibrary perform changes method

[[PHPhotoLibrarysharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequestcreationRequestForAssetFromImageAtFileURL:self->pExportImageURL];
    } completionHandler:^(BOOL success, NSError *error) {
}

Using this process we can apply as many filters we like. Hope you get better understanding of how filter can be applied to GIF image. For complete source code you can visit this link.

https://github.com/whizpool/gif-handler.git

[ssba-buttons]


Share this blog
Group Meeting

This field cannot be blank.

Talk to us and

Get your project moving!