O HotPDF Delphi Component, desde a v2.768.15, remove os caracteres duplicados que o fake bold e os logótipos sobrepostos deixam no texto PDF extraído. Quando um produtor pinta o mesmo glifo duas vezes a uma fração de ponto de distância, o ExtractLoadedPageText, o ExtractLoadedPageTextLayout e o ExtractLoadedPageTextColumns agora leem esse carácter uma vez, por isso "Bold words" já não sai como "Bold wordsBold words"
Este é um daqueles bugs que parece uma falha de descodificador e não é. Todos os glifos descodificam corretamente; o texto está realmente na página duas vezes. A correção tem de decidir qual de dois glifos perfeitamente válidos é a cópia, e fazê-lo sem comer letras repetidas legítimas como o l dobrado de "fill" ou as células idênticas de uma tabela. O resto deste artigo explica a regra que o HotPDF usa, os limiares por trás dela, onde discorda deliberadamente de outras ferramentas de extração, e um segundo bug de extração, sem relação, do mesmo comboio de lançamentos que também cortava texto nos fins de linha
Porque é que o fake bold produz texto duplicado na extração de PDF?
O fake bold duplica o texto extraído porque o produtor desenha a string duas vezes e as duas cópias são operações reais de mostragem de texto. Quando uma família de fontes não tem face bold, ou a ferramenta de autoria simplesmente não se dá ao trabalho, muitos geradores emulam o bold mostrando a mesma string com Tj ou TJ, empurrando a posição do texto uns décimos de ponto para a direita, e mostrando-a outra vez. No ecran as duas cópias fundem-se num traço mais grosso. No content stream há duas corridas completas de códigos de caracteres, e a ISO 32000-1 §9.4 não dá a um extrator flag nenhuma que diga "esta é decoração"
Logótipos sobrepostos atingem o mesmo problema por outro ângulo. Um logótipo pode ser pintado uma vez como fill e outra como stroke ou uma sombra ligeiramente deslocada, usando a mesma fonte e os mesmos caracteres. Um extrator ingénuo que percorre o content stream pela ordem de pintura emite toda a primeira corrida, depois toda a segunda, razão pela qual o resultado lê-se "Bold wordsBold words" e não "BBoolldd". Antes da v2.768.15, a montagem de texto do HotPDF guardava as duas cópias, e o duplicado partia aquilo de que a maioria dos chamadores realmente gosta: uma procura por "Bold words" no texto da página podia falhar porque o limite de palavra entre as duas corridas tinha desaparecido
O verdadeiro modo de renderização de texto 2 (fill e depois stroke, ISO 32000-1 §9.3.6) não tem este problema, porque é uma única operação de mostragem de texto que produz um único conjunto de glifos. Os duplicados só aparecem quando o produtor emite o texto duas vezes
Como decide o HotPDF qual o glifo duplicado?
O HotPDF trata dois glifos como um único carácter pintado quando trazem o mesmo valor Unicode, têm a mesma direção de escrita, estão dentro de 5% um do outro em tamanho, e assentam quase um por cima do outro: não mais de 0.1 do tamanho do glifo de distância ao longo da baseline e não mais de 0.2 do tamanho do glifo através dela. De cada par assim, o glifo pintado primeiro é guardado e o posterior é descartado. Esses são os valores na implementação da v2.768.15, e cada um lá está por uma razão
- Mesmo Unicode, mesma direção. Só caracteres idênticos podem ser duplicados, e um glifo horizontal nunca é comparado com um vertical (WMode 1)
- Tamanho dentro de 5%. Uma letra grande de título e uma letra pequena de legenda que por acaso se sobreponham não são o mesmo texto, por isso uma diferença de tamanho elimina o par
- 0.1 do tamanho ao longo da baseline. Deslocamentos de fake bold são uns décimos de ponto, uns poucos por cento de um tamanho de texto normal. Duas letras adjacentes reais, mesmo um "ll" apertado de kerning, estão separadas por aproximadamente uma largura de avanço completa, uma ordem de grandeza acima desta tolerância
- 0.2 do tamanho através da baseline. Sombras e passagens de contorno deslocam-se muitas vezes para baixo além de para a direita, por isso a tolerância vertical é mais frouxa, mas ainda muito abaixo da distância até à próxima linha
- O primeiro pintado ganha. Guardar o glifo anterior preserva a ordem de leitura da primeira corrida, por isso o texto sobrevivente ainda flui da forma como o produtor o escreveu
As distâncias medem-se no referencial da baseline do próprio glifo e não no X e Y brutos da página, por isso texto rodado e texto numa baseline inclinada usam as mesmas tolerâncias que linhas horizontais comuns. O tamanho usado é o tamanho do glifo como pintado na página, derivado do seu quad, e não o operando de tamanho dado ao Tf; isso importa em documentos que põem o Tf em 1 e escalam tudo através da matriz de texto, onde um tamanho de fonte bruto tornaria cada tolerância inútil
Espaços nunca são candidatos. Pontos de código até U+0020, o espaço inseparável U+00A0 e o espaço ideográfico U+3000 são saltados, porque glifos em branco não trazem tinta visível para duplicar e descartar um fundiria palavras
Manter a verificação rápida em páginas densas
A verificação de duplicados no HotPDF corre em tempo aproximadamente n log n, não n ao quadrado, porque primeiro ordena e só compara glifos dentro de uma pequena janela. Comparar cada glifo com todos os outros seria correto e inutilizável: uma página densa de letra miúda carrega facilmente dezenas de milhares de glifos, e a verificação corre em cada chamada de extração de texto
A rotina interna ordena os glifos sem espaço por valor Unicode, depois pela origem X, depois pela ordem de pintura, usando um merge sort estável. Depois de ordenar, cada duplicado potencial de um glifo assenta imediatamente a seguir a ele na ordem, por isso a varredura só avança enquanto o valor Unicode coincidir e a diferença X se mantiver dentro de 0.25 do tamanho do glifo. A janela de 0.25 é deliberadamente mais larga que a tolerância de 0.1 ao longo da baseline, para um par numa baseline ligeiramente inclinada ainda cair dentro dela antes do teste preciso no referencial da baseline correr. Um caso subtil: se a varredura a partir do glifo A encontra um glifo coincidente pintado antes de A, A é a cópia, por isso A é descartado e a varredura para, deixando o glifo anterior continuar a comparação com o que estiver mais à frente. Essa regra é o que faz "o primeiro pintado ganha" valer para triplas batidas além de pares
A comparação é pelo valor Unicode, não pela fonte ou código de carácter. Dois glifos que mapeiem para o mesmo Unicode através de fontes diferentes, por exemplo uma passagem fill num subconjunto incorporado e uma passagem stroke noutro, continuam a ser reconhecidos como um carácter. O reverso é que um glifo cujo código resolve para Unicode 0 não tem nada fiável para comparar, o que é mais uma razão para vigiar a contagem de não resolvidos coberta em extração de texto de PDFs carregados com o HotPDF
Que chamadas de extração do HotPDF removem glifos duplicados?
Todas as chamadas que produzem texto no HotPDF removem duplicados, e a chamada bruta de glifos não. A eliminação de duplicados vive no THotPDF.ExtractVisibleLoadedPageGlyphs, o passo que também descarta glifos fora da caixa visível da página, e a extração de texto simples, de colunas, de layout e de texto semântico leem todos os seus glifos daí. O THotPDF.ExtractLoadedPageGlyphs continua a devolver cada glifo que o content stream pinta, overprints incluídos, porque chamadores que constroem realce, redação ou ferramentas forenses precisam do registo completo do que foi desenhado
Para trabalho de texto comum nada muda no código chamador. O mesmo loop que extraía texto duplicado antes da v2.768.15 agora extrai cada palavra uma vez
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('brochure.pdf');
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
if Pos('Bold words', PageText) > 0 then
Writeln('Found on page ', I + 1);
finally
Pdf.Free;
end;
end;
O output de layout e de colunas beneficia igualmente, já que uma corrida duplicada empurrava o resto de uma linha para o lado na grelha de caracteres. O ExtractLoadedPageTextColumns mantém o seu multiplicador de espaçamento opcional; o filtro de duplicados corre antes da divisão em colunas, por isso não interage com essa definição
var
Grid, Columns: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1-layout.txt', Grid, TEncoding.UTF8);
// Colunas divididas onde um espaçamento excede 3x o avanço mediano do glifo
if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;
Se quer saber se uma página contém de todo texto sobreposto, compare as duas chamadas de glifos. Tenha em atenção que a diferença também conta glifos que caem fora da crop box, por isso trate-a como um limite superior de duplicados e não como um número exato
var
AllGlyphs, Visible: THPDFGlyphArray;
begin
if Pdf.ExtractLoadedPageGlyphs(0, AllGlyphs) and
Pdf.ExtractVisibleLoadedPageGlyphs(0, Visible) then
Writeln(Format('%d painted, %d read as text (%d overprinted or off-page)',
[Length(AllGlyphs), Length(Visible),
Length(AllGlyphs) - Length(Visible)]));
end;
As regras de espaçamento de palavras e de quebra de linha que transformam estes glifos em texto não são alteradas pelo filtro; estão cobertas em detalhe em como o HotPDF coloca espaços de palavra e quebras de linha no texto da página
Onde a regra discorda deliberadamente de outras ferramentas
O HotPDF descarta duplicados exatos que algumas outras ferramentas de extração guardam, e essa diferença é intencional. Quando a alteração da v2.768.15 foi verificada contra um corpus de referência, um rodapé de página pintado duas vezes exatamente na mesma posição extraiu uma vez no HotPDF e duas vezes num extrator de referência. Um glifo pintado por cima de um glifo idêntico é invisível como carácter separado, por isso lê-lo uma vez é o que a página realmente mostra
O limite honesto corre no outro sentido. Fake bold que desloque a segunda passagem mais de 0.1 do tamanho do glifo ao longo da baseline não é apanhado, e tampouco uma passagem de sombra desenhada num tamanho visivelmente diferente. Esses layouts são raros na prática, porque o efeito visual deixa de parecer bold assim que o deslocamento fica tão grande, mas se vir texto duplicado num título decorativo, compare os valores X dos glifos do ExtractLoadedPageGlyphs antes de assumir que o filtro está partido. O filtro também não faz nada para duplicados que não estejam geometricamente colocalizados, como um cabeçalho corrente repetido numa camada oculta noutro ponto da página; isso é uma questão de conteúdo, não um overprint
Porque é que os fins de linha eram cortados em texto de fontes CID?
Um bug separado corrigido no HotPDF v2.768.12 deixava cair os fins de linha em texto composto com fontes CID-keyed (Type 0) cujo array de larguras /W não estava ordenado por CID. A ISO 32000-1 §9.7.4.3 define o formato do array /W mas não exige que as entradas estejam em ordem ascendente de CID, e alguns geradores escrevem-nas pela ordem em que os glifos foram usados pela primeira vez no documento
O descodificador de extração de texto procurava as larguras com uma pesquisa binária, que assume em silêncio entrada ordenada. Num array por ordenar a maioria das procuras falhava e caía na largura predefinida /DW, que são 1000 unidades, um em inteiro. Texto latino numa fonte assim era composto como se cada letra fosse full-width, as linhas corriam muito para além da borda direita da página, e os glifos que caíam fora da caixa visível eram descartados, por isso os fins de linha desapareciam do texto extraído e da pesquisa. Nada rebentava e nada era reportado, o que tornava o problema difícil de detetar
O HotPDF agora ordena as entradas do /W com um merge sort estável logo depois de as ler, por isso onde duas entradas se sobrepõem a escrita primeiro ainda ganha, a par com a forma como o array se lê pela ordem do ficheiro. O renderizador de páginas nunca foi afetado, porque mantém a sua própria cópia dos dados de larguras e os varre linearmente, e o array de larguras verticais /W2 também é varrido linearmente. Essa divisão vale a pena lembrar quando uma página parece perfeita mas extrai mal: renderização e extração numa biblioteca PDF muitas vezes partilham muito menos código do que se esperaria
FAQ
O HotPDF remove letras repetidas como o l dobrado de "fill"?
Não. Duas letras adjacentes reais estão a cerca de uma largura de avanço de distância, enquanto o HotPDF só funde caracteres idênticos que estejam dentro de 0.1 do tamanho do glifo ao longo da baseline e 0.2 através dela, por isso as repetições legítimas são guardadas
Como obtenho todos os glifos, incluindo duplicados sobrepostos?
Chame o THotPDF.ExtractLoadedPageGlyphs, que devolve cada glifo que a página pinta pela ordem de pintura. O THotPDF.ExtractVisibleLoadedPageGlyphs e todas as chamadas de extração de texto devolvem o conjunto visível e sem duplicados
Que versão do HotPDF corrige o texto duplicado de fake bold?
O HotPDF v2.768.15 acrescentou a remoção de glifos duplicados à extração de texto, layout, colunas e texto semântico, e a v2.768.12 corrigiu os fins de linha em falta em fontes CID com um array /W por ordenar
Ambas as correções vêm no atual HotPDF Delphi PDF component, que extrai texto de PDFs carregados em Delphi e C++Builder ao lado das suas funcionalidades de criação, edição e renderização de PDF