若要將 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 計算驗證。

依固定順序診斷像素位移或重複

以下步驟是根據前述幾何與記憶體規範整理出的診斷輔助方法。症狀能提示優先檢查之處,但無法證明特定原因。

  1. 檢查要求的矩形。 對整個影像檢視畫面,比對快取要求與 bounds。若要匯出子區域,請將其本機原點納入輸入規範。
  2. 先檢查尺寸,再檢查原始位元組。 將快取影像的寬度和高度,與提供給後續每個使用端的尺寸相比。以點寬度取代像素寬度會改變緩衝區的解讀方式。
  3. 檢查列距。 如果每一列的起始位置逐漸偏離預期位置,請先檢查各列起點之間的距離,再調整轉換。
  4. 使用不對稱標記檢查方向。 若上下顛倒,應檢查座標慣例;若是固定位移,應檢查原點和位移量。
  5. 檢查重複是否為預期行為。 檢查是否有繪圖呼叫啟用了平鋪。若明確啟用了平鋪,調整列距無法修正結果。
  6. 最後檢查呈現縮放。 若儲存的光柵正確,但顯示結果不同,請比對輸出影像的邏輯尺寸和接收端影像檢視畫面的縮放設定。

對於每像素四位元組的格式,範例中的 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,請在正規化內容仍存活時完成以下步驟:

  1. 建立寬度和高度符合內容像素尺寸的 2D 紋理。對於範例使用的 BGRA 元件配置,請使用 bgra8Unorm;Apple 將它定義為 BGRA 順序的四個 8 位元正規化無號元件。
  2. 使用 CPU 上傳時,保留系統預設的儲存模式,不要強制指定特定硬體的共享模式。Apple 說明,Apple 晶片上的共享模式為預設值,而 Intel 架構 Mac 上的紋理則預設採用受管理模式。
  3. 對完整紋理區域、mipmap 層級 0 呼叫 replace(region:mipmapLevel:withBytes:bytesPerRow:),並使用內容的資料指標與實際列距。
  4. 在釋放內容之前完成上傳。若要重複使用紋理,也要確保先前使用該紋理的 GPU 作業已完成,再由 CPU 覆寫它。

格式和儲存模式的選擇依據 Apple 的 bgra8Unorm 文件與資源儲存模式指南。Apple 表示,替換方法會立即複製提供的像素,但不會與 GPU 作業同步。若使用私有紋理,文件建議的作法是先使用非私有暫存紋理,再進行 blit。Apple 的紋理替換文件。

紋理上傳會保留提供的位元組;但僅符合元件配置,並不能定義其餘的繪圖流程。請將範例中的預乘 alpha 與 sRGB 選擇納入使用端的設計。正規化是以所選格式重新繪製,因此不應宣稱它能保證將所有可能的 AppKit 表示法轉換成位元組完全相同的資料。

對於此任務,可靠的交接資料包含光柵及其像素尺寸、邏輯呈現尺寸、列距、元件格式、方向慣例和擁有者。請將這些數值一併保存,以不對稱合成圖進行測試,並修正第一個出現差異的階段。如此一來,位移或重複問題就能追溯到明確的規範。

來源