HotPDF récupère les tableaux d’un PDF existant avec ExtractLoadedTypedTables, une API Delphi qui fusionne les fragments de lignes produits par la passe de mise en page, construit une grille de colonnes canonique par tableau, poursuit ce tableau au-delà d’un saut de page lorsque la géométrie le permet et renvoie chaque cellule sous forme de valeur typée accompagnée de sa page, de son étendue de colonne et de ses limites. ExportLoadedTypedTables écrit le même résultat directement en CSV ou en JSON. Le scénario qui justifie cette fonctionnalité est banal et extrêmement fréquent. Un registre de factures de quarante pages, un seul tableau logique, imprimé avec son en-tête répété en haut de chaque page. Exécutez dessus une passe naïve de l’ordre de lecture et vous obtenez quarante tableaux, trente-neuf lignes d’en-tête parasites et une colonne monétaire qui glisse d’une position vers la gauche chaque fois que la cellule centrale était vide. Nettoyer cela en aval, dans l’application appelante, est le point où les projets d’import documentaire vont mourir
Pourquoi une page PDF vous remet-elle des fragments plutôt qu’un tableau ?
Parce qu’une page PDF ne porte aucune sémantique de tableau, sauf si le document est balisé. Le flux de contenu ne contient que des opérateurs d’affichage de texte et des matrices de positionnement (ISO 32000-1 §9.4.3) ; le cadre tracé que vous voyez à l’écran est un tracé sans rapport qu’aucun extracteur n’est obligé de mettre en corrélation avec le texte. Les types d’éléments de structure Table, TR, TH et TD vivent uniquement dans la hiérarchie de structure logique d’un PDF balisé (ISO 32000-1 §14.8.4), et l’immense majorité des documents métier en circulation ne sont pas balisés. Tout ce qui suit est une récupération géométrique, pas une analyse sémantique, et il vaut mieux le dire clairement avant que quelqu’un ne construise un rapport de rapprochement par-dessus
HotPDF exécute donc d’abord une analyse de mise en page sémantique sur les glyphes extraits, la même passe qui alimente l’extraction du texte selon l’ordre structurel d’un PDF chargé ainsi que les exports HTML et XML structurés. Cette passe regroupe les lignes de base en séquences dont les cellules s’alignent verticalement et ne poursuit une séquence que tant que les lignes consécutives ont le même nombre de cellules. Pour un moteur de mise en page, cette règle est correcte et économique. Pour un appelant, elle a la mauvaise forme : une seule ligne avec une cellule intérieure vide sépare un tableau visuel en deux tableaux sources. La couche de tableaux typés se place précisément au-dessus de cette passe pour remettre les morceaux ensemble
Grilles de colonnes canoniques et réglage ColumnTolerance
ExtractLoadedTypedTables fusionne d’abord les fragments d’une même page, avant de faire quoi que ce soit d’autre, et effectue la fusion sur la géométrie des colonnes plutôt que sur le texte des lignes. Deux tableaux sources adjacents sur une page sont réunis lorsqu’ils possèdent chacun au moins deux colonnes, lorsque l’écart vertical entre la dernière ligne du premier et la première ligne du second reste dans la bande de tolérance et lorsque leurs positions de début de colonne sont alignées. Les débuts de colonne distants de moins de ColumnTolerance sont réunis dans une colonne canonique et moyennés au fil des fusions. La tolérance par défaut est de 12 unités de l’espace utilisateur, adaptée à une typographie métier ordinaire, et mérite d’être augmentée pour les mises en page largement crénelées ou fortement indentées
Le traitement d’une ligne qui manque d’une valeur intérieure est le point important. HotPDF rattache chaque cellule au début de colonne canonique le plus proche, puis définit ColumnSpan comme la distance entre cette colonne et la suivante occupée, au lieu de décaler les cellules restantes vers la gauche. Une ligne de trois cellules dans une grille de cinq colonnes conserve ses valeurs sous les bons en-têtes et enregistre exactement l’emplacement des vides. C’est la différence entre un tableau que vous pouvez rapprocher et un tableau qui attribue discrètement de l’argent au mauvais endroit
var
Pdf: THotPDF;
Options: THPDFTypedTableExtractionOptions;
Tables: THPDFTypedTables;
Info: THPDFTypedTableExtractionInfo;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('register.pdf', '') <= 0 then
Exit;
Options := THPDFTypedTableExtractionOptions.Default;
Options.ColumnTolerance := 12; // unités de l’espace utilisateur
Options.MinimumTableConfidence := 0.55; // sous cette valeur, les tableaux sont écartés
Options.DateOrder := ttdoDMY; // 03/04/2026 est le 3 avril
Options.DecimalSeparator := ',';
Options.ThousandsSeparator := '.';
if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
// Info.TableCount par rapport à Info.SourceTableCount indique l’ampleur des fusions
ProcessTables(Tables)
else if Info.Status = ttesBudgetExceeded then
Log(string(Info.Diagnostic));
finally
Pdf.Free;
end;
end;
Que garantit réellement la fusion entre pages ?
Elle garantit la prudence, volontairement. HotPDF ne joint deux tableaux au-delà d’une limite de page que lorsque MergeAcrossPages est activé, que le second tableau commence exactement à l’index de page suivant la fin du premier, que les deux possèdent au moins deux colonnes et qu’au moins deux débuts de colonnes canoniques s’alignent dans ColumnTolerance. La condition de pages consécutives est celle qui porte toute la garantie. Les appelants passent PageIndices comme un tableau ouvert dans l’ordre qu’ils souhaitent, et sans ce contrôle une demande pour les pages 3, 9 et 14 pourrait souder trois tableaux sans rapport en un résultat parfaitement plausible. Le prix est qu’une continuation réelle qui saute une page, une annexe intercalée ou un scan recto-verso avec un verso blanc revient sous la forme de deux tableaux, sans qu’aucune option n’assouplisse la règle. Les réunir à nouveau relève d’une politique que seule l’application appelante peut décider ; l’API expose donc FirstPageIndex, LastPageIndex, SourceTableCount et un PageIndex par ligne, et laisse la décision à sa juste place
Les en-têtes répétés sont étiquetés, jamais supprimés
ExtractLoadedTypedTables ne supprime jamais une ligne d’en-tête répétée du résultat. Lorsqu’une fusion inter-pages constate que le tableau entrant s’ouvre par un texte d’en-tête identique à celui du tableau accumulé, après suppression des espaces superflus et changement de casse, elle marque ces lignes IsHeader et IsRepeatedHeader et les ajoute tout de même dans l’ordre source. La suppression est un choix irréversible et destructeur, tandis que les consommateurs veulent des réponses différentes : un import CSV veut supprimer les répétitions, une piste d’audit veut les conserver avec leur numéro de page et un outil de comparaison veut préserver l’ordre source octet par octet. La bibliothèque signale donc, et l’appelant décide
var
T, R, C: Integer;
Row: THPDFTypedTableRow;
Total: Double;
begin
Total := 0;
for T := 0 to High(Tables) do
for R := 0 to High(Tables[T].Rows) do
begin
Row := Tables[T].Rows[R];
if Row.IsRepeatedHeader then
Continue; // ne conserver que le premier bloc d’en-tête
for C := 0 to High(Row.Cells) do
if Row.Cells[C].ValueKind = ttvkCurrency then
Total := Total + Row.Cells[C].NumberValue;
end;
end;
Valeurs typées et séparateurs à fournir
L’inférence de type suit un ordre fixe qui résout les ambiguïtés dans la seule direction raisonnable : booléen d’abord, puis date, pourcentage, devise et nombre simple, tout ce qui ne correspond à rien restant une chaîne. Cet ordre empêche 2026 dans une colonne de dates d’être interprété par un parseur numérique avant que le parseur de dates ne le voie. Une devise est reconnue à partir d’un $, £, ¥ ou € initial, ou d’un code ISO 4217 de trois lettres suivi d’un espace, et le code est conservé dans CurrencyCode. Point crucial, HotPDF ne devine pas votre locale. DecimalSeparator, ThousandsSeparator et DateOrder viennent des options, car 1.234 désigne soit un nombre, soit mille deux cent trente-quatre selon un fait que le PDF ne contient pas. Le Text Unicode brut est conservé sur chaque cellule à côté de la valeur typée, de sorte qu’une mauvaise supposition reste toujours récupérable sans seconde passe d’extraction
var
Stream: TFileStream;
Info: THPDFTypedTableExtractionInfo;
begin
Stream := TFileStream.Create('tables.json', fmCreate);
try
if not Pdf.ExportLoadedTypedTables([0, 1, 2], ttefJSON,
Stream, Options, Info) then
case Info.Status of
ttesInvalidOptions: ReportBadConfiguration;
ttesBudgetExceeded: ReportOversizedDocument;
ttesCancelled: ReportUserCancelled;
ttesWriteFailed: ReportDestinationProblem;
else
ReportExtractionFailure;
end;
finally
Stream.Free;
end;
end;
Les deux formats d’export répondent à des questions différentes et ne sont volontairement pas équivalents. Le CSV écrit les colonnes de continuation d’une étendue fusionnée comme des champs vides, ce qu’attendent une feuille de calcul ou un chargeur en masse. Le JSON conserve tout ce que l’extraction connaissait : la valeur typée sous son propre genre, columnSpan, la confiance par cellule et par ligne, les limites des cellules, ainsi que la provenance de la page et du tableau source. Les deux formats accumulent le document entier dans un tampon mémoire borné et ne le publient qu’ensuite dans votre flux de destination, en restaurant les octets, la longueur et la position originaux si l’écriture échoue en cours de route ; un export en échec ne laisse donc jamais un fichier à moitié écrit. Les budgets de pages, de glyphes par page, de tableaux, de lignes, de cellules, de caractères et d’octets de sortie sont comptabilisés séparément, et les lignes sont comptées avant l’allocation, car un SetLength par ligne dégénère en copies quadratiques bien avant le plafond par défaut d’un million de lignes
Où la récupération géométrique des tableaux abandonne
Expliciter les modes d’échec est plus utile qu’une liste de fonctionnalités, car chacun de ces cas est un endroit où l’appelant a besoin de sa propre politique plutôt que d’une meilleure valeur d’option
- Les fusions verticales ne sont pas récupérées. HotPDF signale
ColumnSpanpour les étendues horizontales et laisseRowSpanà 1 ; une cellule qui couvre trois lignes du tableau imprimé arrive donc comme une cellule accompagnée de deux vides - La détection des en-têtes dépend des données, pas de l’aspect visuel. Le bloc d’en-tête est la suite de lignes précédant la première ligne qui contient une valeur typée non chaîne ; un tableau dont le corps est entièrement textuel signale donc
HeaderRowCountà zéro, quel que soit son style - Les tableaux sous
MinimumTableConfidencesont écartés du résultat sans erreur. ComparezInfo.TableCountàInfo.SourceTableCountlorsque vous devez savoir si quelque chose a été éliminé - Une séquence doit comporter au moins deux lignes et au moins deux colonnes avant que la passe de mise en page ne l’appelle un tableau, de sorte qu’un pseudo-tableau sur une ligne ou une mise en page à deux colonnes de prose longue n’est correctement, mais peu utilement, pas un tableau
- Les pages numérisées ne contiennent aucun opérateur de texte, et il n’y a donc rien à récupérer géométriquement tant qu’une couche de texte OCR n’existe pas sur la page
Si vos PDF proviennent de votre propre chaîne de reporting, le correctif le moins coûteux est en amont : émettre des tableaux balisés ou conserver les données sources, et traiter l’extraction comme un repli pour les documents que vous n’avez pas produits. Pour le reste, le pipeline mérite d’être appris dans cet ordre, chaque couche s’appuyant sur celle du dessous : commencez par l’extraction de texte simple d’un PDF chargé, passez à l’API de tableaux typés lorsque la géométrie doit être conservée et examinez le rendu d’un tableau de données dans un nouveau PDF lorsque vous êtes du côté génération et pouvez décider du degré de récupérabilité de la sortie
ExtractLoadedTypedTables et ExportLoadedTypedTables sont livrés dans le composant PDF HotPDF pour Delphi, composant VCL natif pour Delphi et C++Builder, sans DLL externe ni dépendance d’exécution ; la page produit contient la référence complète des options, des statuts et des enregistrements de l’API de tableaux typés