PDFium Component transforme un PDF à mise en page fixe en un modèle sémantique recomposable, via BuildReflowDocument, et exporte ce modèle sous forme de HTML autonome via ToHtml. Les titres restent des titres, les éléments de liste restent des éléments de liste, et les tableaux détectés sur la page ressortent sous forme de véritable balisage de tableau avec cellules d'en-tête et fusions préservées. Rien dans la sortie ne référence de script ou de feuille de style externe
La raison de vouloir cela est qu'une page PDF est un ensemble de glyphes positionnés, ce qui est exactement inadapté à un écran de téléphone, un lecteur d'écran ou un index de recherche. Toute tentative de résoudre cela en extrayant du texte brut perd la structure qui rendait le document lisible, et toute tentative de le résoudre en convertissant les pages en images perd le texte entièrement. Un modèle de recomposition conserve les deux : les mots et les relations entre eux
D'où viennent les informations sémantiques ?
Tout part de GetStructuredText, la source unique de texte et de sémantique dans le composant. Lorsque le PDF porte un arbre de structure, un PDF balisé tel que défini à la clause 14.7 d'ISO 32000-1, le modèle suit la hiérarchie logique enregistrée par le producteur. Lorsque ce n'est pas le cas, et la plupart des PDF rencontrés en pratique ne le sont pas, le modèle se rabat sur l'ordre de mise en page physique déjà calculé à des fins d'ordre de lecture
Ce choix maintient une frontière stricte : aucun second analyseur PDF ni second moteur de rendu n'est introduit pour répondre à des questions auxquelles l'existant peut déjà répondre. Le mécanisme d'ordre de lecture sous-jacent est décrit dans les blocs de texte structuré et l'ordre de lecture, et le modèle de recomposition est une couche sémantique par-dessus, non un remplacement
Chaque nœud enregistre d'où provient son information, de sorte qu'un consommateur peut distinguer un titre déclaré par le document d'un titre déduit par les heuristiques de mise en page. Les pipelines sensibles à la confiance devraient lire ce champ plutôt que de traiter tous les nœuds comme également fiables
Un arbre aplati, et pourquoi ce n'est pas un arbre d'objets
Le modèle est un arbre aplati en ordre préfixe : un tableau de nœuds où chaque nœud porte un ParentIndex et une Depth, plutôt qu'un enregistrement récursif ou un graphe d'objets avec propriété. Pages, titres, paragraphes, listes, éléments de liste, figures, légendes, tableaux, lignes et cellules résident tous dans ce seul tableau linéaire
Deux avantages en découlent. Les consommateurs peuvent parcourir le tableau en flux dans l'ordre sans récursion, ce qui fait de la génération de HTML, de Markdown ou d'une vue arborescente une simple boucle. Et la disposition reste portable entre Delphi, C++Builder et Free Pascal, qui diffèrent dans leur façon de gérer les types gérés récursifs à travers une frontière d'ABI. Un enregistrement récursif de tableaux dynamiques est exactement le genre de construction qui compile partout et se comporte de manière subtilement différente dans chacun
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfReflowOptions;
Doc: TPdfReflowDocument;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.LoadDocument;
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.DetectTables := True;
Options.IncludeCss := True; // bloc de style en ligne, pas de fichier externe
Options.MaxNodes := 200000; // budget à échec fermé
Options.MaxCharacters := 4000000;
Doc := Pdf.BuildReflowDocument(Options);
for I := 0 to High(Doc.Nodes) do
case Doc.Nodes[I].Kind of
prnkHeading:
Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
prnkParagraph:
Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
Copy(Doc.Nodes[I].Text, 1, 60)]));
prnkTable:
Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
end;
Writeln(Format('%d node(s), %d table(s), %d character(s)',
[Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
finally
Pdf.Free;
end;
end;
Comment évite-t-on que les tableaux apparaissent deux fois ?
La détection de tableau s'exécute après que le texte structuré a été collecté pour une page, ce qui crée un risque évident : le même contenu de cellule existe à la fois dans les blocs de texte et dans le tableau détecté. Émettre les deux produit un HTML où chaque tableau est suivi de son propre contenu à nouveau, sous forme de paragraphes isolés
La règle qui résout cela est géométrique. Lorsqu'un tableau détecté couvre plus de la moitié de la surface d'un bloc de texte, le nœud de tableau remplace ce bloc plutôt que de s'y ajouter. L'indexation des cellules à l'intérieur d'une ligne est construite en comptant dans des compartiments, de sorte que la construction du modèle reste linéaire en cellules plus lignes au lieu de rebalayer chaque cellule pour chaque ligne, ce qui compte sur les documents financiers où une seule page peut porter des centaines de cellules
La structure détectée assume honnêtement d'être une détection. Un tableau avec des lignes de séparation est reconnu plus fiablement qu'un tableau aligné uniquement par des espaces, et la confiance du nœud le reflète. Pour un contenu où un tableau erroné vaut mieux qu'aucun tableau, gardez la détection activée ; pour une conversion d'archivage où un tableau erroné est pire, filtrez sur la confiance
Exporter un HTML qui reste autonome
ToHtml parcourt le modèle déjà construit et ne revient jamais vers PDFium, donc exporter deux fois ne coûte rien de plus et ne peut pas produire un résultat différent à partir du même modèle. Le texte et les valeurs d'attribut sont échappés de manière uniforme, les niveaux de titre sont bornés à la plage h1 à h6 que le HTML définit réellement, et les cellules d'en-tête, RowSpan et ColumnSpan passent tels qu'écrits
Le CSS optionnel est un simple bloc de style en ligne. Il n'y a aucun script, aucune police web et aucune ressource externe d'aucune sorte, ce qui rend la sortie sûre à intégrer dans un e-mail, une visionneuse d'aide ou un contrôle navigateur en bac à sable :
var
Html: WideString;
Stream: TFileStream;
Bytes: TBytes;
begin
Options := TPdfReflowOptions.Default;
Options.FullDocument := True;
Options.IncludeCss := True;
Options.IncludePageSections := True; // garder les limites de page visibles
Options.PreserveLineBreaks := False; // laisser le navigateur retourner à la ligne les paragraphes
Html := Pdf.BuildReflowDocument(Options).ToHtml;
Bytes := TEncoding.UTF8.GetBytes(string(Html));
Stream := TFileStream.Create('report.html', fmCreate);
try
if Length(Bytes) > 0 then
Stream.WriteBuffer(Bytes[0], Length(Bytes));
finally
Stream.Free;
end;
end;
PreserveLineBreaks est l'option qui mérite le plus réflexion. Un saut de ligne PDF est une décision de composition prise pour une largeur de page fixe, donc le préserver sur un écran étroit reproduit exactement le problème que la recomposition existe pour résoudre. Préservez les sauts pour la poésie, les listages de code et les adresses ; supprimez-les pour la prose
Budgets, annulation et état de la page
Les caractères, les nœuds, les tableaux et les cellules ont chacun un plafond, et chacun est vérifié avant l'allocation plutôt qu'après, de sorte qu'un document malformé ou malveillant échoue proprement au lieu de consommer de la mémoire jusqu'à ce que quelque chose d'autre le fasse. Le jeton d'annulation est vérifié aux limites des pages, blocs, tableaux, lignes et cellules, ce qui garde réactif le balayage annulé d'un document de mille pages
Un comportement compte spécifiquement pour les applications graphiques : tout le balayage du document s'exécute à l'intérieur d'une portée qui restaure la page active, de sorte que le succès, l'échec de budget et l'annulation laissent tous intacte la page courante de l'appelant. Une visionneuse qui laisse l'utilisateur exporter alors qu'il regarde la page 340 se retrouve toujours sur la page 340 ensuite
À quoi sert la recomposition, et à quoi elle ne sert pas
La sortie de recomposition constitue une excellente entrée pour l'indexation de recherche, les vues de lecture accessibles, l'affichage mobile et la migration de contenu. Ce n'est pas un convertisseur préservant la fidélité : les positions absolues, les polices exactes, les visuels vectoriels et la géométrie précise de page sont hors de son objectif par conception. Lorsqu'une tâche a besoin que la page ait le même aspect, restituez-la ; lorsqu'elle a besoin que la page soit lisible ailleurs, recomposez-la
Spécifiquement pour les technologies d'assistance, le modèle de recomposition s'associe aux fonctionnalités de lecture décrites dans la construction d'un lecteur accessible, et les documents portant un véritable arbre de structure produisent des modèles nettement meilleurs, ce qui constitue un bon argument pour valider le balisage en amont, comme décrit dans la validation de l'arbre de structure PDF/UA
La recomposition, le texte structuré, la validation du balisage et le rendu partagent un même objet document dans Delphi, C++Builder et Lazarus ; l'API complète est décrite sur la page PDFium Component pour Delphi