Artigo Técnico

HotPDF TextOut no Delphi: Tamanho, Estilo e Rotação

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

Página do HotPDF TextOut mostrando Arial, Times New Roman e Courier New renderizadas com estilos regular, negrito e itálico em vários conjuntos de caracteres

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

Diagrama da geometria de rotação do TextOut no HotPDF: trechos a 0, 45 e 90 graus girando em sentido antihorário em torno de uma âncora fixa, com exemplos de chamadas TextOut em Delphi para rótulos de lombada, marca d'água e cabeçalhos de coluna inclinados
Um ângulo diferente de zero gira o trecho em sentido antihorário em torno da âncora imóvel, o canto superior esquerdo do texto, e baselines compartilhadas avançam X exatamente como linhas horizontais avançam Y
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

Diagrama de página US Letter do HotPDF mostrando aritmética de posição de linhas com uma margem esquerda, uma linha inicial no topo e um Leading fixo avançando até um limite de margem inferior, ao lado do loop de layout com constantes nomeadas e sua guarda manual de AddPage e reset de SetFont
Âncoras nomeadas transformam uma coluna de números mágicos em aritmética: avance o Y corrente em Leading a cada linha e guarde você mesmo a margem inferior com 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

HotPDF: espaçamento de caracteres Tc espalhando espaço extra uniformemente depois de cada glifo de SUMMARY versus espaçamento de palavras Tw acumulando-o só nos caracteres de espaço, ligado ao ciclo definir, desenhar, redefinir que impede o estado de espaçamento de vazar para parágrafos seguintes
O Tc espalha o ajuste por todos os glifos enquanto o Tw age só no caractere de espaço, e redefinir na linha seguinte mantém o estado de desenho limpo

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

Página do HotPDF TextOut comparando dimensionamento horizontal de texto, espaçamento de caracteres, espaçamento de palavras e modos de renderização fill versus stroke

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