Pour convertir des pixels bruts synthétiques en un CGImage fiable, définissez ensemble leurs dimensions, leur pas de ligne, l’ordre des canaux, la convention alpha, l’espace colorimétrique et la propriété des données. Ce guide construit un convertisseur BGRA vérifié, puis attribue au résultat une taille d’affichage AppKit explicite.

Le lecteur cherche à créer une image en mémoire pour une vignette, un diagramme ou un aperçu généré par une app Mac. L’implémentation ci-dessous accepte un format volontairement restreint. Une séquence de décisions distincte explique comment une sortie Metal synthétique peut parvenir à la même étape de conversion.

Que doit préciser le contrat de conversion ?

L’initialiseur CGImage d’Apple pour les données brutes exige que les octets du fournisseur correspondent aux paramètres d’image indiqués. Sa largeur et sa hauteur sont exprimées en pixels ; bytesPerRow décrit la disposition en mémoire de chaque ligne horizontale. Ces paramètres décrivent le tampon d’entrée : il faut donc les établir avant de construire l’image. Documentation Apple sur l’initialiseur d’image bitmap.

Utilisez le contrat applicatif suivant pour le convertisseur présenté dans cet article. Ses règles strictes de validation sont des choix de conception qui rendent les entrées acceptées non ambiguës.

Propriété Entrée ou comportement accepté
Dimensions Largeur et hauteur entières positives, mesurées en pixels
Composantes Quatre composantes non signées de 8 bits par pixel
Ordre en mémoire Les octets successifs sont bleu, vert, rouge, alpha
Alpha Les composantes RVB sont déjà prémultipliées par l’alpha
Interprétation colorimétrique sRGB
Pas de ligne Au moins width * 4 octets et divisible par quatre
Longueur du tampon Exactement bytesPerRow * height, avec le remplissage de la dernière ligne
Ordre des lignes Conservé à l’identique ; aucune inversion verticale automatique
Propriété des données Copier l’entrée dans un stockage immuable dont le convertisseur est propriétaire
Échec Signaler une erreur pour toute entrée invalide ou échec de construction
Présentation Choisir séparément les dimensions en points pour AppKit

Par exemple, une ligne de deux pixels contient huit octets actifs. Un pas de douze ajoute quatre octets de remplissage après chaque ligne. Deux lignes occupent ainsi vingt-quatre octets. Le premier pixel de la deuxième ligne commence à l’octet douze. Cette disposition d’exemple comporte volontairement du remplissage : un cas de test compact ne révélerait pas un convertisseur qui remplace à tort le pas fourni par width * 4.

Exiger le remplissage complet après la dernière ligne simplifie la gestion de la propriété des données. Un producteur qui alloue moins d’espace pour la dernière ligne doit normaliser sa sortie avant d’appeler ce convertisseur. Le rejet d’une longueur incohérente évite aussi d’accepter par inadvertance un tampon plus grand dont la région d’image visée n’est pas clairement définie.

Quel est le lien entre BGRA, l’ordre des octets, l’alpha et l’espace colorimétrique ?

Pour ce format, combinez CGImageAlphaInfo.premultipliedFirst avec CGBitmapInfo.byteOrder32Little. Apple définit le premier comme plaçant l’alpha dans les bits de poids fort, les composantes de couleur étant déjà multipliées par l’alpha, et le second comme une disposition 32 bits en petit-boutiste. Ensemble, ces définitions donnent la séquence BGRA dans l’ordre croissant des adresses. Alpha prémultiplié en premier, ordre bitmap 32 bits en petit-boutiste.

Le terme « premier » nécessite donc le paramètre d’ordre des octets pour déterminer où l’alpha se trouve en mémoire. Pour un rouge opaque, les quatre octets utilisés ici sont 0, 0, 255, 255. Fournir des octets RGBA avec ces paramètres inverse le rouge et le bleu. Modifier les métadonnées ne réorganise pas un tampon existant.

La prémultiplication a également un sens concret. Dans cette convention illustrative sur 8 bits, un rouge pur avec un alpha 128 devient environ 0, 0, 128, 128 en mémoire BGRA. Le convertisseur vérifie que chaque composante de couleur stockée ne dépasse pas l’alpha. Cela détecte les valeurs impossibles pour le format prémultiplié normalisé retenu, sans toutefois permettre de vérifier comment le producteur a calculé chaque composante.

N’attribuez CGColorSpace.sRGB que si les octets sont bien censés être interprétés ainsi. Apple définit à la fois la colorimétrie sRGB et sa fonction de transfert non linéaire. Une simple étiquette d’espace colorimétrique ne transforme pas des valeurs de composantes RVB linéaires en valeurs sRGB. Définition sRGB d’Apple.

Ce convertisseur effectue la validation et construit l’image. Si un producteur émet de l’alpha droit, un ordre RGBA, des composantes flottantes ou des valeurs colorimétriques linéaires, ajoutez une étape explicite de normalisation avant cette frontière ou implémentez un contrat distinct et documenté pour ce format.

Comment Swift peut-il construire l’image avec un stockage dont il est propriétaire ?

Pour une petite image de bureau générée, utilisez une étape qui copie les données. Apple indique que CFDataCreate copie toujours le tampon fourni dans un stockage interne et que CGDataProvider(data:) lit un objet CFData. L’initialiseur CGImage conserve son fournisseur. L’image n’est ainsi pas liée au pointeur temporaire d’un tableau. Copier des octets dans CFData, créer un fournisseur fondé sur CFData.

Voici un programme complet en ligne de commande Swift pour macOS. Enregistrez-le sous main.swift et exécutez swift main.swift sur un Mac où les outils de développement sont installés. Il construit les deux objets image et affiche leurs dimensions ; il ne crée pas de fenêtre. Le programme Swift a été inspecté, mais n’a pas pu être compilé dans cet environnement Linux. Le nombre d’octets, les décalages et les bornes des composantes prémultipliées de son jeu de données ont été vérifiés séparément avec Python.

import AppKit
import CoreGraphics
import CoreFoundation

 enum PixelError: Error {
    case invalidDimensions, arithmeticOverflow
    case invalidStride, invalidByteCount
    case invalidPremultipliedPixel
    case constructionFailed
}

func makeBGRAImage(
    bytes: [UInt8], width: Int, height: Int, bytesPerRow: Int
) throws -> CGImage {
    guard width > 0, height > 0 else {
        throw PixelError.invalidDimensions
    }
    let (activeRowBytes, rowOverflow) =
        width.multipliedReportingOverflow(by: 4)
    guard !rowOverflow else { throw PixelError.arithmeticOverflow }
    guard bytesPerRow >= activeRowBytes,
          bytesPerRow % 4 == 0 else {
        throw PixelError.invalidStride
    }
    let (requiredBytes, sizeOverflow) =
        bytesPerRow.multipliedReportingOverflow(by: height)
    guard !sizeOverflow else { throw PixelError.arithmeticOverflow }
    guard bytes.count == requiredBytes else {
        throw PixelError.invalidByteCount
    }

    for y in 0..<height {
        for x in 0..<width {
            let i = y * bytesPerRow + x * 4
            let alpha = bytes[i + 3]
            guard bytes[i] <= alpha,
                  bytes[i + 1] <= alpha,
                  bytes[i + 2] <= alpha else {
                throw PixelError.invalidPremultipliedPixel
            }
        }
    }

    let copiedData: CFData? = bytes.withUnsafeBufferPointer { buffer in
        CFDataCreate(nil, buffer.baseAddress, requiredBytes)
    }
    guard let data = copiedData,
          let provider = CGDataProvider(data: data),
          let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) else {
        throw PixelError.constructionFailed
    }
    let info = CGBitmapInfo.byteOrder32Little.union(
        CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
    )
    guard let image = CGImage(
        width: width, height: height,
        bitsPerComponent: 8, bitsPerPixel: 32,
        bytesPerRow: bytesPerRow,
        space: colorSpace, bitmapInfo: info,
        provider: provider, decode: nil,
        shouldInterpolate: false, intent: .defaultIntent
    ) else {
        throw PixelError.constructionFailed
    }
    return image
}

// Two active pixels and four padding bytes per row.
let syntheticBytes: [UInt8] = [
    0, 0, 255, 255,  0, 255, 0, 255,  0, 0, 0, 0,
    255, 0, 0, 255,  0, 0, 128, 128,  0, 0, 0, 0
]

do {
    let cgImage = try makeBGRAImage(
        bytes: syntheticBytes, width: 2, height: 2, bytesPerRow: 12
    )
    let appKitImage = NSImage(
        cgImage: cgImage, size: NSSize(width: 2, height: 2)
    )
    precondition(cgImage.width == 2 && cgImage.height == 2)
    print("Pixels: \(cgImage.width) x \(cgImage.height)")
    print("Points: \(appKitImage.size)")
} catch {
    fatalError("Synthetic image conversion failed: \(error)")
}

Les multiplications vérifiées sont effectuées avant tout accès par indice ou copie du tampon. Une fois les vérifications de longueur et de pas réussies, chaque pixel actif se trouve dans le tableau fourni. Le remplissage est conservé, mais exclu de la validation des pixels. Les cas d’erreur distinguent les dimensions mal formées, le pas, la longueur, les valeurs alpha et les échecs de construction ; une véritable app peut présenter ou consigner ces erreurs dans le cadre de sa gestion des erreurs existante.

La première ligne stockée dans le jeu de données contient du rouge opaque et du vert opaque. La deuxième contient du bleu opaque et du rouge approximativement à moitié transparent. decode: nil ne demande aucun remappage de décodage, tandis que shouldInterpolate: false demande une construction sans lissage des pixels. Ces paramètres ne redimensionnent pas et ne réécrivent pas l’entrée. Paramètres de construction d’image d’Apple.

Pour une optimisation ultérieure, Apple fournit également un initialiseur de fournisseur à accès direct avec une fonction de rappel de libération. Cette méthode requiert un contrat distinct pour la durée de vie de l’allocation : gardez l’allocation valide tant que Core Graphics l’utilise, puis libérez-la via la fonction de rappel. Considérez-la comme une autre implémentation dont la propriété des données doit être conçue explicitement. Initialiseur de fournisseur de données à accès direct.

Comment convertir les dimensions en pixels en taille d’affichage AppKit ?

Traitez les dimensions du raster et les dimensions logiques comme des entrées distinctes. Apple distingue les tailles de représentation, mesurées en points, des largeurs et hauteurs mesurées en pixels. NSImage(cgImage:size:) accepte la taille d’image souhaitée ; lui passer .zero lui fait adopter numériquement les dimensions en pixels de l’image source. Dimensions des représentations d’image, initialiseur CGImage d’AppKit.

À titre d’exemple, un raster de 400 × 200 pixels destiné à fournir deux pixels par point devrait avoir une taille NSImage de 200 × 100 points. Ce calcul exprime la taille logique souhaitée. Le CGImage sous-jacent reste de 400 × 200 pixels ; l’enveloppe ne crée pas d’échantillons supplémentaires.

Le programme choisit deux points pour chaque dimension de son jeu de données de deux pixels. Pour un aperçu plus grand, choisissez une taille logique explicite adaptée à la vue. Gardez le CGImage disponible lorsqu’une opération ultérieure requiert les dimensions exactes du raster, car Apple recommande de ne pas supposer les détails de l’enveloppe NSImage, hormis l’équivalence du rendu avec sa source.

Que faut-il changer lorsque les pixels synthétiques proviennent de Metal ?

Metal ajoute un contrat de disponibilité avant que le convertisseur CPU puisse s’exécuter. Apple définit .bgra8Unorm comme quatre composantes non signées normalisées de 8 bits dans l’ordre BGRA. Son équivalent .bgra8Unorm_srgb spécifie également les conversions entre sRGB et l’espace linéaire. Le seul fait de faire correspondre l’ordre des canaux ne garantit donc pas que le contenu d’une texture respecte les conventions colorimétriques et d’alpha prémultiplié de cet article. Format BGRA8 normalisé, format BGRA8 sRGB.

Suivez cette séquence pour une texture 2D synthétique à un seul échantillon, au niveau de mipmap zéro :

  1. Établissez le format de pixel de la texture, son interprétation colorimétrique, sa convention alpha et l’orientation prévue des lignes.
  2. Terminez les opérations GPU qui écrivent dans la texture et vérifiez leur réussite avant d’accepter le résultat.
  3. Si la texture utilise un stockage géré, encodez la synchronisation GPU-CPU requise et attendez qu’elle soit terminée.
  4. Si la texture utilise un stockage privé, copiez-la avec un blit dans une texture adaptée et accessible au CPU avant de la lire.
  5. Allouez un tampon de destination dont la taille a été vérifiée, copiez la région demandée en utilisant son pas de destination réel, puis transmettez les octets obtenus au contrat CPU.

Ces étapes reflètent deux exigences documentées distinctes. getBytes effectue immédiatement une copie sur le CPU, mais ne synchronise pas les opérations GPU sur la texture, et Apple interdit explicitement son utilisation directe sur des textures privées. Par ailleurs, synchronize(resource:) met à jour la copie CPU d’une ressource gérée ; l’accès CPU a lieu après la fin de cette commande. Exigences de lecture des textures, synchronisation des ressources gérées.

Choisissez le pas de destination en fonction de l’opération de lecture précise et conservez-le lors de la conversion. Pour le contrat BGRA restreint, chaque ligne active requiert quatre octets par pixel. La disposition de transfert d’un blit peut exiger un remplissage différent de celui d’une lecture CPU directe ; consignez le pas à chaque étape au lieu de supposer que tous les tampons partagent le même pas.

Comment diagnostiquer une incompatibilité de contrat ?

Utilisez le jeu de données de quatre pixels pour guider vos décisions avant de tester un graphique généré plus grand. Les diagnostics suivants sont des déductions tirées de valeurs et d’une disposition connues, et non des résultats mesurés dans une app.

Observation Premier point à vérifier
Le rouge et le bleu sont inversés Octets réels des canaux par rapport aux indicateurs d’alpha et d’ordre des octets
La première ligne semble correcte, mais les suivantes se décalent Pas source, pas de destination et remplissage
Le rouge translucide semble trop clair ou trop foncé Prémultiplication et interprétation colorimétrique
Les motifs des deux lignes s’échangent verticalement Ordre des lignes du producteur et transformations de dessin
Le raster est correct, mais la taille logique est incorrecte Dimensions AppKit en points
L’entrée générée par le CPU fonctionne, mais celle générée par le GPU ne fonctionne pas Fin des opérations GPU, mode de stockage, synchronisation et disposition de lecture

Pour l’orientation, ce convertisseur garantit la conservation des lignes octet pour octet. Examinez le jeu de données asymétrique dans la vue de destination réelle. Si le résultat est inversé verticalement par rapport au dessin prévu, corrigez l’ordre des lignes à une étape de normalisation documentée ou corrigez la transformation de dessin. Rendez cette décision explicite afin que deux étapes n’inversent pas toutes deux l’image.

Un contrat complet de conversion des pixels bruts permet de savoir ce que chaque étape reçoit et garantit : le producteur fournit des pixels définis, le convertisseur valide leur disposition et en possède une copie, et AppKit reçoit une taille logique déterminée. Établissez ces frontières avec un petit jeu de données synthétique, puis appliquez les mêmes règles de dimensions, de pas, de couleur, d’alpha et de durée de vie aux images plus grandes.

Sources