HotPDF Delphi Component, depuis la v2.768.15, retire les caractères en double que le faux gras et les logos surimprimés laissent dans le texte PDF extrait. Quand un producteur peint le même glyphe deux fois à une fraction de point d'écart, ExtractLoadedPageText, ExtractLoadedPageTextLayout et ExtractLoadedPageTextColumns lisent désormais ce caractère une seule fois, si bien que "Bold words" ne sort plus en "Bold wordsBold words"
C'est un de ces bugs qui ressemblent à une panne de décodeur et n'en sont pas un. Chaque glyphe se décode correctement ; le texte est bel et bien deux fois sur la page. La correction doit décider lequel des deux glyphes parfaitement valides est la copie, et le faire sans avaler des lettres répétées légitimes comme le double l de "fill" ou les cellules identiques d'un tableau. Le reste de cet article explique la règle qu'utilise HotPDF, les seuils derrière, où elle diffère délibérément des autres outils d'extraction, et un second bug d'extraction sans rapport de la même vague de versions qui coupait aussi le texte en fin de ligne
Pourquoi le faux gras produit-il du texte doublé dans l'extraction PDF ?
Le faux gras double le texte extrait parce que le producteur dessine la chaîne deux fois et que les deux copies sont de vraies opérations d'affichage de texte. Quand une famille de polices n'a pas de graisse bold, ou que l'outil d'auteur ne s'en donne simplement pas la peine, beaucoup de générateurs émulent le gras en affichant la même chaîne avec Tj ou TJ, en poussant la position du texte de quelques dixièmes de point vers la droite, puis en la réaffichant. À l'écran les deux copies fusionnent en un trait plus épais. Dans le flux de contenu il y a deux séquences complètes de codes de caractères, et ISO 32000-1 §9.4 ne donne aucun indicateur à l'extracteur pour dire "celui-là est de la décoration"
Les logos surimprimés touchent le même problème sous un autre angle. Un logotype peut être peint une fois en remplissage et une autre en contour ou en ombre légèrement décalée, avec la même police et les mêmes caractères. Un extracteur naïf qui parcourt le flux de contenu dans l'ordre de peinture émet toute la première séquence puis toute la seconde, voilà pourquoi le résultat se lit "Bold wordsBold words" plutôt que "BBoolldd". Avant la v2.768.15, l'assemblage de texte de HotPDF gardait les deux copies, et le doublon cassait ce dont la plupart des appelants se soucient vraiment : une recherche de "Bold words" dans le texte de la page pouvait échouer parce que la frontière de mot entre les deux séquences avait disparu
Le vrai mode de rendu de texte 2 (remplissage puis contour, ISO 32000-1 §9.3.6) n'a pas ce problème, parce que c'est une seule opération d'affichage qui produit un seul jeu de glyphes. Les doublons n'apparaissent que quand le producteur émet le texte deux fois
Comment HotPDF décide-t-il quel glyphe est le doublon ?
HotPDF traite deux glyphes comme un seul caractère peint quand ils portent la même valeur Unicode, ont la même direction d'écriture, sont à 5% ou moins de taille l'un de l'autre, et se tiennent quasiment l'un sur l'autre : à au plus 0.1 de la taille du glyphe d'écart le long de la ligne de base et à au plus 0.2 de la taille du glyphe d'écart en travers. De chaque telle paire, le glyphe peint en premier est gardé et le suivant est jeté. Ce sont les valeurs de l'implémentation v2.768.15, et chacune y est pour une raison
- Même Unicode, même direction. Seuls des caractères identiques peuvent être des doublons, et un glyphe horizontal n'est jamais comparé à un vertical (WMode 1)
- Taille à 5% près. Une grande lettre de titre et une petite lettre de légende qui se chevauchent par hasard ne sont pas le même texte ; un écart de taille élimine la paire
- 0.1 de la taille le long de la ligne de base. Les décalages du faux gras sont de quelques dixièmes de point, quelques pourcents d'une taille de texte normale. Deux vraies lettres adjacentes, même un "ll" serré au crénage, sont séparées d'environ une chasse entière, un ordre de grandeur de plus que cette tolérance
- 0.2 de la taille en travers de la ligne de base. Les ombres et passes de contour se décalent souvent vers le bas autant que vers la droite, donc la tolérance verticale est plus lâche, mais reste bien en dessous de la distance à la ligne suivante
- Le premier peint gagne. Garder le glyphe antérieur préserve l'ordre de lecture de la première séquence, si bien que le texte survivant coule comme le producteur l'a écrit
Les distances se mesurent dans le repère de ligne de base propre au glyphe plutôt qu'en X et Y bruts de page, si bien que le texte pivoté et le texte sur ligne de base inclinée utilisent les mêmes tolérances que les lignes horizontales ordinaires. La taille utilisée est celle du glyphe telle que peinte sur la page, déduite de son quad, pas l'opérande de taille donné à Tf ; cela compte pour les documents qui mettent Tf à 1 et passent tout par la matrice de texte, où une taille de police brute rendrait chaque tolérance inutile
Les espaces ne sont jamais candidates. Les points de code jusqu'à U+0020, l'espace insécable U+00A0 et l'espace idéographique U+3000 sont sautés, parce que des glyphes vides ne portent aucune encre visible à dupliquer et que j'en supprimer un fusionnerait des mots
Garder la vérification rapide sur les pages denses
La vérification de doublons dans HotPDF tourne en environ n log n, pas en n carré, parce qu'elle trie d'abord et ne compare les glyphes que dans une petite fenêtre. Comparer chaque glyphe à chaque autre serait correct et inutilisable : une page dense en petits caractères porte facilement des dizaines de milliers de glyphes, et la vérification tourne à chaque appel d'extraction de texte
La routine interne trie les glyphes non espace par valeur Unicode, puis par X d'origine, puis par ordre de peinture, avec un tri fusion stable. Après le tri, chaque doublon potentiel d'un glyphe se trouve juste après lui dans l'ordre, donc le scan ne marche vers l'avant que tant que la valeur Unicode correspond et que l'écart en X reste sous 0.25 de la taille du glyphe. La fenêtre de 0.25 est délibérément plus large que la tolérance de 0.1 le long de la ligne de base, pour qu'une paire sur une base légèrement inclinée tombe quand même dedans avant que le test précis en repère de base ne tourne. Un cas subtil : si le scan depuis le glyphe A rencontre un glyphe correspondant peint avant A, alors A est la copie ; A est jeté et le scan s'arrête, laissant le glyphe antérieur poursuivre la comparaison avec tout ce qui suit. Cette règle fait que "premier peint gagne" tient pour les triples frappes comme pour les paires
La comparaison porte sur la valeur Unicode, pas sur la police ni le code de caractère. Deux glyphes qui mappent vers le même Unicode via des polices différentes, par exemple une passe de remplissage dans un sous-ensemble embarqué et une passe de contour dans un autre, restent reconnus comme un seul caractère. La contrepartie : un glyphe dont le code se résout en Unicode 0 n'a rien de fiable à comparer, raison de plus de surveiller le compte de non résolus couvert dans l'extraction de texte des PDF chargés avec HotPDF
Quels appels d'extraction HotPDF retirent les glyphes dupliqués ?
Tous les appels produisant du texte dans HotPDF retirent les doublons, et l'appel de glyphes bruts non. La déduplication vit dans THotPDF.ExtractVisibleLoadedPageGlyphs, l'étape qui écarte aussi les glyphes hors de la boîte visible de la page, et les extractions de texte simple, colonne, layout et texte sémantique lisent toutes leurs glyphes depuis elle. THotPDF.ExtractLoadedPageGlyphs renvoie toujours chaque glyphe peint par le flux de contenu, surimpressions comprises, parce que les appelants qui construisent du surlignage, de la rédaction ou de la forensic ont besoin du registre complet de ce qui a été dessiné
Pour le travail de texte ordinaire, rien ne change dans le code appelant. La même boucle qui extrayait du texte doublé avant la v2.768.15 extrait désormais chaque mot une fois
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;
Les sorties layout et colonnes profitent tout autant, car une séquence dupliquée poussait le reste de la ligne de côté sur la grille de caractères. ExtractLoadedPageTextColumns garde son multiplicateur d'espace optionnel ; le filtre de doublons tourne avant la découpe en colonnes, donc il n'interagit pas avec ce réglage
var
Grid, Columns: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1-layout.txt', Grid, TEncoding.UTF8);
// Colonnes coupées là où un espace dépasse 3x la chasse médiane des glyphes
if Pdf.ExtractLoadedPageTextColumns(0, Columns, 3.0) then
TFile.WriteAllText('page1-columns.txt', Columns, TEncoding.UTF8);
end;
Si vous voulez savoir si une page contient du texte surimprimé, comparez les deux appels de glyphes. Sachez que la différence compte aussi les glyphes tombant hors de la crop box ; traitez-la donc comme une borne haute des doublons plutôt que comme un chiffre exact
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;
Les règles d'espaces de mots et de retours à la ligne qui transforment ces glyphes en texte sont inchangées par le filtre ; elles sont détaillées dans comment HotPDF place les espaces de mots et les retours à la ligne dans le texte de page
Là où la règle diffère délibérément des autres outils
HotPDF jette des doublons exacts que d'autres outils d'extraction gardent, et cette différence est voulue. Quand le changement v2.768.15 a été vérifié contre un corpus de référence, un pied de page peint deux fois à exactement la même position s'est extrait une fois dans HotPDF et deux fois dans un extracteur de référence. Un glyphe peint sur un glyphe identique est invisible comme caractère séparé, donc le lire une fois est ce que la page montre réellement
La limite honnête va dans l'autre sens. Un faux gras qui décale la seconde passe de plus de 0.1 de la taille du glyphe le long de la ligne de base n'est pas attrapé, et une passe d'ombre dessinée à une taille nettement différente non plus. Ces mises en page sont rares en pratique, parce que l'effet visuel cesse de ressembler à du gras dès que le décalage devient si grand ; mais si vous voyez du texte doublé sur un titre décoratif, comparez les valeurs X des glyphes depuis ExtractLoadedPageGlyphs avant de conclure que le filtre est cassé. Le filtre ne fait rien non plus pour des doublons non co-localisés géométriquement, comme un titre courant répété dans une couche cachée ailleurs sur la page ; c'est une question de contenu, pas une surimpression
Pourquoi les fins de ligne étaient-elles coupées dans le texte en polices CID ?
Un bug distinct corrigé dans HotPDF v2.768.12 jetait les fins de lignes du texte composé en polices à CID (Type 0) dont le tableau de largeurs /W n'était pas trié par CID. ISO 32000-1 §9.7.4.3 définit le format du tableau /W mais n'exige pas que ses entrées soient en ordre CID croissant, et certains générateurs les écrivent dans l'ordre de première utilisation des glyphes dans le document
Le décodeur d'extraction de texte cherchait les largeurs par recherche dichotomique, qui suppose silencieusement une entrée triée. Sur un tableau non trié, la plupart des recherches rataient et retombaient sur la largeur par défaut /DW, qui vaut 1000 unités, un em entier. Le texte latin dans une telle police se composait donc comme si chaque lettre était pleine chasse, les lignes filaient bien au-delà du bord droit de la page, et les glyphes atterrissant hors de la boîte visible étaient écartés, si bien que les fins de lignes disparaissaient du texte extrait et de la recherche. Rien ne plantait et rien n'était rapporté, et c'est ce qui rendait le bug difficile à repérer
HotPDF trie désormais les entrées de /W avec un tri fusion stable juste après lecture, si bien que là où deux entrées se chevauchent, celle écrite en premier gagne toujours, comme lit le tableau dans l'ordre du fichier. Le moteur de rendu de page n'a jamais été touché, parce qu'il garde sa propre copie des données de largeur et la scanne linéairement, et le tableau de largeurs verticales /W2 est aussi scanné linéairement. Cette séparation vaut la peine d'être retenue quand une page a l'air parfaite mais s'extrait mal : rendu et extraction dans une bibliothèque PDF partagent souvent bien moins de code qu'on ne s'y attendrait
FAQ
HotPDF retire-t-il les lettres répétées comme le double l de "fill" ?
Non. Deux vraies lettres adjacentes sont à environ une chasse de distance, alors que HotPDF ne fusionne que des caractères identiques à moins de 0.1 de la taille du glyphe le long de la ligne de base et 0.2 en travers ; les répétitions légitimes sont gardées
Comment obtenir chaque glyphe, doublons surimprimés compris ?
Appelez THotPDF.ExtractLoadedPageGlyphs, qui renvoie chaque glyphe peint par la page dans l'ordre de peinture. THotPDF.ExtractVisibleLoadedPageGlyphs et tous les appels d'extraction de texte renvoient l'ensemble dédupliqué et visible
Quelle version de HotPDF corrige le texte doublé du faux gras ?
HotPDF v2.768.15 a ajouté la suppression des glyphes dupliqués aux extractions de texte, layout, colonnes et texte sémantique, et la v2.768.12 a corrigé les fins de lignes manquantes des polices CID à tableau /W non trié
Les deux corrections sont livrées dans le composant HotPDF Delphi PDF actuel, qui extrait le texte de PDF chargés en Delphi et C++Builder aux côtés de ses fonctions de création, d'édition et de rendu PDF