HotPDF Delphi Component reconstruit les espaces de mots et les sauts de ligne dans THotPDF.ExtractLoadedPageText à partir de la géométrie des glyphes, pas à partir de caractères espace. Une espace entre quand l'écart après la largeur propre d'un glyphe dépasse 0.15 de la hauteur du texte, et une nouvelle ligne ne démarre que quand l'origine du texte franchit la direction d'écriture de plus de la moitié de la hauteur du texte. Depuis la v2.768.3, le texte de la page inclut aussi le texte peint via des Form XObjects et laisse dehors les glyphes hors de la zone visible recadrée. Le reste de cet article explique pourquoi chaque règle a la forme qu'elle a, parce que chacune a remplacé une règle plus simple qui produisait une sortie plausible mais fausse sur de vrais documents
Les symptômes sont familiers à quiconque a nourri un index de recherche avec du texte PDF. Une couverture s'extrait en PDFReferenceManualNovember4,1998, un formulaire fiscal se découpe en 156 lignes, un filigrane diagonal arrive à raison d'un caractère par ligne, et une épreuve rognée commence par la slug line d'imprimeur qu'aucun lecteur ne montre. Aucun de ces fichiers n'est cassé. Chacun emploie une façon parfaitement légale de placer du texte qu'un extracteur naïf lit de travers
Pourquoi le texte PDF extrait perd-il ses espaces de mots ?
Le texte extrait perd ses espaces de mots parce qu'un PDF n'est jamais tenu d'en contenir. Un producteur peut séparer les mots en affichant un caractère espace, mais il peut tout aussi bien déplacer le style avec un nombre dans un tableau TJ (ISO 32000-1 §9.4.3) ou avec un Td frais (§9.4.2), et la sortie TeX, beaucoup de fichiers Distiller et la plupart des mises en page justifiées font exactement ça. Avant la v2.766.76, HPDFAssemblePageText ne regardait que le mouvement vertical, donc une rupture de mot faite par positionnement disparaissait purement et simplement. L'assembleur mesure maintenant, le long de la direction d'écriture du glyphe précédent, la distance de la fin de la largeur propre de ce glyphe à l'origine du glyphe courant, et insère une espace quand la distance dépasse 0.15 de la hauteur de boîte du glyphe courant, mesurée de l'ascendante à la descendante en espace utilisateur. Aucune espace n'est ajoutée quand l'un des deux côtés est déjà blanc, ni entre deux caractères CJK, parce que la justification écarte les idéogrammes sans que cet écart signifie une frontière de mot. Les enregistrements de glyphes exposent la même géométrie, donc vous pouvez reproduire la décision quand un fichier particulier vous laisse perplexe
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
// hauteur ascendante-descendante de la boîte du glyphe, en espace utilisateur
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// texte horizontal : écart depuis la fin de la largeur propre du glyphe précédent
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;
Pourquoi mesurer depuis la largeur propre du glyphe plutôt que depuis la position du style ?
HotPDF mesure les écarts de mots depuis GlyphEndX / GlyphEndY parce que la position du style après un glyphe contient déjà de l'espacement qui n'est pas un écart. ISO 32000-1 §9.4.4 définit le déplacement horizontal comme la largeur du glyphe fois la taille de police, plus l'espacement de caractères Tc, plus l'espacement de mots Tw, le tout mis à l'échelle par Tz. BaselineEndX / BaselineEndY portent ce déplacement complet, tandis que GlyphEndX / GlyphEndY ne portent que l'avance de police et Tz. La différence compte pour les producteurs qui resserrent l'interlettrage avec un Tc négatif puis rendent l'espace via un ajustement TJ après chaque glyphe : mesuré depuis la position du style, le rendu ressemble à un écart, et le terme chinois “95后” s'extrayait en “9 5 后”. Le seuil est lié à la hauteur de boîte du glyphe plutôt qu'à la taille Tf pour une raison similaire. Les exports Word écrivent souvent 1 Tf et portent la vraie taille dans un Tm mis à l'échelle, donc Tfs dit 1 alors que le texte fait 10 points, et une règle calée sur Tfs traiterait différemment les deux graphies d'une même page
La règle a des bords honnêtes. Un titre composé avec un interlettrage très lâche, où Tc seul ouvre plus de 0.15 de la hauteur du texte entre les lettres, s'extrait avec une espace entre chaque lettre, ce qui ressemble à la page mais n'est probablement pas ce que vous vouliez indexer. Des morceaux dessinés hors ordre sur une même ligne de base produisent un écart négatif et se collent sans espace. Ni l'un ni l'autre cas n'est courant dans du texte courant, et sur un corpus de test le changement a fait monter les correspondances de mots contre un extracteur de référence sur 28 pages sans en baisser aucune
Quand HotPDF démarre-t-il une nouvelle ligne dans le texte extrait ?
Depuis la v2.766.79, une nouvelle ligne démarre quand le mouvement de l'origine du glyphe précédent à celle du glyphe courant, projeté sur la normale de la direction d'écriture précédente, dépasse la moitié de la plus grande hauteur de boîte des deux glyphes. L'ancienne règle comparait le mouvement Y brut à la moitié de Tfs, ce qui échouait dans deux sens. Avec 1 Tf et un Tm mis à l'échelle, le seuil rétrécissait à une demi-unité, donc un exposant monté par une élévation de texte de 0.4 ou un banal frémissement de ligne de base coupait la ligne. La règle ignorait aussi X entièrement, donc du texte sous un Tm tourné descendait la page à chaque glyphe et sortait un glyphe par ligne. Projeter sur la normale de la direction fait se comporter les suites tournées comme des horizontales, et prendre la plus grande des deux hauteurs garde un grand mot d'exemple et sa petite légende sur une ligne quand ils partagent une ligne de base. Sur le formulaire fiscal cité plus haut, le compte de lignes est tombé de 156 à 97. Le texte vertical en mode d'écriture 1 (§9.7.4.3) suit une voie séparée : ces glyphes sont groupés en colonnes, lus de droite à gauche et de haut en bas, avec un saut de ligne à chaque changement de colonne
Quel texte ExtractLoadedPageText inclut-il ou laisse-t-il dehors ?
ExtractLoadedPageText renvoie le texte qu'un lecteur montre. Depuis la v2.766.80, il travaille à partir des seuls glyphes visibles, en jetant chaque glyphe dont le centre de boîte tombe hors de GetLoadedPageVisibleBox, c'est-à-dire le CropBox clippé à la MediaBox (§14.11.2). Ça retire les slug lines et autres marques d'imprimeur composées en texte hors de la zone de coupe. ExtractLoadedPageGlyphs continue délibérément de renvoyer chaque glyphe du flux de contenu de la page, donc vous pouvez encore trouver ce matériau quand vous en avez besoin. Le filtre est un test de boîte, pas un test de visibilité : le texte caché par un chemin de clipping, peint en blanc ou couvert par une image est quand même extrait
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]));
// chaque glyphe du flux de contenu de la page, slug line comprise
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// seulement ce que la page montre, avec le texte des Form XObjects inséré
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
Le texte peint via des Form XObjects fait partie du texte de page depuis la v2.768.3. Les en-têtes, tampons et filigranes logent très souvent dans des formulaires, et certains documents de normes perdaient 30 à 35 pour cent de leurs caractères avant le changement. THotPDF.InterpretContentWithForms enregistre chaque Do avec la CTM en vigueur, interprète le formulaire à son /Matrix fois cette CTM (§8.10.1), et insère les glyphes du formulaire à la position du Do, en récurse dans les formulaires imbriqués. Un formulaire sans /Resources propre emprunte celles du flux qui le peint, comme le permet §7.8.3. Les glyphes de formulaire portent TokenIndex = -1, et ExtractLoadedPageGlyphs ne renvoie toujours que les glyphes du flux de page, parce que recherche, remplacement et rédaction réécrivent les changements via TokenIndex et éditeraient les mauvais octets si un glyphe de formulaire se glissait dedans. Deux simplifications valent d'être connues : le texte de formulaire n'est pas clippé au /BBox du formulaire, et la récursion s'arrête à 12 niveaux plutôt que par détection de cycles, donc un formulaire mal formé qui se peint lui-même répète son texte jusqu'à atteindre ce plafond
Pourquoi le texte après un opérateur Q se décodait-il en charabia ?
Le texte après Q pouvait se décoder de travers avant la v2.766.73 parce que l'extracteur ne sauvegardait que la CTM sur q. Les paramètres d'état de texte, à savoir police, taille, Tc, Tw, Tz, TL, mode de rendu et élévation, appartiennent à l'état graphique (§9.3.1), donc Q doit les restaurer avec tout le reste de la pile (§8.4.2). Un rapport du secteur sélectionnait une police deux octets Identity-H à l'intérieur de q … Q puis montrait du texte WinAnsi un octet sans Tf propre. L'extracteur gardait la police interne, lisait les filets et le mot “Adobe” de la table des matières comme des codes deux octets, et perdait 15% des caractères de la page. La pile q/Q de l'interpréteur porte maintenant l'état de texte complet. Les règles d'extraction décrites ici s'appliquent à chaque page, donc un document entier peut partir vers un fichier en un seul appel
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// plage vide = toutes les pages ; saut de page entre pages ; BOM UTF-8
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
Quelle API de texte HotPDF employer ?
ExtractLoadedPageText reste dans l'ordre du flux de contenu, qui est le défaut qui convient pour la recherche et l'indexation ; la chaîne de décodage en dessous est couverte dans extraire du texte de PDF chargés avec HotPDF. Pour les documents étiquetés dont l'ordre de composition compte, l'extraction de texte dans l'ordre de structure parcourt l'arbre de structure au lieu de deviner d'après la géométrie, et pour les données coincées dans des tableaux, l'extraction de tableaux typée à travers les sauts de page renvoie des cellules plutôt que des lignes. La référence API complète et un téléchargement d'essai sont sur la page produit HotPDF Delphi PDF Component