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