Ein animiertes GIF über iOS erstellen und exportieren?

74

Ich habe eine Reihe von benutzerdefinierten Bildern in einer iOS-App, die in einem einfachen, Einzelbild-Flipbook-Stil animiert werden.

Meine Frage lautet: Gibt es eine Möglichkeit, Benutzern das Exportieren ihrer Animation als animiertes GIF zu ermöglichen? Im Idealfall möchte ich ihnen ermöglichen, ein animiertes GIF per E-Mail, Social Share (T / FB) oder (im schlimmsten Fall) in ihrem Dokumentenordner zu speichern, um es über iTunes abzurufen.

Ich weiß, wie man eine PNG-Datei in der Fotobibliothek speichert, und habe eine Möglichkeit gefunden, eine Animation als QT-Datei aufzuzeichnen ( http://www.cimgf.com/2009/02/03/record-your-core-animation) -animation / ), aber ich habe keinen Weg gefunden, einfach ein altes animiertes GIF rauszuschmeißen . Vermisse ich etwas in Core Animation oder woanders? Gibt es Ansätze, Frameworks oder Ressourcen, die jeder empfehlen kann? Tut mir leid, wenn die Frage zu allgemein ist - ich habe Probleme, einen Ausgangspunkt zu finden.

crgt
quelle

Antworten:

161

Sie können ein animiertes GIF mit dem Image I / O-Framework (das Teil des iOS SDK ist) erstellen. Sie möchten auch das MobileCoreServicesFramework einschließen , das die GIF-Typkonstante definiert. Sie müssen diese Frameworks zu Ihrem Ziel hinzufügen und ihre Header in die Datei importieren, in der Sie das animierte GIF erstellen möchten.

#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/MobileCoreServices.h>

Es ist am einfachsten, dies anhand eines Beispiels zu erklären. Ich zeige Ihnen den Code, mit dem ich dieses GIF auf meinem iPhone 5 erstellt habe:

animiertes GIF, erstellt mit dem angezeigten Code

Hier ist zunächst eine Hilfsfunktion, die eine Größe und einen Winkel annimmt und eine UIImageder roten Scheiben in diesem Winkel zurückgibt :

static UIImage *frameImage(CGSize size, CGFloat radians) {
    UIGraphicsBeginImageContextWithOptions(size, YES, 1); {
        [[UIColor whiteColor] setFill];
        UIRectFill(CGRectInfinite);
        CGContextRef gc = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(gc, size.width / 2, size.height / 2);
        CGContextRotateCTM(gc, radians);
        CGContextTranslateCTM(gc, size.width / 4, 0);
        [[UIColor redColor] setFill];
        CGFloat w = size.width / 10;
        CGContextFillEllipseInRect(gc, CGRectMake(-w / 2, -w / 2, w, w));
    }
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

Jetzt können wir das GIF erstellen. Zuerst definieren wir eine Konstante für die Anzahl der Frames, da wir sie später zweimal benötigen:

static void makeAnimatedGif(void) {
    static NSUInteger const kFrameCount = 16;

Wir benötigen ein Eigenschaftswörterbuch, um anzugeben, wie oft die Animation wiederholt werden soll:

    NSDictionary *fileProperties = @{
        (__bridge id)kCGImagePropertyGIFDictionary: @{
            (__bridge id)kCGImagePropertyGIFLoopCount: @0, // 0 means loop forever
        }
    };

Und wir benötigen ein weiteres Eigenschaftswörterbuch, das wir jedem Frame anhängen und angeben, wie lange dieser Frame angezeigt werden soll:

    NSDictionary *frameProperties = @{
        (__bridge id)kCGImagePropertyGIFDictionary: @{
            (__bridge id)kCGImagePropertyGIFDelayTime: @0.02f, // a float (not double!) in seconds, rounded to centiseconds in the GIF data
        }
    };

Wir erstellen auch eine URL für das GIF in unserem Dokumentenverzeichnis:

    NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
    NSURL *fileURL = [documentsDirectoryURL URLByAppendingPathComponent:@"animated.gif"];

Jetzt können wir ein erstellen CGImageDestination, das ein GIF an die angegebene URL schreibt:

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

Ich entdeckte , dass Passieren filePropertiesder als letztes Argument CGImageDestinationCreateWithURLtut nicht Arbeit. Du musst benutzen CGImageDestinationSetProperties.

Jetzt können wir unsere Frames erstellen und schreiben:

    for (NSUInteger i = 0; i < kFrameCount; i++) {
        @autoreleasepool {
            UIImage *image = frameImage(CGSizeMake(300, 300), M_PI * 2 * i / kFrameCount);
            CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)frameProperties);
        }
    }

Beachten Sie, dass wir das Frame-Eigenschaften-Wörterbuch zusammen mit jedem Frame-Bild übergeben.

Nachdem wir genau die angegebene Anzahl von Frames hinzugefügt haben, finalisieren wir das Ziel und geben es frei:

    if (!CGImageDestinationFinalize(destination)) {
        NSLog(@"failed to finalize image destination");
    }
    CFRelease(destination);

    NSLog(@"url=%@", fileURL);
}

Wenn Sie dies auf dem Simulator ausführen, können Sie die URL von der Debug-Konsole kopieren und in Ihren Browser einfügen, um das Bild anzuzeigen. Wenn Sie es auf dem Gerät ausführen, können Sie im Xcode Organizer-Fenster die App-Sandbox vom Gerät herunterladen und das Bild anzeigen. Oder Sie können eine solche App verwenden iExplorer, mit der Sie das Dateisystem Ihres Geräts direkt durchsuchen können. (Dies erfordert kein Jailbreaking.)

Ich habe dies auf meinem iPhone 5 mit iOS 6.1 getestet, aber ich glaube, der Code sollte bereits unter iOS 4.0 funktionieren.

Ich habe den gesamten Code zum einfachen Kopieren in diese Liste aufgenommen .

Rob Mayoff
quelle
1
Rob, ich habe deine Technik heute zum Laufen gebracht, aber es gibt eine große Einschränkung. Das System speichert die Daten für alle Bilder in der Animationssequenz im Speicher, bis der Abschlussaufruf abgeschlossen ist. Ich erstelle einige ziemlich große GIF-Sequenzen, und bei 30 FPS dauert es nicht lange, bis der Speicher knapp wird und es zum Absturz kommt. Gibt es eine Möglichkeit, die Frames irgendwie auf die Festplatte zu puffern? Ich erstelle meine CGImages aus OpenGL und verwende einen Datenprovider. Es gibt eine Form von Datenprovider, der den Inhalt aus einer Datei liest. Ich frage mich, ob das jeden Frame nacheinander lesen und freigeben würde, wenn das GIF fertiggestellt ist.
Duncan C
3
Eine sehr schnelle Suche und Inspektion lässt mich denken, dass Giraffe tun könnte, was Sie wollen.
Rob Mayoff
1
@robmayoff Hallo Rob, ich benutze deinen Code schon seit einiger Zeit, aber ich habe einen seltsamen Fehler gefunden. Ich erhalte immer EXE_BAD_ACCESS bei CGImageDestinationFinalize (Ziel), wenn in der Eingabesequenz identische Frames nebeneinander liegen. Irgendeine Idee, warum das passiert?
XY
1
@Pascal Es ist ein Systemfehler, Sie können am Ende Ihrer Sequenz keine identischen Frames haben, dh wenn Sie n Frames haben, können der n-te und der n-te Frame nicht gleich sein. Am Ende habe ich den Schwanz meiner Sequenz auf identische Frames zugeschnitten. (Und ich benutze eine sehr naive Methode, um identische Frames zu erkennen, Dateigröße plus Zeitstempel im EXIF-Tag, hat für mich sehr gut funktioniert ...)
XY
1
@kevin Überprüfen Sie diese Frage und ihre Kommentare. Ich habe nicht getestet, aber ich habe keinen Grund, diese Informationen anzuzweifeln.
Rob Mayoff
1

Für Swift 3

import Foundation
import UIKit
import ImageIO
import MobileCoreServices

extension UIImage {
    static func animatedGif(from images: [UIImage]) {
        let fileProperties: CFDictionary = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]]  as CFDictionary
        let frameProperties: CFDictionary = [kCGImagePropertyGIFDictionary as String: [(kCGImagePropertyGIFDelayTime as String): 1.0]] as CFDictionary

        let documentsDirectoryURL: URL? = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        let fileURL: URL? = documentsDirectoryURL?.appendingPathComponent("animated.gif")

        if let url = fileURL as CFURL? {
            if let destination = CGImageDestinationCreateWithURL(url, kUTTypeGIF, images.count, nil) {
                CGImageDestinationSetProperties(destination, fileProperties)
                for image in images {
                    if let cgImage = image.cgImage {
                        CGImageDestinationAddImage(destination, cgImage, frameProperties)
                    }
                }
                if !CGImageDestinationFinalize(destination) {
                    print("Failed to finalize the image destination")
                }
                print("Url = \(fileURL)")
            }
        }
    }
}

Ich habe es aus der obigen Antwort konvertiert . Ich hoffe, es hilft.

Verfügbar als Kern .

Änderungen sind willkommen.

Nikhil Manapure
quelle
Eine interessante Alternative wäre die Implementierung dieser Methode in einer Erweiterung von Arraywhere Element == UIImage.
Nicolas Miari
Gute Idee @NicolasMiari
Nikhil Manapure
0

Wenn Sie nach einer Swift 3-Lösung suchen, können Sie sich https://github.com/onmyway133/GifMagic ansehen . Es hat Encoderund Decoderwelche GIF-Datei zusammenbaut und zerlegt.

Grundsätzlich sollten Sie verwenden Image IORahmen mit diesen Funktionen CGImageDestinationCreateWithURL, CGImageDestinationSetProperties, CGImageDestinationAddImage,CGImageDestinationFinalize

Auch mit Swift https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/WorkingWithCocoaDataTypes.html

Von annotierten APIs zurückgegebene Core Foundation-Objekte werden in Swift automatisch speicherverwaltet. Sie müssen die Funktionen CFRetain, CFRelease oder CFAutorelease nicht selbst aufrufen.

onmyway133
quelle