Um das NSImageView deiner App zu rasterisieren, verwende die zusammengehörigen Bitmap-Caching-Methoden von AppKit und bewahre anschließend die resultierenden Pixeldimensionen. Wenn du vorhersehbare Bytes für Metal brauchst, zeichne diese Bitmap in einen explizit definierten Core-Graphics-Puffer um.
Dieser Leitfaden behandelt ein synthetisches Bild, das vollständig im Arbeitsspeicher erzeugt wird. Ziel ist es, eine wiederverwendbare Rasterdarstellung seiner Anzeige in einem NSImageView zu erhalten, falsch platzierte oder wiederholte Pixel zu diagnostizieren und das Raster optional auf eine Metal-Textur zu übertragen.
Entscheide, ob du die Darstellung der Ansicht oder ein Bild mit exakter Größe brauchst
Ein Bild und die Ansicht, die es anzeigt, haben unterschiedliche Aufgaben. Die Ansicht wendet ihren Skalierungsmodus an, um das Bild in ihren Frame einzupassen. Standardmäßig ist dies scaleProportionallyDown; daher wird die Darstellung der Ansicht nicht zwangsläufig reproduziert, wenn du das Originalbild extrahierst. Apples Dokumentation zu imageScaling beschreibt dieses Verhalten.
Lege die gewünschte Ausgabe fest, bevor du die API auswählst:
| Gewünschte Ausgabe | Geeigneter Ausgangspunkt | Dimensionsvorgabe |
|---|---|---|
| Das von der Bildansicht gezeichnete Bild | Bitmap-Caching-Methoden von AppKit | Pixeldimensionen der zurückgegebenen Bitmap prüfen |
| Eine synthetische Zeichnung mit exakt festgelegten Abmessungen | Direkt in einen Bitmap-CGContext zeichnen |
Explizite ganzzahlige Pixelbreite und -höhe |
| Vorhersehbare Bytes für eine Metal-Textur | Das Raster in einen definierten Bitmap-Kontext umzeichnen | Explizites Pixelformat, Zeilenabstand und Besitzverhältnis |
Apple dokumentiert bitmapImageRepForCachingDisplay(in:) als Methode, die eine für den angeforderten Ansichtsbereich geeignete Repräsentation erstellt. Übergib diese Repräsentation an cacheDisplay(in:to:); Apple empfiehlt ausdrücklich diese Kombination, um die Pixelformatkompatibilität sicherzustellen. Die zweite Methode zeichnet die Ansicht und ihre untergeordneten Ansichten. Dokumentation zur Bitmap-Zuweisung, Dokumentation zum Caching von Ansichten.
Diese Methoden bieten keinen frei wählbaren Parameter für die Ausgabeauflösung. Wenn du genau 192 × 128 Pixel benötigst, rendere deine synthetische Zeichnung direkt in dieser Größe. Durch nachträgliches Vergrößern eines kleineren Ansichtsrasters lässt sich keine Detailtreue wiederherstellen, die nie gerastert wurde. Das ergibt sich aus den unterschiedlichen Ausgabevorgaben und ist keine zusätzliche Funktion des Ansichts-Cachings.
Punkte, Pixel und Rechtecke auseinanderhalten
Verwende view.bounds, wenn du den gesamten lokalen Zeichenbereich anforderst. Die Bounds einer Ansicht beschreiben ihr eigenes Koordinatensystem; ihr Frame beschreibt ihre Position und Größe im Koordinatensystem ihrer Superview. Übergibst du den Frame an eine Methode, die einen lokalen Bereich erwartet, kann dadurch eine unbeabsichtigte Verschiebung entstehen. Apples Dokumentation zu bounds.
Als Beispiel für feste Abmessungen könnte eine 192 × 128 Pixel große Quellbitmap als logisches Bild mit 96 × 64 Punkten dargestellt werden. Bewahre beide Werte auf: Der erste beschreibt die Abtastwerte, der zweite die Darstellungsgröße. Der Initialisierer von NSImage akzeptiert eine explizite Größe; wird stattdessen null angegeben, übernimmt das Objekt die Pixeldimensionen des Quellbilds. Apples Dokumentation zum Bildinitialisierer.
Verwende zur Diagnose der vorhandenen Geometrie einer Ansicht im Backing Store convertToBacking(_:). Die Methode wandelt ihr lokales Rechteck in Koordinaten des Backing Stores um. Prüfe beim tatsächlich zurückgegebenen Raster pixelsWide und pixelsHigh oder die Abmessungen des zugehörigen CGImage. Apple dokumentiert, dass cgImage einer NSBitmapImageRep dieselben Pixeldimensionen wie die Repräsentation hat. Dokumentation zur Umrechnung in Backing-Koordinaten, Dokumentation zu CGImage von Bitmaps.
Ersetze diese zurückgegebenen Abmessungen nicht durch einen angenommenen Multiplikator von 2×. Halte das angeforderte lokale Rechteck, die zurückgegebene Pixelbreite und -höhe sowie die gewünschte logische Größe gemeinsam fest. So kannst du konkrete Vergleiche anstellen, wenn ein Ergebnis vergrößert, beschnitten oder verschoben erscheint.
Ein vollständiges Beispiel für die Rasterisierung einer synthetischen Zeichnung erstellen
Das folgende Objective-C-Programm erzeugt ein asymmetrisches Testbild, weist es einer im Arbeitsspeicher angelegten Bildansicht zu, rasterisiert die Ansicht und normalisiert das Ergebnis in eine Core-Graphics-Bitmap. Es erstellt keine Dateien. Speichere es auf einem Mac mit installierten Befehlszeilen-Entwicklerwerkzeugen als RasterDemo.m.
clang -fobjc-arc RasterDemo.m -framework Cocoa -o RasterDemo
./RasterDemo
Der Normalisierungskontext verwendet acht Bit pro Komponente, Alpha mit Vorabmultiplikation an der höchstwertigen Stelle und Little-Endian-32-Bit-Speicherung. Zusammen ergeben diese Einstellungen die Byte-Reihenfolge BGRA: Die niederwertigste Blaukomponente steht im Speicher an erster Stelle. Das folgt aus Apples Definitionen von premultipliedFirst und 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;
}
Das Programm gibt bewusst die tatsächlichen Ausgabedimensionen aus, statt vorauszusetzen, dass der Ansichtscache den 192 × 128 Pixeln der Quelle entspricht. Die cyanfarbene Markierung macht Änderungen der Ausrichtung von den wiederholten vertikalen Testlinien unterscheidbar. Wenn du die exakte Quellauflösung benötigst, verwende direkt das von NewTestImage() zurückgegebene CGImage, statt den Umweg über die Ansicht zu nehmen.
Das explizite Löschen sorgt dafür, dass vor der Normalisierung transparente Pixel im Ziel vorhanden sind. Das Zeichnen eines einzelnen Bildes füllt das Zielrechteck; es kachelt das Bild nicht. Apple dokumentiert, dass die entsprechende Core-Graphics-Zeichenoperation das Bild auf das Zielrechteck skaliert und eine einzelne Kopie zeichnet, sofern das Kacheln nicht aktiviert ist. Apples Dokumentation zum Zeichnen von Bildern.
Dieses Programm wurde geprüft, konnte aber in der Linux-Rechercheumgebung nicht kompiliert werden, da dort AppKit und ein C-Compiler fehlen. Die weiter unten verwendeten Berechnungen des Zeilenabstands wurden mit einer kleinen deterministischen Python-Berechnung geprüft.
Verschobene oder wiederholte Pixel in einer festen Reihenfolge diagnostizieren
Die folgende Abfolge dient als Diagnosehilfe und leitet sich aus den oben beschriebenen Geometrie- und Speicherbedingungen ab. Die Symptome geben Hinweise darauf, wo zuerst geprüft werden sollte; sie belegen keine eindeutige Ursache.
- Das angeforderte Rechteck prüfen. Vergleiche bei der gesamten Bildansicht die Cache-Anfrage mit
bounds. Wenn du einen Teilbereich exportierst, behalte dessen lokalen Ursprung als Teil der Eingabebedingungen bei. - Vor den Rohbytes die Abmessungen prüfen. Vergleiche Breite und Höhe des zwischengespeicherten Bilds mit den Abmessungen, die allen nachfolgenden Verarbeitungsschritten übergeben werden. Wird eine Punktbreite anstelle einer Pixelbreite eingesetzt, ändert sich die Interpretation des Puffers.
- Den Zeilenabstand prüfen. Wenn jede Zeile zunehmend weiter von ihrer erwarteten Position beginnt, prüfe den Abstand zwischen den Zeilenanfängen, bevor du Transformationen anpasst.
- Die Ausrichtung anhand der asymmetrischen Markierung prüfen. Bei einer vertikalen Umkehr solltest du die Koordinatenkonventionen prüfen. Bei einer konstanten Verschiebung solltest du Ursprünge und Translationen prüfen.
- Prüfen, ob die Wiederholung beabsichtigt ist. Prüfe alle Zeichenaufrufe, die das Kacheln aktivieren. Eine Änderung des Zeilenabstands behebt kein ausdrücklich gekacheltes Zeichnen.
- Die Skalierung der Darstellung zuletzt prüfen. Wenn das gespeicherte Raster korrekt ist, aber die angezeigte Darstellung abweicht, vergleiche die logische Größe des Ausgabebilds mit den Skalierungseinstellungen der empfangenden Bildansicht.
Bei Pixeln mit vier Byte benötigt der Beispielpuffer mit 192 × 128 Pixeln pro Zeile 768 Bytes und insgesamt 98,304 Bytes. Ein Puffer mit 191 × 128 Pixeln und der beispielhaften 64-Byte-Zeilenausrichtung hat jedoch ebenfalls einen Zeilenabstand von 768 Byte: Jede Zeile enthält vier Padding-Bytes. Ein Schritt von 191 × 4 beziehungsweise 764 würde jede folgende Zeile vier Byte zu früh beginnen lassen. Apple stellt den tatsächlichen Zeilenabstand über die bytesPerRow-Eigenschaft des Kontexts bereit.
Verwende die Adressberechnung base + y * bytesPerRow + x * 4 nur für das hier definierte normalisierte Vier-Byte-Format. Eine beliebige AppKit-Bitmap hat ihre eigene Formatvorgabe; leite die Komponentenbelegung nicht allein aus ihrem Erscheinungsbild ab.
Wende Koordinatenänderungen ebenso gezielt an. Core Graphics stellt getrennte Operationen bereit, um Ursprung und Skalierung des Benutzerraums zu ändern. Wenn du eine vertikale Spiegelung einführst, dokumentiere, welche Stufe diese Umrechnung übernimmt, und prüfe das Ergebnis anhand der Markierung, bevor du eine weitere Spiegelung hinzufügst. Dokumentation zu Translationen, Dokumentation zur Skalierung.
Besitzverhältnisse beim Zurückgeben eines Bildes oder beim Hochladen zu Metal erhalten
Das Beispiel lässt die AppKit-Repräsentation so lange bestehen, wie ihr CGImage gezeichnet wird. Anschließend gibt es einen Snapshot zurück, der aus dem Normalisierungskontext erstellt wurde. Apple dokumentiert, dass makeImage() eine Kopie erstellt, die intern gegebenenfalls Copy-on-Write verwendet, und dass späteres Zeichnen im Kontext das zurückgegebene Bild nicht verändert. Daher kann der Kontext nach Erstellung des Snapshots freigegeben werden. Apples Dokumentation zu Bitmap-Snapshots.
Ein roher Bitmap-Zeiger hat eine andere Lebensdauer. Apple ermöglicht den Zugriff auf Bitmap-Daten, die dem Kontext gehören, auch wenn der Kontext mit einem Null-Datenargument erstellt wurde. Behandle diesen Zeiger so, als gehöre er zum aktiven Kontext. Für eine spätere Verarbeitung kopierst du die Bytes in selbst verwalteten Speicher oder hältst den Kontext bis zum Abschluss der Verarbeitung am Leben. Apples Dokumentation zu Bitmap-Daten.
Führe diese Schritte für einen Metal-Upload aus, solange der normalisierte Kontext noch besteht:
- Erstelle eine zweidimensionale Textur mit der Pixelbreite und -höhe des Kontexts. Verwende
bgra8Unormfür die gezeigte BGRA-Komponentenbelegung; Apple definiert sie als vier normalisierte 8-Bit-Komponenten ohne Vorzeichen in BGRA-Reihenfolge. - Verwende für den Upload von der CPU den standardmäßigen Speichermodus des Systems, statt einen hardwarespezifischen gemeinsam genutzten Modus zu erzwingen. Apple dokumentiert den gemeinsam genutzten Modus als Standard auf Apple Silicon und den verwalteten Modus als Standard für Texturen auf Intel-basierten Macs.
- Rufe
replace(region:mipmapLevel:withBytes:bytesPerRow:)für den gesamten Texturbereich und die Mip-Stufe0auf und verwende dabei den Datenzeiger des Kontexts und dessen tatsächlichen Zeilenabstand. - Schließe den Upload ab, bevor du den Kontext freigibst. Wenn du die Textur wiederverwendest, stelle außerdem sicher, dass frühere GPU-Arbeiten mit dieser Textur abgeschlossen sind, bevor die CPU sie überschreibt.
Die Format- und Speicherwahl folgt Apples Dokumentation zu bgra8Unorm und zu Speichermodi von Ressourcen. Apple gibt an, dass die Ersetzungsmethode die übergebenen Pixel sofort kopiert, jedoch keine Synchronisierung mit GPU-Operationen durchführt. Für eine private Textur ist laut Dokumentation der vorgesehene Weg eine nicht private temporäre Textur, gefolgt von einem Blit. Apples Dokumentation zum Ersetzen von Texturregionen.
Beim Textur-Upload bleiben die übergebenen Bytes erhalten. Die passende Komponentenbelegung allein legt jedoch nicht den Rest deiner Rendering-Pipeline fest. Übernimm die Entscheidungen des Beispiels zu vorab multipliziertem Alpha und sRGB in das Design des Verbrauchers. Bei der Normalisierung wird in ein gewähltes Format umgezeichnet. Daher sollte sie nicht als Garantie einer byteidentischen Umwandlung aus jeder denkbaren AppKit-Repräsentation beschrieben werden.
Für diese Aufgabe umfasst eine zuverlässige Übergabe das Raster samt Pixeldimensionen, logischer Darstellungsgröße, Zeilenabstand, Komponentenformat, Ausrichtungskonvention und Besitzverhältnis. Halte diese Werte zusammen, teste mit einer asymmetrischen synthetischen Zeichnung und korrigiere die erste Stufe, in der sie voneinander abweichen. So lässt sich ein verschobenes oder wiederholtes Ergebnis auf ein nachvollziehbares Problem mit den Schnittstellenbedingungen zurückführen.
Quellen
cacheDisplay(in:to:) — Apple
imageScaling — Apple
bounds — Apple
init(cgImage:size:) — Apple
convertToBacking(_:) — Apple
cgImage — Apple
byteOrder32Little — Apple
draw(_:in:byTiling:) — Apple
bytesPerRow — Apple
translateBy(x:y:) — Apple
scaleBy(x:y:) — Apple
makeImage() — Apple
data — Apple
MTLPixelFormat.bgra8Unorm — Apple
Setting resource storage modes — Apple