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