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 vai ocupar, e precisa saber isso antes de desenhar qualquer coisa. Quebra de linha é um loop de medição enrolado em torno de uma chamada de desenho, e um binding que só desenha entrega a segunda metade. O suporte de medição de texto do PDFium Component fornece a primeira metade com duas funções, MeasureText e MeasureTextWidth, que reportam a extensão renderizada de uma string sem deixar marca em nenhuma 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

uses
  PDFium, FPdfView, FPdfEdit,
  FPdfMeasure;   // a unit do helper; coloca MeasureText no escopo de TPdf

// Com a unit no escopo, os métodos parecem membros de TPdf:
var
  W, H: Double;
begin
  Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
  // W e H agora têm a largura e a 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 caractere 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 no Delphi, sondando um objeto de texto descartável sem tocar na página
O MeasureText constrói um objeto de texto descartável, lê a bounding box e o destrói, então a medição deixa o documento PDF intacto
// A forma do MeasureText, escrita sobre as chamadas verificadas do PDFium.
// Um objeto de texto é criado, medido e destruído; nenhuma página entra na história.
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 intacta
  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 em silêncio. Se não há documento aberto, ou se o objeto de texto não pode ser criado, o resultado é uma extensão zero em vez de uma exceção. Largura e altura começam zeradas no topo e só são sobrescritas depois que uma bounding box foi lida com sucesso. String vazia, documento ausente, fonte que a biblioteca não consegue resolver em um objeto — cada um desses casos retorna zero em vez de lançar exceção

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 guloso de quebra de linha no Delphi que mede linhas candidatas com MeasureTextWidth e quebra na última palavra que cabe
O loop guloso de quebra mede cada linha candidata contra a largura da coluna e só descarrega as linhas que com certeza cabem
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);   // descarrega a linha que coube
      Y    := Y - LineHeight;                    // o Y diminui ao descer
      Line := Words[I];                          // a palavra que transborda começa a próxima linha
    end
    else
      Line := Trial;
  end;
  if Line <> '' then
    Pdf.AddText(Line, Font, FontSize, X, Y);      // descarrega a linha final
end;

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

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