Artigo Técnico

Desenho em Canvas HotPDF em Delphi: Caminhos Vetoriais e Cores

O HotPDF desenha gráficos vetoriais construindo um caminho na página atual e, em seguida, solicitando que ele seja pintado. Não existe uma etapa de bitmap nesse ínterim. Uma linha que você desenhe com MoveTo e LineTo termina como operadores de caminho PDF no fluxo de conteúdo (content stream), de modo que ela se mantém como um vetor verdadeiro: nítida em zoom de 50%, nítida em 1600%, e medindo apenas uma fração do tamanho que uma versão rasterizada custaria. Para diagramas, réguas de tabela, eixos de gráficos e decorações de formulários, isso é exatamente o que você deseja, e a API por trás disso é simples o suficiente para se aprender de uma vez

Toda a superfície de desenho reside em THotPDF.CurrentPage. Entre BeginDoc e EndDoc, você define a cor e a espessura da linha nesse objeto de página, estabelece a geometria e chama um operador de pintura para confirmá-la. As quatro primitivas que você mais utilizará são MoveTo e LineTo para caminhos arbitrários, Rectangle para caixas, Circle para discos, e os dois operadores de pintura, que são Stroke e Fill

O sistema de coordenadas se localiza no canto inferior esquerdo

Esta é a única coisa que confunde todo mundo que vem da VCL. O TCanvas com o qual você pinta os controles coloca a origem no canto superior esquerdo, com o eixo Y crescendo para baixo. O formato PDF faz exatamente o oposto. O HotPDF mede a partir do canto inferior esquerdo da página em pontos (1/72 polegada), com o Y aumentando à medida que você sobe. Um ponto em Y := 720 fica próximo ao topo de uma página tamanho Carta dos EUA, que tem 792 pontos de altura, e Y := 50 fica próximo à parte inferior. Se o seu primeiro desenho sair espelhado verticalmente, é por causa disso: o código portado de gráficos de tela assume a direção errada e ultrapassa a borda inferior

A mesma convenção governa o TextOut, de modo que o texto e as formas compartilhem de um único modelo mental uma vez que você o internaliza. Planeje um layout decidindo onde a parte inferior de cada elemento ficará, e não o topo, assim o resto se seguirá sem problemas

Caminhos: MoveTo, LineTo, Stroke

Um caminho com traçado (stroked path) é uma caneta levantada, colocada e arrastada. O MoveTo levanta a caneta e define o ponto de início sem marcar nada. Cada LineTo estende o caminho atual até um novo ponto. Nada aparece na página até que você faça a chamada ao Stroke, o qual desenha o caminho acumulado usando a cor de traço atual e a espessura da linha atual, e então limpa o caminho para que o próximo MoveTo se reinicie do zero

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // A espessura da linha é em pontos e vale até você alterá-la.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Uma régua horizontal perto do topo da página (Y medido a partir do fundo).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // confirma o caminho; nada foi desenhado antes disto

    // Uma polilinha conectada mais espessa: três segmentos em um único caminho.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Dois detalhes poupam muito tempo real de depuração. A espessura da linha é um estado, e não um argumento: SetLineWidth a define uma vez e todos os Stroke subsequentes utilizam esse valor até que você o altere novamente, sendo este o motivo de a polilinha acima estar mais espessa do que a régua. E o caminho é redefinido após cada Stroke, de modo que um Stroke esquecido significa que a geometria que você desenhou com tanto cuidado nunca será renderizada. Se estiver faltando uma forma na saída, a chamada de pintura é o primeiro local onde você deve procurar

As coordenadas são em pontos, e os pontos são fracionários. O MoveTo e o LineTo aceitam valores Single, portanto um traço fino a 0.5 pontos ou uma posição a 72.25 são legais e significativos, e não arredondados para a unidade inteira mais próxima. Essa precisão importa em duas direções opostas. Uma espessura de linha abaixo de cerca de 0.5 pode ser renderizada como a linha mais fina possível dependente do dispositivo, que desaparece na tela e reaparece quando impressa, de modo que uma régua visível exige uma espessura que você define de propósito, em vez do padrão. Na outra extremidade, o alinhamento (snapping) de réguas de tabela e linhas de grade para coordenadas de ponto inteiro evita que uma grade densa pareça levemente irregular onde linhas adjacentes arredondam de forma diferente. Decida o espaçamento da grade em pontos logo no início e o restante do layout vai herdá-lo

Formas preenchidas e cores

Primitivas fechadas podem ser preenchidas em vez de terem apenas contorno. Rectangle recebe uma posição e um tamanho, Circle recebe um centro e um raio, e qualquer um deles é confirmado com Fill, o qual pinta o interior na cor de preenchimento atual, ou com Stroke, para apenas um contorno. A cor de preenchimento e a cor de traço são partes separadas de estado, definidas com SetRGBFillColor e SetRGBStrokeColor, onde ambos recebem um único TColor. Isso significa que você pode reutilizar as constantes de cores do Delphi e o auxiliar RGB diretamente

// Rectangle(X, Y, Width, Height): X e Y são o canto inferior esquerdo.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X e Y são o centro.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Apenas contorno: defina uma cor de traço e uma espessura, depois Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Preste atenção à forma dos argumentos no Rectangle. Trata-se de posição-mais-tamanho (position-plus-size), X, Y, Width, Height, e não de dois cantos opostos. O TCanvas.Rectangle que os desenvolvedores Delphi conhecem recebe (Left, Top, Right, Bottom), de modo que a memória muscular fornecerá ao HotPDF um segundo canto onde ele espera uma largura e uma altura, e a caixa sairá do tamanho errado. O par (X, Y) é o canto inferior esquerdo, o que é consistente com a origem da página. Para um círculo, (X, Y) é o centro e o terceiro argumento é o raio em pontos

Uma escolha de cor em que o exemplo original errou

Uma versão mais antiga desse exemplo definiu as cores com Random($FFFFFF) para cada forma. Parece animado, mas é o instinto errado para documentos gerados. Um PDF que você constrói a partir do código costuma ser algo que você também deseja testar, e cores de preenchimento aleatórias impossibilitam comparar o resultado entre as execuções: uma comparação byte a byte com um arquivo sabidamente bom falha toda vez, sem nenhum motivo real. Escolha cores explícitas. Quando quiser variedade em uma série de formas, determine isso com base em seus dados ou usando um array de paleta fixa, para que a mesma entrada sempre produza o mesmo arquivo. O determinismo vale muito mais do que a novidade quando o artefato se move através de um pipeline de lançamento (release pipeline)

Juntando as primitivas: uma caixa de destaque

Cada primitiva é simples por si só; o retorno aparece quando um punhado delas se compõe em algo de que um relatório realmente precisa. Uma caixa de destaque (callout), aquela caixa anotada que aponta para uma figura e a explica, usa tudo o que foi abordado até aqui: um retângulo preenchido com uma borda, uma linha de ponteiro traçada, um ponto para ancorar o ponteiro, e texto disposto dentro da caixa usando as mesmas coordenadas do canto inferior esquerdo que as formas usam. O FillAndStroke ganha o seu lugar aqui, pintando o interior e o contorno de um único caminho em uma só confirmação, em vez de construir o retângulo duas vezes

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. A caixa: preenchimento claro mais uma borda visível, um caminho, uma confirmação.
    //    Rectangle é canto inferior esquerdo mais tamanho, Y medido a partir do fundo
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // painel âmbar claro
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // borda mais escura
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. O ponteiro: um segmento traçado da borda da caixa para baixo
    //    em direção ao elemento que está sendo anotado
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // borda esquerda da caixa
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. Um ponto preenchido ancora o ponteiro em seu alvo
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. O rótulo, posicionado em relação ao canto inferior esquerdo da caixa.
    //    Texto e formas compartilham um sistema de coordenadas, portanto os deslocamentos
    //    são pura aritmética em relação a (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Repare como o composto precisa de pouco gerenciamento de estado. A cor de preenchimento, a cor de traço e a espessura da linha são cada uma definidas imediatamente antes da forma que as usa, de modo que cada bloco do desenho se lê como uma unidade autocontida e pode ser reordenado ou extraído para um auxiliar sem arrastar estado oculto junto. Envolva isto em um procedimento que recebe o ponto de ancoragem e as strings, e você terá uma anotação de diagrama reutilizável ao custo de quarenta linhas

Onde o desenho vetorial compensa, e onde não compensa

Acione essas chamadas de caminhos e formas quando a geometria for gerada: linhas de grade de gráficos e barras, as réguas de uma tabela de fatura, caixas de destaque em um diagrama, uma marca de logotipo expressa como um punhado de caminhos. Tudo isso escala sem desfocar e não adiciona quase nada ao tamanho do arquivo, porque um retângulo representa alguns poucos números em vez de milhares de pixels. A contrapartida também é honesta. Se o que você tem de fato é uma fotografia ou uma captura de tela, desenhe-a como uma imagem usando AddImage e ShowImage em seu lugar; traçar um bitmap com chamadas vetoriais não lhe trará vantagem alguma. Os segmentos retos, retângulos e círculos acima carregam a grande maioria do trabalho de relatório real, e os três refinamentos que os desenvolvedores perguntam a seguir, curvas, padrões tracejados e transparência, residem no mesmo objeto de página

Curvas, tracejados e transparência em resumo

Curvas de forma livre estendem a mesma maquinaria de caminho que você já tem. O CurveToC(X1, Y1, X2, Y2, X3, Y3) acrescenta um segmento cúbico de Bezier do ponto atual até (X3, Y3), curvando-se em direção aos dois pontos de controle, e as variantes abreviadas CurveToV e CurveToY cobrem os casos em que um ponto de controle coincide com um ponto extremo. Um caminho pode misturar segmentos LineTo e CurveToC livremente antes que um único Stroke ou Fill o confirme, e é assim que cantos arredondados e linhas suaves de gráficos são construídos

Traços tracejados são um estado, exatamente como a espessura da linha. O SetDash([3, 3], 0) comuta todos os traços subsequentes para um padrão de três pontos ligados, três pontos desligados, com o array especificando os comprimentos das seções ligada/desligada em pontos e o segundo argumento definindo a fase de onde o ciclo começa; o NoDash retorna a caneta a uma linha sólida. Defina-o, trace as linhas de grade que o desejam, e redefina-o antes da próxima régua sólida, ou o tracejado infecta silenciosamente tudo o que se segue

A transparência funciona por meio de um estado gráfico nomeado, em vez de um argumento de cor, porque o alpha no PDF é uma propriedade do dicionário de estado gráfico. Registre um no documento com RegisterExtGState, passando um alpha de preenchimento e um alpha de traço entre 0 e 1, depois aplique o nome que ele retorna com CurrentPage.SetGraphicsState; preenchimentos e traços a partir desse ponto pintam na opacidade registrada. É uma cerimônia mais pesada do que os setters de cor, e vale a pena na primeira vez em que uma barra de destaque precisa ficar sobre o texto sem escondê-lo

O hábito remanescente que vale a pena manter é a verificação. A geometria gerada pode passar na sua máquina e falhar na do cliente, geralmente devido à substituição de fontes em qualquer texto que você misture, ou a uma suposição de tamanho de página que não se sustenta. Abra o arquivo finalizado em alguns níveis de zoom para confirmar que as bordas permanecem limpas, e verifique se todas as formas caem dentro da caixa de margem que você planejou. Com um esquema de cores determinístico, essa verificação pode ser automatizada com relação a um PDF de referência, e não apenas conferida a olho

As chamadas de MoveTo, LineTo, Stroke, Fill e de cores aqui mostradas fazem parte do Componente HotPDF para Delphi e C++Builder