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

uses
  PDFium, FPdfView, FPdfEdit,
  FPdfMeasure;   // the helper unit; brings MeasureText into scope on TPdf

// With the unit in scope the methods read as members of TPdf:
var
  W, H: Double;
begin
  Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
  // W and H are now the rendered width and height in PDF user units
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

// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
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);   // probe discarded, page untouched
  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

procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
  FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
  Words: TArray<WideString>;
  Line, Trial: WideString;
  I: Integer;
  Y: Double;
begin
  Words := WideString(Para).Split([' ']);
  Line  := '';
  Y     := TopY;
  for I := 0 to High(Words) do
  begin
    if Line = '' then
      Trial := Words[I]
    else
      Trial := Line + ' ' + Words[I];
    // Measure the candidate line before drawing anything.
    if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
    begin
      Pdf.AddText(X, Y, Font, FontSize, Line);   // flush the line that fit
      Y    := Y - LineHeight;                    // Y decreases going down
      Line := Words[I];                          // overflowing word starts next line
    end
    else
      Line := Trial;
  end;
  if Line <> '' then
    Pdf.AddText(X, Y, Font, FontSize, Line);      // flush the final line
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