Artigo Técnico

Medir Texto PDF para Composição e Quebra de Linha em Delphi

Compor texto num PDF requer saber quanto espaço horizontal uma cadeia de caracteres vai ocupar antes de a adicionar à página. Isso é necessário para quebra de linha, centralização, alinhamento à direita e cálculos de enquadramento de texto em geral. O PDFium não expõe uma função de medição de texto direta que opere sem uma página ativa, mas fornece os blocos de construção para a construir: criar um objeto de texto temporário, definir o seu conteúdo, consultar os seus limites e destruí-lo sem nunca o adicionar ao conteúdo da página. O PDFiumPas embrulha esse padrão no método auxiliar de classe MeasureText na unidade FPdfMeasure

A sonda de objeto de texto descartável

A medição de texto do PDFium usa quatro chamadas da API. FPDFPageObj_NewTextObj cria um objeto de texto em memória associado ao documento mas não adicionado a nenhuma página. FPDFText_SetText define o conteúdo da cadeia de caracteres. FPDFPageObj_GetBounds devolve a caixa delimitadora do objeto nas coordenadas da página, que inclui a largura do texto renderizado no tamanho de fonte dado. FPDFPageObj_Destroy liberta o objeto sem o ter adicionado à página

O resultado é uma medição que usa as mesmas tabelas de métricas de fonte que o PDFium usaria ao renderizar — incluindo kerning, hinting e qualquer substituição de fonte que possa ocorrer — sem deixar nenhum vestígio na página

// Conceptual sequence — wrapped by MeasureText in FPdfMeasure
var
  TextObj: FPDF_PAGEOBJECT;
  Left, Bottom, Right, Top: Single;
  Width: Single;
begin
  TextObj := FPDFPageObj_NewTextObj(Document, 'Helvetica', FontSize);
  FPDFText_SetText(TextObj, PWideChar(Text));
  FPDFPageObj_GetBounds(TextObj, @Left, @Bottom, @Right, @Top);
  Width := Right - Left;
  FPDFPageObj_Destroy(TextObj);
  // Width is now the rendered width of Text in the given font and size
end;

A API MeasureText

Na prática nunca se escreve essa sequência diretamente. A unidade FPdfMeasure expõe MeasureText e MeasureTextWidth como métodos auxiliares de classe em TPdf. Ambos aceitam um documento, um nome de fonte, um tamanho de fonte e uma cadeia de caracteres, e devolvem a largura em unidades de ponto PDF. MeasureText devolve a caixa delimitadora completa para chamadores que precisam da altura ou das métricas de linha de base; MeasureTextWidth devolve apenas a largura para casos de uso de quebra de linha onde apenas a largura horizontal é necessária

uses
  FPdfMeasure;

var
  Width: Single;
begin
  Width := TPdf.MeasureTextWidth(Document, 'Helvetica', 12.0, 'Hello, world');
  // Width is in PDF points (1/72 inch)
end;

Quando a cadeia de caracteres de entrada está vazia ou o objeto de texto não pode ser criado — por exemplo, porque o nome da fonte não pode ser resolvido — o método devolve zero. O código chamador deve tratar zero como um resultado degenerado em vez de uma largura válida

Implementar quebra de linha greedy

Com uma função de medição de largura disponível, a quebra de linha num parágrafo é um algoritmo greedy direto. Acumule palavras numa linha até que adicionar a próxima palavra exceda a largura da coluna, depois emita a linha atual e comece uma nova. A largura de cada palavra candidata pode ser medida individualmente, ou a linha acumulada pode ser medida como um todo — medir a linha acumulada é tipicamente mais preciso porque captura o kerning entre palavras

function WrapParagraph(
  Doc: FPDF_DOCUMENT;
  const FontName: string;
  FontSize, ColumnWidth: Single;
  const Text: string): TArray<string>;
var
  Words: TArray<string>;
  Line, Candidate: string;
  Word: string;
  Lines: TList<string>;
begin
  Lines := TList<string>.Create;
  try
    Words := Text.Split([' ']);
    Line := '';
    for Word in Words do
    begin
      if Line = '' then
        Candidate := Word
      else
        Candidate := Line + ' ' + Word;

      if TPdf.MeasureTextWidth(Doc, FontName, FontSize, Candidate) <= ColumnWidth then
        Line := Candidate
      else
      begin
        if Line <> '' then
          Lines.Add(Line);
        Line := Word;
      end;
    end;
    if Line <> '' then
      Lines.Add(Line);
    Result := Lines.ToArray;
  finally
    Lines.Free;
  end;
end;

O algoritmo funciona corretamente para entradas degeneradas: uma única palavra mais larga que ColumnWidth é emitida como a sua própria linha em vez de ser descartada. Uma entrada vazia produz um array vazio. Palavras com espaços iniciais ou finais devem ser aparadas antes de passar para a função de medição se a precisão de enquadramento for importante

Medição versus a tabela de métricas de fonte integrada

Uma alternativa à sonda descartável é usar tabelas de métricas de fonte integradas. Para as 14 fontes padrão do PDF (Helvetica, Times, Courier e as suas variantes), as métricas de glifo são publicadas pela Adobe e podem ser codificadas diretamente. Para fontes embebidas ou substituídas, as métricas variam de acordo com o ficheiro de fonte carregado, de modo que as tabelas integradas dão resultados errados a menos que a fonte exata seja conhecida com antecedência

A abordagem de sonda descartável usa as mesmas métricas que o PDFium usa durante a renderização, o que a torna precisa independentemente da fonte. O custo são quatro chamadas de API por medição, que é insignificante para uso interativo e quebra de linha de parágrafo, mas pode adicionar-se se medições de largura individuais forem chamadas para cada caractere num texto longo. Para esse caso, uma cache de largura por fonte-e-tamanho ou uma medição de linha acumulada em vez de por-palavra reduz o número de chamadas

Integrar com composição de página

A medição de texto é o passo de pré-processamento para qualquer operação de composição de texto no PDFiumPas. Um fluxo de trabalho típico: mede a largura de cada palavra com MeasureTextWidth, usa o algoritmo de quebra greedy para dividir o parágrafo em linhas, e depois adiciona cada linha como um objeto de texto à página nas coordenadas calculadas. A função WrapParagraph acima produz as linhas; o chamador avança o cursor y por FontSize * LineSpacing para cada linha ao escrever os objetos de texto

A renderização dessas páginas compostas em bitmaps para visualização é abordada em renderização de PDF em segundo plano com futuros canceláveis e renderização progressiva cancelável. Todas as funcionalidades de composição e renderização estão disponíveis no Componente PDFiumPas para Delphi e Lazarus