Aby zrasteryzować NSImageView aplikacji, użyj sparowanych metod buforowania bitmap AppKit, a następnie zachowaj uzyskane wymiary w pikselach. Jeśli potrzebujesz przewidywalnych bajtów dla Metal, przerysuj tę bitmapę do jawnie zdefiniowanego bufora Core Graphics.
Ten poradnik dotyczy syntetycznego obrazu wygenerowanego w całości w pamięci. Celem jest uzyskanie wielokrotnego użytku rastra jego prezentacji w NSImageView, zdiagnozowanie nieprawidłowo położonych lub powtarzających się pikseli oraz opcjonalne przeniesienie rastra do tekstury Metal.
Wybierz, czy potrzebujesz prezentacji widoku, czy obrazu o dokładnym rozmiarze
Obraz i wyświetlający go widok pełnią różne funkcje. Widok stosuje tryb skalowania, aby dopasować obraz do swojej ramki. Domyślnie jest to scaleProportionallyDown, więc wyodrębnienie oryginalnego obrazu nie musi odtwarzać jego prezentacji w widoku. Dokumentacja Apple dotycząca imageScaling opisuje to zachowanie.
Zanim wybierzesz API, zdecyduj, jaki ma być wynik:
| Oczekiwany wynik | Odpowiedni punkt wyjścia | Kontrakt wymiarów |
|---|---|---|
| Obraz narysowany przez widok obrazu | Metody buforowania bitmap widoku w AppKit | Sprawdź wymiary zwróconej bitmapy w pikselach |
| Rysunek syntetyczny o dokładnie określonych wymiarach | Rysowanie bezpośrednio w CGContext bitmapy |
Jawna całkowita szerokość i wysokość w pikselach |
| Przewidywalne bajty dla tekstury Metal | Przerysowanie rastra do zdefiniowanego kontekstu bitmapy | Jawny format pikseli, krok wiersza i sposób zarządzania pamięcią |
Apple opisuje bitmapImageRepForCachingDisplay(in:) jako metodę tworzącą reprezentację odpowiednią dla żądanego obszaru widoku. Przekaż tę reprezentację do cacheDisplay(in:to:); Apple wyraźnie zaleca tę parę metod ze względu na zgodność formatów pikseli. Druga metoda rysuje widok i jego elementy podrzędne. Dokumentacja alokowania bitmapy, dokumentacja buforowania widoku.
Te metody nie udostępniają argumentu pozwalającego ustawić dowolną rozdzielczość wyjściową. Jeśli wymagany wynik ma mieć dokładnie 192 × 128 pikseli, narysuj rysunek syntetyczny bezpośrednio w tym rozmiarze. Późniejsze przeskalowanie rastra widoku o mniejszej rozdzielczości nie przywróci szczegółów, które nigdy nie zostały zrasteryzowane. Wynika to z dwóch różnych kontraktów wyjściowych, a nie z dodatkowej możliwości buforowania widoku.
Rozróżniaj punkty, piksele i prostokąty
Użyj view.bounds, aby zażądać całego lokalnego obszaru rysowania. Granice widoku opisują jego własny układ współrzędnych, a jego ramka — położenie i rozmiar w układzie współrzędnych widoku nadrzędnego. Przekazanie ramki do metody oczekującej lokalnego obszaru może więc wprowadzić niezamierzone przesunięcie. Dokumentacja Apple dotycząca granic.
Przykładowo bitmapa źródłowa o wymiarach 192 × 128 może być prezentowana jako obraz logiczny o wymiarach 96 × 64. Zachowaj obie wartości: pierwsza opisuje próbki, a druga rozmiar prezentacji. Inicjalizator NSImage przyjmuje jawny rozmiar; przekazanie zera sprawia natomiast, że obraz przyjmuje wymiary pikselowe obrazu źródłowego. Dokumentacja Apple dotycząca inicjalizatora obrazu.
Aby zdiagnozować geometrię bufora widoku, użyj convertToBacking(_:), która przekształca lokalny prostokąt na współrzędne bufora. W przypadku faktycznie zwróconego rastra sprawdź pixelsWide i pixelsHigh albo wymiary jego CGImage. Apple dokumentuje, że cgImage obiektu NSBitmapImageRep ma takie same wymiary w pikselach jak sama reprezentacja. Dokumentacja konwersji współrzędnych bufora, dokumentacja CGImage bitmapy.
Nie zastępuj zwróconych wymiarów domniemanym mnożnikiem 2×. Zapisuj razem żądany lokalny prostokąt, zwróconą szerokość i wysokość w pikselach oraz docelowy rozmiar logiczny. Dzięki temu uzyskasz konkretne wartości do porównania, gdy wynik wydaje się powiększony, przycięty lub przesunięty.
Przygotuj kompletny przykład rasteryzacji syntetycznej
Poniższy program w Objective-C generuje asymetryczny obraz testowy, przypisuje go do widoku obrazu utworzonego w pamięci, rasteruje widok i normalizuje wynik do bitmapy Core Graphics. Nie tworzy żadnych plików. Zapisz go jako RasterDemo.m na Macu z zainstalowanymi narzędziami deweloperskimi wiersza poleceń.
clang -fobjc-arc RasterDemo.m -framework Cocoa -o RasterDemo
./RasterDemo
Kontekst normalizacji używa ośmiobitowych komponentów, alfa premultiplikowanej na najbardziej znaczącej pozycji oraz 32-bitowego zapisu little-endian. Łącznie te ustawienia dają kolejność bajtów BGRA: najmniej znaczący komponent niebieski znajduje się w pamięci jako pierwszy. Wynika to z definicji Apple: premultipliedFirst i 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;
}
Program celowo podaje faktyczne wymiary wyjściowe, zamiast zakładać, że bufor widoku będzie miał rozdzielczość źródła wynoszącą 192 × 128 pikseli. Cyjanowy znacznik pozwala odróżnić zmiany orientacji od powtarzających się pionowych linii testowych. Jeśli wymagane jest dokładne źródłowe próbkowanie, użyj bezpośrednio obiektu CGImage zwróconego przez NewTestImage(), zamiast przepuszczać obraz przez widok.
Jawne czyszczenie kontekstu ustawia przezroczyste piksele docelowe przed normalizacją. Rysowanie pojedynczego obrazu wypełnia prostokąt docelowy; nie wykonuje kafelkowania. Apple dokumentuje, że odpowiadająca temu operacja rysowania Core Graphics skaluje obraz do prostokąta docelowego i rysuje jedną kopię, chyba że włączono kafelkowanie. Dokumentacja Apple dotycząca rysowania obrazów.
Program został sprawdzony, ale nie można było go skompilować w linuksowym środowisku badawczym, które nie zawiera AppKit ani kompilatora C. Obliczenia kroku wiersza użyte poniżej sprawdzono za pomocą niewielkiego, deterministycznego skryptu w Pythonie.
Diagnozuj przesunięte lub powtarzające się piksele w ustalonej kolejności
Poniższa sekwencja ułatwia diagnozę na podstawie opisanych wyżej kontraktów geometrii i pamięci. Objawy wskazują, co warto sprawdzić w pierwszej kolejności, ale nie ustalają jednej, konkretnej przyczyny.
- Sprawdź żądany prostokąt. W przypadku całego widoku obrazu porównaj żądanie buforowania z
bounds. Jeśli eksportujesz podobszar, uwzględnij jego lokalne położenie początkowe w kontrakcie wejściowym. - Przed surowymi bajtami sprawdź wymiary. Porównaj szerokość i wysokość buforowanego obrazu z wymiarami przekazywanymi do każdego kolejnego odbiorcy. Zastąpienie szerokości w punktach szerokością w pikselach zmienia sposób interpretacji bufora.
- Sprawdź krok wiersza. Jeśli każdy wiersz zaczyna się coraz dalej od oczekiwanej pozycji, przed modyfikacją przekształceń sprawdź odległość między początkami wierszy.
- Sprawdź orientację za pomocą asymetrycznego znacznika. Odwrócenie w pionie wymaga sprawdzenia konwencji współrzędnych. Stałe przesunięcie wymaga sprawdzenia położeń początkowych i translacji.
- Sprawdź, czy powtarzanie jest zamierzone. Skontroluj każde wywołanie rysowania, które włącza kafelkowanie. Zmiana kroku wiersza nie skoryguje rysowania, w którym jawnie włączono kafelkowanie.
- Na końcu sprawdź skalowanie prezentacji. Jeśli zapisany raster jest poprawny, ale wyświetlany wynik się różni, porównaj logiczny rozmiar obrazu wyjściowego z ustawieniami skalowania odbierającego widoku obrazu.
Dla pikseli czterobajtowych przykładowy bufor 192 × 128 ma 768 bajtów na wiersz i wymaga 98,304 bajtów. Jednak bufor 191 × 128 z wyrównaniem wierszy do 64 bajtów, jak w przykładzie, również ma krok wynoszący 768 bajtów: każdy wiersz zawiera cztery bajty dopełnienia. Przejście o 191 × 4, czyli 764 bajtów, rozpoczynałoby każdy kolejny wiersz cztery bajty za wcześnie. Apple udostępnia faktyczny krok za pośrednictwem właściwości bytesPerRow kontekstu.
Adresu base + y * bytesPerRow + x * 4 używaj wyłącznie dla znormalizowanego formatu czterobajtowego zdefiniowanego w tym przykładzie. Dowolna bitmapa AppKit ma własny kontrakt formatu; nie wyciągaj wniosków o układzie komponentów wyłącznie na podstawie jej wyglądu.
Podobnie świadomie stosuj zmiany współrzędnych. Core Graphics udostępnia osobne operacje zmiany początku układu i skalowania przestrzeni użytkownika. Jeśli wprowadzasz odwrócenie w pionie, udokumentuj, który etap odpowiada za tę konwersję, i sprawdź wynik za pomocą znacznika, zanim dodasz kolejne odwrócenie. Dokumentacja translacji, dokumentacja skalowania.
Zachowaj własność przy zwracaniu obrazu lub przesyłaniu do Metal
Przykład utrzymuje reprezentację AppKit przy życiu podczas rysowania jej CGImage. Następnie zwraca migawkę utworzoną z kontekstu normalizacji. Apple dokumentuje, że makeImage() tworzy kopię, która może wewnętrznie korzystać z mechanizmu kopiowania przy zapisie, a późniejsze rysowanie w kontekście nie zmienia zwróconego obrazu. Umożliwia to zwolnienie kontekstu po utworzeniu migawki. Dokumentacja Apple dotycząca migawki bitmapy.
Wskaźnik surowej bitmapy ma inny czas życia. Apple zapewnia dostęp do danych bitmapy należących do kontekstu nawet wtedy, gdy kontekst utworzono z pustym argumentem danych. Traktuj ten wskaźnik jako należący do aktywnego kontekstu; jeśli dane mają być użyte później, skopiuj bajty do pamięci, którą kontrolujesz, albo utrzymuj kontekst przy życiu do czasu zakończenia ich używania. Dokumentacja Apple dotycząca danych bitmapy.
Aby przesłać dane do Metal, wykonaj poniższe kroki, gdy znormalizowany kontekst jest jeszcze aktywny:
- Utwórz teksturę dwuwymiarową o szerokości i wysokości w pikselach zgodnych z kontekstem. Użyj
bgra8Unormdla pokazanego układu komponentów BGRA; Apple definiuje go jako cztery ośmiobitowe znormalizowane komponenty bez znaku w kolejności BGRA. - W przypadku przesyłania przez CPU zachowaj domyślny tryb pamięci zamiast wymuszać tryb współdzielony zależny od sprzętu. Apple dokumentuje domyślne ustawienie
sharedna układach Apple silicon imanageddla tekstur na Macach z procesorami Intel. - Wywołaj
replace(region:mipmapLevel:withBytes:bytesPerRow:)dla pełnego obszaru tekstury i poziomu mipmapy0, używając wskaźnika danych kontekstu i jego faktycznego kroku wiersza. - Zakończ przesyłanie przed zwolnieniem kontekstu. Aby ponownie użyć tekstury, zaczekaj też na zakończenie wcześniejszych zadań GPU korzystających z tej tekstury, zanim CPU nadpisze jej dane.
Wybory dotyczące formatu i pamięci wynikają z dokumentacji Apple: bgra8Unorm i wytycznych dotyczących przechowywania zasobów. Apple informuje, że metoda zastępowania natychmiast kopiuje dostarczone piksele, ale nie synchronizuje się z operacjami GPU. Dla tekstury typu private udokumentowana ścieżka obejmuje tymczasową teksturę inną niż private, a następnie operację blit. Dokumentacja Apple dotycząca zastępowania tekstury.
Przesyłanie do tekstury zachowuje dostarczone bajty; samo dopasowanie układu komponentów nie definiuje pozostałej części potoku renderowania. Uwzględnij w projekcie odbiorcy przyjęte w przykładzie decyzje dotyczące premultiplikowanej alfy i przestrzeni sRGB. Normalizacja polega na ponownym rysowaniu w wybranym formacie, dlatego nie należy przedstawiać jej jako gwarancji konwersji identycznej bajt po bajcie dla każdej możliwej reprezentacji AppKit.
W przypadku tego zadania niezawodne przekazanie obejmuje raster wraz z jego wymiarami w pikselach, logicznym rozmiarem prezentacji, krokiem wiersza, formatem komponentów, konwencją orientacji i informacją o właścicielu. Przechowuj te wartości razem, sprawdzaj je za pomocą asymetrycznego rysunku syntetycznego i koryguj pierwszy etap, na którym przestają być zgodne. Dzięki temu problem przesuniętych lub powtarzających się pikseli da się prześledzić do konkretnego naruszenia kontraktu.
Źródła
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