Article technique

Extraire des tableaux de pages PDF en Delphi avec PDFium

PDFium Component détecte les tableaux sur une page PDF et les retourne sous forme de grille de cellules avec fusions de lignes et de colonnes, lignes d'en-tête et une valeur de confiance, via ExtractTables pour une page et ExtractDocumentTables pour un document entier. Chaque tableau se convertit en CSV ou JSON en un seul appel, et les tableaux qui se poursuivent au-delà d'un saut de page peuvent être reliés en une chaîne de continuation

Le PDF n'a pas d'objet tableau. Un tableau dans un PDF est un ensemble de fragments de texte positionnés de façon à ce qu'un humain les lise comme une grille, parfois entourés de lignes tracées et souvent non. Reconstruire la grille signifie reconstituer une intention que le fichier n'a jamais enregistrée, ce qui explique pourquoi chaque outil d'extraction produit des résultats légèrement différents et pourquoi un outil qui indique sa confiance est plus utile qu'un outil qui ne le fait pas

Deux modes de détection pour deux types de tableaux

La détection par lignes tracées utilise les lignes dessinées. Chaque segment de tracé est transformé en coordonnées de page via la matrice de l'objet page, les lignes horizontales et verticales sont intersectées, et les intersections forment des composantes connexes. Chaque composante devient sa propre grille triée de positions X et Y, ce qui empêche deux tableaux distincts sur la même page d'être fusionnés en une grille absurde unique

La détection par espaces gère les tableaux dessinés par alignement plutôt que par des lignes. Les boîtes de mots sont regroupées en lignes visuelles, les écarts à l'intérieur d'une ligne la divisent en colonnes candidates, et un tableau n'est accepté que lorsqu'au moins MinRows lignes répètent au moins MinColumns ancrages alignés à gauche dans AlignmentTolerance. Le facteur d'écart entre lignes vaut 3 par défaut, ce qui couvre l'espacement de ligne de base d'environ 30 points typique d'un texte de 12 points sans laisser une seule ligne contenant plusieurs fragments de texte se faire passer pour un tableau

Schéma du pipeline de détection de tableaux de PDFium Component en Delphi, où les intersections de lignes tracées et les lignes de mots alignés par espaces alimentent un même enregistrement de tableau noté avec export CSV et JSON
La détection par lignes intersecte les tracés dessinés tandis que la détection par espaces compte les lignes de boîtes de mots alignées ; les candidats franchissant MinRows et MinColumns ressortent avec un score de confiance et un DetectionMode associés
uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'annual-report.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 12;                    // basé sur 1

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // la valeur par défaut est 0.5
    Options.HeaderRowCount := 1;

    Tables := Pdf.ExtractTables(Options);
    for I := 0 to High(Tables) do
      Writeln(Format('table %d: %dx%d cells, confidence %.2f, mode %d',
        [I, Tables[I].RowCount, Tables[I].ColumnCount,
         Tables[I].Confidence, Ord(Tables[I].DetectionMode)]));

    if Length(Tables) > 0 then
      SaveText('page12-table0.csv', Tables[0].ToCsv);
  finally
    Pdf.Free;
  end;
end;

Comment les cellules fusionnées sont-elles retrouvées ?

C'est la partie que les extracteurs naïfs traitent mal. Une cellule fusionnée ne peut pas être identifiée à partir de la seule grille globale, car la grille est dérivée de toutes les lignes de la page et une région fusionnée manque simplement de la ligne intérieure qui l'aurait séparée

La règle utilisée ici est locale : deux cellules de base adjacentes sont fusionnées quand aucune ligne de frontière ne couvre l'intervalle entre elles. Une union-find les relie, les composantes rectangulaires résultantes deviennent des valeurs RowSpan et ColumnSpan, et le texte est assigné à une cellule de base par son point central puis suit cette cellule jusqu'à sa racine de fusion. Procéder ainsi garde aussi le coût linéaire en mots plus cellules, au lieu du balayage quadratique que produit le test de chaque mot contre chaque cellule

Schéma de la récupération des cellules fusionnées dans l'extraction de tableaux PDFium pour Delphi, où une union-find relie les cellules de base adjacentes chaque fois qu'aucune ligne de frontière ne couvre l'intervalle entre elles, produisant RowSpan et ColumnSpan
L'union-find fusionne les cellules de base voisines dont l'intervalle partagé ne porte aucune frontière tracée, si bien qu'un en-tête fusionné revient comme une seule cellule avec ColumnSpan défini plutôt qu'une cellule remplie flanquée de cellules vides

L'effet pratique est qu'un tableau financier avec un en-tête « Total » fusionné s'étendant sur trois colonnes ressort avec une cellule de fusion trois, plutôt qu'une cellule peuplée et deux vides mystérieuses

Continuation entre les pages

Les longs tableaux se coupent entre les pages, et traiter le fragment de chaque page comme un tableau indépendant force l'appelant à les recoudre. ExtractDocumentTables peut les relier à la place, mais seulement sous des conditions strictes : le fragment doit être le tableau le plus bas de la page précédente, le suivant doit être le tableau le plus haut de la page suivante, les numéros de page doivent être adjacents, et les frontières de colonnes doivent correspondre

Les quatre conditions réunies sont ce qui empêche l'erreur évidente, qui consisterait à enchaîner tous les tableaux à quatre colonnes d'un document en une méga-table imaginaire simplement parce qu'ils partagent un nombre de colonnes. Quand les conditions sont réunies, les tableaux partagent un identifiant de groupe de continuation et portent des métadonnées de continuation ; quand elles ne le sont pas, vous obtenez des tableaux séparés et pouvez décider par vous-même

Schéma de la continuation de tableau entre pages PDF en Delphi, où quatre conditions strictes décident si le fragment le plus bas d'une page rejoint le fragment le plus haut de la suivante
L'extraction au niveau du document relie les fragments uniquement quand les quatre conditions sont réunies, ce qui empêche des tableaux à quatre colonnes sans rapport de fusionner en une méga-table imaginaire

L'extraction au niveau du document partage les budgets MaxCells et MaxTables entre les pages plutôt que de les réinitialiser par page, et elle restaure la page active dans un bloc finally, si bien qu'une extraction lancée dans un visualiseur laisse l'utilisateur sur la page où il se trouvait

Exporter sans corrompre les données

Les deux exportateurs sont rigoureux sur l'échappement. Le CSV met toujours les champs entre guillemets et double les guillemets internes, ce qui évite l'échec classique où une cellule contenant une virgule devient silencieusement deux colonnes. Pour les cellules fusionnées, le contenu n'est émis qu'à l'ancre supérieure gauche, si bien qu'un aller-retour CSV ne duplique pas un en-tête étendu sur les colonnes qu'il couvre

Le JSON préserve l'Unicode plutôt que de l'échapper en ASCII, échappe les caractères de contrôle, et inclut les métadonnées dont un consommateur a besoin pour juger de la qualité : mode de détection, confiance, limites, valeurs de fusion, indicateurs d'en-tête et informations de continuation. Si vous alimentez un système en aval avec des tableaux extraits, préférez JSON, car une ligne CSV ne peut pas vous dire que le tableau dont elle provient a obtenu un score de confiance de 0,51 :

// Extraction à l'échelle du document, en ne gardant que les tableaux dignes de confiance
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
begin
  if Tables[I].Confidence < 0.75 then
  begin
    Log(Format('page %d table needs review (%.2f)',
      [Tables[I].PageNumber, Tables[I].Confidence]));
    Continue;
  end;
  if Tables[I].ContinuationGroup > 0 then
    AppendToGroup(Tables[I].ContinuationGroup, Tables[I].ToJson)
  else
    EmitStandalone(Tables[I].ToJson);
end;

Réglage, et savoir quand s'arrêter

Trois réglages comptent plus que les autres. MinConfidence est le seuil de qualité, et 0,5 est délibérément permissif ; augmentez-le pour une ingestion automatisée et abaissez-le pour une interface de révision où un humain confirme chaque résultat. MinColumnGap décide de ce qui constitue une frontière de colonne en mode espaces, et les tableaux resserrés des rapports denses peuvent nécessiter de le réduire par rapport à la valeur par défaut de 12 points. MaxRowGapFactor décide quand une distance verticale met fin à un tableau, ce qui compte pour les tableaux comportant des lignes vides occasionnelles

Soyez honnête sur les limites. Les tableaux à lignes tracées s'extraient de façon fiable. Les tableaux à espaces proprement alignés s'extraient bien. Les tableaux avec du texte pivoté, des tableaux imbriqués, ou des cellules dont le contenu se replie en quelque chose qui ressemble à une autre ligne nécessiteront une révision quels que soient les paramètres. Pour ces cas, le modèle de texte structuré vous donne la matière première pour construire un lecteur spécifique à votre domaine, décrit dans les blocs de texte structuré et l'ordre de lecture

Une association utile : quand un document scanné n'a aucun texte du tout, la détection de tableaux n'a rien avec quoi travailler tant qu'une couche de texte n'existe pas. Ajoutez-en une d'abord, comme décrit dans ajouter une couche de texte consultable aux PDF scannés, puis extrayez. Les boîtes de mots qu'un fournisseur OCR retourne sont exactement l'entrée dont la détection par espaces a besoin

L'extraction de tableaux, le texte structuré et le reflux se lisent tous depuis le même modèle de page en Delphi, C++Builder et Lazarus ; l'API complète est décrite sur la page du composant PDFium pour Delphi