Artigo Técnico

Justificação Completa de Texto PDF no Delphi com HotPDF

A justificação completa é o layout que faz uma coluna de texto se alinhar nas bordas esquerda e direita ao mesmo tempo, o visual que você espera de um livro impresso ou de um relatório formal. É fácil de descrever e surpreendentemente fácil de errar, porque a resposta à pergunta "onde vai o espaço extra" não é a mesma para o inglês e para o japonês, e porque a forma ingênua de medir cada linha transforma uma página rápida em uma lenta. O HotPDF oferece justificação com reconhecimento de script através de uma única chamada de layout de caixa, e por baixo dessa chamada está uma melhoria de desempenho clássica que vale a pena entender por si só

Este artigo percorre ambas. Primeiro, a regra tipográfica que decide como o espaço residual é distribuído para scripts com espaços entre palavras versus scripts sem eles. Segundo, a mudança de medição que reduziu o custo por página da justificação em aproximadamente oitenta vezes sem nenhuma diferença visível na saída. Ambas importam se você gera documentos em volume e quer que eles se pareçam com composição tipográfica real em vez de saída em espaçamento fixo esticada para caber

O que a justificação completa realmente exige

Uma linha de texto desenhada em sua largura natural quase nunca atinge a borda direita de sua coluna. Há sempre um restante, o espaço residual, entre onde termina o último glifo e onde fica o limite da coluna. O alinhamento à esquerda deixa esse espaço à direita. O alinhamento à direita o move para a esquerda. A centralização o divide. A justificação completa o elimina alargando a própria linha até que ambas as bordas encontrem a caixa, e a única forma honesta de fazer isso é empurrar os glifos para longe um do outro por dentro

A regra que separa a boa justificação da má é onde você coloca o espaço residual. Um script que escreve palavras com espaços entre elas, como o inglês e o restante da família latina, tem costuras naturais em cada espaço entre palavras. Alargar esses espaços é invisível aos olhos porque os leitores já aceitam que os espaços entre palavras variam. Um script que escreve sem espaços entre palavras, como os caracteres Han do chinês, as kanas japonesas ou o Hangul coreano, não tem essas costuras. Ali o espaço residual tem que ser distribuído uniformemente entre glifos adjacentes, que é o princípio que os tipógrafos japoneses chamam de kintou-waritsuke, espaçamento uniforme. Colocar o espaçamento de estilo latino com espaço entre palavras em uma linha CJK, ou concentrar todo o espaço residual no único lugar que uma linha CJK acontece de ter um espaço, produz os rios e lacunas que marcam a saída amadora

Como o HotPDF decide onde vai o espaço

O HotPDF toma essa decisão por intervalo, não por linha. Ao justificar uma linha, ele percorre cada par adjacente de glifos e pergunta se há um limite elástico entre eles. Um limite é elástico quando qualquer lado é um espaço ou tabulação, o caso latino, ou quando ambos os lados são caracteres quebráveis CJK, o caso de espaçamento uniforme. Ele conta esses limites, divide o espaço residual da linha igualmente entre eles, e adiciona essa parcela a cada intervalo qualificado

A consequência surge naturalmente. Uma linha em inglês tem limites elásticos apenas nos seus espaços entre palavras, portanto todo o espaço residual fica ali e as palavras se separam enquanto as letras dentro de cada palavra mantêm seu espaçamento natural. Uma linha de Han ou kana tem um limite elástico entre quase cada par de glifos, portanto o espaço residual se distribui uniformemente por toda a linha, exatamente o espaçamento uniforme entre glifos que esses scripts requerem. Uma linha que é uma única palavra latina longa sem espaço interno não tem nenhum limite elástico, então o HotPDF a deixa em sua largura natural em vez de separar a palavra letra por letra. A mesma lógica lida com sequências latinas e CJK misturadas em uma linha sem casos especiais, porque a decisão é local a cada limite

Um limite é deliberadamente excluído em todos os casos. A posição após o glifo final de uma linha nunca é tratada como um intervalo, porque esticar ali apenas reintroduziria um restante à direita, que é o oposto da justificação

Por que a última linha é deixada sozinha

A linha final de um parágrafo é especial, e errar nisso é o erro de justificação mais comum. A última linha de um parágrafo geralmente é curta, frequentemente apenas algumas palavras, e esticá-la até a largura total da coluna arrasta essas palavras pela página em uma linha esparsa e quebrada. A tipografia correta deixa a última linha em sua largura natural, alinhada à esquerda

O HotPDF detecta a linha final pela posição. Ao quebrar o texto em linhas, ele sabe quando a linha que acabou de separar atinge o final da string fornecida. Essa linha final é emitida com alinhamento simples à esquerda e mantém sua largura natural. Cada linha antes dela é justificada em ambas as bordas. Quebras de linha explícitas que você escreve no texto são respeitadas como escritas, portanto uma linha curta intencional também nunca é esticada. O leitor vê um bloco de texto retangular limpo cuja última linha termina naturalmente, que é o que o olho espera

O custo de medição que tornava a justificação lenta

Para justificar uma linha, você precisa saber sua largura exata e o avanço de cada glifo para poder posicionar o espaço extra com precisão. A primeira implementação obtinha esses números da maneira óbvia. Ela media a linha inteira com uma consulta de largura Unicode completa, depois media prefixo após prefixo para recuperar o avanço de cada glifo por diferenciação. Para uma linha de N glifos, isso é N+1 chamadas ao motor de medição, e cada chamada é uma viagem GDI completa, pedindo ao sistema operacional para modelar e medir o texto e retornar a resposta

Por linha parece barato. Em uma página não é. Considere uma página A4 densa de texto no corpo, aproximadamente quarenta e cinco linhas de cerca de oitenta caracteres cada. Com N+1 viagens por linha, isso é cerca de 81 viagens para cada linha e aproximadamente 3.645 para a página, quase todas gastas remediando texto que o motor já havia analisado momentos antes. Em um trabalho em lote produzindo milhares de páginas, esse overhead domina o tempo de layout, e cada viagem cruza o limite entre o seu processo e o subsistema gráfico

Uma chamada em vez de N mais um

A correção é o tipo de mudança que parece pequena e rende muito. O GDI já pode reportar a largura total de uma string e a posição de cada glifo em uma única consulta. O HotPDF expõe isso através de GetWideCharAdvances, que preenche um array com o avanço natural de cada glifo, incluindo kerning, e retorna a largura total, em uma chamada em vez de N+1. A rotina de justificação, _HPDFEmitJustifiedWideLine internamente, pede todos os avanços de uma vez, computa o espaço residual, distribui-o pelos limites elásticos e emite a linha

Para essa mesma página A4, a medição por linha cai de cerca de 81 viagens para uma, portanto a página cai de aproximadamente 3.645 viagens para cerca de 45, uma redução de quase oitenta vezes. A saída é idêntica byte a byte, porque nada sobre a medição mudou, exceto quantas vezes é solicitada. O mesmo motor GDI, as mesmas métricas de fonte, o mesmo kerning alimentam os mesmos números. Apenas a contagem de viagens caiu. Quando uma medição já é correta, a otimização certa é parar de pedi-la repetidamente, não aproximá-la

Como a linha chega à página

Uma vez que o espaço residual é distribuído, o HotPDF emite a linha com ExtTextOut e um array de avanço por glifo, o array Dx. Cada entrada é a distância da origem de um glifo ao próximo, que é o avanço natural desse glifo mais sua parcela do espaço residual quando um limite elástico o segue. Isso mapeia diretamente para o modelo de imagem PDF. O texto posicionado é escrito com o operador TJ, um array que intercala sequências de glifos com ajustes horizontais explícitos, e os valores Dx se tornam exatamente esses ajustes. É por isso que o espaço extra fica entre glifos em posições sub-ponto precisas em vez de ser falsificado com caracteres de preenchimento, e por que uma linha HotPDF justificada se mede corretamente se uma ferramenta downstream a lê de volta

Você não chama ExtTextOut diretamente para parágrafos justificados. O ponto de entrada é WideTextOutBox, que envolve uma string Unicode em uma caixa e aplica o alinhamento que você solicita. Ele divide o texto em linhas que cabem na largura da caixa, posiciona cada linha ao longo da altura da caixa e retorna o número de caracteres que conseguiu encaixar antes de ficar sem espaço vertical. O alinhamento é escolhido pelo enum de justificação

type
  THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);

Os três primeiros são os alinhamentos autoexplicativos à esquerda, centralizado e à direita. O quarto, jtJustify, é a justificação completa nas duas bordas descrita aqui, e é o valor que WideTextOutBox lê para ativar o espaçamento com reconhecimento de script

Justificando um parágrafo na prática

Um exemplo completo cria um documento, define uma fonte e despeja um parágrafo em uma caixa com justificação completa. O mesmo código justifica texto latino e CJK sem alterar nenhuma flag, porque o reconhecimento de script fica abaixo da API

uses
  HPDFDoc;

procedure JustifyParagraph;
var
  Pdf: THotPDF;
  Body: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Justified.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', 11);

    Body :=
      'Full justification spreads the slack on each filled line so both ' +
      'edges meet the column, while the last line keeps its natural width. ' +
      'For scripts with word gaps the space lands between words; for ' +
      'scripts without them it spreads evenly between glyphs.';

    // X, Y, LineSpacing, BoxWidth, BoxHeight, Text, Align
    Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Para desenhar o mesmo bloco alinhado à esquerda, centralizado ou à direita, mude apenas o argumento final para jtLeft, jtCenter ou jtRight. O quebra-linhas, o posicionamento das linhas e o valor de retorno permanecem os mesmos. A largura medida que orienta todos os quatro caminhos vem de GetWideTextWidth, a consulta de largura com reconhecimento Unicode que mede uma WideString corretamente onde a medição mais antiga em bytes mediria incorretamente qualquer coisa além do Latin-1, que é o que faz a caixa quebrar texto CJK e com pares substitutos no lugar certo para começar

A justificação é uma camada de uma pilha de modelagem de texto maior. Quando uma linha contém scripts que reordenam ou unem seus glifos, as decisões de espaçamento aqui se situam em cima do trabalho descrito em nosso artigo sobre modelagem de texto em scripts complexos, e quando uma fonte carrega variantes tipográficas que você deseja selecionar, veja como acionar alternativas estilísticas OpenType GSUB. Tudo isso é fornecido no HotPDF Component para Delphi e C++Builder, ao lado das APIs de texto, layout e documento mais amplas cobertas neste blog