Artigo Técnico

Medindo Texto PDF para Layout e Quebra de Linha no Delphi

A chamada que coloca texto em uma página PDF é simples. Você dá a AddText uma string, uma fonte, um tamanho e uma posição, e os glifos aparecem. O que ela não faz é dizer qual será a largura dessa string depois de desenhada, e ela também não quebra uma string longa em várias linhas. Uma única chamada pinta uma sequência de texto em uma posição. Se a sequência for mais larga que a coluna na qual devia caber, ela simplesmente ultrapassa a borda, e nada na chamada de desenho avisa. No momento em que você quer um parágrafo em vez de um único rótulo, a peça que falta é a largura de uma string na fonte e no tamanho escolhidos, medida antes de confirmá-la na página

Este é o problema clássico de layout. Para quebrar um parágrafo em uma coluna você precisa saber, palavra por palavra, quanto espaço horizontal cada linha candidata ocupará, e precisa saber isso antes de desenhar qualquer coisa. A quebra de linha é um loop de medição envolto em uma chamada de desenho, e um binding que só desenha fornece apenas a segunda metade. O suporte à medição de texto no PDFium component fecha essa lacuna com duas funções, MeasureText e MeasureTextWidth, que relatam a extensão renderizada de uma string sem deixar nenhuma marca em qualquer página

Por que a medição é um class helper, não um novo método em TPdf

O suporte à medição chega como um class helper do Delphi para TPdf, vivendo em sua própria unit, em vez de novos métodos adicionados à classe TPdf. Um class helper é um recurso da linguagem que permite anexar métodos a um tipo existente de fora de sua declaração. Uma vez que a unit esteja no escopo, os novos métodos são chamados exatamente como se pertencessem à classe, então um método helper é lido como Pdf.MeasureTextWidth(...) sem nenhum objeto separado para construir ou passar

A razão para organizá-lo dessa forma é a separação. O tipo TPdf central permanece como está, sem campo adicionado e sem assinatura existente tocada, então um projeto que nunca precisa de layout nunca carrega o código de medição. Um projeto que precisa dele adiciona uma unit a uma cláusula uses e os métodos aparecem. A capacidade torna-se opt-in na granularidade de uma única unit, que é a maneira mais limpa de estender um tipo que você não possui ou não quer perturbar

__CODE_BLOCK_
uses
  PDFium, FPdfView, FPdfEdit,
  FPdfMeasure;   // a unit auxiliar; traz MeasureText ao escopo de TPdf

// Com a unit no escopo, os métodos se leem como membros de TPdf:
var
  W, H: Double;
begin
  Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
  // W e H agora são a largura e altura renderizadas em unidades de usuário PDF
end;

Medindo sem tocar na página

A medição precisa ser livre de efeitos colaterais. Ela deve relatar uma largura sem deixar nada para trás, porque você a chama muitas vezes enquanto decide um layout e a página deve parecer exatamente como seria se você nunca tivesse medido nada. A técnica que torna isso possível é construir um objeto de texto, pedir seu tamanho e descartá-lo antes de ser anexado a uma página

A sequência é quatro chamadas do PDFium. FPDFPageObj_NewTextObj cria um objeto de texto contra o documento, dado o nome da fonte e o tamanho. FPDFText_SetText define a string que esse objeto carrega. FPDFPageObj_GetBounds lê de volta a caixa delimitadora do objeto. FPDFPageObj_Destroy libera o objeto. Crucialmente, nada nessa sequência chama a API de inserção de página. O objeto é criado, consultado e destruído em isolamento, então o documento permanece inalterado quando a função retorna. É uma sonda descartável cuja única saída são os quatro números de sua caixa delimitadora

Esta é a maneira robusta de fazer isso porque o PDFium não expõe uma largura de avanço conveniente por glifo que você poderia somar. As métricas de glifos dependem do programa de fonte, da codificação e de como o PDFium carrega a face, e não há chamada pública que lhe forneça o avanço de cada caráter em uma string. A caixa delimitadora de um objeto de texto real, por outro lado, é calculada pelo mesmo mecanismo que disporia os glifos para desenho, então reflete a extensão renderizada real em vez de uma aproximação. Construir um objeto descartável e ler seus limites é a medição mais confiável que a biblioteca pode fornecer

Diagrama das quatro chamadas PDFium por trás do MeasureText em Delphi, sondando um objeto de texto descartável sem tocar na página
MeasureText constrói um objeto de texto descartável, lê sua caixa delimitadora e o destrói, então a medição deixa o documento PDF intocado
__CODE_BLOCK_
// A forma de MeasureText, expressa em termos das chamadas PDFium verificadas.
// Um objeto de texto é construído, medido e destruído; nenhuma página é envolvida.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
  FontSize: Single; out Width, Height: Double);
var
  TextObject: FPDF_PAGEOBJECT;
  L, B, R, T: Single;
begin
  Width  := 0;
  Height := 0;
  if Self.Document = nil then
    Exit;
  TextObject := FPDFPageObj_NewTextObj(Self.Document,
    FPDF_BYTESTRING(AnsiString(Font)), FontSize);
  if TextObject = nil then
    Exit;
  try
    if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
      Exit;
    if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
    begin
      Width  := R - L;
      Height := T - B;
    end;
  finally
    FPDFPageObj_Destroy(TextObject);   // sonda descartada, página intocada
  end;
end;

Coordenadas e unidades do resultado

A caixa delimitadora retorna como quatro bordas, esquerda, inferior, direita e superior, e as duas dimensões resultam por subtração. A largura é direita menos esquerda e a altura é superior menos inferior. Ambas são expressas em unidades de usuário PDF, onde uma unidade é um setenta e dois avos de polegada, o mesmo espaço de coordenadas no qual você posiciona texto na página. Não há unidade de dispositivo oculta e nenhum pixel envolvido neste estágio. Uma largura de 36 significa meia polegada de página, qualquer que seja a resolução de renderização final

O eixo vertical funciona da forma que o PDF define, com Y aumentando para cima, razão pela qual a altura é superior menos inferior e não o inverso. Esse detalhe importa quando você avança um cursor para baixo em uma coluna. Você mede a altura de uma linha, depois a subtrai da linha de base atual para encontrar a próxima, porque mover para baixo na página significa mover em direção a valores menores de Y. Se seu destino é uma tela em vez de papel, você converte unidades de usuário para pixels de dispositivo com a resolução de exibição: um valor em unidades de usuário multiplicado pelo DPI e dividido por 72 fornece pixels, então uma largura de coluna definida em pontos pode ser comparada com uma sequência medida antes de você decidir onde ocorre a quebra

O que acontece com entrada degenerada

As funções são escritas para falhar silenciosamente. Se não houver documento aberto, ou se o objeto de texto não puder ser criado, o resultado é uma extensão zero em vez de uma exceção levantada. A largura e a altura são inicializadas em zero no topo e só são substituídas depois que uma caixa delimitadora foi lida com sucesso. Uma string vazia, um documento ausente, uma fonte que a biblioteca não consegue resolver em um objeto - cada um desses retorna zero em vez de lançar

Essa escolha mantém um loop de medição simples, porque um loop que roda sobre milhares de palavras não é o lugar para tratamento de exceções em cada iteração. O custo é que o chamador carrega a verificação. Uma largura zero é um sentinela, não um fato sobre o texto, então código que divide por uma largura medida ou assume um valor positivo precisa proteger contra zero antes de confiar nele. Trate zero como "não foi possível medir" e o contrato fica claro; ignore-o e uma entrada degenerada silenciosamente se torna um layout com uma coluna de glifos sobrepostos

Uma quebra de linha gulosa construída sobre a medição

Com uma função de largura em mãos, a quebra de linha é um loop guloso curto. Você divide o parágrafo em palavras, mantém uma linha atual e, para cada palavra, você mede como seria a linha se você acrescentasse essa palavra. Enquanto a linha candidata ainda couber na largura da coluna, você continua adicionando; quando ela transbordaria, você libera a linha atual com AddText e começa uma nova com a palavra que não coube. O acúmulo é feito inteiramente com MeasureTextWidth, e a única coisa que alguma vez chega à página é uma linha que você já confirmou que cabe

Diagrama de um loop greedy de quebra de palavras em Delphi que mede linhas de teste com MeasureTextWidth e quebra na última palavra que cabe
O loop de quebra guloso mede cada linha de tentativa contra a largura da coluna e só libera linhas confirmadas como cabíveis
__CODE_BLOCK_

O loop mede a linha candidata em vez de medir cada palavra e somar, porque a largura de uma linha não é a soma das larguras de suas palavras. Os espaços entre as palavras contribuem, e uma sequência medida captura isso diretamente. A regra gulosa - encaixar o máximo de palavras que a coluna permite e quebrar na última que cabe - é a mesma regra que preenche a lacuna entre um AddText bruto e um parágrafo real. A chamada de desenho nunca foi a parte difícil. A medição que precisa precedê-la é, e é exatamente isso que o helper fornece

procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
  FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
  Words: TArray<string>;
  Line, Trial: WideString;
  I: Integer;
  Y: Double;
begin
  Words := string(Para).Split([' ']);
  Line  := '';
  Y     := TopY;
  for I := 0 to High(Words) do
  begin
    if Line = '' then
      Trial := Words[I]
    else
      Trial := Line + ' ' + Words[I];
    // Meça a linha candidata antes de desenhar qualquer coisa.
    if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
    begin
      Pdf.AddText(Line, Font, FontSize, X, Y);   // libera a linha que coube
      Y    := Y - LineHeight;                    // Y diminui indo para baixo
      Line := Words[I];                          // a palavra que transbordou começa a próxima linha
    end
    else
      Line := Trial;
  end;
  if Line <> '' then
    Pdf.AddText(Line, Font, FontSize, X, Y);      // libera a linha final
end;

Onde isso se encaixa

A medição é a camada entre gerar conteúdo e renderizá-lo, então ela se combina naturalmente com o restante de um fluxo de trabalho de documento do zero. Se você está montando páginas e posicionando texto em primeiro lugar, o fundamento está em criando documentos PDF do zero com o PDFium component no Delphi, onde AddText e a configuração de página são abordados por completo. Quando a fonte que você está medindo importa tanto quanto a string, porque as métricas dependem da face, analisando propriedades de fontes PDF com o PDFium component no Delphi mostra como a biblioteca relata as informações de fonte que impulsionam essas caixas delimitadoras. Ambos se baseiam no mesmo binding, o PDFium Component para Delphi e Lazarus, onde o helper de medição é fornecido junto com as APIs de documento, página e texto descritas ao longo deste blog