Technical Article

Desenho Vetorial em PDF com Delphi: Caminhos e Gradientes

A maior parte do código Delphi que interage com PDF trata o formato como um contentor para duas coisas: blocos de texto e alguns bitmaps posicionados. Essa visão é correta até certo ponto, mas deixa inutilizada a parte mais capaz do formato. Uma página PDF é uma tela (canvas) 2D independente da resolução, baseada no mesmo modelo de imagem do PostScript. Pode desenhar linhas, curvas, áreas preenchidas, gradientes e padrões repetitivos, tudo sob a forma de vetores que se mantêm nítidos em qualquer nível de zoom e são impressos com a resolução total do dispositivo. Se estiver a desenhar um logótipo, um gráfico, uma marca de água ou a moldura de um certificado, o caminho vetorial é quase sempre a primitiva correta, sendo menor e mais nítida do que a imagem rasterizada que muitos programas preferem usar.

Este artigo aborda o modelo vetorial conforme definido pela norma ISO 32000-1 e apresenta as chamadas do PDFlibPas correspondentes. O objetivo é concretizar a especificação, dado que a API se mapeia de forma muito próxima na mesma, e compreender uma ajuda a compreender a outra.

A página é uma máquina de caminhos

A norma ISO 32000-1, secção 8.5, descreve elementos gráficos em duas fases que nunca se sobrepõem. Primeiro, constrói-se um caminho, que é pura geometria sem qualquer resultado visível. De seguida, pinta-se esse caminho numa única operação que desenha o seu contorno (stroke), preenche o seu interior (fill) ou executa ambos. Nada surge na página durante a construção. O caminho é uma sequência abstrata de pontos e segmentos mantidos no estado gráfico até que um operador de pintura a consuma, momento em que é renderizada e descartada.

Um caminho é composto por um ou mais subcaminhos. Um subcaminho começa num ponto e cresce anexando segmentos: linhas retas, curvas de Bezier cúbicas e, em algumas plataformas, retângulos inteiros adicionados como o seu próprio subcaminho fechado. No PDFlibPas, abre um caminho com StartPath, que define o ponto de partida, e depois estende-o com AddLineToPath and AddCurveToPath. Cada chamada faz avançar um ponto atual implícito, de modo que o segmento seguinte continua a partir do local onde o último terminou. O método ClosePath desenha um segmento reto final de volta ao início do subcaminho, o que é importante para o contorno porque produz uma junção de linhas real no vértice de fecho, 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 utilizam o método AddCurveToPath, que recebe dois pontos de controlo Bezier e um ponto final: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). A curva estende-se desde o ponto atual até (EndX, EndY), sendo atraída em direção aos dois pontos de controlo ao longo do percurso. Os arcos circulares estão disponíveis através de AddArcToPath(CenterX, CenterY, TotalAngle), onde o raio é determinado pela distância entre o ponto atual e o centro, e o motor emite o arco como uma cadeia de segmentos Bezier. Os retângulos dispõem de um atalho, AddBoxToPath(Left, Top, Width, Height), que anexa um retângulo fechado completo como subcaminho próprio sem necessidade de um StartPath prévio.

Duas regras de preenchimento e por que divergem

Ao preencher um caminho que se cruza a si próprio ou que contém um laço interno, o renderizador necessita de uma regra para decidir quais as regiões que estão dentro da forma e quais as que são aberturas (furos). A norma ISO 32000-1, secção 8.5.3.3, define duas regras, que podem pintar a mesma geometria de maneira diferente. A regra do enrolamento diferente de zero (nonzero winding rule) conta os cruzamentos com sinal de um raio traçado desde um ponto de teste até ao infinito, adicionando um para cada segmento que cruza da esquerda para a direita e subtraindo um para cada um que cruza no sentido oposto; o ponto está dentro quando o total não é zero. A regra par-ímpar (even-odd rule) ignora a direção e conta simplesmente os cruzamentos, considerando o ponto no interior quando a contagem é ímpar.

O caso clássico onde divergem é uma forma com um furo, como um anel ou uma anilha. Desenhe um limite externo e um limite interno dentro dele. Sob a regra par-ímpar, o laço interno esculpe sempre um furo, porque qualquer ponto entre os dois limites é cruzado uma vez e qualquer ponto dentro do laço interno é cruzado duas vezes. Sob a regra do enrolamento diferente de zero, o furo só surge se o laço interno se enrolar na direção oposta ao externo; se se enrolarem na mesma direção, os enrolamentos reforçam-se em vez de se anularem, e a região interna é preenchida por completo. Uma estrela de cinco pontas desenhada como um único contorno autocomposto apresenta a mesma divisão: a regra par-ímpar deixa o pentágono central vazio, enquanto a regra do enrolamento diferente de zero o preenche totalmente.

O PDFlibPas seleciona a regra através da chamada de pintura que efetua, e não por uma sinalização. DrawPath preenche com a regra do enrolamento diferente de zero; DrawPathEvenOdd preenche com a regra par-ímpar. Ambos aceitam o mesmo modo inteiro: 0 desenha apenas o contorno, 1 apenas preenche e 2 preenche e desenha o contorno. A regra par-ímpar é a ferramenta mais fácil para recortar furos precisamente porque não exige que faça a gestão da 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

Os gradientes axiais variam a cor ao longo de uma linha

Uma cor de preenchimento lisa é um value único em toda a região. Um gradiente varia a cor continuamente, e o tipo mais simples é o gradiente axial ou linear. A norma ISO 32000-1, secção 8.7.4.5, especifica-o como um sombreado axial do Tipo 2: são fornecidos 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 da sua projeção perpendicular sobre o eixo, de modo que o gradiente se desenvolve em faixas ortogonais à linha que une os dois pontos.

No PDFlibPas, um gradiente é um recurso de documento nomeado que cria uma vez e depois seleciona como a pintura ativa. NewRGBAxialShader regista-o. A assinatura é NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): os dois pontos finais do eixo, os triplos RGB em cada extremidade como valores no intervalo de 0 a 1 e uma sinalização Extend. Com a sinalização Extend definida como 1, as cores finais prolongam-se como preenchimento sólido para além dos pontos finais do eixo, o que normalmente se pretende para que os cantos de uma região fora do eixo não fiquem por pintar; 0 deixa-os intactos. Uma vez registado o sombreador, liga-o com SetFillShader para regiões preenchidas, SetLineShader para contornos desenhados ou SetTextShader para texto. A ligação permanece ativa para as chamadas de desenho seguintes, pelo que o caminho que pintar a seguir assumirá o gradiente em vez de uma cor lisa.

// 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

Padrões de ladrilho repetem uma célula

Enquanto um gradiente varia uma única cor suavemente, um padrão de ladrilho (tiling pattern) repete uma pequena peça artística ao longo de uma região. A norma ISO 32000-1, secção 8.7.3.1, define um padrão de ladrilho como uma célula de padrão, um pedaço independente de conteúdo, que o renderizador replica numa grelha fixa para cobrir a área que está a ser pintada. É desta forma que constrói tracejados para preenchimentos de engenharia, um motivo de marca repetido sob um cabeçalho ou um fundo texturado que se mantém nítido como vetor e que não pesa quase nada no arquivo, independentemente do tamanho da área, dado que a célula é guardada apenas uma vez e referenciada em toda a parte.

O PDFlibPas cria a célula de padrão a partir do conteúdo de página capturado. Captura uma página ou região com CapturePage, transforma a captura num padrão nomeado com NewTilingPatternFromCapturedPage(PatternName, CaptureID) e depois seleciona esse padrão como o preenchimento atual com SetFillTilingPattern(PatternName). A partir desse momento, qualquer caminho que preencher será pintado com a célula repetitiva em vez de uma cor lisa, funcionando exatamente como um preenchimento por sombreador, mas com uma célula em ladrilho como fonte de pintura. A sequência é mais complexa do que uma única chamada, por isso, se a etapa de captura for desconhecida, trate o padrão como uma operação em duas fases: produza primeiro a célula capturada e depois associe-a como um preenchimento por nome antes de desenhar a região que pretende ladrilhar.

Combinar as primitivas

As peças compõem-se diretamente. Uma forma Bezier preenchida é um caminho de curvas pintado com DrawPath. O mesmo contorno pintado com DrawPathEvenOdd, após a adição de um laço interno, apresenta um furo que o preenchimento por enrolamento teria fechado. Um retângulo preenchido com gradiente é uma caixa associada a um sombreador. O exemplo abaixo desenha os três em sequência, para que a diferença entre as duas regras de preenchimento seja visível numa única página, e coloca depois um painel de gradiente sob os mesmos.

// 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);

Há dois detalhes fundamentais a reter. A chamada de pintura decide a regra de preenchimento, pelo que a escolha entre DrawPath e DrawPathEvenOdd constitui a escolha entre o enrolamento diferente de zero e a regra par-ímpar. No caso de formas com furos, a regra par-ímpar poupa-o de ter de analisar a direção do subcaminho. Adicionalmente, o estado gráfico é amostrado no momento em que pinta: defina as cores, a largura da linha e a associação do sombreador antes da chamada de pintura, uma vez que esse é o estado lido pelo motor. Construir primeiro, configurar o estado a seguir, pintar por fim: deste modo, o modelo vetorial comporta-se de forma previsível em todas as situações.

A partir daqui, os passos seguintes naturais são a leitura de vetores e texto a partir de um documento existente, assunto tratado no nosso artigo sobre extração de texto, imagem e tipos de letra, e a renderização do mesmo modelo de desenho num contexto de dispositivo (device context) Windows para pré-visualização no ecrã e impressão, coberto no guia de impressão e pré-visualização. As chamadas de caminho, sombreador e padrão aqui descritas são fornecidas como parte da Delphi PDF Library, a par das APIs de texto, imagem, formulário e assinatura abrangidas noutros locais deste blogue.