O HotPDF Delphi Component reconstrói espaços entre palavras e quebras de linha no THotPDF.ExtractLoadedPageText a partir da geometria dos glifos, e não de caracteres de espaço. Um espaço entra quando o vão depois da largura própria de um glifo excede 0.15 da altura do texto, e uma linha nova começa só quando a origem do texto se move através da direção de escrita por mais de metade da altura do texto. Desde a v2.768.3 que o texto da página também inclui texto pintado através de Form XObjects e deixa de fora glifos fora da área de recorte visível. O resto deste artigo explica porque cada regra tem o aspeto que tem, porque todas elas substituíram uma regra mais simples que produzia output plausível mas errado em documentos reais
Os sintomas são familiares a quem já meteu texto PDF num índice de pesquisa. Uma capa extrai como PDFReferenceManualNovember4,1998, um formulário fiscal parte-se em 156 linhas, uma marca de água diagonal chega com um carácter por linha, e uma prova recortada abre com a linha de slug da impressora que visualizador nenhum mostra. Nenhum destes ficheiros está partido. Cada um usa uma forma perfeitamente legal de posicionar texto que um extrator ingénuo lê mal
Porque é que o texto PDF extraído perde os espaços entre palavras?
O texto extraído perde os espaços entre palavras porque um PDF nunca é obrigado a contê-los. Um produtor pode separar palavras mostrando um carácter de espaço, mas pode igualmente mover a caneta com um número dentro de uma matriz TJ (ISO 32000-1 §9.4.3) ou com um Td fresco (§9.4.2), e o output de TeX, muitos ficheiros Distiller e a maioria dos layouts justificados fazem exatamente isso. Antes da v2.766.76, o HPDFAssemblePageText só olhava ao movimento vertical, por isso uma quebra de palavra feita por posicionamento simplesmente desaparecia. O assembler agora mede, ao longo da direção de escrita do glifo anterior, a distância do fim da largura própria desse glifo à origem do glifo corrente, e insere um espaço quando a distância excede 0.15 da altura da caixa do glifo corrente, medida de ascendente a descendente no user space. Não se acrescenta espaço quando qualquer dos lados já é branco, nem entre dois caracteres CJK, porque a justificação estica os ideogramas sem esse esticar significar uma fronteira de palavra. Os registos de glifo expõem a mesma geometria, por isso consegue reproduzir a decisão quando um ficheiro concreto o deixa perplexo
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// altura de ascendente a descendente da caixa do glifo, no user space
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// texto horizontal: vão desde o fim da largura própria do glifo anterior
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
Porque medir pela largura própria do glifo em vez da posição da caneta?
O HotPDF mede os vãos entre palavras a partir de GlyphEndX / GlyphEndY porque a posição da caneta depois de um glifo já contém espaçamento que não é um vão. A ISO 32000-1 §9.4.4 define o deslocamento horizontal como a largura do glifo vezes o tamanho da fonte, mais o espaçamento de caracteres Tc, mais o espaçamento de palavras Tw, tudo escalado por Tz. O BaselineEndX / BaselineEndY guarda esse deslocamento completo, enquanto o GlyphEndX / GlyphEndY guarda só o avanço da fonte e o Tz. A diferença interessa para produtores que apertam o tracking com um Tc negativo e depois devolvem o espaço através de um ajuste TJ depois de cada glifo: medido pela posição da caneta, a devolução parece um vão, e o termo chinês “95后” extraía-se como “9 5 后”. O limiar está ligado à altura da caixa do glifo em vez do tamanho Tf por razão semelhante. Exportações do Word escrevem frequentemente 1 Tf e carregam o tamanho real num Tm escalado, por isso o Tfs diz 1 enquanto o texto tem 10 pontos de altura, e uma regra chaveada no Tfs trataria as duas grafias da mesma página de forma diferente
A regra tem arestas honestas. Um título composto com tracking muito frouxo, onde o Tc sozinho abre mais de 0.15 da altura do texto entre letras, extrai com um espaço entre cada letra, que é o que a página parece mas provavelmente não o que queria indexar. Pedaços desenhados fora de ordem na mesma linha de base produzem um vão negativo e juntam-se sem espaço. Nenhum dos casos é comum em texto corrido, e num corpus de testes a alteração subiu a correspondência de palavras contra um extrator de referência em 28 páginas sem baixar nenhuma
Quando é que o HotPDF começa uma linha nova no texto extraído?
Desde a v2.766.79, uma linha nova começa quando o movimento da origem do glifo anterior para a corrente, projetado na normal da direção de escrita anterior, excede metade da maior altura de caixa dos dois glifos. A regra anterior comparava o movimento Y cru com metade do Tfs, o que falhava em duas direções. Com 1 Tf e um Tm escalado o limiar encolhia para meia unidade, por isso um expoente elevado por uma subida de texto de 0.4 ou a trepidação banal da linha de base partia a linha. A regra também ignorava o X por completo, por isso texto sob um Tm rodado descia a página a cada glifo e saía com um glifo por linha. Projetar na normal da direção faz corridas rodadas comportarem-se como horizontais, e pegar na maior das duas alturas mantém uma palavra de amostra grande e a sua legenda pequena na mesma linha quando partilham a linha de base. No formulário fiscal referido acima, a contagem de linhas caiu de 156 para 97. Texto vertical em modo de escrita 1 (§9.7.4.3) segue um caminho separado: esses glifos são agrupados em colunas, lidos da direita para a esquerda e de cima para baixo, com uma quebra de linha a cada mudança de coluna
Que texto o ExtractLoadedPageText inclui ou deixa de fora?
O ExtractLoadedPageText devolve o texto que um visualizador mostra. Desde a v2.766.80 que trabalha só com os glifos visíveis, deixando cair todo o glifo cujo centro de caixa caia fora do GetLoadedPageVisibleBox, que é a CropBox recortada ao MediaBox (§14.11.2). Isso remove linhas de slug e outras marcas de impressora postas como texto fora da área de corte. O ExtractLoadedPageGlyphs mantém deliberadamente a devolver todos os glifos do content stream da página, por isso ainda consegue encontrar esse material quando precisar. O filtro é um teste de caixa, não um teste de visibilidade: texto escondido por um clipping path, pintado a branco ou coberto por uma imagem continua a ser extraído
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// todos os glifos do content stream da página, linha de slug incluída
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// só o que a página mostra, com texto de Form XObject interpolado
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
Texto pintado através de Form XObjects faz parte do texto da página desde a v2.768.3. Cabeçalhos, carimbos e marcas de água vivem muitas vezes em formulários, e alguns documentos de normas perdiam 30 a 35 por cento dos seus caracteres antes da alteração. O THotPDF.InterpretContentWithForms regista cada Do junto com a CTM em vigor, interpreta o formulário pela sua /Matrix vezes essa CTM (§8.10.1), e interpola os glifos do formulário na posição do Do, recorrendo a formulários aninhados. Um formulário sem /Resources próprio toma emprestados os do stream que o pinta, como a §7.8.3 permite. Os glifos de formulário carregam TokenIndex = -1, e o ExtractLoadedPageGlyphs continua a devolver só glifos do stream da página, porque pesquisar, substituir e redigir escrevem alterações de volta através do TokenIndex e editariam os bytes errados se um glifo de formulário se esgueirasse. Duas simplificações valem a pena conhecer: o texto de formulário não é recortado ao /BBox do formulário, e a recursão para aos 12 níveis em vez de por deteção de ciclos, por isso um formulário mal formado que se pinta a si mesmo repete o seu texto até chegar a esse teto
Porque é que o texto depois de um operador Q descodificava como lixo?
Texto depois de Q podia descodificar mal antes da v2.766.73 porque o extrator só guardava a CTM no q. Os parâmetros de estado de texto, nomeadamente fonte, tamanho, Tc, Tw, Tz, TL, modo de renderização e subida, pertencem ao estado gráfico (§9.3.1), por isso o Q tem de os repor junto com tudo o resto na pilha (§8.4.2). Um relatório da indústria selecionava uma fonte Identity-H de dois bytes dentro de q … Q e depois mostrava texto WinAnsi de um byte sem Tf próprio. O extrator mantinha a fonte interior, lia os pontinhos e a palavra “Adobe” no índice como códigos de dois bytes, e largava 15% dos caracteres da página. A pilha de q/Q do intérprete agora guarda o estado de texto completo. As regras de extração aqui descritas aplicam-se a todas as páginas, por isso um documento inteiro pode ir para um ficheiro numa só chamada
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// intervalo vazio = todas as páginas; form feed entre páginas; BOM UTF-8
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
Que API de texto do HotPDF deve usar?
O ExtractLoadedPageText fica pela ordem do content stream, que é a predefinição certa para pesquisa e indexação; a cadeia de descodificação por baixo dele está coberta em extrair texto de PDFs carregados com o HotPDF. Para documentos etiquetados cuja ordem de autoria interessa, a extração de texto pela ordem da estrutura percorre a árvore de estrutura em vez de adivinhar pela geometria, e para dados presos em tabelas, a extração de tabelas tipadas através de quebras de página devolve células em vez de linhas. A referência completa da API e uma transferência de avaliação estão na página de produto do HotPDF Delphi PDF Component