Artigo Técnico

Relatórios PDF no Delphi com HotPDF: fontes e imagens

Gerar um relatório se resume a colocar três coisas em uma página e fazê-las concordar sobre onde ficam: texto em coordenadas conhecidas, fontes que renderizam no servidor igual ao que você vê na sua máquina, e imagens dimensionadas para caber. Tudo o mais que uma biblioteca de relatórios faz se organiza em torno desses três pontos. O HotPDF, a biblioteca de geração de PDF da losLab para Delphi e C++Builder, entrega cada um deles como uma chamada direta no objeto de página, e o único atrito real é o sistema de coordenadas por baixo, que corre no sentido oposto ao do canvas da VCL a que você está acostumado. Acerte essa orientação primeiro e o resto do trabalho de layout para de brigar com você

Posicionamento de texto e a origem no canto inferior esquerdo

O primeiro relatório de quase todo mundo sai de cabeça para baixo. O título aparece perto da borda inferior e cada linha seguinte sobe em direção ao topo. Nada está com defeito. O espaço do usuário do PDF, definido na ISO 32000-1 §8.3, coloca a origem no canto inferior esquerdo, com Y crescendo para cima, que é a imagem espelhada do canvas do GDI, onde Y cresce para baixo a partir do canto superior esquerdo. Cinco minutos gastos em fazer as pazes com isso salvam um layout que, de outro modo, você reescreveria assim que os números deixassem de fazer sentido

Diagrama do HotPDF contrastando a origem de coordenadas no canto superior esquerdo da VCL com a origem no canto inferior esquerdo do PDF, em que TextOut posiciona um título a 50 pontos do topo de uma página Letter em Y 792 menos 50
O espaço do usuário do PDF espelha o canvas da VCL, então um título a 50pt do topo de uma página Letter é TextOut(50, 792 - 50, 0, 'INVOICE') e a mesma conversão mantém intuitiva toda coordenada do relatório

A chamada central do objeto de página é TextOut(X, Y, Angle, Text). X e Y localizam o texto em pontos a partir do canto inferior esquerdo, e Angle o rotaciona em graus, que é como um carimbo diagonal de DRAFT ou COPY é desenhado sem nenhum suporte especial. O truque que mantém funcionando a intuição treinada na VCL é expressar Y como a altura da página menos a distância que você quer a partir do topo:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // 50pt do topo da Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // carimbo rotacionado
    Pdf.AddPage;                                                // CurrentPage aponta para cá agora
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // o estado de fonte não é herdado
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Os dois comportamentos com estado dessa listagem respondem pela maioria dos bugs que só aparecem na página dois. AddPage reaponta CurrentPage para a página que acabou de criar, então uma referência de página que você guardou antes já não desenha onde você espera. A seleção de fonte também é por página, não por documento. Se você pular o SetFont depois de um AddPage, o primeiro TextOut na página nova recai no padrão com que a página começou, e não na fonte de título em negrito que você definiu três páginas atrás. O hábito seguro é tratar "começar uma página nova" e "restabelecer o estado de texto" como um único passo inseparável dentro do laço do relatório

Fontes que existem no servidor, não só na sua máquina

A maioria dos problemas de fonte é, na verdade, um problema de implantação disfarçado. A sua máquina de desenvolvimento tem a fonte corporativa instalada, então o relatório fica certo na sua tela e vai para produção. O servidor de produção roda o serviço com uma conta que nunca teve aquela fonte instalada, o renderizador substitui em silêncio por algo que consegue encontrar, e a primeira notícia que alguém tem disso é um cliente perguntando por que o papel timbrado mudou. A saída é parar de confiar no diretório de fontes do sistema operacional e carregar a fonte de um arquivo que o seu instalador coloca em disco. A chamada de registro Unicode do HotPDF recebe um caminho e faz exatamente isso:

Diagrama de um problema de implantação de fontes em PDF no Delphi: o servidor de produção substitui em silêncio uma fonte ausente, enquanto RegisterUnicodeTTF carrega o TTF de um arquivo implantado e o incorpora no PDF
Depender do diretório de fontes do sistema quebra quando a conta de serviço de produção não tem a fonte, enquanto carregar o TTF de um arquivo implantado incorpora os glifos e todo servidor renderiza igual
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));

TextOut aceita uma WideString diretamente, o que importa mais do que parece à primeira vista. Um nome de cliente com acento, uma rua alemã, uma cidade polonesa: esses não são casos extremos, são o conteúdo normal de uma tabela de clientes, e passam pela mesma chamada dos rótulos ASCII que você fixa no código, desde que a fonte registrada contenha de fato os glifos. Uma restrição de versão vem junto com as fontes incorporadas: o documento precisa ser PDF 1.5 ou posterior, então, se um requisito não relacionado estiver prendendo você a uma versão mais antiga, é isso que vai quebrar em silêncio. Escritas da direita para a esquerda, como árabe e hebraico, precisam de shaping de verdade, e não de uma consulta direta de glifos, e isso tem um pipeline próprio; veja nosso artigo sobre shaping de texto de escritas complexas com HotPDF

Quando nenhuma fonte instalada consegue expressar o que você precisa, pense em caracteres MICR em um cheque ou em um conjunto proprietário de símbolos, as fontes Type 3 preenchem a lacuna. Você define cada glifo como um pequeno content stream por meio de RegisterType3Font e AddType3Glyph. É um canto especializado da API e você raramente vai recorrer a ele, mas é bem mais limpo do que espalhar centenas de pequenos bitmaps de símbolo por uma página

Imagens: os argumentos do meio são largura e altura, não um canto

O tratamento de imagens se divide em dois passos, e manter os dois separados é a questão toda. AddImage recebe um TBitmap ou um TJPEGImage, incorpora a imagem uma vez e devolve um índice. Arte em PNG precisa ser decodificada para bitmap antes de chegar lá. ShowImage então desenha esse índice onde e quantas vezes você quiser. A ordem dos argumentos de ShowImage é o único ponto em que vale desacelerar para ler:

Diagrama do pipeline de imagens do HotPDF em que AddImage incorpora o bitmap uma vez e devolve um índice, ShowImage o posiciona por largura e altura, e a ordem dos argumentos não é um par de cantos
AddImage incorpora os pixels uma vez e toda chamada de ShowImage reaproveita esse índice, e os argumentos do meio são largura e altura, não as coordenadas de um canto oposto
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // decodifica o PNG para um bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // sem perdas para arte de cor chapada
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): não (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Os dois números depois da posição são uma largura e uma altura. Não são as coordenadas do canto oposto, e o argumento final é um ângulo de rotação em graus. Leia a assinatura como uma caixa X1/Y1/X2/Y2 e um logotipo de 120 por 40 posicionado em (50, 700) passa a se esticar dali até (120, 40), espalhando-se pela maior parte da página. A saída deixa o erro óbvio enquanto o código-fonte parece perfeitamente razoável, e é isso que faz você perder uma tarde. KeepImageAspectRatio vem como True por padrão, então uma caixa com as proporções erradas aplica bordas à imagem em vez de distorcê-la; mude para False só quando você realmente quiser esticar

A separação entre registrar e posicionar compensa em execuções longas. Como AddImage incorpora os pixels uma vez e todo ShowImage com aquele índice aponta de volta para o mesmo objeto incorporado, onde você chama AddImage decide o tamanho do arquivo. Chame dentro do laço de páginas para um extrato de 500 páginas e o mesmo logotipo é incorporado 500 vezes. Chame uma vez antes do laço, guarde o índice, e o logotipo é armazenado uma única vez. Um pequeno dicionário com chave no caminho do arquivo já basta para garantir que cada imagem distinta seja registrada exatamente uma vez

A escolha do codec é a outra alavanca de tamanho. Conteúdo fotográfico, anexos digitalizados e afins, pertence ao JPEG: passe icJpeg para AddImage e baixe JpegQuality para algo em torno de 85, já que a propriedade começa em 100 e a diferença em 85 é invisível em uma página impressa. Arte de cor chapada, como logotipos, gráficos e desenhos de linha, pertence ao icFlate, em que a compressão sem perdas já é compacta e o JPEG borraria um halo visível em volta das bordas duras. Uma tiragem de extratos que empurra uma foto em qualidade máxima para cada página pode inchar até gigabytes; o mesmo conteúdo em JPEG 85 fica por volta de um décimo do tamanho, e nenhum leitor percebe

Fios, caixas e sombreado com primitivas de caminho

A linha horizontal sob o cabeçalho de uma tabela e a caixa cinza atrás de um valor de total não precisam ser imagens. Desenhe-as como vetores e elas continuam nítidas em qualquer zoom, imprimem com precisão e quase nada acrescentam ao arquivo. O HotPDF segue o mesmo modelo que os content streams de PDF usam: construa um caminho e depois chame um operador que o pinte

// Fio horizontal sob o cabeçalho da tabela
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Caixa de totais sombreada: X, Y, largura, altura
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

A ordem não é opcional: defina o estado de pintura, construa o caminho e então chame Stroke ou Fill. Um caminho que você constrói mas nunca pinta não contribui em nada para a página, o que quase sempre é a resposta quando um fio "não está aparecendo". SetRGBFillColor recebe um único TColor, então as constantes familiares da VCL, como clNavy e clBlack, entram direto, e Rectangle usa os mesmos argumentos de largura e altura do posicionamento de imagens, e não dois cantos. Um cuidado com linhas finas: qualquer coisa abaixo de meio ponto pode parecer elegante em um monitor e depois sumir em uma impressora de escritório de 600 dpi, então 0,75pt é um piso razoável para qualquer fio que precise sobreviver à impressão

Paginação contra dados reais, não dados de exemplo

Um detalhe a acertar antes de o layout endurecer: colunas numéricas devem ser alinhadas pela borda direita, e a forma de fazer isso é medir a largura renderizada de cada valor e posicioná-lo a partir do limite da coluna, e não preencher a string com espaços à esquerda. O preenchimento com espaços só se alinha em fonte monoespaçada, e ninguém compõe um relatório financeiro em fonte monoespaçada. Passe os valores primeiro pelas rotinas sensíveis à localidade do Delphi, como FormatFloat, para que o separador de milhar cuja largura você mede seja o mesmo que a localidade do cliente vai de fato exibir

O perigo da paginação é que você a escreve contra o conjunto de dados de demonstração, em que dez linhas curtas cabem em uma página e o laço nunca precisa quebrar. A produção entrega um cliente cujo nome de empresa tem 140 caracteres e um extrato com 4.000 itens, e agora o laço precisa quebrar corretamente todas as vezes. O padrão que se sustenta é um único cursor Y que se move para baixo conforme você subtrai a altura de cada linha, e uma verificação que inicia uma página nova assim que o cursor cruzaria a margem inferior. Para baixo, aqui, significa Y decrescente, que é o único ponto em que a origem no canto inferior esquerdo continua contraintuitiva. Mantenha tudo isso em uma rotina que também reemita SetFont e redesenhe o cabeçalho corrente na página nova, e os bugs de uma página a mais ou a menos nunca ganham espaço. Quando os mesmos relatórios também precisam atender a regras de arquivamento ou de acessibilidade, as escolhas que você faz bem aqui, quais fontes incorpora, se a saída é marcada, quais espaços de cor usa, são justamente as que essas normas fiscalizam; o guia de PDF/A, PDF/X e PDF/UA do HotPDF vale a leitura antes de o modelo endurecer

Toda chamada mostrada aqui, o posicionamento de texto, o registro de fontes, a incorporação de imagens e o desenho de caminhos, acompanha o HotPDF Delphi Component para Delphi e C++Builder, cuja referência documenta a API de saída completa junto dos recursos de formulários, criptografia e assinatura ao lado dos quais ela vive