Para rasterizar a NSImageView do seu app, use os métodos pareados de cache de bitmap do AppKit e preserve as dimensões em pixels resultantes. Se precisar de bytes previsíveis para o Metal, redesenhe esse bitmap em um buffer do Core Graphics definido explicitamente.

Este guia aborda uma imagem sintética gerada inteiramente na memória. O objetivo é obter uma rasterização reutilizável da imagem exibida em uma NSImageView, diagnosticar pixels deslocados ou repetidos e, opcionalmente, transferir a rasterização para uma textura do Metal.

Escolha se você precisa da apresentação da view ou de uma imagem com tamanho exato

Uma imagem e a view que a exibe têm responsabilidades diferentes. A view aplica o modo de escala para ajustar a imagem ao seu quadro. O padrão é scaleProportionallyDown, portanto, extrair a imagem original não reproduz necessariamente a apresentação da view. A documentação da Apple sobre imageScaling descreve esse comportamento.

Escolha a saída antes de escolher a API:

Saída desejada Ponto de partida adequado Contrato de dimensões
A imagem conforme desenhada pela image view Métodos de cache de bitmap da view no AppKit Inspecione as dimensões em pixels do bitmap retornado
Um desenho sintético com dimensões especificadas exatamente Desenhe diretamente em um CGContext de bitmap Largura e altura explícitas em pixels inteiros
Bytes previsíveis para uma textura do Metal Redesenhe a rasterização em um contexto de bitmap definido Formato de pixel, stride e propriedade explícitos

A Apple documenta bitmapImageRepForCachingDisplay(in:) como criador de uma representação adequada para a região da view solicitada. Passe essa representação para cacheDisplay(in:to:); a Apple recomenda especificamente esse par para compatibilidade de formato de pixel. O segundo método desenha a view e suas descendentes. Documentação sobre alocação de bitmap, documentação sobre cache de view.

Esses métodos não expõem um argumento de resolução de saída arbitrária. Se o requisito for exatamente 192 × 128 pixels, renderize o desenho sintético diretamente nesse tamanho. Redimensionar depois uma rasterização menor da view não recupera detalhes que nunca foram rasterizados. Isso decorre dos dois contratos de saída diferentes, e não de um recurso adicional do cache de view.

Mantenha pontos, pixels e retângulos separados

Use view.bounds ao solicitar toda a região local de desenho. Os bounds de uma view descrevem seu próprio sistema de coordenadas; o frame descreve sua posição e tamanho no sistema de coordenadas da superview. Portanto, passar o frame para um método que espera uma região local pode introduzir um deslocamento indesejado. Documentação da Apple sobre bounds.

Como exemplo de contrato de tamanho, um bitmap de origem de 192 × 128 pode ser apresentado como uma imagem lógica de 96 × 64. Preserve ambos os valores: o primeiro descreve as amostras, enquanto o segundo descreve o tamanho de apresentação. O inicializador de NSImage aceita um tamanho explícito; fornecer zero faz com que ele adote as dimensões em pixels da imagem de origem. Documentação da Apple sobre o inicializador de imagem.

Ao diagnosticar a geometria de backing de uma view existente, use convertToBacking(_:), que converte seu retângulo local em coordenadas do backing store. Para a rasterização realmente retornada, inspecione pixelsWide e pixelsHigh, ou as dimensões de seu CGImage. A Apple documenta que cgImage de um NSBitmapImageRep tem as mesmas dimensões em pixels que a representação. Documentação sobre conversão para coordenadas de backing, documentação sobre CGImage de bitmap.

Não substitua essas dimensões retornadas por um multiplicador 2× presumido. Registre juntos o retângulo local solicitado, a largura e a altura retornadas em pixels e o tamanho lógico pretendido. Assim, você terá uma comparação concreta quando o resultado parecer ampliado, cortado ou deslocado.

Crie um exemplo completo de rasterização sintética

O programa Objective-C a seguir gera uma imagem de teste assimétrica, atribui-a a uma image view em memória, rasteriza a view e normaliza o resultado em um bitmap do Core Graphics. Ele não cria arquivos. Salve-o como RasterDemo.m em um Mac com as ferramentas de desenvolvimento de linha de comando instaladas.

clang -fobjc-arc RasterDemo.m -framework Cocoa -o RasterDemo
./RasterDemo

O contexto de normalização usa componentes de oito bits, alfa pré-multiplicado na posição mais significativa e armazenamento de 32 bits em little-endian. Em conjunto, essas escolhas produzem a ordem de bytes BGRA: o componente azul menos significativo vem primeiro na memória. Isso segue as definições da Apple de premultipliedFirst e 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;
}

O programa informa deliberadamente as dimensões reais da saída, em vez de afirmar que o cache da view corresponde aos 192 × 128 pixels da origem. O marcador ciano permite distinguir mudanças de orientação das linhas verticais de teste repetidas. Se o requisito for a resolução exata da origem, use diretamente o CGImage retornado por NewTestImage(), sem passar pela view.

A limpeza explícita estabelece pixels de destino transparentes antes da normalização. O desenho de uma única imagem preenche o retângulo de destino; não cria mosaicos. A Apple documenta que a operação correspondente do Core Graphics escala para o retângulo de destino e desenha uma cópia, a menos que o modo de mosaico esteja habilitado. Documentação da Apple sobre desenho de imagens.

Este programa foi inspecionado, mas não pôde ser compilado no ambiente de pesquisa Linux, que não tem AppKit nem um compilador C. Os cálculos de stride usados abaixo foram verificados com um pequeno cálculo determinístico em Python.

Diagnostique pixels deslocados ou repetidos em uma ordem fixa

A sequência a seguir é um auxílio de diagnóstico derivado dos contratos de geometria e memória descritos acima. Os sintomas indicam onde começar a inspeção; não determinam uma causa única.

  1. Verifique o retângulo solicitado. Para a image view inteira, compare a solicitação de cache com bounds. Se estiver exportando uma sub-região, mantenha sua origem local como parte do contrato de entrada.
  2. Verifique as dimensões antes dos bytes brutos. Compare a largura e a altura da imagem em cache com as dimensões fornecidas a cada consumidor posterior. Substituir a largura em pixels pela largura em pontos altera a interpretação do buffer.
  3. Verifique o stride das linhas. Se cada linha começar progressivamente mais distante da posição esperada, inspecione a distância entre os inícios das linhas antes de ajustar as transformações.
  4. Verifique a orientação com o marcador assimétrico. Uma inversão vertical indica que é preciso inspecionar as convenções de coordenadas. Um deslocamento constante indica que é preciso inspecionar as origens e translações.
  5. Verifique se a repetição é intencional. Inspecione qualquer chamada de desenho que habilite o mosaico. Alterar o stride das linhas não corrigirá um desenho explicitamente em mosaico.
  6. Verifique a escala de apresentação por último. Se a rasterização armazenada estiver correta, mas o resultado exibido for diferente, compare o tamanho lógico da imagem de saída e as configurações de escala da image view que a recebe.

Para pixels de quatro bytes, o buffer ilustrativo de 192 × 128 tem 768 bytes por linha e requer 98,304 bytes. Porém, um buffer de 191 × 128 com o alinhamento de linha de 64 bytes do exemplo também tem um stride de 768 bytes: cada linha inclui quatro bytes de preenchimento. Avançar em passos de 191 × 4, ou 764, faria cada linha seguinte começar quatro bytes antes do esperado. A Apple expõe o stride real por meio da propriedade bytesPerRow do contexto.

Use o cálculo de endereço base + y * bytesPerRow + x * 4 somente para o formato normalizado de quatro bytes definido aqui. Um bitmap arbitrário do AppKit tem seu próprio contrato de formato; não infira a disposição de seus componentes apenas pela aparência.

Da mesma forma, aplique mudanças de coordenadas deliberadamente. O Core Graphics oferece operações separadas para alterar a origem e a escala do espaço do usuário. Se introduzir uma inversão vertical, documente qual etapa é responsável por essa conversão e confira o resultado com o marcador antes de adicionar outra. Documentação sobre translação, documentação sobre escala.

Preserve a propriedade ao retornar uma imagem ou enviar dados ao Metal

O exemplo mantém a representação do AppKit viva enquanto desenha seu CGImage. Em seguida, retorna um snapshot criado a partir do contexto de normalização. A Apple documenta que makeImage() cria uma cópia, possivelmente usando cópia na gravação internamente, e que desenhos subsequentes no contexto não alteram a imagem retornada. Isso permite liberar o contexto depois de criar o snapshot. Documentação da Apple sobre snapshot de bitmap.

Um ponteiro bruto para bitmap tem outro ciclo de vida. A Apple fornece acesso aos dados de bitmap pertencentes ao contexto mesmo quando ele foi criado com um argumento de dados nulo. Trate esse ponteiro como pertencente ao contexto ativo; para um consumidor posterior, copie os bytes para um armazenamento sob seu controle ou mantenha o contexto ativo até o fim do consumo. Documentação da Apple sobre dados de bitmap.

Para enviar dados ao Metal, execute estas etapas enquanto o contexto normalizado ainda estiver ativo:

  1. Crie uma textura bidimensional com a largura e a altura em pixels do contexto. Use bgra8Unorm para a disposição de componentes BGRA demonstrada; a Apple o define como quatro componentes normalizados sem sinal de oito bits, na ordem BGRA.
  2. Para o envio pela CPU, mantenha o modo de armazenamento padrão do sistema em vez de forçar um modo compartilhado específico do hardware. A Apple documenta padrões compartilhados em dispositivos Apple silicon e padrões gerenciados para texturas em Macs com processadores Intel.
  3. Chame replace(region:mipmapLevel:withBytes:bytesPerRow:) para a região completa da textura, no nível de mip 0, usando o ponteiro de dados do contexto e o stride real das linhas.
  4. Conclua o envio antes de liberar o contexto. Para reutilizar a textura, verifique também se o trabalho anterior da GPU que a utiliza terminou antes de a CPU sobrescrevê-la.

As opções de formato e armazenamento seguem a documentação da Apple sobre bgra8Unorm e as orientações sobre armazenamento de recursos. A Apple afirma que o método de substituição copia imediatamente os pixels fornecidos, mas não sincroniza com operações da GPU. Para uma textura privada, o caminho documentado é usar uma textura temporária não privada e, em seguida, um blit. Documentação da Apple sobre substituição de textura.

O envio da textura preserva os bytes fornecidos; corresponder apenas à disposição dos componentes não define o restante do seu pipeline de renderização. Incorpore ao projeto do consumidor as decisões do exemplo sobre alfa pré-multiplicado e sRGB. A normalização é um redesenho em um formato escolhido, portanto, não deve ser descrita como garantia de conversão com bytes idênticos a partir de todas as representações possíveis do AppKit.

Para esta tarefa, uma transferência confiável inclui a rasterização e suas dimensões em pixels, o tamanho lógico de apresentação, o stride das linhas, o formato dos componentes, a convenção de orientação e o responsável pela propriedade. Mantenha esses valores juntos, teste com um desenho sintético assimétrico e corrija a primeira etapa em que divergirem. Assim, um resultado deslocado ou repetido se torna um problema rastreável de contrato.

Fontes