Artigo Técnico

Renderização de Padrões Ladrilhados PDF em Delphi com HotPDF

Hachura que renderiza como um bloco cinza plano é a falha clássica de padrão ladrilhado. O HotPDF, o componente VCL nativo de PDF para Delphi e C++Builder, pinta PatternType 1 transformando o caminho atual em um recorte temporário e reproduzindo o content stream do padrão uma vez por ladrilho visível, com a seleção de padrão mantida no estado gráfico e restaurada por q e Q

Os sintomas chegam em dois sabores, e parecem não relacionados até você conhecer a causa. Um desenho CAD perde sua hachura de seção e volta como preenchimentos sólidos, porque o renderizador resolveu o padrão para uma cor média e pintou isso. Ou a hachura escapa: um bloco de título que deveria ser branco liso pega as linhas diagonais de uma vista de detalhe dois caminhos antes. Ambos são problemas de estado de padrão, e apenas um deles tem a ver com desenhar ladrilhos

Por que um padrão ladrilhado vaza para o próximo caminho?

Porque o nome de padrão selecionado faz parte do estado gráfico, não uma propriedade do operador que o usou. A ISO 32000-1 §8.6.6.2 define um espaço de cor Pattern como aquele cujo valor de cor é um nome de padrão fornecido a scn ou SCN, e todo outro componente do estado de cor é salvo por q e restaurado por Q. O nome do padrão precisa seguir a mesma regra. O HotPDF o mantém no registro de estado como FillPatternName e StrokePatternName, junto com a família de espaço de cor de preenchimento e traço, então um Q coloca a seleção anterior de volta exatamente como coloca a CTM anterior de volta

Armazene esse nome em uma variável local dentro do despachante de operadores, em vez disso, e ele sobrevive a todo Q no stream. A falha então aparece em algum lugar inesperado: um Form XObject desenhado depois do caminho com padrão herda uma seleção de padrão que seu próprio content stream nunca fez, e seus preenchimentos saem hachurados. Formulários aninhados pioram isso, porque cada nível de aninhamento empilha e desempilha estado que a variável perdida ignora. Definir um espaço de cor sem padrão com cs ou CS, ou emitir um simples g / rg / k, também precisa limpar o nome do padrão, senão a seleção obsoleta sobrevive ao espaço de cor que lhe dava significado

q
  /Pattern cs              % pattern colour space, ISO 32000-1 8.6.6.2
  /P1 scn                  % coloured tiling pattern, PaintType 1
  10 10 200 120 re f       % this rectangle is hatched
Q
0 0 300 200 re f           % must be black again, not hatched

q
  /Cs2 cs                  % [/Pattern /DeviceCMYK] array
  0 0.6 1 0 /P2 scn        % uncoloured pattern plus its underlying colour
  20 20 160 90 re f*
Q

Um padrão é pintado através de um recorte, nunca como um preenchimento

O modelo correto é subtrativo: restrinja o recorte de dispositivo à forma sendo pintada, depois execute o conteúdo do padrão dentro dela. O HotPDF nunca desenha uma aproximação sólida primeiro e a repinta por cima, porque o sólido intermediário ficaria visível através das lacunas entre ladrilhos e entraria em conflito com qualquer transparência no conteúdo do ladrilho. A §8.7.3.2 descreve um padrão ladrilhado como um content stream replicado em intervalos horizontais e verticais fixos, e a replicação só faz sentido contra um recorte que já tem a forma certa. Para preenchimentos a conversão é direta: HPDFSelectFillPathClip define o modo de preenchimento de polígono como ALTERNATE para f*, B* e b* e como WINDING para as variantes nonzero, constrói o caminho GDI, e o intersecta no recorte com SelectClipPath. Essa única linha é o que faz um preenchimento com padrão par-ímpar deixar os mesmos buracos que um preenchimento sólido par-ímpar, que é exatamente o que uma região hachurada em forma de rosquinha precisa

Os traços são a parte fácil de errar. Um caminho traçado não tem interior, então intersectar o próprio caminho no recorte produz uma região vazia e nada é pintado. HPDFSelectStrokePathClip portanto constrói primeiro uma caneta geométrica a partir do estado atual, usando PS_GEOMETRIC com a ponta de J, a junção de j, o limite de gaveta de M, e PS_USERSTYLE quando um array de traço tracejado está ativo, depois chama WidenPath para converter o contorno traçado em uma região preenchível antes de recortar. Comportamento de ponta, junção, gaveta e tracejado em um caminho traçado com padrão então correspondem a um traço normal por construção em vez de por uma segunda implementação. Dois limites honestos vivem aqui: larguras de linha abaixo de uma unidade de dispositivo são fixadas em um pixel, e o array de traço tracejado é truncado em dezesseis entradas, que é o teto que ExtCreatePen aceita

Quais ladrilhos estão realmente visíveis?

O intervalo visível vem de rodar a transformação ao contrário. O posicionamento de ladrilhos acontece no espaço do padrão, mas a única coisa que sabe quanto da página está sendo tocada é a caixa de recorte do dispositivo, que está no espaço do dispositivo. O HotPDF compõe BaseMatrix := CTM * PatternMatrix, a inverte, e mapeia os quatro cantos da caixa de recorte GDI de volta através da inversa. Os limites alinhados aos eixos desses quatro cantos mapeados dão o retângulo em espaço de padrão que pode possivelmente ser coberto, e dividir esse retângulo por XStep e YStep contra o BBox do padrão dá intervalos de índice fechados. Cada célula então renderiza com uma CTM de CTM * PatternMatrix * Translate(i * XStep, j * YStep), e é recortada uma segunda vez ao seu próprio polígono BBox transformado. Esse segundo recorte importa quando XStep é menor que a largura da caixa delimitadora, que é como designs de ladrilho sobrepostos são expressos; sem ele, células vizinhas se pintariam umas sobre as outras fora de sua extensão declarada. Se o recorte por célula voltar como NULLREGION, a célula é pulada sem tokenizar ou executar nada

// Map the device clip box back into pattern space through the inverse of
// CTM * PatternMatrix, then convert those bounds into tile index ranges.
BaseMatrix := HPDFMatMul(FGSStack.State.CTM, PatternMatrix);
if not HPDFMatInvert(BaseMatrix, InverseMatrix) then Exit;   // singular: refuse
if GetClipBox(FDC, ClipRect) = ERROR then Exit;

// MinX..MaxY are the axis-aligned bounds of the four mapped clip corners.
I0 := Floor((MinX - BBox[2]) / StepXAbs);
I1 := Ceil ((MaxX - BBox[0]) / StepXAbs);
J0 := Floor((MinY - BBox[3]) / StepYAbs);
J1 := Ceil ((MaxY - BBox[1]) / StepYAbs);

PlannedTiles := Int64(I1 - I0 + 1) * Int64(J1 - J0 + 1);
if (PlannedTiles <= 0) or (PlannedTiles > FPatternTilesRemaining) then Exit;
Dec(FPatternTilesRemaining, Integer(PlannedTiles));

Padrões incolores e a cor que vem de fora

Um padrão PaintType 2 carrega forma mas não cor, e a cor chega junto com o nome do padrão. A §8.7.3.2 especifica que um padrão incolor é usado apenas com um espaço de cor Pattern que declara um espaço subjacente, então scn recebe os valores de componente primeiro e o nome do padrão por último. O HotPDF resolve esses componentes através do espaço subjacente armazenado na entrada de espaço de cor do padrão, o que significa que uma hachura incolor pode ser tingida com uma tinta Separation ou uma combinação DeviceN exatamente como qualquer outro preenchimento; a mecânica dessa resolução é coberta em renderização de cores especiais Separation e DeviceN. Dentro do ladrilho, os dois tipos de pintura divergem fortemente. Para PaintType 2 o renderizador define um sinalizador de supressão de operador de cor durante o ladrilho, então qualquer g, rg, k ou scn no conteúdo do padrão é ignorado e toda marca usa a cor fornecida externamente. Para PaintType 1 o oposto se aplica: o estado de preenchimento e traço é reiniciado para os padrões do PDF, DeviceGray preto com um espaço de cor identidade, e o ladrilho se colore sozinho. Pular esse reinício deixa a cor que por acaso estava ativa no operador f vazar para um padrão que deveria ser autodescritivo

Por que a profundidade da pilha de estado gráfico precisa ser restaurada após cada ladrilho?

Porque um content stream de padrão pode estar desbalanceado, e o dano se acumula entre células. Um ladrilho cujo stream contém três operadores q e dois operadores Q deixa a pilha um quadro mais funda do que começou. Restaurar apenas o registro de estado atual entre células e a profundidade continua crescendo, então a célula número duzentos executa a partir de um quadro de pilha que pertence à célula número cento e noventa e nove, com qualquer CTM e recorte que esse quadro carregava. O HotPDF portanto tira um instantâneo do registro de estado e da profundidade da pilha antes do laço de ladrilho e chama RestoreSnapshot no início de cada iteração, o que trunca a pilha de volta ao comprimento salvo e reinstala o estado salvo em um único passo. O dicionário Resources da página e o sinalizador de supressão de operador de cor são restaurados no mesmo limite, já que um ladrilho pode referenciar seus próprios recursos e não deve entregá-los ao seu vizinho. O estado de recorte GDI recebe o mesmo tratamento através de um par SaveDC / RestoreDC ao redor de cada célula, então um ladrilho que instala seu próprio recorte W n não pode encolher a região disponível para o próximo

Orçamentos, recusas, e o que o renderizador não vai desenhar

Padrões ladrilhados são o lugar mais fácil em um PDF para escrever um arquivo de negação de serviço, então os limites são números rígidos em vez de heurísticas. O aninhamento de padrões é limitado à profundidade 4, a mesma proteção usada para recursão de Form XObject, que impede um padrão que referencia a si mesmo através de seu próprio dicionário de recursos. Uma única pintura de caminho pode executar no máximo 16.384 ladrilhos no total, contados de forma decrescente através de padrões aninhados e reiniciados apenas quando a pintura do padrão mais externo começa. Uma grade de ladrilhos cuja contagem planejada de células excede o que resta desse orçamento é rejeitada de imediato, antes de uma única célula rodar

Geometria degenerada é recusada em vez de aproximada. Um BBox ausente ou de área zero, um XStep ou YStep cuja magnitude está abaixo de 1e-6, um produto CTM * PatternMatrix sem inversa, coordenadas de recorte mapeadas além de 1e9, ou uma magnitude de índice além de um milhão, todos fazem a pintura do padrão retornar sem desenhar. O resultado é uma região não pintada em vez de uma thread de renderização travada, que é a troca que você quer em um conversor em lote. O desempenho vem de uma decisão: o stream do padrão é tokenizado uma vez por pintura com HPDFTokenizeContentStream e o array de tokens é reutilizado em toda célula visível, então a contagem de ladrilhos multiplica o custo de execução mas nunca o custo de análise léxica

Renderizando uma página com padrão a partir do Delphi

Nada sobre o suporte a padrões muda o código de chamada. Carregue o documento, peça uma página, e o trabalho de ladrilho acontece dentro do interpretador de content stream que a renderização de página para bitmap já conduz. O mesmo interpretador alimenta contextos de dispositivo de bitmap, metarquivo e impressora, então um desenho hachurado que parece certo em uma miniatura de pré-visualização imprime com a mesma geometria de ladrilho. Padrões de sombreamento PatternType 2 seguem um ramo diferente que compartilha seu caminho de avaliação com o operador sh puro, descrito em detalhe em renderização de sombreamentos axiais e radiais

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('assembly-drawing.pdf') > 0 then
    begin
      // Section hatching that previously flattened to a solid block now
      // replays the tile content once per visible cell.
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 200);
      if Assigned(Bmp) then
      try
        Bmp.SaveToFile('sheet1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Quando uma região com padrão ainda parece errada, verifique as três classes de falha em ordem. Uma região totalmente em branco geralmente significa uma recusa: inspecione XStep, YStep e BBox em busca de valores degenerados, ou conte quantos ladrilhos a grade precisaria contra o teto de 16.384. Uma região pintada em uma única cor plana significa que o nome do padrão nunca chegou ao operador de pintura, o que aponta para a ordenação de cs e scn no stream. Um padrão que aparece onde não pertence significa restauração de estado, e o lugar para olhar é o tratamento de q / Q ao redor do formulário ou caminho que o herdou

Padrões ladrilhados são um daqueles recursos de PDF que permanecem invisíveis até o arquivo que precisa deles cair na sua caixa de entrada, e então eles são o trabalho inteiro. Se você está construindo visualizadores de desenho, conversores de documentos de engenharia ou renderizadores de relatório em Delphi ou C++Builder, o componente completo e sua API de renderização estão documentados na página do componente PDF HotPDF para Delphi