iOS-GIF-Export prüfen: Bilddauer, Schleifen und Rücklesen
Die exportierte GIF-Datei lässt sich öffnen, doch dieselbe Animation bleibt plötzlich beim ersten Bild stehen, beendet die Schleife zu früh oder läuft insgesamt kürzer. Ändern Sie nicht gleich den Player. Erzeugen Sie zunächst auf einem Cloud-Mac eine feste Testdatei und lesen Sie diese mit ImageIO wieder ein. So lässt sich unterscheiden, ob die Datei falsch geschrieben wurde oder der Player sie anders wiedergibt. Das folgende Skript verwendet ausschließlich System-Frameworks und die Swift-Toolchain. Es benötigt weder eine Verbindung zu externen Diensten noch eine Signierung.
Zuerst die Vorgaben für die Animation festhalten
Verwenden Sie drei einfarbige Einzelbilder mit einheitlich 16×16 Pixeln. Ihre Anzeigedauer beträgt der Reihe nach 0.12, 0.24 und 0.36 Sekunden. Setzen Sie das Schleifenfeld auf 0, also auf endlose Wiederholung. Für einen Durchlauf ergibt sich eine erwartete Dauer von 0.72 Sekunden. Diese Zahlen sind Testeingaben, keine Ergebnisse einer Leistungsmessung.
GIF speichert die Bildverzögerung in Hundertstelsekunden. Liefert die Anwendungslogik feinere Zeitwerte, legen Sie zuerst die Rundungsregel fest und berechnen Sie daraus die erwarteten Werte. Überlassen Sie diese Entscheidung nicht der impliziten Konvertierung des Encoders. Wählen Sie für den ersten Test Werte, die sich in dieser Zeiteinheit exakt darstellen lassen. So vermischen sich Quantisierungsfehler nicht mit Fehlern beim Encoding.
Die Prüfung der Dateieigenschaften beantwortet die Frage „Was wurde gespeichert?“. Der Wiedergabetest beantwortet die Frage „Was sieht der Nutzer?“. Erst wenn beide Prüfungen erfolgreich sind, gilt der Animationsexport als bestanden.
Nehmen Sie Bildgröße, Zeitwerte und Schleifenregel in die Versionsverwaltung auf. Eine beiläufig aufgenommene Animation sollte nicht die einzige Referenz sein. Ändert sich das Ausgangsmaterial, lassen sich Fehler sonst nur schwer erklären.
Eine feste Testdatei mit ImageIO erzeugen
Speichern Sie den folgenden Inhalt in einem eigenen Verzeichnis als gif-check.swift. Das Skript erzeugt drei Einzelbilder und öffnet die Datei nach Abschluss des Schreibvorgangs erneut. Schlägt eine Prüfung fehl, endet es mit einem Exit-Status ungleich null. Da der Dateiname fest vorgegeben ist, braucht jeder parallel laufende Job ein eigenes Arbeitsverzeichnis.
import Foundation
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers
let url = URL(fileURLWithPath: "timing-fixture.gif")
let delays = [0.12, 0.24, 0.36]
let colors: [[CGFloat]] = [
[1, 0, 0, 1],
[0, 1, 0, 1],
[0, 0, 1, 1]
]
func require(_ condition: Bool, _ message: String) {
if !condition {
FileHandle.standardError.write(Data((message + "\n").utf8))
exit(1)
}
}
guard let destination = CGImageDestinationCreateWithURL(
url as CFURL, UTType.gif.identifier as CFString, delays.count, nil
) else {
fatalError("Cannot create GIF destination")
}
CGImageDestinationSetProperties(destination, [
kCGImagePropertyGIFDictionary: [
kCGImagePropertyGIFLoopCount: 0
]
] as CFDictionary)
for (index, components) in colors.enumerated() {
guard let context = CGContext(
data: nil,
width: 16,
height: 16,
bitsPerComponent: 8,
bytesPerRow: 64,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
fatalError("Cannot create bitmap context")
}
context.setFillColor(
CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(),
components: components)!
)
context.fill(CGRect(x: 0, y: 0, width: 16, height: 16))
guard let image = context.makeImage() else {
fatalError("Cannot create frame")
}
CGImageDestinationAddImage(destination, image, [
kCGImagePropertyGIFDictionary: [
kCGImagePropertyGIFDelayTime: delays[index]
]
] as CFDictionary)
}
require(CGImageDestinationFinalize(destination), "GIF finalization failed")
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
fatalError("Cannot reopen GIF")
}
require(CGImageSourceGetCount(source) == delays.count, "Frame count mismatch")
let global = CGImageSourceCopyProperties(source, nil) as NSDictionary?
let globalGIF = global?[kCGImagePropertyGIFDictionary] as? NSDictionary
let loops = globalGIF?[kCGImagePropertyGIFLoopCount] as? NSNumber
require(loops?.intValue == 0, "Loop count mismatch")
var total = 0.0
for index in delays.indices {
guard let image = CGImageSourceCreateImageAtIndex(source, index, nil) else {
fatalError("Cannot decode frame")
}
require(image.width == 16 && image.height == 16, "Frame size mismatch")
let properties = CGImageSourceCopyPropertiesAtIndex(
source, index, nil
) as NSDictionary?
let gif = properties?[kCGImagePropertyGIFDictionary] as? NSDictionary
let raw = gif?[kCGImagePropertyGIFUnclampedDelayTime] as? NSNumber
let fallback = gif?[kCGImagePropertyGIFDelayTime] as? NSNumber
guard let delay = (raw ?? fallback)?.doubleValue else {
fatalError("Missing frame delay")
}
require(abs(delay - delays[index]) < 0.001, "Frame delay mismatch")
total += delay
}
require(abs(total - 0.72) < 0.001, "Total duration mismatch")
Protokollieren Sie vor der Ausführung zunächst die Toolchain und starten Sie anschließend die Prüfung:
set -eu
xcrun swift --version > toolchain.txt
xcrun swift gif-check.swift
shasum -a 256 timing-fixture.gif > fixture.sha256
Die Prüfsumme dient dazu, die in diesem Lauf erzeugte Datei eindeutig zu identifizieren. Verschiedene Betriebssystemversionen müssen keine byteidentischen Dateien erzeugen. Farbtabelle und Kompressionsentscheidungen können variieren: Gleiche Bedeutung heißt nicht gleiche Binärdaten.
Den tatsächlichen Exporter testen, nicht nur das Framework
Das obige Programm weist zunächst nach, dass die aktuelle Toolchain grundlegendes Encoding und Decoding beherrscht. Ersetzen Sie beim Einbau in das Projekt den Teil, der die Zieldatei erzeugt, durch den Aufruf Ihres Anwendungsexporters. Behalten Sie dabei dieselben Assertions für das erneute Einlesen bei. Andernfalls besteht lediglich das ImageIO-Beispiel den Test, nicht die Implementierung Ihres Produkts.
Ergänzen Sie drei weitere Eingabefälle: ein einzelnes Bild, ein leeres Frame-Array und Bilder mit unterschiedlichen Abmessungen. Eine leere Eingabe muss einen eindeutigen Fehler auslösen und darf keine Datei hinterlassen, die wie ein erfolgreicher Export aussieht. Unterschiedliche Abmessungen müssen entweder vorab abgelehnt oder auf eine vereinbarte Zeichenfläche vereinheitlicht werden. Ein stillschweigendes Beschneiden ist nicht zulässig.
Das aktuelle Skript prüft nur die Abmessungen, nicht die Farbreihenfolge. Die eigentliche Freigabeprüfung sollte außerdem jedes Einzelbild in ein einheitliches RGBA-Bitmap zeichnen und den mittleren Pixel auslesen, um die Reihenfolge Rot, Grün, Blau zu bestätigen. Vergleichen Sie nicht direkt die Rohbytes der decodierten Puffer: Farbraum, Zeilenabstand und Pixellayout können sich unterscheiden.
Kurze Verzögerungen und transparente Bilder getrennt prüfen
Legen Sie eine zusätzliche, unabhängige Testdatei speziell für sehr kurze Verzögerungen an. Prüfen Sie beim Einlesen vorrangig die Eigenschaft für die unveränderte, nicht begrenzte Bildverzögerung. Nur wenn sie fehlt, greifen Sie auf die gewöhnliche Verzögerungseigenschaft zurück. Protokollieren Sie außerdem, welches Feld tatsächlich gelesen wurde. Setzen Sie eine fehlende Eigenschaft nicht mit null gleich. Sonst kann eine beschädigte Testdatei wie eine schnell laufende Animation erscheinen.
Auch transparente Einzelbilder benötigen einen eigenen Test. Zeichnen Sie zunächst einen vollständigen Hintergrund und legen Sie darüber ein Objekt, das sich innerhalb eines Teilbereichs bewegt. Prüfen Sie, ob das vorherige Bild korrekt gelöscht wird. Transparenz und Farbpalette sind bei GIF eingeschränkt. Das Erscheinungsbild eines halbtransparenten PNG darf daher nicht ohne Weiteres als erwartetes Ergebnis gelten.
Player können extrem kurze Verzögerungen eigenständig anpassen oder unterschiedliche Verfahren zur Bildkomposition verwenden. Prüfen Sie in der vorgesehenen iOS-Wiedergabekomponente zwei aufeinanderfolgende Durchläufe und achten Sie auf den Übergang vom letzten zum ersten Bild sowie auf Geisterbilder. Leiten Sie die Dateidauer nicht aus der Länge einer Remote-Desktop-Bildschirmaufnahme ab. Übertragung und Aufnahme verursachen selbst Abweichungen.
Fehlerdaten sichern, die bei der Behebung helfen
Die Pipeline sollte mindestens die Toolchain-Version, die Eingabevorgaben, die exportierte Datei und die Eigenschaften jedes Einzelbilds aufbewahren. Ein Fehlerbericht mit Frame-Index, Sollwert, Istwert und der Eigenschaft, aus der der Wert stammt, ist hilfreicher als die bloße Meldung „Animation stimmt nicht überein“. Enthält das Material der Anwendung vertrauliche Inhalte, reproduzieren Sie den Fehler für den Bericht ausschließlich mit der festen Testdatei.
Grenzen Sie Fehler auf drei Ebenen ein: Schlägt das Schreiben fehl, prüfen Sie zuerst den Zielpfad und das Ergebnis des Schreibabschlusses. Bei Strukturfehlern prüfen Sie Bildanzahl, Abmessungen und das Dictionary mit den Zeitangaben. Erst wenn die Strukturprüfung erfolgreich ist, die Wiedergabe aber weiterhin auffällt, untersuchen Sie die Interpretation der Schleifenangabe, Mindestzeiten für kurze Einzelbilder und die Komposition transparenter Bilder. Beginnen Sie nicht damit, Caches zu löschen oder den Rechner zu wechseln.
Für die abschließende Abnahme müssen alle Bedingungen erfüllt sein: Der tatsächliche Exporter endet erfolgreich, sämtliche Einzelbilder lassen sich decodieren, die quantisierten Zeitwerte entsprechen den Vorgaben, das Schleifenfeld ist korrekt und der Zielplayer spielt zwei Durchläufe hintereinander ohne Auffälligkeiten ab. Bewahren Sie fehlerhafte Testdateien im Regressionstestsatz auf und führen Sie diese Tests bei der nächsten Änderung der Encoding-Logik zuerst aus.
Häufig gestellte Fragen
Speichert GIF beliebige Verzögerungen millisekundengenau?
Nein. GIF speichert Verzögerungen in Hundertstelsekunden. Legen Sie eine Rundungsregel fest und prüfen Sie die gerundeten Werte. Player können sehr kurze Verzögerungen zusätzlich begrenzen.
Ist nach erfolgreichem Rücklesen noch ein iOS-Test nötig?
Ja. Rücklesen prüft Struktur und Eigenschaften der Datei. Schleifen, kurze Verzögerungen und die Komposition transparenter Bilder müssen im Zielplayer getestet werden.
Mieten Sie einen Cloud-Mac für Ihren nächsten Build
Exklusiver physischer Mac mini, keine virtuelle Maschine. Wählen Sie eine Laufzeit von einem Tag, einer Woche, einem Monat oder einem Quartal. Konfiguration und Verfügbarkeit entnehmen Sie bitte der Konsole.