Vérifier un export GIF iOS : durée des images et boucles

Vérifier un export GIF iOS : durée des images et boucles

Vérifier un export GIF iOS : durée des images et boucles

Le GIF exporté s’ouvre, mais la première image reste affichée trop longtemps, la boucle s’arrête trop tôt ou la durée totale est plus courte que prévu. Ne commencez pas par modifier le lecteur. Générez un échantillon fixe sur un Mac cloud, puis relisez le fichier avec ImageIO pour distinguer une erreur d’écriture d’une différence de lecture. Le script ci-dessous utilise uniquement les frameworks système et la chaîne d’outils Swift : il ne fait appel à aucun service externe et ne nécessite aucune signature.

Définir d’abord le contrat de l’animation

Prenez trois images de couleur unie, toutes de 16×16 pixels, avec des durées d’affichage respectives de 0.12, 0.24 et 0.36 seconde. Réglez le champ de boucle sur 0 pour obtenir une répétition infinie. La durée attendue d’un cycle est de 0.72 seconde. Ces valeurs sont des données de test, pas des résultats de mesure de performances.

Le format GIF enregistre les durées en centièmes de seconde. Si votre application fournit des valeurs plus précises, définissez la règle d’arrondi avant de calculer les valeurs attendues ; ne laissez pas la conversion implicite de l’encodeur en décider. Pour le premier test, choisissez des durées exactement représentables dans cette unité afin de ne pas confondre erreurs de quantification et problèmes d’encodage.

L’inspection des propriétés du fichier indique « ce qui a été écrit » ; le contrôle dans le lecteur indique « ce que l’utilisateur a vu ». L’export de l’animation n’est validé que si les deux contrôles réussissent.

Placez les dimensions, le tableau des durées et la politique de boucle sous gestion de versions. Ne faites pas d’une animation enregistrée à la volée votre seule référence : si le contenu change, les échecs deviennent difficiles à expliquer.

Générer un échantillon fixe avec ImageIO

Dans un répertoire dédié, enregistrez le contenu suivant dans gif-check.swift. Le script génère trois images, termine l’écriture, puis rouvre le fichier. Tout échec d’une vérification entraîne une sortie avec un code non nul. Le nom du fichier étant fixe, chaque tâche exécutée en parallèle doit disposer de son propre répertoire de travail.

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")

Commencez par consigner la version de la chaîne d’outils, puis lancez les vérifications :

set -eu
xcrun swift --version > toolchain.txt
xcrun swift gif-check.swift
shasum -a 256 timing-fixture.gif > fixture.sha256

La somme de contrôle sert ici à identifier le fichier produit par cette exécution. Il n’est pas nécessaire que différentes versions du système génèrent exactement les mêmes octets. La palette et les choix de compression peuvent varier : une sémantique identique n’implique pas une identité binaire.

Tester l’exporteur réel, pas seulement le framework

Le programme ci-dessus établit d’abord que la chaîne d’outils actuelle sait effectuer un encodage et un décodage de base. Pour l’intégrer au projet, remplacez la partie qui génère le fichier cible par un appel à l’exporteur de votre application, tout en conservant les mêmes assertions de relecture. Sinon, le test valide uniquement l’exemple ImageIO, pas l’implémentation de votre produit.

Ajoutez trois jeux d’entrées : une seule image, un tableau d’images vide et des images de dimensions différentes. Une entrée vide doit déclencher une erreur explicite sans laisser de fichier donnant l’impression d’un export réussi. Si les dimensions diffèrent, rejetez les données en amont ou normalisez-les sur le canevas convenu ; aucun recadrage silencieux ne doit avoir lieu.

Le script actuel vérifie uniquement les dimensions, pas l’ordre des couleurs. Le contrôle bloquant utilisé en production doit aussi dessiner chaque image dans un bitmap RGBA au format uniforme, puis lire le pixel central pour confirmer l’ordre rouge, vert, bleu. Ne comparez pas directement les octets bruts des tampons de décodage : l’espace colorimétrique, le pas de ligne et l’agencement des pixels peuvent différer.

Traiter séparément les durées courtes et les images transparentes

Ajoutez un échantillon distinct consacré aux durées d’affichage très courtes. À la lecture, consultez en priorité la propriété de durée non bornée et ne vous rabattez sur la propriété de durée standard que si elle est absente. Consignez également le champ effectivement utilisé. Ne remplacez pas une propriété absente par zéro : un échantillon endommagé pourrait alors passer pour une animation rapide.

Les images transparentes nécessitent elles aussi un test séparé. Dessinez d’abord un fond complet, puis superposez un objet mobile qui n’occupe qu’une partie de l’image, et vérifiez que l’image précédente est correctement effacée. Les capacités du GIF en matière de transparence et de palette sont limitées : le rendu d’un PNG semi-transparent ne doit pas constituer une attente systématique.

Le lecteur peut ajuster de lui-même les durées extrêmement courtes ou utiliser une autre méthode de composition. Dans le composant de lecture iOS cible, observez deux cycles consécutifs pour vérifier la transition entre la fin et le début ainsi que l’absence de rémanence. Ne déduisez pas les durées du fichier à partir d’un enregistrement d’écran réalisé via un bureau à distance : la transmission et l’enregistrement introduisent leurs propres écarts.

Produire des diagnostics qui orientent la correction

Le pipeline doit conserver au minimum la version de la chaîne d’outils, le contrat d’entrée, le fichier exporté et un relevé des propriétés de chaque image. Un rapport indiquant l’index de l’image, la valeur attendue, la valeur obtenue et la propriété dont elle provient est bien plus utile qu’un simple « animation non conforme ». Si les contenus de l’application sont sensibles, reproduisez l’échec dans le rapport uniquement à l’aide de l’échantillon fixe.

Procédez au diagnostic en trois niveaux : en cas d’échec d’écriture, vérifiez d’abord le chemin cible et le résultat de la finalisation ; en cas d’échec structurel, examinez le nombre d’images, leurs dimensions et le dictionnaire des durées ; si la structure est valide mais que la lecture reste anormale, examinez l’interprétation des boucles, les limites imposées aux durées courtes et la composition des transparences. Ne commencez pas par vider les caches ou changer de machine.

La validation finale doit réunir toutes les conditions suivantes : l’exporteur réel se termine avec succès, toutes les images sont décodables, les durées après quantification respectent le contrat, le champ de boucle est correct et le lecteur cible joue deux cycles consécutifs sans anomalie. Conservez les échantillons ayant échoué dans la suite de non-régression et exécutez-les en premier lors de la prochaine modification de la logique d’encodage.

Questions fréquentes

Le GIF conserve-t-il toute durée à la milliseconde près ?

Non. Les délais sont enregistrés en centièmes de seconde. Définissez une règle d’arrondi et vérifiez les valeurs arrondies. Le lecteur peut aussi imposer un délai minimal.

Faut-il encore tester la lecture sur iOS après la relecture ?

Oui. La relecture valide la structure et les propriétés du fichier. Le lecteur cible doit aussi être testé pour les boucles, les délais courts et la composition des images transparentes.

Louez un Mac cloud pour votre prochain build

Mac mini physique dédié, pas une machine virtuelle. Choisissez une durée de location à la journée, à la semaine, au mois ou au trimestre. La configuration et la disponibilité réelle sont indiquées dans la console.