PDFiumPas renvoie le texte d'une page sous forme de structure plutôt que de chaîne. GetStructuredText produit un TPdfStructuredTextPage contenant des blocs, chacun contenant des lignes, chacune contenant des segments stylés, avec des limites en espace page à chaque niveau et les indices de caractère source préservés afin que tout fragment puisse être remappé vers la page de texte sous-jacente
L'extraction en chaîne plate par laquelle démarre la plupart du code est toujours là et toujours correcte pour son objectif. Elle cesse d'être suffisante dès que vous devez savoir quels mots formaient un titre, lesquels appartenaient à la colonne de gauche, ou où sur la page se situe réellement une correspondance
Pourquoi une chaîne plate est-elle la mauvaise sortie pour la plupart des tâches ?
Parce que les questions que l'on pose au texte extrait ne sont presque jamais « quels caractères se trouvent sur cette page ». Ce sont « quel est le titre », « est-ce un tableau », « ce paragraphe appartient-il à la section 4 », « où dois-je dessiner le surlignage ». Une simple chaîne ne répond à aucune d'entre elles, et chaque réponse que vous en reconstruisez est une heuristique dont vous héritez désormais
Les mises en page à deux colonnes rendent le problème concret. Extrayez un article à deux colonnes sous forme de chaîne et, selon la façon dont le producteur a écrit le flux de contenu, vous pouvez obtenir la colonne un suivie de la colonne deux, ou bien la ligne un de la colonne un, la ligne un de la colonne deux, la ligne deux de la colonne un, et ainsi de suite le long de la page. Les deux sortent d'un PDF conforme. Aucune des deux n'est fausse au niveau du format, car PDF décrit des marques sur une page, pas un plan de document. Un modèle par blocs laisse l'extracteur prendre explicitement la décision d'ordonnancement et vous dire quelle décision il a prise
Ordre du contenu ou mise en page physique ?
TPdfStructuredTextOptions.ReadingOrder choisit entre roContentOrder et roPhysicalLayout, et la bonne réponse dépend de ce en quoi vous avez le plus confiance, le producteur ou la géométrie
L'ordre du contenu renvoie le texte dans la séquence où le flux de contenu le dessine. C'est rapide, et pour des documents générés par un producteur bien élevé, c'est généralement l'ordre de lecture prévu. La mise en page physique ignore la séquence du flux et reconstruit l'ordre à partir de l'emplacement réel des caractères, en les regroupant en lignes puis en colonnes. C'est ce qu'il vous faut pour des pages numérisées puis passées à l'OCR, pour la sortie d'outils qui émettent le texte dans l'ordre des polices plutôt que dans l'ordre de lecture, et pour tout cas où le résultat visuel est la seule chose sur laquelle vous pouvez vous fier
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfStructuredTextOptions;
Page: TPdfStructuredTextPage;
B, L: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'article.pdf';
Pdf.LoadDocument;
Pdf.PageNumber := 1; // base 1
Options := TPdfStructuredTextOptions.Default;
Options.ReadingOrder := roPhysicalLayout;
Options.IncludeFontInfo := True;
Options.IncludeSemantics := True;
Options.MaxCharacters := 200000; // budget à échec fermé
Page := Pdf.GetStructuredText(Options);
for B := 0 to High(Page.Blocks) do
begin
if Page.Blocks[B].Kind = cfHeading then
Emit(Format('H%d: %s',
[Page.Blocks[B].HeadingLevel, Page.Blocks[B].Text]))
else
for L := 0 to High(Page.Blocks[B].Lines) do
Emit(Page.Blocks[B].Lines[L].Text);
end;
finally
Pdf.Free;
end;
end;
Qu'apporte le balisage que la géométrie ne peut pas apporter ?
L'intention. Avec IncludeSemantics activé, les blocs d'un PDF balisé portent un Kind tiré de l'arbre de structure, si bien qu'un titre est un titre parce que le producteur l'a dit, pas parce que sa police était plus grande que la moyenne. Les types couvrent les formes qui comptent pour la réutilisation : cfParagraph, cfHeading avec un HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure et le repli non balisé cfPlain
Le champ Source enregistre d'où vient chaque classification, rosStructure pour l'arbre de structure et rosHeuristic pour l'inférence, le champ à journaliser quand vous décidez jusqu'où faire confiance à un pipeline d'extraction sur un ensemble de documents. Les figures sont un cas particulier qui mérite d'être connu : pour un bloc cfFigure, le texte provient de la description alternative plutôt que d'un quelconque glyphe, puisqu'une figure n'a aucun caractère propre. Un texte alternatif sans correspondance est quand même représenté au lieu d'être supprimé, ce qui permet à un audit d'accessibilité de constater qu'une description existe même quand rien sur la page ne la dessine. Le modèle de balisage lui-même est couvert dans la validation de l'arbre de structure PDF/UA
Les segments portent le style et la provenance
Chaque TPdfStructuredTextSpan porte son texte, ses limites en espace page, FontName, FontSize, FontWeight et Angle, plus SourceStartIndex et SourceCharacterCount. Les segments se coupent là où le style change, si bien qu'une phrase avec trois mots en gras devient trois segments, et reconstruire l'emphase en HTML ou en Markdown devient une question de lecture de propriétés plutôt que de devinette à partir des noms de police
Les deux champs d'indice source sont ceux qui transforment l'extraction en fonctionnalité plutôt qu'en simple rapport. Ils pointent en retour vers la séquence de caractères de la page, ce qui signifie qu'un bloc trouvé lors d'une recherche peut être converti en géométrie de sélection au niveau caractère ou en rectangle de surlignage sans une seconde passe, différemment ordonnée, sur le texte ; la mécanique est décrite dans la sélection de ligne de texte visuelle avec des boîtes de caractère. Le champ Angle compte plus qu'il n'y paraît : un texte pivoté dans un tampon ou un filigrane atterrit dans le même espace de coordonnées que le texte du corps, et un pipeline qui ignore l'angle fusionnera sans broncher un « DRAFT » diagonal au milieu d'un paragraphe
Budget, et les deux compteurs de qualité
MaxCharacters est un budget à échec fermé, pas un paramètre de troncature : une page qui le dépasse s'arrête au lieu de renvoyer silencieusement une partie du contenu. Sur un chemin d'entrée non fiable, c'est le comportement que vous voulez, car une page avec un million de caractères est soit un monstre généré par machine, soit une tentative de faire de votre extracteur la partie la plus lente du système
Deux compteurs sur la page renvoyée décrivent directement la qualité d'extraction. UnmappedCharacterCount compte les caractères sans correspondance Unicode utilisable, le symptôme classique d'une police en sous-ensemble incorporée sans CMap /ToUnicode ; un tel texte se rend parfaitement mais s'extrait sans rien d'utile. GeometryFailureCount compte les caractères dont la boîte englobante n'a pas pu être déterminée, ce qui dégrade l'ordonnancement en mise en page physique. Journalisez les deux. Un ensemble de documents où ces nombres restent systématiquement proches de zéro peut être indexé en toute confiance, et un ensemble où ce n'est pas le cas vous indique que certains producteurs de votre pipeline ont besoin d'attention avant qu'aucun résultat en aval ne soit digne de confiance
var
Page: TPdfStructuredTextPage;
B, S, L: Integer;
Emphasised: Boolean;
begin
Page := Pdf.GetStructuredText(Options);
if Page.UnmappedCharacterCount > 0 then
Log(Format('page %d: %d characters without a Unicode mapping',
[Page.PageNumber, Page.UnmappedCharacterCount]));
if Page.GeometryFailureCount > 0 then
Log(Format('page %d: %d characters without geometry',
[Page.PageNumber, Page.GeometryFailureCount]));
for B := 0 to High(Page.Blocks) do
for L := 0 to High(Page.Blocks[B].Lines) do
for S := 0 to High(Page.Blocks[B].Lines[L].Spans) do
begin
Emphasised := Page.Blocks[B].Lines[L].Spans[S].FontWeight >= 600;
AppendRun(Page.Blocks[B].Lines[L].Spans[S].Text, Emphasised,
Page.Blocks[B].Lines[L].Spans[S].SourceStartIndex);
end;
end;
Performance sur des pages réelles
L'extraction en mise en page physique est le mode coûteux, et l'implémentation est construite pour des pages réellement volumineuses : l'ordonnancement des caractères s'exécute en O(n log n) plutôt que par balayage répété, les tampons de lignes et de segments croissent géométriquement au lieu d'être réalloués par caractère, le texte Unicode est construit dans des tampons plutôt que par concaténation de chaînes, et les recherches de police pour les objets de texte adjacents sont mises en cache. C'est cette combinaison qui garde une page dense de 5 000 caractères prévisible plutôt que quadratique
Pour une tâche à fort volume de pages, il vaut quand même la peine de choisir le mode le moins coûteux là où vous le pouvez. Utilisez roContentOrder avec la sémantique activée pour les documents balisés auxquels vous faites confiance, et réservez roPhysicalLayout pour le matériel numérisé et hérité où la géométrie est le seul signal. Si tout ce dont vous avez besoin est une simple chaîne, l'API plus simple décrite dans l'extraction de texte depuis des documents PDF reste le chemin le plus rapide, et quand vous devez retracer le texte jusqu'aux identifiants de contenu marqué, la lecture et l'écriture du contenu marqué BDC et MCID couvre cette couche
Le modèle par blocs correspond aussi proprement à ce que veulent les pipelines de récupération : un titre avec ses paragraphes est un fragment avec un titre, et les limites permettent à une citation de pointer vers un emplacement sur une page plutôt que vers un document. PDFiumPas est un composant Delphi et Lazarus autour du moteur PDFium, documenté avec des exemples sur la page PDFium Delphi component