A chamada que coloca texto numa página PDF é direta: fornece-se a AddText uma cadeia de carateres, um tipo de letra, um tamanho e uma posição, e os glifos aparecem. O que essa chamada não faz é indicar qual será a largura dessa cadeia depois de desenhada, nem quebra uma cadeia longa por várias linhas. Uma única chamada pinta um trecho de texto numa posição. Se o trecho for mais largo do que a coluna onde deveria caber, ultrapassa simplesmente o limite, e nada na chamada de desenho avisa disso. No momento em que se pretende um parágrafo em vez de uma única etiqueta, a peça em falta é a largura de uma cadeia no tipo de letra e tamanho escolhidos, medida antes de a colocar na página
Este é o problema clássico de composição de texto. Para quebrar um parágrafo dentro de uma coluna é necessário saber, palavra a palavra, quanto espaço horizontal cada linha candidata vai ocupar, e é preciso sabê-lo antes de desenhar seja o que for. A quebra de linha (word wrap) é um ciclo de medição envolvido à volta de uma chamada de desenho, e uma ligação (binding) que apenas desenha fornece só metade da solução. O suporte de medição de texto no PDFium Component fecha essa lacuna com duas funções, MeasureText e MeasureTextWidth, que reportam a extensão visual de uma cadeia sem deixar qualquer marca em nenhuma página
Por que a medição é um class helper, e não um novo método em TPdf
O suporte de medição chega sob a forma de um class helper de Delphi para TPdf, alojado na sua própria unit, em vez de novos métodos acrescentados diretamente à classe TPdf. Um class helper é uma funcionalidade da linguagem que permite associar métodos a um tipo já existente a partir de fora da sua declaração. Assim que a unit está no âmbito (scope), os novos métodos são chamados exatamente como se pertencessem à classe, pelo que um método do helper se lê como Pdf.MeasureTextWidth(...), sem qualquer objeto separado para construir ou transportar
A razão para o organizar desta forma é a separação. O tipo central TPdf permanece como está, sem qualquer campo acrescentado e sem qualquer assinatura existente tocada, pelo que um projeto que nunca precise de composição de texto nunca transporta o código de medição. Um projeto que precise dela acrescenta uma unit a uma cláusula uses e os métodos ficam disponíveis. A funcionalidade passa a ser opcional ao nível de uma única unit, que é a forma mais limpa de estender um tipo que não se possui ou que não se quer perturbar
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // a unit do helper; traz MeasureText para o âmbito de TPdf
// Com a unit no âmbito, os métodos leem-se como membros de TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W e H são agora a largura e a altura visuais em unidades de utilizador do PDF
end;
Medir sem tocar na página
A medição tem de estar livre de efeitos secundários. Tem de reportar uma largura sem deixar nada para trás, porque é chamada muitas vezes enquanto se decide uma composição, e a página tem de ficar exatamente como ficaria se nunca tivesse havido qualquer medição. A técnica que torna isto possível é construir um objeto de texto, perguntar-lhe o seu tamanho e descartá-lo antes de alguma vez ser associado a uma página
A sequência é composta por quatro chamadas PDFium. FPDFPageObj_NewTextObj cria um objeto de texto associado ao documento, dado o nome e o tamanho do tipo de letra. FPDFText_SetText define a cadeia que esse objeto transporta. FPDFPageObj_GetBounds lê de volta a caixa delimitadora do objeto. FPDFPageObj_Destroy liberta o objeto. Fundamentalmente, nada nesta sequência chama a API de inserção na página. O objeto é criado, consultado e destruído de forma isolada, pelo que o documento fica inalterado quando a função termina. É uma sonda descartável cuja única saída são os quatro números da sua caixa delimitadora
Esta é a forma robusta de o fazer porque o PDFium não expõe uma largura de avanço por glifo conveniente que se pudesse somar diretamente. As métricas dos glifos dependem do programa do tipo de letra, da codificação e da forma como o PDFium carrega a face tipográfica, e não existe nenhuma chamada pública que forneça o avanço de cada caráter numa cadeia. A caixa delimitadora de um objeto de texto real, por outro lado, é calculada pelo mesmo mecanismo que dispõe os glifos para o desenho, pelo que reflete a extensão visual real e não uma aproximação. Construir um objeto descartável e ler os seus limites é a medição mais fiável que a biblioteca consegue dar
// A forma de MeasureText, expressa em função das chamadas PDFium verificadas.
// Um objeto de texto é construído, medido e destruído; nenhuma página está 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 é devolvida como quatro limites, esquerdo, inferior, direito e superior, e as duas dimensões resultam de uma subtração. A largura é o direito menos o esquerdo, e a altura é o superior menos o inferior. Ambas são expressas em unidades de utilizador do PDF, em que uma unidade equivale a um septuagésimo segundo de uma polegada, o mesmo espaço de coordenadas em que se posiciona o texto na página. Não há qualquer unidade de dispositivo escondida nem qualquer pixel envolvido nesta fase. Uma largura de 36 significa meia polegada de página, seja qual for a resolução de renderização final
O eixo vertical segue a definição do PDF, com o Y a aumentar para cima, razão pela qual a altura é o superior menos o inferior e não o contrário. Esse pormenor importa quando se avança um cursor ao longo de uma coluna, linha após linha. Mede-se a altura de uma linha e depois subtrai-se essa altura à linha de base atual para encontrar a seguinte, porque descer a página significa mover-se para valores de Y mais pequenos. Se o destino for um ecrã em vez de papel, convertem-se as unidades de utilizador em pixels de dispositivo com a resolução do ecrã: um valor em unidades de utilizador multiplicado pelos DPI e dividido por 72 dá pixels, pelo que uma largura de coluna definida em pontos pode ser comparada com um trecho medido antes de se decidir onde colocar a quebra
O que acontece com entradas degeneradas
As funções estão escritas para falhar em silêncio. Se não houver nenhum documento aberto, ou se o objeto de texto não puder ser criado, o resultado é uma extensão nula em vez de uma exceção. A largura e a altura são inicializadas a zero no topo e só são substituídas depois de uma caixa delimitadora ter sido lida com sucesso. Uma cadeia vazia, um documento em falta, um tipo de letra que a biblioteca não consegue resolver num objeto: cada um destes casos devolve zero em vez de gerar uma exceção
Essa escolha mantém simples um ciclo de medição, porque um ciclo que percorre milhares de palavras não é o lugar para tratamento de exceções em cada iteração. O custo é que a verificação fica a cargo de quem chama a função. Uma largura zero é uma sentinela, não um facto sobre o texto, pelo que código que divide por uma largura medida ou assume um valor positivo tem de se proteger contra o zero antes de confiar nele. Tratando o zero como "não foi possível medir", o contrato fica claro; ignorando-o, uma entrada degenerada transforma-se silenciosamente numa composição com uma coluna de glifos sobrepostos
Uma quebra de linha greedy construída sobre a medição
Com uma função de largura disponível, a quebra de linha resume-se a um pequeno ciclo greedy. Divide-se o parágrafo em palavras, mantém-se uma linha atual e, para cada palavra, mede-se como ficaria a linha se essa palavra fosse acrescentada. Enquanto a linha de teste ainda couber na largura da coluna, continua-se a acrescentar; quando ultrapassaria o limite, esvazia-se a linha atual com AddText e começa-se uma nova com a palavra que não coube. A acumulação é feita inteiramente com MeasureTextWidth, e a única coisa que chega alguma vez à página é uma linha já confirmada como cabendo
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];
// Mede a linha candidata antes de desenhar seja o que for.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(Line, Font, FontSize, X, Y); // esvazia a linha que coube
Y := Y - LineHeight; // Y diminui à medida que desce
Line := Words[I]; // a palavra que ultrapassou inicia a linha seguinte
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(Line, Font, FontSize, X, Y); // esvazia a linha final
end;
O ciclo mede a linha de teste em vez de medir cada palavra e somar, porque a largura de uma linha não é a soma das larguras das suas palavras. Os espaços entre palavras contribuem para essa largura, e um trecho medido capta isso diretamente. A regra greedy, encaixar tantas palavras quantas a coluna permitir e quebrar na última que ainda coube, é a mesma regra que preenche a lacuna entre um AddText em bruto e um parágrafo real. A chamada de desenho nunca foi a parte difícil. A medição que tem de a preceder é que o é, e é exatamente isso que o helper fornece
Onde isto se encaixa
A medição é a camada entre gerar conteúdo e renderizá-lo, pelo que se combina naturalmente com o resto de um fluxo de trabalho de documentos criados de raiz. Para quem está a montar páginas e a colocar texto em primeiro lugar, a base está descrita em criar documentos PDF de raiz com o PDFium Component em Delphi, onde AddText e a configuração de páginas são abordados na íntegra. Quando o tipo de letra que se está a medir importa tanto quanto a cadeia, porque as métricas dependem da face tipográfica, analisar propriedades de tipos de letra em PDF com o PDFium Component em Delphi mostra como a biblioteca reporta a informação de tipo de letra que determina essas caixas delimitadoras. Ambos assentam na mesma ligação (binding), o PDFium Component para Delphi e Lazarus, onde o helper de medição é fornecido juntamente com as APIs de documento, página e texto descritas ao longo deste blogue