Uma primeira linha escrita do ponto de vista da especificação pousa em y = 770. O raciocínio é sólido no papel: a ISO 32000-1 põe a origem do user space no canto inferior esquerdo, uma página A4 tem 842 pontos de altura, então 770 fica uma polegada abaixo da borda superior. Rode esse programa contra um componente que mede de cima e a saudação pousa cerca de uma polegada acima da borda inferior, com o resto da página vazio acima dela. Adicione uma segunda página, e a fonte escolhida com cuidado vira Arial 12 de novo. As duas surpresas vêm das mesmas duas decisões que todo programa PDF toma na primeira página dele: qual fonte está atual, e de qual canto os números contam
O menor programa HotPDF no Delphi
O Hello World do HotPDF é um programa de console com quatro chamadas numa ordem fixa: definir as propriedades do documento, BeginDoc, desenhar com SetFont e TextOut, e então EndDoc. Todo o resto é Delphi comum. Aqui está ele, escrevendo uma página que saúda o leitor em onze idiomas:
program HelloWorld;
{$APPTYPE CONSOLE}
uses
Winapi.Windows, // DEFAULT_CHARSET; em Delphi anterior ao XE2 use só Windows
HPDFDoc; // unit principal do HotPDF
procedure CreateHelloWorld(const FileName: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
// Configurações de documento: todas vão antes do BeginDoc
Pdf.FileName := FileName;
Pdf.AutoLaunch := False; // o padrão é True: abre o viewer depois do EndDoc
Pdf.Compression := cmFlateDecode; // o padrão é cmNone
Pdf.FontEmbedding := True; // já é o padrão, escrito para o leitor
Pdf.EnableFontSubsetting := True; // embute só os glifos de fato desenhados
Pdf.BeginDoc; // cria a página 1 (A4)
// O 4º argumento tem ANSI_CHARSET como padrão; passe DEFAULT_CHARSET explicitamente
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
// X, Y em pontos a partir do canto SUPERIOR ESQUERDO; Y é a borda superior da linha
Pdf.CurrentPage.TextOut(72, 72, 0, 'Hello, Delphi PDF world!'); // inglês
Pdf.CurrentPage.TextOut(72, 102, 0, 'Hola, mundo Delphi PDF!'); // espanhol
Pdf.CurrentPage.TextOut(72, 132, 0, 'Hallo, Delphi PDF Welt!'); // alemão
Pdf.CurrentPage.TextOut(72, 162, 0, 'Bonjour, monde PDF Delphi!'); // francês
Pdf.CurrentPage.TextOut(72, 192, 0, 'Ciao, mondo Delphi PDF!'); // italiano
Pdf.CurrentPage.TextOut(72, 222, 0, 'Olá, mundo Delphi PDF!'); // português
Pdf.CurrentPage.TextOut(72, 252, 0, 'Здравствуйте, Delphi PDF мир!'); // russo
Pdf.CurrentPage.TextOut(72, 282, 0, 'こんにちは、Delphi PDFの世界!'); // japonês
Pdf.CurrentPage.TextOut(72, 312, 0, 'Merhaba, Delphi PDF dünyası!'); // turco
Pdf.CurrentPage.TextOut(72, 342, 0, '你好,Delphi PDF世界'); // chinês
Pdf.CurrentPage.TextOut(72, 372, 0, '여보세요, Delphi PDF 세계!'); // coreano
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
begin
CreateHelloWorld('HelloWorld.pdf');
Writeln('Wrote HelloWorld.pdf');
end.

THotPDF.Create(nil) emparelhado com try/finally ... Free é o padrão comum de propriedade do Delphi, e produzir um PDF não o muda. As linhas marcham para baixo a partir de Y = 72, com 30 pontos de distância, o que deixa um entrelinha confortável para texto de 14 pontos. Rode-o e o HelloWorld.pdf aparece no diretório de trabalho: uma página, conteúdo comprimido, a fonte embutida como subset
Propriedades do documento ficam antes do BeginDoc
O BeginDoc inicia o documento e cria a primeira página dele, então qualquer coisa que molda o arquivo inteiro precisa ser atribuída antes dele. O HotPDF impõe isso para algumas propriedades em vez de deixar por convenção. Atribuir Compression depois do BeginDoc faz o setter levantar uma exceção com a mensagem Cannot set Compression value - document in progress.; o AutoLaunch é guardado do mesmo jeito. Barulhento é bom aqui. Ponha toda configuração de documento num bloco acima do BeginDoc e você nunca vê essa mensagem
Dois dos padrões merecem um segundo olhar. Compression começa como cmNone, então um programa que nunca o toca escreve content streams descomprimidos; para uma página de texto isso é só desperdício, para um relatório com centenas de páginas é a primeira coisa que qualquer um nota no tamanho do arquivo. AutoLaunch começa como True, o que significa que uma ferramenta de console abre o PDF pronto no viewer associado no momento em que o EndDoc retorna. Isso é agradável na sua mesa e errado dentro de um serviço, um job agendado ou um build agent. Para esses ambientes, defina-o como False no código; para execuções de CI em que você não pode mudar o código, a variável de ambiente HOTPDF_SUPPRESS_AUTO_LAUNCH definida como 1 também desliga a abertura
O tratamento de fontes é onde tomamos um lado. FontEmbedding é True por padrão, então a face viaja com o arquivo e renderiza numa máquina que nunca teve a Arial Unicode MS instalada. EnableFontSubsetting, por outro lado, fica desligado por padrão para manter fontes completas disponíveis para fluxos de edição. Para uma saudação, fatura ou relatório gerado, ligue o subsetting. A Arial Unicode MS cobre uma faixa enorme de scripts, e embutir a face inteira para mostrar onze linhas curtas deixaria o arquivo megabytes maior sem benefício visível. Deixe desligado só quando alguém a jusante vai editar o texto e precisa de glifos que você não desenhou
Onde fica o (0, 0) numa página HotPDF?
O HotPDF mede as coordenadas de página a partir do canto superior esquerdo com o Y crescendo para baixo, o mesmo quadro que um TCanvas da VCL usa. No Resolution padrão de 72, uma unidade é um ponto, 1/72 de polegada. O arquivo PDF em si continua armazenando coordenadas de baixo para cima, como a ISO 32000-1 §8.3.2.3 exige; o HotPDF converte todo Y que você passa subtraindo-o da altura da página quando escreve o content stream
A aritmética por trás do exemplo de abertura é curta. A página padrão que o BeginDoc e o AddPage criam é A4, 595 por 842 pontos. Uma linha que deve ficar a uma polegada do topo é Y = 72 em termos do HotPDF e y = 770 em termos de PDF bruto, porque 842 − 72 = 770. Passe 770 ao TextOut e o texto vai 770 pontos para baixo a partir do topo, o que é 72 pontos acima da borda inferior
Há uma segunda armadilha, menor, na mesma chamada. Por padrão o Y que você passa é a borda superior do texto, e o HotPDF desce pela altura do texto antes de posicionar a baseline. Código portado do GDI normalmente espera exatamente isso. Código portado de outra biblioteca de PDF, ou do seu próprio escritor de content streams, normalmente espera que o Y seja a baseline, e toda linha sai uma altura de texto abaixo do planejado. THPDFPage.TextFromBaseLine(True) muda a página atual para posicionamento por baseline:
// Ponte para coordenadas que vêm de uma fonte de baixo para cima (um layout PDF,
// um diagrama de spec, outra biblioteca). Helper escrito para este artigo,
// não faz parte do HotPDF. Válido no Resolution padrão de 72
function FromBottomUp(Page: THPDFPage; PdfY: Single): Single;
begin
Result := Page.Height - PdfY;
end;
procedure DrawFromPdfCoordinates(Pdf: THotPDF);
begin
Pdf.CurrentPage.SetFont('Arial', [], 12);
// Y agora nomeia a baseline, do jeito que os operadores de texto do próprio PDF posicionam glifos
Pdf.CurrentPage.TextFromBaseLine(True);
// Baseline 770 pontos acima da borda inferior, ou seja, Y = 72 a partir do topo
Pdf.CurrentPage.TextOut(72, FromBottomUp(Pdf.CurrentPage, 770), 0,
'Baseline one inch below the top edge');
end;
Resolution é a outra configuração que muda o que os números significam. Definir THotPDF.Resolution como 96 e as coordenadas X/Y, margens e resultados de TextWidth passam a ser expressos em 1/96 de polegada, o que é útil quando o layout foi desenhado em pixels de tela. Width e Height da página continuam reportando pontos, porém, então código de layout que mistura limites de página com coordenadas de desenho deve ler CurrentPage.UserWidth e CurrentPage.UserHeight, que voltam nas mesmas unidades que X e Y. Em 72 os dois pares concordam. Se você prefere pensar em milímetros, a propriedade MeasurementUnit (muPoints por padrão, com muMM, muInches, muCentimeters, muTwips e muPixels disponíveis) é o interruptor mais limpo. Escolha uma convenção por programa. Nunca as empilhe
Por que a página dois imprime em Arial 12?
Porque toda página nova começa de um estado conhecido. O THotPDF.AddPage torna a página nova atual, dimensiona-a A4 e chama SetFont('Arial', [], 12) nela, mantendo o tamanho em 12 pontos qualquer que seja o Resolution. O BeginDoc cria a página um pelo mesmo caminho, e é por isso que o Hello World define a fonte logo depois do BeginDoc e não antes. A consequência para saída de várias páginas é fácil de perder: a SetFont que você chamou na página um não se transporta. Linhas latinas na página dois ainda parecem boas em Arial, só menores. As linhas CJK são a delação, porque a Arial não tem glifos para elas e o leitor mostra quadrados vazios ou nada
Uma fonte inicial previsível é o desenho certo para um objeto de página: o código de desenho da página cinco não depende silenciosamente do que quer que a página quatro tenha deixado para trás. O custo é que a configuração da página precisa ser repetida, e o jeito mais limpo de repeti-la é um procedimento único pelo qual toda página passa, incluindo a primeira:
uses
Winapi.Windows, Vcl.Graphics, HPDFDoc;
// Seu próprio helper: um lugar que sabe o que uma página nova precisa
procedure PreparePage(Pdf: THotPDF; IsFirst: Boolean);
begin
if not IsFirst then
Pdf.AddPage; // reseta a fonte para Arial 12
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
end;
procedure WriteGreetings(Pdf: THotPDF; Lines: array of string);
const
TopMargin = 72;
Leading = 30;
LinesPerPage = 20;
var
I, Row: Integer;
begin
PreparePage(Pdf, True); // a página 1 já existe depois do BeginDoc
Row := 0;
for I := 0 to High(Lines) do
begin
if Row = LinesPerPage then
begin
PreparePage(Pdf, False);
Row := 0;
end;
Pdf.CurrentPage.TextOut(72, TopMargin + Row * Leading, 0, Lines[I]);
Inc(Row);
end;
// Um título em negrito precisa do próprio SetFont, depois a fonte do corpo volta
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial Unicode MS', [fsBold], 18, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 72, 0, 'Appendix');
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 110, 0, 'Body text after the heading');
end;
Se as suas páginas também mudam para posicionamento por baseline com TextFromBaseLine, ponha essa chamada no PreparePage também, ao lado da fonte. Manter toda configuração por página num procedimento significa que um requisito novo, uma cor ou um tamanho diferente é adicionado uma vez e alcança toda página
Uma fonte para muitos scripts, e onde ela para
Uma única SetFont pode carregar latim, cirílico, japonês e chinês numa página porque a Arial Unicode MS agrupa glifos para uma faixa muito ampla de scripts numa face só. O quarto argumento da SetFont é o character set do Windows, e o padrão dele é ANSI_CHARSET, que prende a seleção de fonte à página de código ocidental. Passar DEFAULT_CHARSET (valor 1) deixa o Windows escolher a face pelo nome, que é o que uma página multilíngue quer
A fonte precisa existir na máquina que constrói o PDF. O HotPDF embute qualquer fonte à qual o Windows resolva o nome, e quando a Arial Unicode MS está ausente, o Windows substitui outra face sem reclamar. A saída então embute a substituta, e linhas em scripts que essa face não cobre saem como quadrados vazios. Um servidor de build é o lugar de costume onde isso aparece, porque ninguém instalou fontes do Office lá. Confira a lista de fontes embutidas do primeiro arquivo que uma máquina nova produz antes de confiar nela para um lote
Quando nenhuma face única cobre todas as strings, o TextOutWithFallback recebe os mesmos argumentos que o TextOut, resolve uma fonte para cada caractere, desenha cada trecho com a face que consegue renderizá-lo e restaura a sua fonte original depois. Scripts da direita para a esquerda como árabe e hebraico precisam de mais que cobertura de glifos: precisam de shaping e ordem de trechos invertida, que é o território da função RtLTextOut, normalmente emparelhada com um arquivo TrueType carregado via RegisterUnicodeTTF
Daqui, os próximos passos naturais são tamanho, estilo e rotação de texto, e posicionar vários blocos deliberadamente numa página, que é o que a amostra TextOut percorre. Linhas, caixas e imagens usam o mesmo objeto CurrentPage e as mesmas coordenadas de canto superior, então tudo acima sobre origens e configuração por página se aplica a eles sem mudança
A página do HotPDF Delphi Component cobre o resto da API de desenho para Delphi e C++Builder, de formas e imagens a formulários, uma vez que este Hello World rode limpo contra as suas próprias fontes