Artigo Técnico

Desenho em Canvas HotPDF em Delphi

Um filete de rodapé copiado de um exemplo de content stream de PDF diz 72 72 m 540 72 l S: mova para (72, 72), desenhe uma linha até (540, 72), trace. Em PDF cru, essa linha fica uma polegada acima da borda inferior de uma página US Letter. Dê os mesmos quatro números a uma API de desenho que mede a partir do canto superior esquerdo e o filete pousa uma polegada abaixo do topo, atravessando o timbre. A linha em si está perfeita. Só o marco de referência mudou, e alternar entre os dois marcos numa página de 792 pontos é uma única subtração: 792 menos Y

Onde fica Y = 0 numa página HotPDF?

As chamadas de desenho de página do HotPDF põem a origem no canto superior esquerdo da página, com X crescendo para a direita e Y crescendo para baixo, o mesmo marco que a TCanvas da VCL usa. O user space do PDF, definido na ISO 32000-1 §8.3.2.3, começa no canto inferior esquerdo com Y crescendo para cima, e o HotPDF converte cada Y que você passa na hora de escrever o content stream. Então Y := 72 em THotPDF.CurrentPage significa uma polegada abaixo da borda superior, seja qual for o tamanho da página

O tamanho da página importa para a outra ponta. Numa página US Letter, 612 por 792 pontos, Y := 720 fica uma polegada acima da base. Uma página HotPDF nova é A4 a menos que você diga o contrário, e A4 tem 842 pontos de altura, então o mesmo Y := 720 deixa 122 pontos, cerca de 1,7 polegada, abaixo dele. Rodapés posicionados subtraindo de 792 sobem no A4 por exatamente esse motivo. Leia a altura de CurrentPage.Height em vez de fixá-la no código, ou defina CurrentPage.Size explicitamente logo depois do BeginDoc

Comparação da TCanvas da VCL e da API de desenho de página do HotPDF, ambas medindo a partir do canto superior esquerdo com Y crescendo para baixo, enquanto o HotPDF escreve o mesmo ponto perto do topo de uma página US Letter como y = 720 no user space do PDF de baixo para cima
O HotPDF desenha no mesmo marco topo-esquerda que a TCanvas, então um ponto uma polegada abaixo do topo da página de 612 por 792 é Y = 72 nos dois, e o HotPDF o escreve no arquivo como y = 720 do user space do PDF

A unidade é o ponto, 1/72 de polegada, porque THotPDF.Resolution tem padrão 72. Subir o Resolution faz coordenadas e tamanhos contarem em 1/Resolution de polegada. Para trabalho vetorial, deixe em 72: larguras de linha, comprimentos de tracejado e coordenadas então todos se leem na mesma unidade que o arquivo PDF guarda, e um valor que você vê no content stream bate com um valor no seu fonte

Texto segue o mesmo modelo. Por padrão, o Y que você dá ao TextOut é a borda superior do texto, então um rótulo e a caixa em volta dele são planejados numa única grade. Algumas chamadas trabalham no arquivo PDF em vez de na superfície de desenho e usam coordenadas PDF de baixo para cima: os setters de page-box como SetCropBox(Left, Bottom, Right, Top), e a família StampLoadedPageText / StampLoadedPageImage que carimba um documento já carregado. Misturar essas com chamadas de path é de onde vem a maioria dos layouts invertidos, então guarde-as em rotinas separadas

Construindo um path e pintando ele

Um path é construído primeiro e pintado depois. O MoveTo define um ponto de partida sem marcar nada, cada LineTo adiciona um segmento reto, e nada chega à página até uma chamada de pintura como o Stroke usar a cor de stroke e a largura de linha atuais. Em termos de PDF, o Stroke emite o operador S, e todo operador de pintura encerra o path atual, então o próximo MoveTo começa limpo

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := Letter;    // 612 x 792 pt; o padrão é A4

    // Largura e cor são estado da página: defina uma vez, todo Stroke seguinte usa
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Um filete uma polegada abaixo do topo, margens de 72 pt dos dois lados
    Pdf.CurrentPage.MoveTo(72, 72);
    Pdf.CurrentPage.LineTo(540, 72);
    Pdf.CurrentPage.Stroke;

    // O filete do rodapé da abertura: uma polegada acima da borda inferior
    Pdf.CurrentPage.MoveTo(72, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.LineTo(540, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.Stroke;

    // Três segmentos conectados em um path, mais grosso e azul
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 152);
    Pdf.CurrentPage.LineTo(172, 102);
    Pdf.CurrentPage.LineTo(272, 172);
    Pdf.CurrentPage.LineTo(372, 112);
    Pdf.CurrentPage.Stroke;

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

Largura de linha é estado, não argumento. O SetLineWidth escreve um operador w e todo stroke seguinte na página usa aquela largura até você mudá-la, e é por isso que a polilinha sai com 3 pontos enquanto os dois filetes ficam em 1.5. Se você nunca definir, vale o padrão do PDF de 1.0 (ISO 32000-1 §8.4.3.2). Formas que faltam têm uma causa mais sem graça: geometria lançada sem uma chamada de pintura depois nunca rende

Coordenadas e larguras são Single, então 72.25 e um fio de 0.5 ponto sobrevivem até o arquivo. Zero é o caso especial. A especificação define largura 0 como a linha mais fina que o dispositivo de saída consegue desenhar, um pixel na tela em qualquer zoom e 1/600 de polegada numa impressora de 600 dpi, o que faz a mesma regra parecer grossa num visualizador e quase sumir no papel. Escolha uma largura real como 0.25 ou 0.5 para fios, e a linha mantém o peso dela em todo dispositivo

Modelo de path do HotPDF: MoveTo, LineTo, Rectangle e Circle constroem um caminho atual invisível em memória, e só o commit de Stroke, Fill ou FillAndStroke o pinta usando o estado gráfico persistente de cor de stroke, cor de fill e largura de linha antes de limpar o buffer
A geometria se acumula em silêncio no path atual até um operador de pintura confirmá-lo com a cor de stroke, a cor de fill e a largura de linha guardadas — uma chamada de pintura esquecida deixa a forma sem desenhar

Por que um triângulo fechado tem um entalhe num canto?

Um triângulo desenhado como três chamadas LineTo que voltam ao ponto de partida parece fechado, mas em PDF ele ainda é um subcaminho aberto. A ISO 32000-1 §8.5.3.2 aplica o line join só onde segmentos se encontram dentro de um subcaminho; as duas pontas livres recebem line caps. Com o butt cap padrão e uma caneta de 6 pontos, o canto inicial mostra uma mordida quadrada onde os outros dois cantos têm miters limpos. O ClosePath (o operador h) anexa o último segmento e diz ao renderizador para juntá-lo ao primeiro

Pdf.CurrentPage.SetLineWidth(6);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

// Parece fechado, não é: butt caps se encontram em (72, 500) e deixam um entalhe
Pdf.CurrentPage.MoveTo(72, 500);
Pdf.CurrentPage.LineTo(132, 420);
Pdf.CurrentPage.LineTo(192, 500);
Pdf.CurrentPage.LineTo(72, 500);
Pdf.CurrentPage.Stroke;

// Subcaminho fechado: os três cantos recebem o mesmo miter join
Pdf.CurrentPage.MoveTo(232, 500);
Pdf.CurrentPage.LineTo(292, 420);
Pdf.CurrentPage.LineTo(352, 500);
Pdf.CurrentPage.ClosePath;
Pdf.CurrentPage.Stroke;              // ou ClosePathStroke numa chamada só

// Fill dispensa ClosePath: preencher fecha todo subcaminho implicitamente
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.MoveTo(392, 500);
Pdf.CurrentPage.LineTo(452, 420);
Pdf.CurrentPage.LineTo(512, 500);
Pdf.CurrentPage.Fill;

Feche todo polígono que você pretende tracear com ClosePath ou ClosePathStroke, mesmo quando o último ponto já coincide com o primeiro. Com 1 ponto, o entalhe é invisível na tela; com 6 pontos ele aparece no zoom normal, e na legenda de um gráfico impresso se lê como um glitch de renderização. Para formas preenchidas a pergunta não surge, e o EoFill está lá para quando um path se cruza e você quer a regra even-odd em vez de nonzero winding

Retângulos, círculos e o hábito da TCanvas

Primitivas fechadas poupam a contabilidade do MoveTo. O Rectangle(X, Y, Width, Height) recebe o canto superior esquerdo e um tamanho, o Circle(X, Y, Radius) recebe um centro e um raio, e qualquer um é pintado com Fill, Stroke ou FillAndStroke. Cor de fill e cor de stroke são independentes: SetRGBFillColor e SetRGBStrokeColor recebem cada um um TColor, então constantes do Delphi e RGB(...) funcionam direto, e o SetRGBColor define os dois de uma vez

A forma dos argumentos do Rectangle é a armadilha. O TCanvas.Rectangle recebe (Left, Top, Right, Bottom), e anos de código de pintura VCL treinam os seus dedos a passar um segundo canto. Entregue Rectangle(72, 202, 232, 292) ao HotPDF e você ganha uma caixa de 232 pontos de largura por 292 de altura em vez de 160 por 90. Quando os números que você tem são realmente dois cantos, chame MFRectangle(X, Y, X1, Y1), que recebe coordenadas de cantos e calcula o tamanho por você; reserve o Rectangle para código que pensa em tamanhos

Um arquivo PDF não tem operador de círculo. O Circle escreve quatro segmentos de Bezier cúbica com o fator usual de ponto de controle 0.5523, que você pode ver se descomprimir o content stream da página: um m seguido de quatro operadores c. O Ellipse(X, Y, Width, Height) faz o mesmo dentro de uma bounding box cujo canto superior esquerdo é (X, Y)

O HotPDF também expõe THotPDF.Canvas, uma TCanvas padrão para a página atual, e o programa CanvasDraw das demos Graphic enche uma página Ledger com chamadas comuns de Canvas.Rectangle, Ellipse e RoundRect. Esse caminho faz sentido ao portar código de pintura VCL existente. Código de desenho novo fica melhor em CurrentPage, porque cada chamada mapeia para um operador PDF em pontos e a saída é previsível até o número

Uma caixa de destaque a partir de quatro commits

Uma caixa de destaque usa tudo o que veio acima de uma vez: um painel preenchido e contornado, uma linha de ponteiro, um ponto de âncora e duas linhas de texto posicionadas dentro do painel com as mesmas coordenadas topo-esquerda. O FillAndStroke (o operador B) pinta o interior e a borda de um path numa única chamada, então o retângulo é construído uma vez em vez de duas

Anatomia de uma caixa de destaque montada a partir de primitivas do HotPDF: um retângulo confirmado com FillAndStroke, uma linha de ponteiro traceada com MoveTo e LineTo, um ponto de âncora preenchido, e barras de rótulo TextOut que compartilham a mesma grade topo-esquerda das formas
Quatro commits constroem a caixa de destaque — o FillAndStroke pinta painel e borda juntos, o ponteiro e o ponto reutilizam o estado de stroke e fill, e cada deslocamento de rótulo é aritmética pura contra o canto da caixa (90, 122)
procedure DrawCallout(Page: THPDFPage);
begin
  // 1. Painel: preenchimento pálido e uma borda mais escura, um path, um commit
  Page.SetRGBFillColor(RGB(255, 244, 214));
  Page.SetRGBStrokeColor(RGB(180, 130, 40));
  Page.SetLineWidth(1);
  Page.Rectangle(90, 122, 240, 70);      // topo-esquerda (90, 122), 240 x 70
  Page.FillAndStroke;

  // 2. Ponteiro da borda esquerda do painel descendo até o alvo
  Page.SetLineWidth(1.5);
  Page.MoveTo(90, 177);
  Page.LineTo(66, 246);
  Page.Stroke;

  // 3. Ponto de âncora no fim do ponteiro, na cor da borda
  Page.SetRGBFillColor(RGB(180, 130, 40));
  Page.Circle(64, 250, 3);
  Page.Fill;

  // 4. Rótulos: cada Y é o topo do texto, medido na mesma grade
  Page.SetFont('Arial', [fsBold], 10);
  Page.TextOut(102, 134, 0, 'Check this total');
  Page.SetFont('Arial', [], 9);
  Page.TextOut(102, 152, 0, 'Rounded to two decimals per line,');
  Page.TextOut(102, 164, 0, 'then summed');
end;

// Chamada entre BeginDoc e EndDoc:
//   DrawCallout(Pdf.CurrentPage);

Cada bloco define a própria cor de fill, cor de stroke e largura logo antes da forma que precisa delas, mesmo quando o bloco anterior deixou o mesmo valor no lugar. Isso custa alguns operadores redundantes no arquivo. Em troca, qualquer bloco pode se mover, ser apagado ou virar uma procedure própria sem herdar um fill vermelho perdido do que quer que tenha rodado antes, e para código de desenho que muda conforme os relatórios mudam, esse trade vale a pena toda vez

Cores merecem a mesma disciplina. Semear fills com Random($FFFFFF) deixa uma página de demo animada e um relatório gerado impossível de comparar entre execuções, já que dois builds com dados idênticos produzem bytes diferentes. Comande a cor pelos dados ou por um array de paleta fixa, como a demo CanvasDraw faz com a tabela CanvasColors de oito entradas, e a mesma entrada rende o mesmo arquivo

Tracejados, curvas e transparência

O SetDash é estado de página como a largura de linha. O SetDash([3, 3], 0) faz todo stroke seguinte ficar três pontos ligado e três desligado, com o segundo argumento deslocando onde o padrão começa, e o NoDash volta a uma caneta sólida. O array de tracejado é array of Byte, então os comprimentos são pontos inteiros de 1 a 255, e são escritos no content stream exatamente como dados. Um tracejado definido para um grupo de linhas de grade continua ligado para todo filete depois dele, então embrulhe a seção tracejada em GStateSave e GStateRestore: eles emitem q e Q, e restaurar o estado salvo traz de volta o tracejado, a largura e as cores anteriores num passo só

Curvas estendem o mesmo path. O CurveToC(X1, Y1, X2, Y2, X3, Y3) adiciona uma Bezier cúbica do ponto atual até (X3, Y3) com dois pontos de controle, enquanto o CurveToV e o CurveToY cobrem os casos em que o primeiro ou o segundo ponto de controle coincide com um ponto final. Um único path pode misturar segmentos LineTo e CurveToC antes de um Stroke ou Fill, que é como cantos arredondados e linhas de gráfico suavizadas são desenhados

A transparência vive num estado gráfico nomeado, porque o PDF guarda alpha num dicionário ExtGState em vez de na cor. O THotPDF.RegisterExtGState(FillAlpha, StrokeAlpha) recebe valores de 0 a 1 e retorna um nome; o CurrentPage.SetGraphicsState(Name) aplica a tudo que for pintado depois. Registre cada opacidade uma vez por documento e reutilize o nome, para que o arquivo carregue um dicionário por opacidade não importa quantas formas a usem. Alpha exige PDF 1.4, e o HotPDF eleva a versão de saída para 1.4 quando necessário; com StrictVersionLock definido, ele deixa a versão em paz e retorna um nome vazio. Quando o PDFACompliance seleciona um perfil PDF/A-1, alpha abaixo de 1 levanta exceção, já que a ISO 19005-1 §6.4 proíbe transparência ali. Os perfis PDF/A-2 e PDF/A-3 aceitam

var
  Half: AnsiString;
begin
  // Registre uma vez por documento, reutilize o nome retornado em qualquer página
  Half := Pdf.RegisterExtGState(0.5, 1.0);   // fill de 50%, stroke opaco

  Pdf.CurrentPage.GStateSave;                // q
  Pdf.CurrentPage.SetDash([3, 3], 0);
  Pdf.CurrentPage.SetLineWidth(0.5);
  Pdf.CurrentPage.SetRGBStrokeColor(clGray);
  Pdf.CurrentPage.MoveTo(72, 400);
  Pdf.CurrentPage.LineTo(540, 400);
  Pdf.CurrentPage.Stroke;

  Pdf.CurrentPage.SetGraphicsState(Half);
  Pdf.CurrentPage.SetRGBFillColor(clYellow);
  Pdf.CurrentPage.Rectangle(72, 380, 200, 16);  // destaque sobre a linha de texto
  Pdf.CurrentPage.Fill;
  Pdf.CurrentPage.GStateRestore;             // Q: caneta sólida, opacidade total de volta
end;

Chamadas vetoriais servem para geometria que o seu programa calcula: filetes de tabela, eixos e barras de gráfico, caixas de destaque, um logo montado de uma dúzia de paths. Uma foto ou screenshot pertence ao AddImage e ao ShowImage; rastrear pixels com paths só deixa o arquivo maior. Ao conferir a saída, dê zoom de 1600% nos cantos de strokes grossos e em qualquer linha tracejada, já que entalhes de paths não fechados e tracejados que vazaram da seção deles são óbvios nessa ampliação e fáceis de perder a 100%

Toda chamada deste artigo vive no objeto de página do HotPDF Delphi Component, que roda em projetos VCL de Delphi e C++Builder e escreve o resultado como operadores de path PDF puros