Vérifier la transparence des exports PNG iOS sur un Mac cloud
Lorsqu’un liseré noir apparaît autour d’un sticker exporté, le PNG n’est généralement pas corrompu : des couleurs prémultipliées ont été interprétées comme des couleurs non prémultipliées, ou un fond opaque a été ajouté pendant le dessin. Pour valider l’export sur un Mac cloud, commencez par fixer le contrat d’encodage à l’aide de trois pixels, puis contrôlez les fichiers réellement exportés. Ne partez pas d’une comparaison de captures d’écran complètes : le redimensionnement, le fond d’affichage et les conversions colorimétriques brouilleraient les résultats.
Fixer d’abord le contrat des pixels
Ces tests portent sur des PNG SDR, 8 bits, sRGB. Ils ne couvrent ni les espaces à gamut étendu ni le HDR. Préparez une ligne de trois pixels : un pixel entièrement transparent, un rouge semi-transparent et un bleu entièrement opaque. Les dimensions de sortie doivent rester de 3×1, et la transparence doit être conservée après encodage et relecture.
Il faut distinguer deux représentations. Le format PNG utilise un alpha non prémultiplié ; un contexte bitmap CoreGraphics peut utiliser une représentation prémultipliée. Dans un tampon RGBA explicitement déclaré comme prémultiplié, le rouge semi-transparent présente un canal rouge d’environ 128 et un alpha d’environ 128, et non un canal rouge à 255.
Ne traitez pas directement les octets du dataProvider d’un CGImage comme un tampon RGBA de référence. L’espace colorimétrique, l’ordre des canaux et le nombre d’octets par ligne peuvent varier. Dessinez d’abord l’image dans un contexte bitmap que vous avez vous-même défini, puis lisez ses octets.
Les composantes RGB du pixel entièrement transparent ne font pas partie des critères de validation. Sa couleur est invisible et ses valeurs initiales ne sont pas nécessairement conservées après encodage et conversion. Vérifiez seulement que son alpha est nul et qu’il ne modifie pas le fond lors de la composition.
Créer un test minimal d’encodage et de relecture en Swift
Sur un nœud disposant d’une installation complète de Xcode, enregistrez le code suivant dans alpha-check.swift. Il utilise uniquement les frameworks système, génère un PNG puis le décode à nouveau, en définissant explicitement l’ordre des octets, l’espace colorimétrique et le format prémultiplié. Le fichier est écrit dans le répertoire courant : placez-vous dans un répertoire de test distinct avant l’exécution.
import Foundation
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers
let width = 3
let height = 1
let space = CGColorSpace(name: CGColorSpace.sRGB)!
let info = CGBitmapInfo.byteOrder32Big.rawValue
| CGImageAlphaInfo.premultipliedLast.rawValue
func context() -> CGContext {
CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: space,
bitmapInfo: info
)!
}
let source = context()
source.clear(CGRect(x: 0, y: 0, width: width, height: height))
source.setFillColor(red: 1, green: 0, blue: 0, alpha: 0.5)
source.fill(CGRect(x: 1, y: 0, width: 1, height: 1))
source.setFillColor(red: 0, green: 0, blue: 1, alpha: 1)
source.fill(CGRect(x: 2, y: 0, width: 1, height: 1))
let url = URL(fileURLWithPath: "alpha-fixture.png")
let destination = CGImageDestinationCreateWithURL(
url as CFURL, UTType.png.identifier as CFString, 1, nil
)!
CGImageDestinationAddImage(destination, source.makeImage()!, nil)
precondition(CGImageDestinationFinalize(destination), "PNG write failed")
let reader = CGImageSourceCreateWithURL(url as CFURL, nil)!
let image = CGImageSourceCreateImageAtIndex(reader, 0, nil)!
precondition(image.width == width && image.height == height)
func pixels(background: CGFloat?) -> [UInt8] {
let target = context()
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
target.clear(bounds)
if let value = background {
target.setFillColor(
red: value, green: value, blue: value, alpha: 1
)
target.fill(bounds)
}
target.interpolationQuality = .none
target.draw(image, in: bounds)
return Array(UnsafeBufferPointer(
start: target.data!.assumingMemoryBound(to: UInt8.self),
count: width * 4
))
}
func near(_ actual: UInt8, _ expected: Int) -> Bool {
abs(Int(actual) - expected) <= 2
}
let rgba = pixels(background: nil)
precondition(rgba[3] == 0)
precondition(near(rgba[4], 128) && near(rgba[7], 128))
precondition(rgba[8] == 0 && rgba[10] == 255 && rgba[11] == 255)
let black = pixels(background: 0)
let white = pixels(background: 1)
precondition(near(black[4], 128) && black[5] == 0 && black[6] == 0)
precondition(white[4] == 255 && near(white[5], 127))
precondition(near(white[6], 127))
precondition(Array(white[0...3]) == [255, 255, 255, 255])
precondition(Array(black[0...3]) == [0, 0, 0, 255])
Conservez le code de sortie lors de l’exécution. Ne jugez pas la réussite d’après la couleur de fond d’une visionneuse d’images :
mkdir -p alpha-check
cd alpha-check
xcrun swift ../alpha-check.swift
Si une assertion échoue, le script se termine avec un code de sortie non nul. N’exécutez pas ce script de validation avec des options d’optimisation qui suppriment les vérifications d’assertion. La tolérance de deux niveaux sert à absorber la quantification sur 8 bits ; elle ne justifie pas d’élargir arbitrairement la marge d’erreur.
Distinguer deux types d’erreurs avec des fonds noir et blanc
Dans le contexte bitmap défini ci-dessus, le rouge semi-transparent doit donner environ (128,0,0) sur fond noir et (255,127,127) sur fond blanc. Ces valeurs définissent le résultat de composition attendu dans ce contexte précis, et non une garantie de couleur valable pour tous les moteurs de rendu.
Un test limité au fond noir peut laisser passer certains défauts. Par exemple, si un fond noir a déjà été ajouté avant l’export, le rouge reste visible, mais les pixels transparents sont devenus noirs et opaques. Le contrôle sur fond blanc révèle immédiatement ce type de contamination par le fond.
Si un rouge déjà prémultiplié est multiplié une seconde fois par l’alpha, il s’assombrit et un halo noir peut apparaître sur les contours semi-transparents. À l’inverse, des couleurs non prémultipliées interprétées comme prémultipliées peuvent produire des bords trop clairs. Pour diagnostiquer le problème, vérifiez d’abord le format déclaré du tampon, puis l’endroit où s’effectuent les conversions. N’essayez pas de le corriger en accentuant la netteté.
Intégrer les données de test au véritable circuit d’export iOS
Le script précédent vérifie la fiabilité des outils et des assertions ; il ne valide pas pour autant la logique d’export de l’application. L’étape suivante consiste à fournir les mêmes trois pixels de référence à la fonction d’encodage de production, puis à décoder les données qu’elle renvoie, au lieu d’appeler une nouvelle fois l’encodeur de l’exemple.
Valider séparément le dessin UIKit
Si l’application utilise UIGraphicsImageRenderer, définissez explicitement format.opaque = false dans la cible de test iOS. Pour tester les dimensions en pixels, définissez également format.scale = 1, afin de ne pas confondre dimensions logiques et dimensions du fichier en pixels. Si la production utilise une autre échelle, ajoutez un jeu d’assertions correspondant aux dimensions attendues.
Décodez les données PNG renvoyées avec ImageIO, puis dessinez systématiquement le résultat dans le même contexte sRGB. Ne considérez pas directement UIImage.size comme le nombre de pixels de sortie. Les tests impliquant du dessin sur le thread principal doivent s’exécuter sur l’acteur principal, pour éviter que le test lui-même introduise un comportement indéterminé.
Ajoutez ensuite une image de test comportant un bord oblique anticrénelé. Vérifiez qu’elle contient bien des pixels de bord dont l’alpha est strictement compris entre zéro et la valeur maximale, puis composez-la sur fond noir et sur fond blanc. Ne figez pas tous les octets du contour d’après le résultat d’une version particulière du système.
Conserver des éléments de diagnostic en cas d’échec
À chaque échec, enregistrez au minimum le PNG d’origine, les dimensions après décodage, l’espace colorimétrique spécifié, les coordonnées des pixels échantillonnés, les valeurs RGBA attendues et obtenues, ainsi que la version de la chaîne d’outils. Des images synthétiques suffisent pour ces tests ; inutile de téléverser des images d’utilisateurs.
Si les dimensions sont correctes mais que toute l’image est opaque, vérifiez d’abord le réglage opaque du contexte de dessin et le remplissage du fond. Si l’alpha est correct mais que les bords s’assombrissent, recherchez une double prémultiplication. Si les couleurs sont décalées dans l’ensemble de l’image, contrôlez d’abord les conversions d’espace colorimétrique avant de revoir la tolérance sur les pixels.
La validation finale doit produire trois conclusions indépendantes : les dimensions sont conservées, la transparence est préservée et la composition sur fond respecte le contrat. Le simple fait de pouvoir ouvrir le fichier n’est qu’un prérequis. La logique commune de traitement des pixels peut être exécutée automatiquement sur un Mac cloud, mais l’export UIKit réel et le fichier final obtenu après partage doivent toujours être validés séparément, afin de détecter un éventuel réencodage intermédiaire.
Questions fréquentes
La présence d’un canal alpha suffit-elle ?
Non. Toutes ses valeurs peuvent être opaques. Il faut relire des pixels transparents et translucides connus, puis vérifier leur composition sur deux fonds.
Le test sur Mac remplace-t-il la validation sur appareil iOS ?
Pas entièrement. Il valide le code d’encodage partagé et les pixels du fichier. Le rendu UIKit, le gamut et le parcours réel de partage exigent des tests iOS.
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.