O HotPDF Delphi Component, desde a v2.768.15, remove os caracteres duplicados que fake bold e logos sobreimpressos 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 caractere uma vez, então "Bold words" não sai mais como "Bold wordsBold words"
Esse é um daqueles bugs que parecem falha de decodificador e não são. Todo glifo decodifica corretamente; o texto realmente está na página duas vezes. A correção precisa decidir qual de dois glifos perfeitamente válidos é a cópia, e fazer isso sem engolir letras repetidas legítimas como o l duplo em "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 ela diverge deliberadamente de outras ferramentas de extração, e um segundo bug de extração, não relacionado, do mesmo trem de releases que também cortava texto do fim das linhas
Por que fake bold produz texto dobrado na extração de PDF?
Fake bold dobra o texto extraído porque o produtor desenha a string duas vezes e as duas cópias são operações reais de exibição 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 bold mostrando a mesma string com Tj ou TJ, empurrando a posição do texto alguns décimos de ponto para a direita e mostrando de novo. Na tela as duas cópias se fundem em um traço mais pesado. No content stream há duas passadas completas de códigos de caracteres, e a ISO 32000-1 §9.4 não dá ao extrator nenhuma flag que diga "esta é decoração"
Logos sobreimpressos atingem o mesmo problema por outro ângulo. Um logotipo pode ser pintado uma vez como fill e outra como stroke ou uma sombra levemente deslocada, usando a mesma fonte e os mesmos caracteres. Um extrator ingênuo que percorre o content stream na ordem de pintura emite toda a primeira passada, depois toda a segunda, que é o motivo de o resultado ler "Bold wordsBold words" em vez de "BBoolldd". Antes da v2.768.15, a montagem de texto do HotPDF mantinha as duas cópias, e a duplicata quebrava a coisa com que a maioria dos chamadores realmente se importa: uma busca por "Bold words" no texto da página podia falhar porque o limite de palavra entre as duas passadas tinha sumido
O modo de renderização de texto 2 real (fill depois stroke, ISO 32000-1 §9.3.6) não tem esse problema, porque é uma única operação de exibição de texto que produz um único conjunto de glifos. As duplicatas só aparecem quando o produtor emite o texto duas vezes
Como o HotPDF decide qual glifo é a duplicata?
O HotPDF trata dois glifos como um caractere pintado quando eles carregam o mesmo valor Unicode, têm a mesma direção de escrita, estão dentro de 5% um do outro em tamanho e ficam quase um sobre o outro: no máximo 0.1 do tamanho do glifo de distância ao longo da baseline e no máximo 0.2 do tamanho do glifo através dela. De cada par assim, o glifo pintado primeiro é mantido e o posterior é descartado. Esses são os valores da implementação v2.768.15, e cada um está ali por um motivo
- Mesmo Unicode, mesma direção. Só caracteres idênticos podem ser duplicatas, 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 sobrepõem não são o mesmo texto, então uma diferença de tamanho elimina o par
- 0.1 do tamanho ao longo da baseline. Deslocamentos de fake bold são de alguns décimos de ponto, alguns por cento de um tamanho de texto normal. Duas letras adjacentes reais, mesmo um "ll" com kerning apertado, ficam separadas por aproximadamente uma largura de avanço completa, uma ordem de grandeza a mais do que essa tolerância
- 0.2 do tamanho através da baseline. Sombras e passadas de contorno costumam se deslocar para baixo além de para a direita, então a tolerância vertical é mais frouxa, mas ainda muito abaixo da distância até a próxima linha
- Quem foi pintado primeiro vence. Manter o glifo anterior preserva a ordem de leitura da primeira passada, de modo que o texto sobrevivente ainda flui como o produtor escreveu
As distâncias são medidas no próprio referencial de baseline do glifo, e não no X e Y brutos da página, de modo que texto rotacionado e texto em baseline inclinada usam as mesmas tolerâncias das linhas horizontais comuns. O tamanho usado é o do glifo pintado na página, derivado do seu quad, e não o operando de tamanho dado ao Tf; isso importa para documentos que definem Tf como 1 e escalam tudo pela matriz de texto, em que um tamanho de fonte bruto tornaria toda tolerância inútil
Espaços nunca são candidatos. Code points até U+0020, o espaço inseparável U+00A0 e o espaço ideográfico U+3000 são pulados, porque glifos em branco não carregam tinta visível para duplicar e descartar um fundiria palavras
Mantendo a verificação rápida em páginas densas
A verificação de duplicatas no HotPDF roda em tempo aproximadamente n log n, não n ao quadrado, porque ela ordena primeiro e só compara glifos dentro de uma janela pequena. Comparar todo glifo com todo outro glifo seria correto e inutilizável: uma página densa de letra miúda pode carregar dezenas de milhares de glifos, e a verificação roda em cada chamada de extração de texto
A rotina interna ordena os glifos que não são espaços por valor Unicode, depois por origem X, depois por ordem de pintura, usando um merge sort estável. Depois de ordenar, toda duplicata em potencial de um glifo fica imediatamente depois dele na ordem, então a varredura só anda para frente enquanto o valor Unicode casa e a diferença de X fica 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 que um par em uma baseline levemente inclinada ainda caia dentro dela antes do teste preciso no referencial de baseline rodar. Um caso sutil: se a varredura a partir do glifo A encontra um glifo correspondente pintado antes de A, A é a cópia, então A é descartado e a varredura para, deixando o glifo anterior continuar a comparação com o que vier depois. Essa regra é o que faz "quem foi pintado primeiro vence" valer para triplas além de pares
A comparação é pelo valor Unicode, não pela fonte ou pelo código de caractere. Dois glifos que mapeiam para o mesmo Unicode através de fontes diferentes, por exemplo uma passada de fill em um subset embutido e uma passada de stroke em outro, ainda são reconhecidos como um caractere. O reverso é que um glifo cujo código resolve para Unicode 0 não tem nada confiável para comparar, o que é mais um motivo para ficar de olho na contagem de não resolvidos coberta em extração de texto de PDFs carregados com HotPDF
Quais chamadas de extração do HotPDF removem glifos duplicados?
Todas as chamadas que produzem texto no HotPDF removem duplicatas, e a chamada bruta de glifos não. A deduplicação mora 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 todos leem os seus glifos dela. O THotPDF.ExtractLoadedPageGlyphs ainda retorna todo glifo que o content stream pinta, overprints incluídos, porque chamadores que constroem ferramentas de realce, redação ou forense precisam do registro completo do que foi desenhado
Para trabalho de texto comum nada muda no código chamador. O mesmo loop que extraía texto dobrado 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;
A saída de layout e colunas se beneficia tanto quanto, já que uma passada duplicada empurrava o resto de uma linha para o lado na grade de caracteres. O ExtractLoadedPageTextColumns mantém o seu multiplicador opcional de vão; o filtro de duplicatas roda antes da divisão em colunas, então não interage com essa configuração
var
Grid, Columns: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1-layout.txt', Grid, TEncoding.UTF8);
// Colunas dividem onde um vão excede 3x a largura de avanço mediana dos glifos
if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;
Se você quer saber se uma página contém texto sobreimpresso, compare as duas chamadas de glifos. Esteja ciente de que a diferença também conta glifos que caem fora do crop box, então trate-a como um limite superior de duplicatas 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 quebras de linha que transformam esses glifos em texto não são alteradas pelo filtro; elas são cobertas em detalhe em como o HotPDF posiciona espaços de palavra e quebras de linha no texto da página
Onde a regra diverge deliberadamente de outras ferramentas
O HotPDF descarta duplicatas exatas que algumas outras ferramentas de extração mantêm, e essa diferença é intencional. Quando a mudança da v2.768.15 foi conferida contra um corpus de referência, um rodapé de página pintado duas vezes exatamente na mesma posição foi extraído uma vez no HotPDF e duas vezes em um extrator de referência. Um glifo pintado sobre um glifo idêntico é invisível como caractere separado, então lê-lo uma vez é o que a página realmente mostra
O limite honesto corre no outro sentido. Fake bold que desloca a segunda passada em mais de 0.1 do tamanho do glifo ao longo da baseline não é pego, e uma passada de sombra desenhada em tamanho visivelmente diferente também não. Esses layouts são raros na prática, porque o efeito visual deixa de parecer bold quando o deslocamento fica tão grande, mas se você vê texto dobrado vindo de um título decorativo, compare os valores de X dos glifos do ExtractLoadedPageGlyphs antes de assumir que o filtro quebrou. O filtro também não faz nada para duplicatas que não são co-localizadas geometricamente, como um cabeçalho corrido repetido em uma camada oculta em outro lugar da página; isso é uma questão de conteúdo, não de overprint
Por que os fins de linha eram cortados em texto com fontes CID?
Um bug separado corrigido no HotPDF v2.768.12 derrubava os fins das linhas 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 crescente de CID, e alguns geradores as escrevem na ordem em que os glifos foram usados pela primeira vez no documento
O decodificador de extração de texto buscava larguras com busca binária, que silenciosamente assume entrada ordenada. Em um array não ordenado a maioria das buscas falhava e caía na largura padrão /DW, que é 1000 units, um em inteiro. Texto latino em tal fonte era então composto como se cada letra fosse full-width, as linhas corriam muito além da borda direita da página, e os glifos que caíam fora da caixa visível eram descartados, então os fins das linhas sumiam do texto extraído e da busca. Nada crashava e nada era reportado, que é o que tornava difícil de notar
O HotPDF agora ordena as entradas do /W com um merge sort estável logo depois de lê-las, de modo que onde duas entradas se sobrepõem a que foi escrita primeiro ainda vence, casando com a leitura do array na ordem do arquivo. O renderizador de página nunca foi afetado, porque ele mantém a sua própria cópia dos dados de largura 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 em uma biblioteca PDF costumam compartilhar bem menos código do que você esperaria
FAQ
O HotPDF remove letras repetidas como o l duplo em "fill"?
Não. Duas letras adjacentes reais ficam a aproximadamente uma largura de avanço de distância, enquanto o HotPDF só funde caracteres idênticos que estão dentro de 0.1 do tamanho do glifo ao longo da baseline e 0.2 através dela, então repetições legítimas são mantidas
Como obtenho todos os glifos, incluindo duplicatas sobreimpressas?
Chame THotPDF.ExtractLoadedPageGlyphs, que retorna todo glifo que a página pinta na ordem de pintura. O THotPDF.ExtractVisibleLoadedPageGlyphs e todas as chamadas de extração de texto retornam o conjunto deduplicado e visível
Qual versão do HotPDF corrige texto dobrado de fake bold?
O HotPDF v2.768.15 adicionou remoção de glifos duplicados à extração de texto, layout, colunas e texto semântico, e a v2.768.12 corrigiu fins de linha ausentes em fontes CID com array /W não ordenado
As duas correções vêm no atual componente HotPDF Delphi para PDF, que extrai texto de PDFs carregados em Delphi e C++Builder junto com os seus recursos de criação, edição e renderização de PDF