Tester l’export JPEG iOS sur un Mac cloud : taille limite et échec explicite
Lors de l’envoi d’un avatar, l’aperçu local s’affiche correctement, mais le serveur refuse le fichier. Le problème vient souvent non pas de l’extension, mais du nombre d’octets du JPEG encodé, qui dépasse la limite de l’API. Choisir une valeur de qualité fixe ne résout le problème que pour certaines images, sans apporter de garantie. Sur un Mac cloud, on peut d’abord isoler la logique d’export dans un script de validation, puis reprendre les mêmes contrats dans la cible de tests iOS.
Définir le budget avant tout, sans assimiler la qualité à un pourcentage de taille
Si l’API impose une limite en octets, le client doit lui aussi la stocker sous forme d’entier afin d’éviter toute confusion entre unités décimales et binaires. La qualité est un paramètre d’entrée de l’encodeur, pas un pourcentage de la taille de sortie. À dimensions identiques, un fond uni et une texture finement détaillée donnent des résultats de compression très différents.
Dans cet article, on suppose que l’orientation a déjà été normalisée, la transparence traitée et l’espace colorimétrique choisi. L’export ne modifie pas les dimensions en pixels. Il parcourt un ensemble fini de paliers de qualité, du plus élevé au plus bas, et renvoie le premier résultat qui respecte le budget. Ce résultat est simplement « le candidat admissible le plus prioritaire parmi les paliers définis » : il ne prétend pas offrir la qualité optimale sur un intervalle continu.
La taille du fichier est une condition de sortie à vérifier impérativement ; la qualité visuelle constitue un autre critère de validation. Respecter la première ne garantit pas que les contours du texte, les tons chair ou les dégradés soient satisfaisants.
Remplacer la baisse de qualité sans fin par une recherche bornée
Dans le terminal du Mac distant, enregistrez le code suivant dans jpeg-budget.swift. Une chaîne d’outils Swift opérationnelle doit être configurée. Le script utilise uniquement les frameworks système, ne lit aucune photo personnelle et ne modifie aucun réglage de la machine.
import Foundation
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers
enum ExportError: Error {
case invalidBudget
case encoding
case budgetUnreachable
case decoding
}
func encodeJPEG(_ image: CGImage, quality: Double) throws -> Data {
let buffer = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(
buffer as CFMutableData,
UTType.jpeg.identifier as CFString,
1,
nil
) else {
throw ExportError.encoding
}
let properties = [
kCGImageDestinationLossyCompressionQuality: quality
] as CFDictionary
CGImageDestinationAddImage(destination, image, properties)
guard CGImageDestinationFinalize(destination) else {
throw ExportError.encoding
}
return buffer as Data
}
func exportJPEG(
_ image: CGImage,
maxBytes: Int
) throws -> (data: Data, quality: Double) {
guard maxBytes > 0 else {
throw ExportError.invalidBudget
}
let levels = [0.9, 0.8, 0.7, 0.6, 0.5]
for quality in levels {
let data = try encodeJPEG(image, quality: quality)
if data.count <= maxBytes {
return (data, quality)
}
}
throw ExportError.budgetUnreachable
}
func verify(_ data: Data, width: Int, height: Int) throws {
guard let source = CGImageSourceCreateWithData(
data as CFData, nil
),
CGImageSourceGetType(source) == UTType.jpeg.identifier as CFString,
let decoded = CGImageSourceCreateImageAtIndex(source, 0, nil),
decoded.width == width,
decoded.height == height else {
throw ExportError.decoding
}
}
let space = CGColorSpace(name: CGColorSpace.sRGB)!
let context = CGContext(
data: nil,
width: 320,
height: 240,
bitsPerComponent: 8,
bytesPerRow: 0,
space: space,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)!
context.setFillColor(
CGColor(red: 0.2, green: 0.4, blue: 0.6, alpha: 1)
)
context.fill(CGRect(x: 0, y: 0, width: 320, height: 240))
let image = context.makeImage()!
let result = try exportJPEG(image, maxBytes: 1_000_000)
precondition(result.data.count <= 1_000_000)
try verify(result.data, width: 320, height: 240)
do {
_ = try exportJPEG(image, maxBytes: 1)
preconditionFailure("Expected budget rejection")
} catch ExportError.budgetUnreachable {
}
Exécutez xcrun swift jpeg-budget.swift. En cas de réussite, le script ne produit aucune sortie ; en cas d’échec, il se termine avec un code non nul. L’image unie sert ici à valider le déroulement du traitement, pas à mesurer les capacités de compression sur des images réelles. Les déballages forcés des optionnels ne sont utilisés que pour ce jeu de données de test fixe ; dans une application qui reçoit des images externes, le code doit renvoyer des erreurs explicites.
Vérifier les cas d’échec, pas seulement les fichiers exportés avec succès
Le script distingue un budget invalide, un échec d’encodage et un budget impossible à respecter. Ces trois situations ne doivent pas toutes se traduire par un fichier vide : un budget invalide est une erreur d’appel, un échec d’encodage exige d’examiner l’entrée ou l’encodeur, et l’impossibilité de respecter le budget doit être gérée selon les règles du produit.
Les tests complémentaires doivent couvrir au minimum les budgets nul et négatif, un budget exactement égal au nombre d’octets produit, ainsi que le cas où aucun palier ne convient. L’égalité doit être acceptée. N’affirmez pas directement qu’« un budget inférieur d’un octet au résultat obtenu à la qualité maximale entraîne forcément un échec » : un palier suivant peut encore satisfaire la contrainte.
Tester séparément les branches de la recherche
Isolez l’encodage réel dans une fonction injectable, puis vérifiez la recherche avec des résultats de longueurs prédéfinies : un seul appel si le premier palier convient, arrêt de l’encodage dès qu’un palier intermédiaire convient, et autant de tentatives que de candidats si tous dépassent la limite. On peut ainsi vérifier que l’arrêt anticipé fonctionne, sans dépendre d’une image qui produirait par hasard une taille précise.
Écarter une recherche dichotomique non validée évite de supposer implicitement que la taille varie de façon strictement monotone. Le coût d’un parcours fini est connu, mais les grandes images sont encodées plusieurs fois. Il faut donc aussi plafonner le nombre de pixels en entrée et éviter de traiter trop d’images simultanément.
Constituer des images de test qui révèlent les problèmes
Le jeu de tests doit réunir des aplats de couleur, du texte aux traits fins, des dégradés et des textures très détaillées. Ces images peuvent être générées par programme ou provenir d’échantillons fixes que l’équipe est autorisée à conserver. Pour chaque échantillon, consignez les dimensions et une brève description du contenu. Ne générez pas de nouvelles images aléatoires à chaque exécution sans conserver la graine.
En plus de vérifier Data.count, relisez le résultat pour confirmer son format et ses dimensions en pixels. Un fichier corrompu peut être très petit sans pour autant être acceptable. Le JPEG est un format avec perte : on ne peut pas exiger une égalité pixel par pixel après décodage. Si le projet nécessite une évaluation automatique de l’écart visuel, fixez l’espace de calcul et les tolérances, et conservez des échantillons pour une vérification manuelle.
Une même image ne doit pas nécessairement produire la même empreinte sur différentes versions du système. Des assertions plus robustes portent sur le format, la conservation des dimensions, le respect du budget d’octets et le nombre borné de tentatives. Consignez aussi le palier de qualité effectivement retenu pour faciliter les investigations.
Réintégrer la logique dans l’export iOS et la validation continue
Une fois le script validé sur Mac, placez les fonctions dans un module réutilisable par l’application et exécutez les mêmes jeux de données dans l’environnement d’exécution iOS cible. Les résultats d’encodage des frameworks système peuvent varier : une réussite sous macOS ne constitue pas, à elle seule, une preuve pour iOS.
Si tous les paliers échouent, l’interface doit indiquer clairement que l’export est impossible dans les conditions actuelles. Si la réduction des dimensions est autorisée, définissez une série de tailles distincte et une longueur minimale de côté, puis informez-en explicitement l’utilisateur. Ne modifiez pas discrètement les dimensions pendant la recherche du niveau de qualité. Si l’image doit ensuite être intégrée à une requête avant son envoi, vérifiez séparément la limite du fichier image et celle du corps de requête complet.
Au final, il suffit de conserver l’identifiant de l’image de test en entrée, la version du système, les paliers candidats, la qualité retenue et le nombre d’octets produit. Il n’est pas nécessaire d’enregistrer les photos personnelles d’origine. En traitant séparément la contrainte de taille, les exigences visuelles et le comportement en cas d’échec, on évite de masquer les problèmes d’export derrière une valeur de qualité qui ne cesse de baisser.
Questions fréquentes
Une qualité JPEG de 0,5 divise-t-elle la taille par deux ?
Non. Elle règle l’encodage, pas le taux de réduction du fichier. Il faut vérifier Data.count une fois l’encodage terminé.
Que faire si aucun palier ne respecte la limite ?
Terminer par une erreur explicite. Si une réduction des dimensions est autorisée, elle doit suivre une procédure distincte avec une taille minimale définie.
Un test réussi sur Mac remplace-t-il la validation iOS ?
Non. Le Mac permet de vérifier rapidement la logique, mais le même contrat doit être testé dans l’environnement iOS visé. Les fichiers JPEG ne doivent pas nécessairement être identiques octet par octet.
Testez votre Mac à distance avec vos propres tâches.
Louez un Mac mini M4 dédié et utilisez macOS en interface graphique ou en ligne de commande. Idéal pour tester la connexion et vos flux de travail. Pour des tâches hors ligne de longue durée ou du matériel professionnel externe, comparez aussi avec l’achat.
Louer un Mac mini dédié