Aby przekształcić syntetyczne surowe piksele w niezawodny obiekt CGImage, określ jednocześnie ich wymiary, odstęp między wierszami, kolejność kanałów, konwencję alfa, przestrzeń barw i sposób zarządzania pamięcią. W tym przewodniku powstaje konwerter BGRA z kontrolą poprawności, a następnie wynik otrzymuje jawnie określony rozmiar wyświetlania w AppKit.

Zadanie czytelnika polega na utworzeniu obrazu w pamięci dla generowanej próbki kolorów, diagramu lub podglądu w aplikacji na Maca. Poniższa implementacja obsługuje jeden celowo wąsko zdefiniowany format. Osobna sekwencja decyzji wyjaśnia, jak syntetyczne dane wyjściowe Metal mogą trafić do tego samego etapu konwersji.

Co należy określić w kontrakcie konwersji?

Inicjalizator CGImage przyjmujący surowe dane wymaga, by bajty dostawcy odpowiadały przekazanym parametrom obrazu. Szerokość i wysokość są wyrażone w pikselach; bytesPerRow opisuje układ pamięci każdego poziomego wiersza. Te parametry opisują bufor wejściowy, więc należy ustalić je przed utworzeniem obrazu. Dokumentacja Apple dotycząca inicjalizatora obrazu bitmapowego.

W konwerterze z tego artykułu zastosuj poniższy kontrakt na poziomie aplikacji. Jego rygorystyczne reguły walidacji to decyzje projektowe, które jednoznacznie określają dopuszczalne dane wejściowe.

Właściwość Dopuszczalne dane wejściowe lub zachowanie
Wymiary Dodatnie liczby całkowite określające szerokość i wysokość w pikselach
Składowe Cztery 8-bitowe składowe bez znaku na piksel
Kolejność w pamięci Kolejne bajty to: niebieski, zielony, czerwony, alfa
Alfa Składowe RGB są już przemnożone przez alfę
Interpretacja barw sRGB
Odstęp między wierszami Co najmniej width * 4 bajtów i wielokrotność czterech
Długość bufora Dokładnie bytesPerRow * height, z uwzględnieniem dopełnienia ostatniego wiersza
Kolejność wierszy Zachowana dokładnie; bez automatycznego odwracania w pionie
Zarządzanie pamięcią Skopiowanie danych wejściowych do własnego niezmiennego magazynu
Błąd Zgłoszenie błędu przy niepoprawnych danych wejściowych lub niepowodzeniu konstrukcji
Prezentacja Osobny wybór wymiarów w punktach dla AppKit

Na przykład wiersz złożony z dwóch pikseli zawiera osiem aktywnych bajtów. Odstęp równy dwunastu oznacza cztery bajty dopełnienia po każdym wierszu. Dwa takie wiersze zajmują dwadzieścia cztery bajty. Pierwszy piksel drugiego wiersza zaczyna się pod offsetem dwunastu bajtów. Ten przykład celowo uwzględnia dopełnienie: ciasno upakowany zestaw testowy nie wykryłby konwertera, który błędnie zastępuje podany odstęp wartością width * 4.

Wymóg pełnego dopełnienia ostatniego wiersza upraszcza granicę zarządzania pamięcią. Producent używający krótszej alokacji dla ostatniego wiersza musi znormalizować dane wyjściowe przed wywołaniem tego konwertera. Odrzucenie niezgodnej długości zapobiega też przypadkowemu zaakceptowaniu większego bufora o niejasnym obszarze obrazu.

Jak łączą się BGRA, kolejność bajtów, alfa i przestrzeń barw?

W tym formacie połącz CGImageAlphaInfo.premultipliedFirst z CGBitmapInfo.byteOrder32Little. Apple definiuje pierwszy z tych elementów jako alfę w najbardziej znaczących bitach, przy czym składowe barw są już przemnożone przez alfę, a drugi jako 32-bitowy układ little-endian. Razem definicje te dają sekwencję bajtów BGRA w kolejności rosnących adresów. Alfa premultiplied-first, 32-bitowa kolejność bajtów little-endian.

Samo słowo „first” wymaga więc ustawienia kolejności bajtów, by określić, gdzie w pamięci znajduje się alfa. Dla nieprzezroczystej czerwieni używane są tu cztery bajty 0, 0, 255, 255. Podanie bajtów RGBA przy tych ustawieniach zamienia miejscami czerwień i błękit. Zmiana metadanych nie przestawia istniejącego bufora.

Premultiplikacja jest równie konkretna. W tej przykładowej konwencji 8-bitowej pełna czerwień z alfą 128 przyjmuje w pamięci BGRA wartość około 0, 0, 128, 128. Konwerter sprawdza, czy każda zapisana składowa barwy jest nie większa od alfy. Pozwala to wykryć wartości niemożliwe w wybranym znormalizowanym formacie z premultiplikacją, choć nie dowodzi, jak producent obliczył każdą składową.

Przypisuj CGColorSpace.sRGB tylko wtedy, gdy takie jest zamierzone znaczenie bajtów. Apple określa zarówno kolorymetrię sRGB, jak i jej nieliniową funkcję przenoszenia. Sama etykieta przestrzeni barw nie przekształca liniowych wartości składowych RGB w wartości sRGB. Definicja sRGB firmy Apple.

Ten konwerter przeprowadza walidację i tworzy obraz. Jeśli producent emituje dane ze zwykłą alfą, kolejnością RGBA, składowymi zmiennoprzecinkowymi lub liniowymi wartościami barw, przed tą granicą dodaj jawny etap normalizacji albo zaimplementuj osobny, udokumentowany kontrakt dla tego formatu.

Jak w Swift utworzyć obraz z własnym magazynem danych?

W przypadku małego, generowanego obrazu na komputer użyj granicy kopiującej dane. Apple dokumentuje, że CFDataCreate zawsze kopiuje podany bufor do pamięci wewnętrznej, a CGDataProvider(data:) odczytuje obiekt CFData. Inicjalizator CGImage zachowuje dostawcę. Dzięki temu obraz nie jest zależny od wskaźnika do tymczasowej tablicy. Kopiowanie bajtów do CFData, tworzenie dostawcy opartego na CFData.

Poniżej znajduje się kompletny program w Swift dla wiersza poleceń macOS. Zapisz go jako main.swift i uruchom swift main.swift na Macu z zainstalowanymi narzędziami deweloperskimi. Program tworzy oba obiekty obrazu i wypisuje ich wymiary; nie otwiera okna. Kod programu Swift został przejrzany, ale nie można go było skompilować w tym środowisku Linux. Liczbę bajtów, offsety i zakresy składowych premultiplikowanych w zestawie testowym sprawdzono osobno za pomocą Pythona.

import AppKit
import CoreGraphics
import CoreFoundation

 enum PixelError: Error {
    case invalidDimensions, arithmeticOverflow
    case invalidStride, invalidByteCount
    case invalidPremultipliedPixel
    case constructionFailed
}

func makeBGRAImage(
    bytes: [UInt8], width: Int, height: Int, bytesPerRow: Int
) throws -> CGImage {
    guard width > 0, height > 0 else {
        throw PixelError.invalidDimensions
    }
    let (activeRowBytes, rowOverflow) =
        width.multipliedReportingOverflow(by: 4)
    guard !rowOverflow else { throw PixelError.arithmeticOverflow }
    guard bytesPerRow >= activeRowBytes,
          bytesPerRow % 4 == 0 else {
        throw PixelError.invalidStride
    }
    let (requiredBytes, sizeOverflow) =
        bytesPerRow.multipliedReportingOverflow(by: height)
    guard !sizeOverflow else { throw PixelError.arithmeticOverflow }
    guard bytes.count == requiredBytes else {
        throw PixelError.invalidByteCount
    }

    for y in 0..<height {
        for x in 0..<width {
            let i = y * bytesPerRow + x * 4
            let alpha = bytes[i + 3]
            guard bytes[i] <= alpha,
                  bytes[i + 1] <= alpha,
                  bytes[i + 2] <= alpha else {
                throw PixelError.invalidPremultipliedPixel
            }
        }
    }

    let copiedData: CFData? = bytes.withUnsafeBufferPointer { buffer in
        CFDataCreate(nil, buffer.baseAddress, requiredBytes)
    }
    guard let data = copiedData,
          let provider = CGDataProvider(data: data),
          let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) else {
        throw PixelError.constructionFailed
    }
    let info = CGBitmapInfo.byteOrder32Little.union(
        CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
    )
    guard let image = CGImage(
        width: width, height: height,
        bitsPerComponent: 8, bitsPerPixel: 32,
        bytesPerRow: bytesPerRow,
        space: colorSpace, bitmapInfo: info,
        provider: provider, decode: nil,
        shouldInterpolate: false, intent: .defaultIntent
    ) else {
        throw PixelError.constructionFailed
    }
    return image
}

// Two active pixels and four padding bytes per row.
let syntheticBytes: [UInt8] = [
    0, 0, 255, 255,  0, 255, 0, 255,  0, 0, 0, 0,
    255, 0, 0, 255,  0, 0, 128, 128,  0, 0, 0, 0
]

do {
    let cgImage = try makeBGRAImage(
        bytes: syntheticBytes, width: 2, height: 2, bytesPerRow: 12
    )
    let appKitImage = NSImage(
        cgImage: cgImage, size: NSSize(width: 2, height: 2)
    )
    precondition(cgImage.width == 2 && cgImage.height == 2)
    print("Pixels: \(cgImage.width) x \(cgImage.height)")
    print("Points: \(appKitImage.size)")
} catch {
    fatalError("Synthetic image conversion failed: \(error)")
}

Kontrole mnożenia z obsługą przepełnień wykonują się przed indeksowaniem lub kopiowaniem bufora. Gdy kontrole długości i odstępu zakończą się powodzeniem, każdy aktywny piksel znajduje się w podanej tablicy. Dopełnienie jest zachowywane, ale pomijane podczas walidacji pikseli. Przypadki błędów rozróżniają niepoprawne wymiary, odstęp, długość, wartości alfa i niepowodzenia konstrukcji; rzeczywista aplikacja może wyświetlać te błędy lub zapisywać je w dzienniku za pomocą istniejącego mechanizmu obsługi błędów.

Pierwszy zapisany wiersz zestawu testowego zawiera nieprzezroczystą czerwień i nieprzezroczystą zieleń. Drugi zawiera nieprzezroczysty błękit i częściowo przezroczystą czerwień o przezroczystości około połowy. decode: nil oznacza brak mapowania dekodowania, a shouldInterpolate: false żąda utworzenia obrazu bez wygładzania pikseli. Te parametry nie zmieniają rozmiaru ani nie przekształcają danych wejściowych. Parametry tworzenia obrazu w dokumentacji Apple.

Do późniejszej optymalizacji Apple udostępnia również inicjalizator dostawcy z bezpośrednim dostępem i funkcją zwalniającą. Ta ścieżka wymaga osobnego kontraktu dotyczącego czasu życia alokacji: alokacja musi pozostać dostępna, gdy korzysta z niej Core Graphics, a następnie zostać zwolniona przez funkcję zwalniającą. Potraktuj ją jako inną implementację, której sposób zarządzania pamięcią trzeba zaprojektować jawnie. Inicjalizator dostawcy danych z bezpośrednim dostępem.

Jak przełożyć wymiary pikselowe na rozmiar wyświetlania w AppKit?

Traktuj wymiary rastrowe i logiczne jako osobne dane wejściowe. Apple rozróżnia rozmiary reprezentacji wyrażone w punktach oraz szerokości i wysokości mierzone w pikselach. NSImage(cgImage:size:) przyjmuje żądany rozmiar obrazu; przekazanie .zero powoduje, że przyjmuje on liczbowo wymiary źródłowego obrazu w pikselach. Wymiary reprezentacji obrazu, inicjalizator CGImage w AppKit.

Przykładowo rastrowi o wymiarach 400 × 200 pikseli, który ma dostarczać dwa piksele na punkt, należy przypisać obiektowi NSImage rozmiar 200 × 100 punktów. To obliczenie określa zamierzony rozmiar logiczny. Bazowy CGImage nadal ma wymiary 400 × 200 pikseli; opakowanie nie tworzy dodatkowych próbek.

Program przypisuje każdemu wymiarowi swojego dwupikselowego zestawu testowego dwa punkty. W przypadku większego podglądu wybierz jawny rozmiar logiczny odpowiedni dla widoku. Zachowaj dostęp do CGImage, gdy dalsza operacja wymaga dokładnych wymiarów rastrowych, ponieważ Apple zaleca, by nie zakładać szczegółów opakowania NSImage wykraczających poza równoważność rysowania z obrazem źródłowym.

Co się zmienia, gdy syntetyczne piksele pochodzą z Metal?

Metal wprowadza dodatkowy wymóg gotowości, który należy spełnić przed uruchomieniem konwertera CPU. Apple definiuje .bgra8Unorm jako cztery znormalizowane 8-bitowe składowe bez znaku w kolejności BGRA. Odpowiednik .bgra8Unorm_srgb określa też konwersję między sRGB a przestrzenią liniową. Sama zgodność kolejności kanałów nie gwarantuje więc, że zawartość tekstury spełnia konwencje dotyczące barw i premultiplikowanej alfy z tego artykułu. Znormalizowany format BGRA8, format BGRA8 sRGB.

Dla syntetycznej tekstury 2D z pojedynczą próbką na poziomie mipmapy zero zastosuj poniższą sekwencję:

  1. Ustal format pikseli tekstury, interpretację barw, konwencję alfy i zamierzoną orientację wierszy.
  2. Zakończ pracę GPU zapisującą teksturę i upewnij się, że zakończyła się powodzeniem, zanim zaakceptujesz jej wynik.
  3. Jeśli tekstura korzysta z pamięci zarządzanej, zakoduj wymaganą synchronizację GPU z CPU i zaczekaj na jej zakończenie.
  4. Jeśli tekstura korzysta z pamięci prywatnej, skopiuj ją operacją blit do tekstury dostępnej dla CPU przed odczytem.
  5. Przydziel bufor docelowy z kontrolą rozmiaru, skopiuj żądany obszar przy użyciu rzeczywistego odstępu w buforze docelowym, a następnie przekaż uzyskane bajty do kontraktu CPU.

Kroki te wynikają z dwóch odrębnych, udokumentowanych wymagań. getBytes natychmiast kopiuje dane na CPU, ale nie synchronizuje się z operacjami na teksturze GPU; Apple wprost zabrania bezpośredniego używania go na teksturach prywatnych. Osobno synchronize(resource:) aktualizuje kopię zasobu zarządzanego dostępną dla CPU; dostęp CPU jest możliwy po zakończeniu tej komendy. Wymagania dotyczące odczytu danych tekstury, synchronizacja zasobów zarządzanych.

Wybierz odstęp docelowy odpowiedni dla danej operacji odczytu zwrotnego i zachowaj go podczas konwersji. W wąsko zdefiniowanym kontrakcie BGRA każdy aktywny wiersz wymaga czterech bajtów na piksel. Układ etapowy operacji blit może wymagać innego dopełnienia niż bezpośredni odczyt CPU; zapisuj odstęp na każdej granicy zamiast zakładać, że każdy bufor ma taki sam skok wiersza.

Jak diagnozować niezgodność z kontraktem?

Zanim przetestujesz większą generowaną grafikę, wykorzystaj czteropikselowy zestaw testowy jako pomoc w diagnozie. Poniższe diagnozy to wnioski oparte na znanych wartościach i układzie zestawu, a nie wyniki pomiarów w aplikacji.

Obserwacja Co sprawdzić najpierw
Czerwień i błękit zamieniły się miejscami Rzeczywiste bajty kanałów oraz flagi alfy i kolejności bajtów
Pierwszy wiersz wygląda wiarygodnie, ale kolejne są przesunięte Odstęp źródłowy, odstęp docelowy i dopełnienie
Przezroczysta czerwień jest zbyt jasna lub zbyt ciemna Premultiplikację i interpretację barw
Wzory z dwóch wierszy zamieniły się miejscami w pionie Kolejność wierszy producenta i transformacje rysowania
Raster jest poprawny, ale rozmiar logiczny jest zły Wymiary w punktach dla AppKit
Dane utworzone na CPU działają, a utworzone na GPU nie Zakończenie pracy GPU, tryb pamięci, synchronizację i układ odczytu zwrotnego

Jeśli chodzi o orientację, ten konwerter gwarantuje zachowanie kolejności bajtów wierszy. Sprawdź asymetryczny zestaw testowy w docelowym widoku. Jeśli wynik jest odwrócony w pionie względem zamierzonego projektu, skoryguj kolejność wierszy na udokumentowanej granicy normalizacji albo popraw transformację rysowania. Jawnie określ tę decyzję, by dwa etapy nie odwróciły obrazu jednocześnie.

Kompletny kontrakt konwersji surowych pikseli pozwala ustalić, co otrzymuje i gwarantuje każdy etap: producent dostarcza zdefiniowane piksele, konwerter weryfikuje ich układ i tworzy własną kopię, a AppKit otrzymuje celowo określony rozmiar logiczny. Ustal te granice przy użyciu małego syntetycznego zestawu testowego, a następnie stosuj te same reguły dotyczące wymiarów, odstępu, barw, alfy i czasu życia pamięci dla większych obrazów.

Źródła