Artigo Técnico

Computação Gráfica 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 desenhando 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 PDF Library for Delphi 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

PDF Library for Delphi: diagrama do ciclo de vida de um caminho PDF mostrando a construção como geometria pura invisível seguida por um operador de pintura que traça, preenche ou ambos, então consome e descarta o caminho
Construir o quadrilátero não pinta nada — a chamada DrawPath lê o estado gráfico uma vez, renderiza stroke, fill ou ambos, e descarta o path

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 PDF Library for Delphi, 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

// Um quadrilátero fechado, primeiro stroke depois fill
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // abre o path no primeiro vértice
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // segmento reto de volta para (150, 100)
PDF.DrawPath(2);                   // 2 = fill e stroke; o path é consumido

As curvas utilizam o método AddCurveToPath, que recebe dois pontos de controle 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 controle 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 PDF Library for Delphi 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

PDF Library for Delphi: retângulos aninhados preenchidos sob as regras even-odd e nonzero winding: even-odd recorta o retângulo interno como um buraco, enquanto nonzero winding preenche sólido a menos que o loop interno gire em sentido oposto ao externo
A mesma geometria de arruela sob duas respostas legais — DrawPathEvenOdd ignora a direção do enrolamento e sempre esculpe o furo, enquanto DrawPath soma enrolamentos com sinal que se reforçam quando os subpaths compartilham a direção
// As mesmas duas caixas, duas fill rules, dois resultados diferentes
// Nonzero winding: as duas caixas giram no mesmo sentido, então a de dentro
// NÃO abre buraco e a caixa externa fica preenchida por inteiro
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: a caixa de dentro é cruzada um número par de vezes,
// então ela abre um buraco retangular limpo na caixa externa
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 PDF Library for Delphi, 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

PDF Library for Delphi: anatomia de um gradiente RGB axial mostrando faixas de cor perpendiculares ao eixo do shading, projeção sobre o eixo, e Extend continuando as cores finais como preenchimento sólido além dos pontos extremos
Um shader axial interpola ao longo do eixo entre duas cores de extremidade, com cada ponto amostrando sua projeção perpendicular sobre essa linha — Extend = 1 mantém as cores finais correndo além dos pontos extremos para que os cantos fiquem pintados
// Define um gradiente vertical uma vez só: azul embaixo, branco em cima
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // ponto inicial e RGB inicial
  0, 260,   1.00, 1.00, 1.00,    // ponto final e RGB final
  1);                            // 1 = estende as pontas como cor sólida

// Seleciona o gradiente como fill e depois pinta um retângulo com ele
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill, agora preenchido pelo shader

O eixo aqui é vertical, de y=100 a y=260 num x fixo, portanto as faixas de cor correm na horizontal e o retângulo esmaece de azul na base a branco no topo. Como o shader é identificado por nome, uma única definição pode preencher quantas formas houver na página, e voltar a uma cor lisa é apenas mais uma chamada de SetFillColor antes do próximo caminho

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á sendo 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 PDF Library for Delphi 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. Uma forma Bezier preenchida (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. O mesmo contorno, mais um loop interno, com fill even-odd para abrir um buraco
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);                              // o buraco é aberto

// 3. Um retângulo preenchido com um gradiente axial
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 tela 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