Is it possible to compile images into an APNG using Swift?

Hello,

I'm wondering if there is a way to programmatically write a series of UIImages into an APNG, similar to what the code below does for GIFs (credit: https://github.com/AFathi/ARVideoKit/tree/swift_5). I've tried implementing a similar solution but it doesn't seem to work. My code is included below

I've also done a lot of searching and have found lots of code for displaying APNGs, but have had no luck with code for writing them.

Any hints or pointers would be appreciated.

    func generate(gif images: [UIImage], with delay: Float, loop count: Int = 0, _ finished: ((_ status: Bool, _ path: URL?) -> Void)? = nil) {
        currentGIFPath = newGIFPath
        gifQueue.async {
            let gifSettings = [kCGImagePropertyGIFDictionary as String : [kCGImagePropertyGIFLoopCount as String : count]]
            let imageSettings = [kCGImagePropertyGIFDictionary as String : [kCGImagePropertyGIFDelayTime as String : delay]]
            
            guard let path = self.currentGIFPath else { return }
            guard let destination = CGImageDestinationCreateWithURL(path as CFURL, __UTTypeGIF as! CFString, images.count, nil)
            else { finished?(false, nil); return }
            //logAR.message("\(destination)")
            
            CGImageDestinationSetProperties(destination, gifSettings as CFDictionary)
            for image in images {
                if let imageRef = image.cgImage {
                    CGImageDestinationAddImage(destination, imageRef, imageSettings as CFDictionary)
                }
            }
            
            if !CGImageDestinationFinalize(destination) {
                finished?(false, nil); return
            } else {
                finished?(true, path)
            }
        }
    }

My adaptation of the above code for APNGs (doesn't work; outputs empty file):

func generateAPNG(images: [UIImage], delay: Float, count: Int = 0) {
        let apngSettings = [kCGImagePropertyPNGDictionary as String : [kCGImagePropertyAPNGLoopCount as String : count]]
        let imageSettings = [kCGImagePropertyPNGDictionary as String : [kCGImagePropertyAPNGDelayTime as String : delay]]
        
        guard let destination = CGImageDestinationCreateWithURL(outputURL as CFURL, UTType.png.identifier as CFString, images.count, nil)
        else { fatalError("Failed") }
        
        CGImageDestinationSetProperties(destination, apngSettings as CFDictionary)
        for image in images {
            if let imageRef = image.cgImage {
                CGImageDestinationAddImage(destination, imageRef, imageSettings as CFDictionary)
            }
        }
    }
Answered by mji83 in 782649022

Apple's documentation actually provides a Swift example of building a .apng that is quite old but still works just fine with a few modifications (I just tested it): https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/ImageIOGuide/ikpg_dest/ikpg_dest.html

the second example (listing 3-3) is what you're interested in. The reference to kUTTypePNG, which is deprecated, can be updated to UTType.png.identifier as CFString. Otherwise, you'll need to actually write the code to provide the UIImages. There's also this mysterious line in their example let radians = M_PI * 2.0 * Double(i) / Double(frameCount) which doesn't do anything and can be removed. My guess is the author was going to use it to render a rotating image but didn't bother to actually implement it.

One other thing to note is that it seems like you may want to write the file with a ".png" extension even if it's an apng. I found instances where the ".apng" extension actually caused it to not work, but i'm not sure what the intended behavior is.

I was hoping to find a Swift-based solution but there seems to be none. For whatever reason, both the AFathi's GIF generator and my APNG adaptation of it simply doesn't work. I suspect it has something to do with the UTType mess after kUTType was deprecated in iOS 15.

I went ahead and adapted the Obj-C solution by Radif Sharafullin at https://github.com/radif/MSSticker-Images. I've included the updated code that has been confirmed to be working with iOS 17.2 below.

The code is, as far as I can tell, basically the same as the Swift code, so I'm baffled as to why the Swift version doesn't work. If anyone figures out why, please let us know here! I won't mark this thread as having an answer since my stop-gap solution isn't Swift-based:

#import "mcbAnimatedImagePersister.h"
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>


@implementation mcbAnimatedImagePersister

+(instancetype)shared{
    static mcbAnimatedImagePersister * instance=nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance=mcbAnimatedImagePersister.new;
    });
    return instance;
}

-(void)persistAnimatedImageSequenceGIF:(NSArray<UIImage *> *)images frameDelay:(CGFloat)frameDelay numberOfLoops:(NSInteger)numberOfLoops toURL:(NSURL *)toURL{
    NSDictionary *fileProperties = @{
        (__bridge id)kCGImagePropertyGIFDictionary: @{
            (__bridge id) kCGImagePropertyGIFLoopCount: @(numberOfLoops),
        }
    };
    
    NSDictionary *frameProperties = @{
        (__bridge id)kCGImagePropertyGIFDictionary: @{
            (__bridge id)kCGImagePropertyGIFDelayTime: @(frameDelay),
        }
    };
    
    CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)toURL, (__bridge CFStringRef)UTTypeGIF.identifier, images.count, NULL);
    CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)fileProperties);
    for (UIImage * image in images) {
        CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
    }
    
    if (!CGImageDestinationFinalize(destination)) {
        NSLog(@"failed to finalize image destination");
    }
    CFRelease(destination);
}

-(void)persistAnimatedImageSequenceAPNG:(NSArray<UIImage *> *)images frameDelay:(CGFloat)frameDelay numberOfLoops:(NSInteger)numberOfLoops toURL:(NSURL *)toURL{
    NSDictionary *fileProperties = @{
        (__bridge id)kCGImagePropertyPNGDictionary: @{
            (__bridge id)kCGImagePropertyAPNGLoopCount: @(numberOfLoops),
        }
    };
    
    NSDictionary *frameProperties = @{
        (__bridge id)kCGImagePropertyPNGDictionary: @{
            (__bridge id)kCGImagePropertyAPNGDelayTime: @(frameDelay),
        }
    };
    
    CGImageDestinationRef destination = CGImageDestinationCreateWithURL((__bridge CFURLRef)toURL, (__bridge CFStringRef)UTTypePNG.identifier, images.count, NULL);
    CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)fileProperties);
    for (UIImage * image in images) {
        CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
    }
    
    if (!CGImageDestinationFinalize(destination)) {
        NSLog(@"failed to finalize image destination");
    }
    CFRelease(destination);
}

@end

I suspect it has something to do with the UTType mess after kUTType was deprecated in iOS 15.

Hmmmm, that doesn’t seem likely given that the new UTI framework is just a fancy typesafe wrapper around a bunch of strings.

My understanding is that:

  • Your Objective-C -persistAnimatedImageSequenceAPNG:frameDelay:numberOfLoops:toURL: method works.

  • Your Swift generateAPNG(images:delay:count:) routine fails.

Is that right?

If so, a difference that leapt out was that the Objective-C code calls CGImageDestinationFinalize and the Swift code doesn’t. Did you try the Swift code with that call?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Apple's documentation actually provides a Swift example of building a .apng that is quite old but still works just fine with a few modifications (I just tested it): https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/ImageIOGuide/ikpg_dest/ikpg_dest.html

the second example (listing 3-3) is what you're interested in. The reference to kUTTypePNG, which is deprecated, can be updated to UTType.png.identifier as CFString. Otherwise, you'll need to actually write the code to provide the UIImages. There's also this mysterious line in their example let radians = M_PI * 2.0 * Double(i) / Double(frameCount) which doesn't do anything and can be removed. My guess is the author was going to use it to render a rotating image but didn't bother to actually implement it.

One other thing to note is that it seems like you may want to write the file with a ".png" extension even if it's an apng. I found instances where the ".apng" extension actually caused it to not work, but i'm not sure what the intended behavior is.

Is it possible to compile images into an APNG using Swift?
 
 
Q