PNG-Transparenz für iOS auf dem Cloud-Mac testen
Schwarze Ränder an exportierten Stickern entstehen meist nicht durch eine beschädigte PNG-Datei. Häufig werden vormultiplizierte Farbwerte als nicht vormultiplizierte Werte interpretiert, oder beim Zeichnen wurde bereits ein deckender Hintergrund eingefügt. Führen Sie die Prüfung auf einem Cloud-Mac durch: Legen Sie zunächst mit drei Pixeln die Vorgaben für die Kodierung fest und prüfen Sie anschließend die tatsächliche Exportdatei. Beginnen Sie nicht mit dem Vergleich ganzer Screenshots. Sonst fließen Skalierung, Anzeigehintergrund und Farbkonvertierung gleichzeitig in das Ergebnis ein.
Zuerst die Pixelvorgaben festlegen
Dieser Test gilt für SDR-PNGs mit 8 Bit und sRGB. Breite Farbräume und HDR sind nicht abgedeckt. Erstellen Sie eine Zeile mit drei Pixeln: vollständig transparent, halbtransparentes Rot und vollständig deckendes Blau. Die Ausgabe muss 3×1 Pixel groß bleiben; die Transparenz darf beim Kodieren und erneuten Einlesen nicht verloren gehen.
Dabei sind zwei Darstellungsebenen zu unterscheiden. PNG verwendet nicht vormultipliziertes Alpha; ein CoreGraphics-Bitmap-Kontext kann dagegen mit vormultiplizierten Werten arbeiten. In einem ausdrücklich als vormultipliziert definierten RGBA-Puffer hat halbtransparentes Rot einen Rotwert von ungefähr 128 und einen Alphawert von ungefähr 128 — keinen Rotwert von 255.
Behandeln Sie die Bytes aus dem dataProvider eines CGImage nicht unmittelbar als einheitliche RGBA-Daten. Farbraum, Kanalreihenfolge und Zeilenabstand können unterschiedlich sein. Zeichnen Sie das Bild zuerst in einen selbst definierten Bitmap-Kontext und lesen Sie dann dessen Bytes aus.
Die RGB-Werte des vollständig transparenten Pixels sind kein Abnahmekriterium. Seine Farbe ist unsichtbar, und die ursprünglichen Werte bleiben beim Kodieren und Konvertieren nicht unbedingt erhalten. Prüfen Sie nur, dass Alpha null ist und der Pixel den Hintergrund beim Compositing nicht verändert.
Einen minimalen Roundtrip-Test mit Swift erstellen
Speichern Sie den folgenden Code auf einem Knoten mit vollständig installiertem Xcode als alpha-check.swift. Er verwendet ausschließlich Systemframeworks, erzeugt eine PNG-Datei und dekodiert sie anschließend erneut. Byte-Reihenfolge, Farbraum und vormultipliziertes Format werden ausdrücklich festgelegt. Die Ausgabe landet im aktuellen Verzeichnis. Wechseln Sie deshalb vor der Ausführung in ein separates Testverzeichnis.
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])
Werten Sie beim Ausführen den Exit-Code aus. Beurteilen Sie den Erfolg nicht anhand der Hintergrundfarbe eines Bildbetrachters:
mkdir -p alpha-check
cd alpha-check
xcrun swift ../alpha-check.swift
Schlägt eine Assertion fehl, endet das Skript mit einem Exit-Status ungleich null. Führen Sie diesen Abnahmetest nicht mit Optimierungsoptionen aus, die Assertions entfernen. Die Toleranz von zwei Stufen berücksichtigt die 8-Bit-Quantisierung; sie ist kein Freibrief, die zulässige Abweichung beliebig zu erhöhen.
Zwei Fehlerarten mit schwarzem und weißem Hintergrund unterscheiden
Im oben definierten Bitmap-Kontext sollte halbtransparentes Rot auf Schwarz ungefähr (128,0,0) ergeben, auf Weiß ungefähr (255,127,127). Diese Vorgabe beschreibt das Compositing-Ergebnis in genau diesem Kontext. Sie ist keine allgemeine Farbgarantie für sämtliche Renderer.
Ein Test ausschließlich auf Schwarz übersieht bestimmte Probleme. Wurde beispielsweise vor dem Export bereits ein schwarzer Hintergrund eingefügt, zeigt die Datei weiterhin Rot an. Die transparenten Pixel sind jedoch zu deckenden schwarzen Pixeln geworden. Die Prüfung auf Weiß macht eine solche Verunreinigung durch den Hintergrund sofort sichtbar.
Wird bereits vormultipliziertes Rot ein zweites Mal mit Alpha multipliziert, wird es dunkler; an halbtransparenten Konturen entstehen leicht schwarze Säume. Werden dagegen nicht vormultiplizierte Farbwerte als vormultipliziert interpretiert, können die Ränder zu hell werden. Prüfen Sie bei der Fehlersuche zuerst die Deklaration des Puffers und anschließend die Stellen, an denen konvertiert wird. Zusätzliche Schärfung ist keine Lösung.
Die Testdaten durch den tatsächlichen iOS-Exportpfad schicken
Das obige Skript prüft, ob Werkzeuge und Assertions zuverlässig sind. Es belegt noch nicht, dass die Exportlogik der App korrekt arbeitet. Übergeben Sie im nächsten Schritt dieselben Testdaten mit den drei Farben an die produktive Kodierungsfunktion und lesen Sie die von ihr zurückgegebenen Daten wieder ein. Rufen Sie dafür nicht erneut den Beispiel-Encoder auf.
UIKit-Zeichnung separat prüfen
Verwendet die App UIGraphicsImageRenderer, setzen Sie im iOS-Test-Target ausdrücklich format.opaque = false. Für den Test der Pixelabmessungen muss außerdem format.scale = 1 gesetzt sein. Andernfalls werden logische Größe und Pixelabmessungen der Datei leicht verwechselt. Verwendet die App im Produktivbetrieb einen anderen Skalierungsfaktor, ergänzen Sie dafür eigene Assertions zu den entsprechenden Abmessungen.
Dekodieren Sie die zurückgegebenen PNG-Daten mit ImageIO und zeichnen Sie sie einheitlich in denselben sRGB-Kontext. Verwenden Sie UIImage.size nicht direkt als Anzahl der ausgegebenen Pixel. Tests, die Zeichnung auf dem Hauptthread erfordern, sollten auf dem Main Actor laufen, damit der Test selbst kein unbestimmtes Verhalten verursacht.
Ergänzen Sie ein weiteres Testbild mit einer geglätteten schrägen Kante. Prüfen Sie, dass tatsächlich Randpixel mit einem Alphawert zwischen null und dem Maximalwert vorhanden sind, und setzen Sie das Bild anschließend jeweils auf Schwarz und Weiß. Binden Sie den Test nicht an sämtliche Randbytes einer bestimmten Betriebssystemversion.
Bei Fehlern aussagekräftige Diagnosedaten sichern
Speichern Sie bei jedem Fehler mindestens die ursprüngliche PNG-Datei, die dekodierten Abmessungen, den vorgegebenen Farbraum, die Abtastkoordinaten, die erwarteten und tatsächlichen RGBA-Werte sowie die Toolchain-Version. Künstlich erzeugte Testbilder reichen aus; Nutzerbilder müssen nicht hochgeladen werden.
Stimmen die Abmessungen, sind aber alle Pixel deckend, prüfen Sie zuerst die opaque-Einstellung des Zeichenkontexts und die Hintergrundfüllung. Ist Alpha korrekt, während die Ränder zu dunkel werden, prüfen Sie auf eine doppelte Vormultiplikation. Bei einer Verschiebung der gesamten Farbwiedergabe kontrollieren Sie zunächst die Farbraumkonvertierung, bevor Sie über Pixeltoleranzen sprechen.
Die abschließende Freigabe umfasst drei unabhängige Ergebnisse: übereinstimmende Abmessungen, erhaltene Transparenz und ein Compositing mit dem Hintergrund gemäß den festgelegten Vorgaben. Dass sich die Datei öffnen lässt, ist lediglich eine Voraussetzung. Gemeinsame Pixellogik kann automatisiert auf dem Cloud-Mac laufen. Der tatsächliche UIKit-Export und die endgültige Datei nach dem Teilen müssen dennoch separat geprüft werden, damit eine erneute Kodierung in einem Zwischenschritt nicht unbemerkt bleibt.
Häufig gestellte Fragen
Beweist ein Alphakanal, dass Transparenz erhalten blieb?
Nein. Alle Werte können trotzdem vollständig deckend sein. Transparente und halbtransparente Testpixel müssen zurückgelesen und auf zwei Hintergründen geprüft werden.
Ersetzt der Test auf dem Mac Tests auf iOS-Geräten?
Nicht vollständig. Er prüft gemeinsame Kodierungslogik und Dateipixel. UIKit-Darstellung, Bildschirmfarbraum und der tatsächliche Freigabeweg brauchen zusätzliche iOS-Tests.
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.