若要將 app 的 NSImageView 光柵化,請使用 AppKit 成對的點陣圖快取方法,並保留產生結果的像素尺寸。若 Metal 需要可預期的位元組資料,請將該點陣圖重新繪製到明確定義的 Core Graphics 緩衝區中。
本指南涵蓋完全在記憶體中產生的合成影像。目標是取得 NSImageView 中影像呈現效果的可重複使用光柵、診斷像素錯位或重複的原因,並可選擇將光柵傳送至 Metal 紋理。
選擇需要檢視畫面的呈現效果,還是固定尺寸的影像
影像與顯示影像的檢視畫面各有不同職責。檢視畫面會套用縮放模式,讓影像符合其影格。預設值為 scaleProportionallyDown,因此擷取原始影像不一定會重現檢視畫面的呈現效果。Apple 的 imageScaling 文件說明了此行為。
請先決定輸出,再選擇 API:
| 所需輸出 | 合適的起始方式 | 尺寸規範 |
|---|---|---|
| 影像檢視畫面繪製出的影像 | AppKit 的檢視畫面點陣圖快取方法 | 檢查傳回點陣圖的像素尺寸 |
| 尺寸完全符合指定值的合成繪圖 | 直接繪製到點陣圖 CGContext |
明確指定整數像素寬度和高度 |
| 可供 Metal 紋理使用的可預期位元組資料 | 將光柵重新繪製到明確定義的點陣圖內容 | 明確指定像素格式、列距和所有權 |
Apple 說明,bitmapImageRepForCachingDisplay(in:) 會建立適合所要求檢視區域的表示法。請將該表示法傳給 cacheDisplay(in:to:);Apple 特別建議這種配對,以確保像素格式相容。第二個方法會繪製檢視畫面及其子檢視畫面。點陣圖配置文件、檢視畫面快取文件。
這些方法不會提供任意輸出解析度的參數。如果需求是剛好 192 × 128 像素,請直接以該尺寸繪製合成圖。事後放大較小的檢視畫面光柵,無法還原從未光柵化的細節。這是兩種不同輸出規範所造成的結果,並非檢視畫面快取額外提供的能力。
區分點、像素與矩形
要求完整的區域繪圖時,請使用 view.bounds。檢視畫面的 bounds 描述其自身座標系統;frame 則描述它在 superview 座標系統中的位置和尺寸。因此,將 frame 傳給要求區域參數的方法,可能會造成非預期的位移。Apple 的 bounds 文件。
以尺寸規範為例,192 × 128 的來源點陣圖可能以 96 × 64 的邏輯影像尺寸呈現。請保留兩個數值:前者描述取樣點,後者描述呈現尺寸。NSImage 初始化器接受明確的尺寸;若提供零,則會採用來源影像的像素尺寸。Apple 的影像初始化器文件。
若要診斷現有檢視畫面的後備幾何尺寸,請使用 convertToBacking(_:),它會將檢視畫面的本機矩形轉換為後備儲存座標。若要取得實際傳回的光柵,請檢查 pixelsWide 和 pixelsHigh,或其 CGImage 尺寸。Apple 說明,NSBitmapImageRep 的 cgImage 與該表示法具有相同的像素尺寸。後備座標轉換文件、點陣圖 CGImage 文件。
不要以假設的 2× 倍率取代這些傳回的尺寸。請一併記錄要求的本機矩形、傳回的像素寬度與高度,以及預期的邏輯尺寸。當結果看起來放大、裁切或偏移時,這些資料可供明確比對。
建立完整的合成光柵化範例
以下 Objective-C 程式會產生一張不對稱的測試影像,將它指派給記憶體中的影像檢視畫面,將檢視畫面光柵化,再把結果正規化為 Core Graphics 點陣圖。程式不會建立任何檔案。請在安裝命令列開發工具的 Mac 上,將它存成 RasterDemo.m。
clang -fobjc-arc RasterDemo.m -framework Cocoa -o RasterDemo
./RasterDemo
正規化內容使用 8 位元元件、最重要位置的預乘 alpha,以及小端序 32 位元儲存方式。這些設定合起來會產生 BGRA 位元組順序:記憶體中最先出現的是最低有效位元的藍色元件。這符合 Apple 對 premultipliedFirst 與 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;
}
此程式刻意回報實際輸出尺寸,不會假設檢視畫面快取與來源的 192 × 128 像素相符。青色標記能讓方向變化與重複的垂直測試線有所區別。如果需求是精確的來源解析度,請直接使用 NewTestImage() 傳回的 CGImage,不要經過檢視畫面。
明確清除會在正規化前,先建立透明的目的地像素。單次影像繪製會填滿目的地矩形,不會平鋪。Apple 說明,對應的 Core Graphics 繪圖作業會縮放至目的地矩形,並只繪製一份,除非啟用平鋪。Apple 的影像繪製文件。
此程式已完成檢視,但無法在 Linux 研究環境中編譯,因為該環境缺少 AppKit 和 C 編譯器。以下使用的列距計算已透過簡單且具確定性的 Python 計算驗證。
依固定順序診斷像素位移或重複
以下步驟是根據前述幾何與記憶體規範整理出的診斷輔助方法。症狀能提示優先檢查之處,但無法證明特定原因。
- 檢查要求的矩形。 對整個影像檢視畫面,比對快取要求與
bounds。若要匯出子區域,請將其本機原點納入輸入規範。 - 先檢查尺寸,再檢查原始位元組。 將快取影像的寬度和高度,與提供給後續每個使用端的尺寸相比。以點寬度取代像素寬度會改變緩衝區的解讀方式。
- 檢查列距。 如果每一列的起始位置逐漸偏離預期位置,請先檢查各列起點之間的距離,再調整轉換。
- 使用不對稱標記檢查方向。 若上下顛倒,應檢查座標慣例;若是固定位移,應檢查原點和位移量。
- 檢查重複是否為預期行為。 檢查是否有繪圖呼叫啟用了平鋪。若明確啟用了平鋪,調整列距無法修正結果。
- 最後檢查呈現縮放。 若儲存的光柵正確,但顯示結果不同,請比對輸出影像的邏輯尺寸和接收端影像檢視畫面的縮放設定。
對於每像素四位元組的格式,範例中的 192 × 128 緩衝區每列有 768 個位元組,總共需要 98,304 個位元組。但採用範例中 64 位元組列對齊的 191 × 128 緩衝區,其列距也有 768 個位元組:每列包含四個填充位元組。若每次移動 191 × 4,也就是 764,後續各列就會提早四個位元組開始。Apple 提供內容的 bytesPerRow 屬性,可取得實際列距。
只有在此處建立的正規化四位元組格式下,才使用位址計算式 base + y * bytesPerRow + x * 4。任意 AppKit 點陣圖都有自己的格式規範;不要只憑外觀推斷其元件配置。
同樣地,座標變更也要謹慎處理。Core Graphics 提供不同作業來變更使用者空間的原點和縮放比例。如果加入垂直翻轉,請記錄由哪個階段負責轉換,並先對照標記確認,再加入其他轉換。平移文件、縮放文件。
傳回影像或上傳至 Metal 時保留所有權
範例在繪製其 CGImage 時,會讓 AppKit 表示法保持存活。接著,它會傳回由正規化內容建立的快照。Apple 說明,makeImage() 會建立副本,內部可能使用寫入時複製機制;後續在內容中的繪圖不會改變已傳回的影像。因此,建立快照後即可釋放內容。Apple 的點陣圖快照文件。
原始點陣圖指標的生命週期不同。Apple 提供存取內容所擁有之點陣圖資料的方式,即使建立內容時資料引數為 null 也適用。請將該指標視為隸屬於仍存活的內容;若稍後才由其他使用端處理,請將位元組複製到自行擁有的儲存空間,或讓內容存活至處理完成。Apple 的點陣圖資料文件。
若要上傳至 Metal,請在正規化內容仍存活時完成以下步驟:
- 建立寬度和高度符合內容像素尺寸的 2D 紋理。對於範例使用的 BGRA 元件配置,請使用
bgra8Unorm;Apple 將它定義為 BGRA 順序的四個 8 位元正規化無號元件。 - 使用 CPU 上傳時,保留系統預設的儲存模式,不要強制指定特定硬體的共享模式。Apple 說明,Apple 晶片上的共享模式為預設值,而 Intel 架構 Mac 上的紋理則預設採用受管理模式。
- 對完整紋理區域、mipmap 層級
0呼叫replace(region:mipmapLevel:withBytes:bytesPerRow:),並使用內容的資料指標與實際列距。 - 在釋放內容之前完成上傳。若要重複使用紋理,也要確保先前使用該紋理的 GPU 作業已完成,再由 CPU 覆寫它。
格式和儲存模式的選擇依據 Apple 的 bgra8Unorm 文件與資源儲存模式指南。Apple 表示,替換方法會立即複製提供的像素,但不會與 GPU 作業同步。若使用私有紋理,文件建議的作法是先使用非私有暫存紋理,再進行 blit。Apple 的紋理替換文件。
紋理上傳會保留提供的位元組;但僅符合元件配置,並不能定義其餘的繪圖流程。請將範例中的預乘 alpha 與 sRGB 選擇納入使用端的設計。正規化是以所選格式重新繪製,因此不應宣稱它能保證將所有可能的 AppKit 表示法轉換成位元組完全相同的資料。
對於此任務,可靠的交接資料包含光柵及其像素尺寸、邏輯呈現尺寸、列距、元件格式、方向慣例和擁有者。請將這些數值一併保存,以不對稱合成圖進行測試,並修正第一個出現差異的階段。如此一來,位移或重複問題就能追溯到明確的規範。
來源
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