Artigo Técnico

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

Uma hachura que renderiza como um único bloco cinzento plano é a falha clássica de padrão de ladrilho. O HotPDF, o componente VCL nativo de PDF para Delphi e C++Builder, pinta PatternType 1 transformando o caminho atual num clip temporário e reproduzindo o stream de conteúdo do padrão uma vez por ladrilho visível, com a seleção do padrão retida no estado gráfico e restaurada por q e Q

Os sintomas chegam em dois sabores, e parecem não relacionados até saber a causa. Um desenho CAD perde a sua hachura de secçã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 devia ser branco simples apanha as linhas diagonais de uma vista de detalhe anterior. Ambos são problemas de estado do padrão, e apenas um deles é sobre desenhar ladrilhos de todo

Por que sangra um padrão de ladrilho para o caminho seguinte?

Porque o nome do 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 um cujo valor de cor é um nome de padrão fornecido a scn ou SCN, e todos os outros componentes do estado de cor são guardados por q e restaurados por Q. O nome do padrão tem de seguir a mesma regra. O HotPDF mantém-no no registo de estado como FillPatternName e StrokePatternName, ao lado da família de espaço de cor de preenchimento e traço, pelo que um Q repõe a seleção anterior exatamente como repõe a CTM anterior

Guarde esse nome numa variável local dentro do despachante de operadores em vez disso e sobrevive a cada Q no stream. A falha então aparece nalgum lugar inesperado: um Form XObject desenhado depois do caminho com padrão herda uma seleção de padrão que o seu próprio stream de conteúdo nunca fez, e os seus preenchimentos saem hachurados. Formulários aninhados pioram isto, porque cada nível de aninhamento empurra e retira estado que a variável perdida ignora. Definir um espaço de cor não-padrão com cs ou CS, ou emitir um simples g / rg / k, também tem de limpar o nome do padrão, caso contrário a seleção obsoleta sobrevive ao espaço de cor que lhe deu 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 clip, nunca como um preenchimento

O modelo correto é subtrativo: restringir o clip do dispositivo à forma que está a ser pintada, depois executar o conteúdo do padrão dentro dele. O HotPDF nunca desenha primeiro uma aproximação sólida e a sobrepõe, porque o sólido intermédio seria visível através dos espaços entre ladrilhos e entraria em conflito com qualquer transparência no conteúdo do ladrilho. A §8.7.3.2 descreve um padrão de ladrilho como um stream de conteúdo replicado a intervalos horizontais e verticais fixos, e a replicação só faz sentido contra um clip que já tem a forma certa. Para preenchimentos, a conversão é direta: HPDFSelectFillPathClip define o modo de preenchimento do polígono para ALTERNATE para f*, B* e b* e para WINDING para as variantes nonzero, constrói o caminho GDI, e interseta-o no clip 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 que é fácil de fazer mal. Um caminho traçado não tem interior, pelo que intersetar o próprio caminho no clip resulta numa região vazia e nada é pintado. HPDFSelectStrokePathClip constrói por isso primeiro uma pena geométrica a partir do estado atual, usando PS_GEOMETRIC com o remate de J, a junção de j, o limite de bisel de M, e PS_USERSTYLE quando um array de traço tracejado está ativo, depois chama WidenPath para converter o contorno traçado numa região preenchível antes do clip. O remate, a junção, o bisel e o comportamento do tracejado num caminho traçado com padrão correspondem então a um traço normal por construção em vez de por uma segunda implementação. Vivem aqui dois limites honestos: as larguras de linha abaixo de uma unidade de dispositivo são fixadas a um pixel, e o array tracejado é truncado a dezasseis entradas, que é o teto que ExtCreatePen aceita

Que ladrilhos estão realmente visíveis?

O intervalo visível vem de executar a transformação ao contrário. A colocação de ladrilhos acontece em espaço de padrão, mas a única coisa que sabe quanto da página está a ser tocada é a caixa de clip do dispositivo, que está em espaço de dispositivo. O HotPDF compõe BaseMatrix := CTM * PatternMatrix, inverte-a, e mapeia os quatro cantos da caixa de clip 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 é depois renderizada com uma CTM de CTM * PatternMatrix * Translate(i * XStep, j * YStep), e é sujeita a um segundo clip contra o seu próprio polígono BBox transformado. Esse segundo clip importa quando XStep é menor do que a largura da caixa delimitadora, que é como desenhos de ladrilhos sobrepostos são expressos; sem ele, células vizinhas pintariam umas sobre as outras fora da sua extensão declarada. Se o clip por célula voltar como NULLREGION, a célula é saltada sem ser tokenizada ou executada

// 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 sem cor e a cor que vem de fora

Um padrão PaintType 2 transporta forma mas não cor, e a cor chega com o nome do padrão. A §8.7.3.2 especifica que um padrão sem cor só é usado com um espaço de cor Pattern que declare um espaço subjacente, pelo que scn recebe primeiro os valores dos componentes 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 sem cor pode ser tingida com uma tinta Separation ou uma combinação DeviceN exatamente como qualquer outro preenchimento; a mecânica dessa resolução está coberta em renderizar cores diretas Separation e DeviceN. Dentro do ladrilho, os dois tipos de pintura divergem drasticamente. Para PaintType 2, o renderizador define uma flag de supressão de operador de cor durante o ladrilho, pelo que qualquer g, rg, k ou scn no conteúdo do padrão é ignorado e cada marca assume a cor fornecida externamente. Para PaintType 1 aplica-se o oposto: o estado de preenchimento e traço é reposto para as predefinições do PDF, DeviceGray preto com um espaço de cor de identidade, e o ladrilho colore-se a si próprio. Saltar essa reposição deixa a cor que calhou de estar ativa no operador f infiltrar-se num padrão que era suposto ser autodescritivo

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

Porque um stream de conteúdo de padrão pode estar desequilibrado, e o dano acumula-se ao longo das células. Um ladrilho cujo stream contém três operadores q e dois operadores Q deixa a pilha um nível mais profunda do que começou. Restaurar apenas o registo de estado atual entre células e a profundidade continua a crescer, pelo que a célula número duzentos executa a partir de um nível de pilha que pertence à célula número cento e noventa e nove, com qualquer CTM e clip que esse nível transportava. O HotPDF tira por isso um instantâneo do registo de estado e da profundidade da pilha antes do ciclo de ladrilhos e chama RestoreSnapshot no início de cada iteração, o que trunca a pilha de volta ao comprimento guardado e reinstala o estado guardado num único passo. O dicionário de recursos da página e a flag de supressão de operador de cor são restaurados na mesma fronteira, uma vez que um ladrilho pode referenciar os seus próprios recursos e não deve entregá-los ao seu vizinho. O estado de clip GDI recebe o mesmo tratamento através de um par SaveDC / RestoreDC em torno de cada célula, pelo que um ladrilho que instala o seu próprio clip W n não consegue encolher a região disponível para o seguinte

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

Os padrões de ladrilho são o local mais fácil num PDF para escrever um ficheiro de negação de serviço, pelo que os limites são números rígidos em vez de heurísticas. O aninhamento de padrões tem um teto de profundidade 4, a mesma proteção usada para a recursão de Form XObject, que trava um padrão que se refere a si próprio através do seu próprio dicionário de recursos. Um único caminho pintado pode executar no máximo 16.384 ladrilhos no total, contados ao longo de padrões aninhados e reiniciados apenas quando a pintura do padrão mais externo começa. Uma grelha de ladrilhos cuja contagem de células planeada excede o que resta desse orçamento é rejeitada por completo, antes de qualquer célula correr

A geometria degenerada é recusada em vez de aproximada. Um BBox em falta ou de área zero, um XStep ou YStep cuja magnitude esteja abaixo de 1e-6, um produto CTM * PatternMatrix sem inversa, coordenadas de clip mapeadas além de 1e9, ou uma magnitude de índice além de um milhão fazem todos com que a pintura do padrão retorne sem desenhar. O resultado é uma região não pintada em vez de uma thread de renderização suspensa, que é a troca que se quer num conversor por lotes. 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 cada célula visível, pelo que a contagem de ladrilhos multiplica o custo de execução mas nunca o custo de análise léxica

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

Nada na suporte a padrões altera o código de chamada. Carregue o documento, peça uma página, e o trabalho de ladrilhos acontece dentro do interpretador de stream de conteúdo que a renderização de página para bitmap já conduz. O mesmo interpretador alimenta contextos de dispositivo de bitmap, metaficheiro e impressora, pelo que um desenho hachurado que parece correto numa miniatura de pré-visualização imprime com a mesma geometria de ladrilho. Os padrões de sombreamento PatternType 2 seguem um ramo diferente que partilha o seu percurso de avaliação com o operador sh simples, 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 continua a parecer errada, verifique as três classes de falha por ordem. Uma região inteiramente em branco normalmente significa uma recusa: inspecione XStep, YStep e BBox em busca de valores degenerados, ou conte quantos ladrilhos a grelha precisaria contra o teto de 16.384. Uma região pintada numa ú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 restauro de estado, e o local a verificar é o tratamento de q / Q em torno do formulário ou caminho que o herdou

Os padrões de ladrilho são uma daquelas funcionalidades de PDF que permanecem invisíveis até que o ficheiro que precisa delas chegue à sua caixa de entrada, e nessa altura são o trabalho inteiro. Se estiver a construir visualizadores de desenho, conversores de documentos de engenharia ou renderizadores de relatórios em Delphi ou C++Builder, o componente completo e a sua API de renderização estão documentados na página do componente HotPDF Delphi PDF