Technical Article

Computação Gráfica Vetorial em PDF com Delphi: Caminhos e Gradientes

A maior parte do código Delphi que lida com PDF trata o formato como um contêiner para duas coisas: sequências de texto e algumas imagens bitmap posicionadas. Essa visão está correta até certo ponto, mas deixa sem uso a parte mais capacitada do formato. Uma página PDF é uma tela 2D independente de resolução baseada no mesmo modelo de imagem do PostScript. Ela pode desenhar linhas, curvas, regiões preenchidas, gradientes e padrões repetitivos, tudo como vetores que permanecem nítidos em qualquer nível de zoom e são impressos na resolução máxima do dispositivo. Se você está desenhando um logotipo, um gráfico, uma marca d'água ou a borda de um certificado, o caminho vetorial é quase sempre o primitivo correto, sendo menor e mais nítido do que a imagem rasterizada que muitos programas usam em seu lugar.

Este artigo descreve o modelo vetorial conforme a ISO 32000-1 o define e mostra as chamadas correspondentes no PDFlibPas. O objetivo é tornar a especificação concreta, já que a API mapeia-se de forma próxima a ela, e entender uma ensina a outra.

A página é uma máquina de caminhos

ISO 32000-1 §8.5 descreve os elementos gráficos em duas fases que nunca se sobrepõem. Primeiro, constrói-se um caminho (path), que é pura geometria sem resultado visível. Em seguida, pinta-se esse caminho em uma única operação que traça seu contorno, preenche seu interior ou faz ambos. Nada aparece na página durante a construção. O caminho é uma sequência abstrata de pontos e segmentos mantida no estado gráfico até que um operador de pintura a consuma, momento em que ela é renderizada e descartada.

Um caminho é feito de um ou mais subcaminhos (subpaths). Um subcaminho começa em um ponto e cresce ao anexar segmentos: linhas retas, curvas cúbicas de Bezier e, em algumas plataformas, retângulos inteiros adicionados como seus próprios subcaminhos fechados. No PDFlibPas, você inicia um caminho com StartPath, que define o ponto inicial, e depois o estende com AddLineToPath e AddCurveToPath. Cada chamada avança um ponto atual implícito, para que o próximo segmento continue de onde o anterior terminou. ClosePath desenha um segmento reto final de volta ao início do subcaminho, o que é importante para o traçado de contorno (stroke) porque produz uma junção de linhas real no vértice de fechamento, em vez de duas extremidades soltas.

// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // straight segment back to (150, 100)
PDF.DrawPath(2);                   // 2 = fill and stroke; path is consumed

As curvas usam AddCurveToPath, que recebe dois pontos de controle de Bezier e um ponto final: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). A curva vai do ponto atual até (EndX, EndY), sendo puxada em direção aos dois pontos de controle ao longo do trajeto. Arcos circulares estão disponíveis por meio de AddArcToPath(CenterX, CenterY, TotalAngle), onde o raio é derivado da distância entre o ponto atual e o centro, e o motor emite o arco como uma cadeia de segmentos de Bezier. Os retângulos têm um atalho, AddBoxToPath(Left, Top, Width, Height), que anexa um retângulo fechado completo como seu próprio subcaminho sem necessidade de um StartPath prévio.

Duas regras de preenchimento, e por que elas divergem

Quando você preenche um caminho que se cruza ou contém um contorno interno, o renderizador precisa de uma regra para decidir quais regiões estão dentro da forma e quais são furos. A ISO 32000-1 §8.5.3.3 define duas, e elas podem pintar a mesma geometria de maneiras diferentes. A regra do número de voltas diferente de zero (nonzero winding rule) conta os cruzamentos sinalizados de um raio traçado de um ponto de teste ao infinito, somando um para cada segmento que cruza da esquerda para a direita e subtraindo um para cada um que cruza no sentido oposto; o ponto estará dentro quando o total não for zero. A regra par-ímpar (even-odd rule) ignora a direção e apenas conta os cruzamentos, considerando o ponto interno quando a contagem for ímpar.

O caso clássico em que elas divergem é uma forma com um furo, como uma rosca ou uma arruela. Desenhe um contorno externo e um contorno interno dentro dele. Sob a regra par-ímpar, o loop interno sempre cria um furo, porque qualquer ponto entre os dois limites é cruzado uma vez e qualquer ponto dentro do loop interno é cruzado duas vezes. Sob a regra do número de voltas diferente de zero, o furo só aparece se o loop interno girar na direção oposta ao externo; se girarem na mesma direção, as voltas se reforçam em vez de se anularem, e a região interna é preenchida por completo. Uma estrela de cinco pontas desenhada como um único contorno auto-intersecionante mostra a mesma divisão: par-ímpar deixa o pentágono central vazio, enquanto a regra diferente de zero o preenche.

PDFlibPas selects the rule by the call you make to paint, not by a flag. DrawPath preenche com a regra do número de voltas diferente de zero; DrawPathEvenOdd preenche com a regra par-ímpar. Ambos recebem o mesmo modo inteiro: 0 apenas traça o contorno, 1 apenas preenche e 2 preenche e traça o contorno. A regra par-ímpar é a ferramenta mais fácil para furos vazados precisamente porque não exige que você gerencie a direção do subcaminho.

// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120);   // outer
PDF.AddBoxToPath(140, 130, 120,  60);   // inner
PDF.DrawPath(1);                         // 1 = fill, nonzero winding

// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120);   // outer
PDF.AddBoxToPath(140, 330, 120,  60);   // inner cut-out
PDF.DrawPathEvenOdd(1);                  // 1 = fill, even-odd

Gradientes axiais variam a cor ao longo de uma linha

Uma cor de preenchimento sólida tem um único valor em toda a região. Um gradiente varia a cor continuamente, e o tipo mais simples é o gradiente axial ou linear. A ISO 32000-1 §8.7.4.5 o especifica como um sombreamento axial do Tipo 2: você fornece dois pontos que definem um eixo, uma cor inicial no primeiro ponto e uma cor final no segundo, e o renderizador interpola a cor ao longo desse eixo. Cada ponto na região preenchida assume a cor de sua projeção perpendicular no eixo, de modo que o gradiente é exibido em faixas perpendiculares à linha entre os dois pontos.

No PDFlibPas, um gradiente é um recurso de documento nomeado que você cria uma vez e depois seleciona como a pintura ativa. NewRGBAxialShader o registra. A assinatura é NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): os dois pontos finais do eixo, os trios RGB em cada extremidade como valores no intervalo de 0 a 1, e um sinalizador Extend. Com Extend definido como 1, as cores finais continuam como preenchimento sólido além dos pontos finais do eixo, o que geralmente é o desejado para que os cantos de uma região fora do eixo não fiquem sem pintura; 0 os deixa intocados. Depois que o sombreador (shader) existe, você o associa com SetFillShader para regiões preenchidas, SetLineShader para contornos traçados ou SetTextShader para texto. A associação permanece ativa para as chamadas de desenho que se seguem, de modo que o próximo caminho pintado assume o gradiente em vez de uma cor sólida.

// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // start point and start RGB
  0, 260,   1.00, 1.00, 1.00,    // end point and end RGB
  1);                            // 1 = extend ends as solid color

// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill, now filled by the shader

O eixo aqui é vertical, de y=100 a y=260 em um x fixo, de modo que as faixas de cor correm horizontalmente e o retângulo desbota de azul em sua base para branco no topo. Como o sombreador é identificado por nome, uma única definição pode preencher qualquer número de formas na página, e voltar para uma cor sólida é apenas mais uma chamada de SetFillColor antes do próximo caminho.

Padrões lado a lado repetem uma célula

Enquanto um gradiente varia uma única cor suavemente, um padrão de repetição (tiling pattern) replica um pequeno pedaço de arte ao longo de uma região. A ISO 32000-1 §8.7.3.1 define um padrão de repetição como uma célula de padrão, um conteúdo independente que o renderizador replica em uma grade fixa para cobrir a área pintada. É assim que você cria hachuras para desenhos de engenharia, um logotipo repetido atrás de um cabeçalho ou um fundo texturizado que permanece nítido como vetor e quase não pega nada, independentemente do tamanho da área, já que a célula é armazenada apenas uma vez e referenciada em todos os lugares.

O PDFlibPas constrói a célula do padrão a partir do conteúdo de página capturado. Você captura uma página ou região com CapturePage, transforma a captura em um padrão nomeado com NewTilingPatternFromCapturedPage(PatternName, CaptureID) e depois seleciona esse padrão como o preenchimento atual com SetFillTilingPattern(PatternName). A partir desse ponto, qualquer caminho que você preencher será pintado com a célula repetida em vez de uma cor sólida, funcionando exatamente como um preenchimento por sombreador, mas com a célula repetida como fonte de pintura. A sequência é mais complexa do que uma única chamada, portanto, se a etapa de captura for nova para você, trate o padrão como uma operação de duas etapas: primeiro produza a célula capturada e depois associe-a pelo nome como um preenchimento antes de desenhar a região que deseja cobrir.

Juntando as primitivas

As peças se compõem diretamente. Um elemento de Bezier preenchido é um caminho de curvas pintado com DrawPath. O mesmo contorno pintado com DrawPathEvenOdd após a adição de um loop interno mostra um furo que a regra diferente de zero teria preenchido. Um retângulo preenchido com gradiente é uma caixa vinculada a um sombreador. O exemplo abaixo desenha os três em sequência para que a diferença entre as duas regras de preenchimento fique visível em uma única página, e depois coloca um painel de gradiente sob eles.

// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480);   // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480);   // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1);                                     // 1 = fill

// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300);                              // new subpath: the hole
PDF.AddArcToPath(200, 300, 360);                     // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1);                              // hole is punched out

// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
  60, 100,  0.95, 0.55, 0.10,
  60, 200,  0.20, 0.10, 0.40,
  1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);

Dois detalhes devem ser lembrados. A chamada de pintura decide a regra de preenchimento, de modo que a escolha entre DrawPath e DrawPathEvenOdd é a escolha entre a regra diferente de zero e a par-ímpar, e para formas com furos a regra par-ímpar poupa você de ter que planejar a direção do subcaminho. E o estado gráfico é amostrado no momento em que você pinta: defina suas cores, largura de linha e associação do sombreador antes da chamada de pintura, pois esse é o estado lido pelo motor. Construa primeiro, configure o estado depois, pinte por último, e o modelo vetorial se comportará de maneira previsível todas as vezes.

A partir daqui, os próximos passos naturais são a leitura de vetores e textos de um documento existente, assunto de nosso artigo sobre extração de texto, imagem e fonte, e a renderização do mesmo modelo de desenho para um contexto de dispositivo do Windows para visualização na tela e impressão, abordada no passo a passo sobre impressão e visualização. As chamadas de caminho, sombreador e padrão descritas aqui fazem parte da Delphi PDF Library junto com as APIs de texto, imagem, formulário e assinatura tratadas em outras partes deste blog.