Uma string desenhada em (72, 72) cai uma polegada abaixo da borda de cima da página, o que pega bem quem aprendeu PDF pelo lado do content stream, onde y = 72 fica uma polegada acima da base. A segunda surpresa aparece algumas centenas de linhas depois. Um relatório montado para US Letter volta com a primeira página de 612 por 792 pontos e todas as seguintes com 595 por 842, porque a linha que definia o tamanho do papel executou exatamente uma vez
Os dois efeitos têm a mesma raiz. Texto num PDF é uma curta sequência de mudanças de estado (fonte e tamanho, espaçamento de caracteres, espaçamento de palavras, uma matriz de texto para posição e rotação) seguida de um operador show-text, e uma chamada de texto de alto nível é um wrapper fino sobre essa sequência. A chamada lê o estado. Ela nunca o reinicia por você, e uma página nova começa de um conjunto fixo de padrões, não de onde a página anterior parou
Onde o TextOut coloca (X, Y) na página
O HotPDF desenha texto com THPDFPage.TextOut(X, Y: Single; angle: Extended; Text: WideString), acessada como Pdf.CurrentPage.TextOut. X e Y são medidos do canto superior esquerdo da página com Y crescendo para baixo, e com a Resolution padrão de 72 uma unidade é um ponto, 1/72 de polegada. A página converte cada Y para o espaço de usuário do PDF, que cresce de baixo para cima, subtraindo-o da altura da página. O ângulo é em graus, e como o texto é um WideString, Unicode passa direto. Uma segunda sobrecarga recebe um PWORD mais um comprimento para código que já segura buffers UTF-16
A geometria da página também é fixa. O BeginDoc adiciona a primeira página, e toda página criada por BeginDoc ou AddPage começa como A4, 595 por 842 pontos, com Arial 12 como fonte corrente. US Letter (612 por 792) está a uma atribuição de distância via CurrentPage.Size := Letter, mas essa atribuição pertence à página, então precisa ser repetida para cada página nova
Por padrão, o Y que você passa é o topo do texto, e a baseline fica um ascent de fonte abaixo dele. O TextFromBaseLine(True) troca a página para posicionamento por baseline. Ligue para qualquer coisa que misture tamanhos numa mesma linha. No modo topo, um código de moeda de 9 pontos ao lado de um valor de 14 pontos tem o topo alinhado com o topo do valor, então fica visivelmente alto; no modo baseline os dois trechos ficam na mesma linha, do jeito que um tipógrafo os comporia. O último bloco do próximo exemplo faz exatamente isso
Tamanho e estilo vivem no SetFont
O TextOut não tem parâmetro de tamanho nem de estilo. O SetFont(FontName, FontStyle, ASize, FontCharset, IsVertical) carrega tudo isso, com os dois últimos argumentos com padrão ANSI_CHARSET e texto horizontal, e o que ele define vale para todo TextOut seguinte até o próximo SetFont. A maior parte da confusão do primeiro dia volta para aí. Uma linha sai em negrito porque três chamadas antes algo definiu [fsBold] e nada limpou
var
W: Single;
begin
Pdf.BeginDoc; // primeira página: A4, Arial 12
Pdf.CurrentPage.TextFromBaseLine(True); // todo Y abaixo é uma baseline
Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
Pdf.CurrentPage.TextOut(72, 72, 0, 'Quarterly Report'); // 24 pt normal
Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
Pdf.CurrentPage.TextOut(72, 104, 0, 'Revenue'); // 12 pt negrito
Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
Pdf.CurrentPage.TextOut(72, 120, 0, 'figures in thousands'); // 11 pt itálico
Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsUnderline], 10);
Pdf.CurrentPage.TextOut(72, 136, 0, '+18.4% YoY'); // estilos se combinam
// Tamanhos misturados numa baseline: meça enquanto a fonte de 14 pt está ativa
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
W := Pdf.CurrentPage.GetWideTextWidth('1,284.50');
Pdf.CurrentPage.TextOut(72, 160, 0, '1,284.50');
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(72 + W + 3, 160, 0, 'EUR'); // espaço de 3 pt
...
end;
O argumento de estilo é um conjunto TFontStyles comum, então [] é normal e [fsBold, fsItalic] é negrito itálico. fsUnderline e fsStrikeOut são desenhados como barras finas preenchidas sob ou através do trecho, e seguem o trecho quando ele é rotacionado. O tamanho é em pontos, a mesma unidade das coordenadas, o que mantém a aritmética vertical simples: texto de corpo de 11 pontos com passo de 16 pontos deixa uns cinco pontos de ar entre linhas. Não há avanço de linha automático. Cada Y é sua conta fazer, o que é tedioso para um parágrafo e exatamente certo para um formulário cujos campos ficam em posições fixas
O GetWideTextWidth mede com a fonte que estiver corrente no momento da chamada, então no último bloco a medição precisa acontecer enquanto a fonte de 14 pontos negrito ainda está definida. Mova para baixo do segundo SetFont e o sufixo de 9 pontos cai em cima dos últimos dígitos do valor
O nome da fonte é resolvido contra as fontes instaladas na máquina que gera o PDF, e uma fonte que resolve no desktop do desenvolvedor pode não existir num servidor de build, então a fonte no arquivo pronto depende de qual máquina o produziu. A cobertura de scripts é a outra metade. Uma fonte só latina obrigada a desenhar cirílico ou CJK produz caixas de glifo ausente, e isso é propriedade do arquivo de fonte, não da chamada. Quando uma string mistura scripts que nenhuma fonte única cobre, o TextOutWithFallback recebe os mesmos quatro argumentos, troca de fonte trecho por trecho para os caracteres que a fonte principal não tem e restaura a fonte original no final
Como o argumento de ângulo rotaciona o texto?
Um ângulo diferente de zero gira o trecho em sentido antihorário por tantos graus em torno da âncora dele, e a âncora é o canto superior esquerdo do texto no referencial do próprio texto. A âncora não se move. Só a direção em que os glifos marcham muda, então a coordenada que posicionava um rótulo horizontal posiciona também o seu gêmeo rotacionado. Trechos rotacionados mantêm essa âncora de topo mesmo com a página em modo baseline; o TextFromBaseLine afeta só trechos horizontais
var
W, AxisMidY: Single;
begin
AxisMidY := 400; // meio do eixo Y do gráfico
// Um trecho de 90 graus lê-se de baixo para cima e cresce para cima a partir de Y,
// então centralizá-lo no eixo significa começar metade da largura abaixo do meio
Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);
W := Pdf.CurrentPage.GetWideTextWidth('Units sold');
Pdf.CurrentPage.TextOut(40, AxisMidY + W / 2, 90, 'Units sold');
// Uma marca d'água na diagonal
Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');
// Cabeçalhos de coluna inclinados 60 graus: mesmo Y, X em passos
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');
end;
Noventa graus cobre rótulos de eixo e títulos de lombada. A 90 graus os glifos ficam à direita do X da âncora, com uma linha de altura, e o trecho sobe em direção ao topo da página pela própria largura. É por isso que centralizar soma metade da largura ao Y em vez de subtrair, e por que um rótulo ancorado no próprio ponto médio fica inteiramente acima dele. Sessenta graus serve para cabeçalhos de coluna, deixando um rótulo longo de pé sobre uma coluna estreita sem invadir a vizinha, e 45 graus é o ângulo clássico de marca d'água. Trechos que dividem uma baseline recebem o mesmo Y e um X em passos, exatamente como linhas horizontais empilhadas dividem um X e dão passos no Y
Por que a segunda página sai como A4 em Arial 12?
O AddPage retorna o índice da página nova e a torna corrente, e a página nova começa dos padrões: A4, Arial 12, posicionamento por topo, zero espaçamento de caracteres e de palavras. Nada da página anterior atravessa. Esse desenho mantém cada página autossuficiente, já que o content stream de uma página de PDF começa com um graphics state novo de qualquer forma, mas significa que qualquer código de layout que configura a página uma vez no topo do documento vai trocar silenciosamente o tamanho do papel e a fonte na página dois. A correção é colocar toda configuração por página em uma rotina e chamá-la depois do BeginDoc e depois de cada AddPage
procedure StartReportPage(Pdf: THotPDF);
begin
Pdf.CurrentPage.Size := Letter; // uma página nova começa como A4
Pdf.CurrentPage.TextFromBaseLine(True); // uma página nova começa em modo topo
Pdf.CurrentPage.SetFont('Arial', [], 11); // uma página nova começa em Arial 12
end;
procedure WriteReport(Pdf: THotPDF; Lines: TStrings);
const
LeftMargin = 72; // 1 polegada
TopLine = 72; // primeira baseline, 1 polegada abaixo
BottomMargin = 72;
Leading = 16;
var
I: Integer;
Y: Single;
begin
StartReportPage(Pdf);
Y := TopLine;
for I := 0 to Lines.Count - 1 do
begin
if Y > Pdf.CurrentPage.Height - BottomMargin then // 720 no Letter
begin
Pdf.AddPage;
StartReportPage(Pdf);
Y := TopLine;
end;
Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Lines[I]);
Y := Y + Leading;
end;
end;
Não existe layout de fluxo embaixo do TextOut, e um Y além da base da página continua sendo uma coordenada válida para o content stream: o texto é escrito, só que cai fora do media box e nenhum visualizador o mostra. Por isso o loop checa o Y antes de cada linha e abre uma página nova por conta própria. Ler o limite de CurrentPage.Height em vez de fixar 720 no código mantém o loop correto se alguém voltar o helper para A4, onde a mesma margem de uma polegada põe o limite em 770
A checagem mais rápida é a que o diálogo de impressão de um leitor faria. Abra a saída, vá à página dois e veja as propriedades do documento: uma página Letter reporta 8,5 por 11 polegadas, uma A4 cerca de 8,27 por 11,69. Se a primeira página e a segunda discordam, alguma configuração por página vive fora do helper
Espaçamento de caracteres e de palavras também são estado de página
Às vezes a string está certa e a largura está errada: um cabeçalho que deve alcançar uma régua fixa, um código que lê melhor com dígitos mais folgados, uma linha de rótulos cujos intervalos precisam abrir. O PDF tem dois parâmetros de estado de texto para isso na ISO 32000-1, o espaçamento de caracteres Tc (§9.3.2), somado depois de cada glifo, e o espaçamento de palavras Tw (§9.3.3), somado a cada espaço. O HotPDF os define com SetCharacterSpacing(Spacing: Single) e SetWordSpacing(Spacing: Single), em pontos na resolução padrão, e como a fonte eles valem até serem mudados
O espaçamento de palavras tem uma armadilha escrita na especificação. A §9.3.3 aplica o Tw apenas ao código de caractere de um byte 32, e explicitamente não a um byte 32 dentro de um código multibyte, então por si só o operador não faz nada para texto escrito com códigos de glifo de dois bytes, que é como fontes Unicode embutidas normalmente são escritas. O HotPDF escreve trechos Unicode desse jeito, e nesse caminho ele aplica o espaçamento de palavras como ajustes explícitos de posição depois de cada glifo de espaço, então o SetWordSpacing alarga os intervalos de qualquer forma. O gatilho continua sendo o espaço simples, U+0020. Uma sequência de chinês ou japonês sem espaços ASCII não ganha espaço nenhum, e um espaço ideográfico (U+3000) é tratado como um glifo comum
O espaçamento de caracteres é a ferramenta certa quando um cabeçalho precisa alcançar uma largura alvo, porque espalha a mudança de forma uniforme em vez de acumulá-la nos espaços. O GetWideTextWidth conta o espaçamento de caracteres e de palavras em vigor no momento, então meça com os dois em zero e derive o espaçamento:
const
Heading = 'SUMMARY';
Target = 200; // largura da régua que o cabeçalho deve alcançar, em pontos
var
Natural, Tc: Single;
begin
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.SetCharacterSpacing(0); // meça sem espaçamento residual
Natural := Pdf.CurrentPage.GetWideTextWidth(Heading);
// O Tc entra depois de cada glifo, mas só aparecem os intervalos entre glifos
Tc := (Target - Natural) / (Length(Heading) - 1);
Pdf.CurrentPage.SetCharacterSpacing(Tc); // valores negativos apertam
Pdf.CurrentPage.TextOut(72, 72, 0, Heading);
Pdf.CurrentPage.SetCharacterSpacing(0); // redefina antes do texto de corpo
Pdf.CurrentPage.SetWordSpacing(6);
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 100, 0, 'Name Department Extension');
Pdf.CurrentPage.SetWordSpacing(0);
end;
As linhas de reset importam mais que as linhas de definição. Deixe um espaçamento de caracteres de 4 pontos depois de um cabeçalho e todo parágrafo abaixo herda o esticamento, que lê como um erro vago que sobrevive a uma revisada rápida e depois aparece na cópia impressa. Defina o valor, desenhe o único trecho que precisa dele e zere na linha seguinte, para que nenhum código posterior precise saber o que uma seção anterior fez. A mesma página oferece SetHorizontalScaling (Tz, 100 por padrão) para comprimir um trecho sem mudar a altura dele, e SetTextRenderingMode com valores como trStroke para texto contornado, ambos persistentes exatamente do mesmo jeito
Se esta é a sua primeira página de saída, o exemplo Hello World do HotPDF cobre a configuração do documento e o salvamento. Para texto que precisa preencher uma coluna de ponta a ponta, a justificação completa de texto em PDF tem um passo a passo próprio, e CJK de cima para baixo é coberto em layout de texto vertical no HotPDF. O TextOut, o SetFont, as chamadas de espaçamento e as configurações de tamanho de página acima vêm todos com o HotPDF Delphi Component para Delphi e C++Builder