Per rasterizzare l’NSImageView della tua app, usa i metodi AppKit abbinati per la memorizzazione in bitmap, quindi mantieni le dimensioni in pixel ottenute. Se ti servono byte prevedibili per Metal, ridisegna quella bitmap in un buffer Core Graphics definito esplicitamente.

Questa guida tratta un’immagine sintetica generata interamente in memoria. L’obiettivo è ottenere una rasterizzazione riutilizzabile di come viene visualizzata in un NSImageView, diagnosticare pixel spostati o ripetuti e, facoltativamente, trasferire la rasterizzazione a una texture Metal.

Scegli se ti serve la presentazione della vista o un’immagine di dimensioni esatte

Un’immagine e la vista che la visualizza hanno responsabilità diverse. La vista applica la propria modalità di ridimensionamento per adattare l’immagine al frame. Il valore predefinito è scaleProportionallyDown, quindi estrarre l’immagine originale non riproduce necessariamente come viene visualizzata dalla vista. La documentazione Apple su imageScaling descrive questo comportamento.

Scegli il risultato prima di scegliere l’API:

Risultato desiderato Punto di partenza appropriato Vincolo sulle dimensioni
L’immagine così come viene disegnata dalla vista immagine I metodi AppKit per la memorizzazione in bitmap della vista Verifica le dimensioni in pixel della bitmap restituita
Un disegno sintetico con dimensioni specificate esattamente Disegna direttamente in un CGContext bitmap Larghezza e altezza in pixel interi esplicite
Byte prevedibili per una texture Metal Ridisegna la rasterizzazione in un contesto bitmap definito Formato pixel, passo di riga e proprietà dei dati espliciti

Apple documenta che bitmapImageRepForCachingDisplay(in:) crea una rappresentazione adatta alla regione della vista richiesta. Passa tale rappresentazione a cacheDisplay(in:to:); Apple raccomanda specificamente questa combinazione per garantire la compatibilità del formato pixel. Il secondo metodo disegna la vista e le sue viste discendenti. Documentazione sull’allocazione delle bitmap, documentazione sulla memorizzazione in cache delle viste.

Questi metodi non espongono un argomento arbitrario per la risoluzione di output. Se il requisito è esattamente 192 × 128 pixel, esegui il rendering del disegno sintetico direttamente a quelle dimensioni. Ridimensionare successivamente una rasterizzazione più piccola della vista non può recuperare dettagli mai rasterizzati. Ciò deriva dai due diversi vincoli di output, non da una funzionalità aggiuntiva della memorizzazione in cache della vista.

Tieni separati punti, pixel e rettangoli

Usa view.bounds quando richiedi l’intera area di disegno locale. I bounds di una vista descrivono il suo sistema di coordinate; il frame ne descrive posizione e dimensioni nel sistema di coordinate della supervista. Passare il frame a un metodo che si aspetta una regione locale può quindi introdurre un offset indesiderato. Documentazione Apple sui bounds.

Come esempio di vincolo sulle dimensioni, una bitmap sorgente di 192 × 128 potrebbe essere visualizzata come immagine logica di 96 × 64. Mantieni entrambi i valori: il primo descrive i campioni, il secondo le dimensioni di presentazione. L’inizializzatore di NSImage accetta una dimensione esplicita; fornendo zero, invece, adotta le dimensioni in pixel dell’immagine sorgente. Documentazione Apple sull’inizializzatore di immagini.

Per diagnosticare la geometria di backing di una vista esistente, usa convertToBacking(_:), che converte il rettangolo locale della vista in coordinate del backing store. Per la rasterizzazione effettivamente restituita, verifica pixelsWide e pixelsHigh, oppure le dimensioni della sua CGImage. Apple documenta che cgImage di un NSBitmapImageRep ha le stesse dimensioni in pixel della rappresentazione. Documentazione sulla conversione alle coordinate di backing, documentazione Apple su CGImage delle bitmap.

Non sostituire quelle dimensioni restituite con un presunto moltiplicatore 2×. Registra insieme il rettangolo locale richiesto, la larghezza e l’altezza in pixel restituite e le dimensioni logiche previste. Avrai così un confronto concreto quando il risultato appare ingrandito, ritagliato o spostato.

Crea un esempio completo di rasterizzazione sintetica

Il seguente programma Objective-C genera un’immagine di prova asimmetrica, la assegna a una vista immagine in memoria, rasterizza la vista e normalizza il risultato in una bitmap Core Graphics. Non crea file. Salvalo come RasterDemo.m su un Mac con gli strumenti di sviluppo da riga di comando installati.

clang -fobjc-arc RasterDemo.m -framework Cocoa -o RasterDemo
./RasterDemo

Il contesto di normalizzazione usa componenti a otto bit, alfa premoltiplicata nella posizione più significativa e archiviazione a 32 bit in ordine little-endian. Nel complesso, queste scelte producono l’ordine di byte BGRA: in memoria il componente blu meno significativo viene per primo. Ciò deriva dalle definizioni Apple di premultipliedFirst e byteOrder32Little.

#import <Cocoa/Cocoa.h>

static CGContextRef NewBitmap(size_t w, size_t h) {
    // Illustrative allocation budget, not an API dimension limit.
    if (w == 0 || h == 0 || w > 4096 || h > 4096) return NULL;
    size_t rowBytes = (w * 4 + 63) & ~(size_t)63;
    CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
    if (!space) return NULL;
    CGContextRef c = CGBitmapContextCreate(
        NULL, w, h, 8, rowBytes, space,
        kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
    CGColorSpaceRelease(space);
    return c;
}

static CGImageRef NewTestImage(void) {
    CGContextRef c = NewBitmap(192, 128);
    if (!c) return NULL;
    CGContextSetRGBFillColor(c, 1, 1, 1, 1);
    CGContextFillRect(c, CGRectMake(0, 0, 192, 128));
    CGContextSetRGBFillColor(c, 0, 0, 0, 1);
    for (size_t x = 0; x < 192; x += 16) {
        CGContextFillRect(c, CGRectMake(x, 0, 1, 128));
    }
    CGContextSetRGBFillColor(c, 0, 0.7, 0.8, 1);
    CGContextFillRect(c, CGRectMake(8, 92, 24, 20));
    CGImageRef image = CGBitmapContextCreateImage(c);
    CGContextRelease(c);
    return image;
}

static CGImageRef NewNormalizedImage(CGImageRef input) {
    size_t w = CGImageGetWidth(input);
    size_t h = CGImageGetHeight(input);
    CGContextRef c = NewBitmap(w, h);
    if (!c) return NULL;
    CGContextClearRect(c, CGRectMake(0, 0, w, h));
    CGContextSetBlendMode(c, kCGBlendModeCopy);
    CGContextSetInterpolationQuality(c, kCGInterpolationNone);
    CGContextDrawImage(c, CGRectMake(0, 0, w, h), input);
    NSLog(@"Normalized: %zu x %zu pixels; stride %zu bytes",
          w, h, CGBitmapContextGetBytesPerRow(c));
    CGImageRef result = CGBitmapContextCreateImage(c);
    CGContextRelease(c);
    return result;
}

int main(void) {
    @autoreleasepool {
        [NSApplication sharedApplication];
        CGImageRef source = NewTestImage();
        if (!source) { NSLog(@"Source allocation failed"); return 1; }
        NSImage *image = [[NSImage alloc] initWithCGImage:source
                                                  size:NSMakeSize(96, 64)];
        CGImageRelease(source);
        NSImageView *view = [[NSImageView alloc]
                            initWithFrame:NSMakeRect(0, 0, 96, 64)];
        view.image = image;
        view.imageScaling = NSImageScaleNone;
        view.imageAlignment = NSImageAlignCenter;
        view.imageFrameStyle = NSImageFrameNone;
        [view layoutSubtreeIfNeeded];
        NSRect region = view.bounds;
        NSBitmapImageRep *rep =
            [view bitmapImageRepForCachingDisplayInRect:region];
        if (!rep) { NSLog(@"View bitmap allocation failed"); return 1; }
        [view cacheDisplayInRect:region toBitmapImageRep:rep];
        CGImageRef cached = rep.CGImage;
        if (!cached) { NSLog(@"View bitmap has no CGImage"); return 1; }
        CGImageRef raster = NewNormalizedImage(cached);
        if (!raster) { NSLog(@"Normalization failed"); return 1; }
        NSImage *result = [[NSImage alloc] initWithCGImage:raster
                                                   size:region.size];
        NSLog(@"Result logical size: %@", NSStringFromSize(result.size));
        CGImageRelease(raster);
    }
    return 0;
}

Il programma riporta intenzionalmente le dimensioni effettive dell’output invece di affermare che la bitmap della vista corrisponda ai 192 × 128 pixel della sorgente. Il marcatore ciano permette di distinguere i cambiamenti di orientamento dalle linee verticali ripetute di prova. Se il requisito è la risoluzione esatta della sorgente, usa direttamente la CGImage restituita da NewTestImage() invece di passare dalla vista.

La cancellazione esplicita inizializza a trasparenti i pixel di destinazione prima della normalizzazione. Il disegno di una singola immagine riempie il rettangolo di destinazione; non crea motivi ripetuti. Apple documenta che la corrispondente operazione di disegno Core Graphics ridimensiona l’immagine al rettangolo di destinazione e ne disegna una sola copia, a meno che non sia abilitata la ripetizione. Documentazione Apple sul disegno di immagini.

Questo programma è stato esaminato, ma non è stato possibile compilarlo nell’ambiente di ricerca Linux, che non dispone di AppKit né di un compilatore C. I calcoli del passo di riga usati di seguito sono stati verificati con un piccolo calcolo deterministico in Python.

Diagnostica i pixel spostati o ripetuti seguendo un ordine fisso

La sequenza seguente è un aiuto diagnostico derivato dai vincoli geometrici e di memoria descritti sopra. I sintomi suggeriscono da dove iniziare l’ispezione, ma non individuano una causa univoca.

  1. Controlla il rettangolo richiesto. Per l’intera vista immagine, confronta la richiesta di memorizzazione in cache con bounds. Se esporti una sottoregione, includi la sua origine locale tra i parametri.
  2. Controlla le dimensioni prima dei byte grezzi. Confronta larghezza e altezza dell’immagine memorizzata in cache con le dimensioni passate a ogni consumer successivo. Sostituire una larghezza in punti a una in pixel cambia l’interpretazione del buffer.
  3. Controlla il passo di riga. Se ogni riga inizia progressivamente più lontano dalla posizione prevista, verifica la distanza tra gli inizi delle righe prima di modificare le trasformazioni.
  4. Controlla l’orientamento con il marcatore asimmetrico. Un’inversione verticale richiede di esaminare le convenzioni sulle coordinate. Uno spostamento costante richiede di esaminare origini e traslazioni.
  5. Controlla se la ripetizione è intenzionale. Esamina ogni chiamata di disegno che abilita la ripetizione. Modificare il passo di riga non correggerà un disegno esplicitamente ripetuto.
  6. Controlla per ultima la scalatura di presentazione. Se la rasterizzazione memorizzata è corretta ma il risultato visualizzato è diverso, confronta le dimensioni logiche dell’immagine di output e le impostazioni di ridimensionamento della vista immagine che la riceve.

Per pixel a quattro byte, il buffer esemplificativo 192 × 128 ha 768 byte per riga e richiede 98,304 byte. Tuttavia, anche un buffer 191 × 128 con l’allineamento a 64 byte dell’esempio ha un passo di 768 byte: ogni riga include quattro byte di padding. Avanzare di 191 × 4, ovvero 764, farebbe iniziare ogni riga successiva quattro byte troppo presto. Apple espone il passo effettivo tramite la proprietà bytesPerRow del contesto.

Usa il calcolo dell’indirizzo base + y * bytesPerRow + x * 4 solo per il formato normalizzato a quattro byte definito qui. Una bitmap AppKit arbitraria ha un proprio formato; non dedurre la disposizione dei componenti solo dal suo aspetto.

Analogamente, applica deliberatamente le modifiche alle coordinate. Core Graphics fornisce operazioni distinte per modificare l’origine e scalare lo spazio utente. Se introduci un’inversione verticale, documenta in quale fase viene effettuata e verificala con il marcatore prima di aggiungerne un’altra. Documentazione sulla traslazione, documentazione sulla scalatura.

Mantieni la proprietà dei dati quando restituisci un’immagine o la carichi in Metal

L’esempio mantiene viva la rappresentazione AppKit mentre disegna la sua CGImage. Restituisce quindi un’istantanea creata dal contesto di normalizzazione. Apple documenta che makeImage() crea una copia, potenzialmente usando internamente la copia su scrittura, e che i disegni successivi nel contesto non modificano l’immagine restituita. Questo consente di rilasciare il contesto dopo aver creato l’istantanea. Documentazione Apple sulle istantanee delle bitmap.

Un puntatore a bitmap grezza ha una durata diversa. Apple fornisce accesso ai dati bitmap posseduti dal contesto anche quando questo è stato creato con un argomento data nullo. Considera il puntatore appartenente al contesto attivo; per un consumer successivo, copia i byte in uno spazio di archiviazione di tua proprietà oppure mantieni vivo il contesto finché non termina l’uso. Documentazione Apple sui dati delle bitmap.

Per un caricamento in Metal, completa questi passaggi mentre il contesto normalizzato è ancora attivo:

  1. Crea una texture bidimensionale con la larghezza e l’altezza in pixel del contesto. Usa bgra8Unorm per la disposizione dei componenti BGRA illustrata; Apple la definisce come quattro componenti normalizzati senza segno a otto bit, nell’ordine BGRA.
  2. Per il caricamento tramite CPU, mantieni la modalità di archiviazione predefinita del sistema anziché forzare una modalità condivisa specifica dell’hardware. Apple documenta valori predefiniti condivisi sui chip Apple e gestiti per le texture sui Mac con processore Intel.
  3. Chiama replace(region:mipmapLevel:withBytes:bytesPerRow:) per l’intera regione della texture, al livello mip 0, usando il puntatore ai dati del contesto e il passo di riga effettivo.
  4. Completa il caricamento prima di rilasciare il contesto. Se riutilizzi la texture, assicurati anche che le operazioni GPU precedenti che la usano siano terminate prima che la CPU ne sovrascriva i dati.

Le scelte di formato e archiviazione seguono la documentazione Apple su bgra8Unorm e le indicazioni sull’archiviazione delle risorse. Apple afferma che il metodo di sostituzione copia immediatamente i pixel forniti, ma non si sincronizza con le operazioni GPU. Per una texture privata, il percorso documentato prevede una texture temporanea non privata seguita da un’operazione blit. Documentazione Apple sulla sostituzione dei dati della texture.

Il caricamento della texture conserva i byte forniti; la sola corrispondenza della disposizione dei componenti non definisce il resto della pipeline di rendering. Mantieni le decisioni dell’esempio sull’alfa premoltiplicata e sullo spazio colore sRGB anche nella progettazione del consumer. La normalizzazione consiste nel ridisegnare in un formato scelto, quindi non va descritta come una garanzia di conversione identica byte per byte da qualsiasi rappresentazione AppKit.

Per questo compito, un passaggio di consegne affidabile comprende la rasterizzazione e le relative dimensioni in pixel, dimensioni logiche di presentazione, passo di riga, formato dei componenti, convenzione di orientamento e proprietario dei dati. Mantieni insieme questi valori, verifica il risultato con un disegno sintetico asimmetrico e correggi la prima fase in cui divergono. Così un risultato spostato o ripetuto diventa un problema di vincoli tracciabile.

Fonti