Article technique

Extraction d'images de PDF avec le composant PDFium dans Delphi

Le format PDF stocke les images en tant qu'objets de première classe (first-class objects) dans ses flux de contenu (content streams). Lorsqu'une page fait référence à une photographie, une numérisation (scan) ou un diagramme, les données de pixels se trouvent dans un dictionnaire XObject à côté de la géométrie de la page. Le composant PDFium fait remonter cela via deux propriétés sur TPdf : BitmapCount, qui renvoie le nombre de bitmaps intégrés (embedded bitmaps) sur la page courante, et Bitmap[Index], qui décode l'un d'eux dans un TBitmap que vous possédez et devez libérer. C'est tout le modèle d'extraction. La boucle fait quatre lignes ; ce qui demande du discernement (judgment), c'est la plomberie environnante

Ouverture du document

La première chose à savoir sur TPdf est qu'Active := True ne lève jamais d'exception. Échecs de chargement, mots de passe incorrects, fichiers corrompus : tous sont engloutis (swallowed) en interne et le composant reste simplement inactif. Vous devez vérifier le drapeau vous-même après l'affectation, sinon vous passerez dans la boucle de page avec PageCount renvoyant zéro et vous vous demanderez pourquoi rien n'a été extrait

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Impossible d''ouvrir : ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // procéder à l'extraction
  finally
    Pdf.Free;
  end;
end;

Les fichiers protégés par mot de passe suivent le même modèle : affectez Pdf.Password avant de définir Active := True. Si le mot de passe est incorrect, Active reste False et vous n'obtenez aucune exception à intercepter. Dans un outil de traitement par lots traitant des centaines de fichiers, ce comportement silencieux est en fait utile : vous accumulez les échecs dans une liste plutôt que de dérouler (unwinding) la pile d'appels pour chacun d'eux

Itération des pages et extraction (pulling) des bitmaps

BitmapCount est par page, vous définissez donc Pdf.PageNumber avant de le lire. Les numéros de page sont en base 1 ; la valeur par défaut est 0, ce qui signifie qu'aucune page n'est chargée. La propriété Bitmap[Index] est en base 0 et renvoie un TBitmap appartenant à l'appelant (caller-owned). Vous devez le libérer (free). Négligez la libération (free) dans une longue boucle sur un document volumineux et la mémoire grimpe rapidement, car chaque bitmap peut représenter plusieurs mégaoctets de données de pixels brutes (raw pixel data) avant toute compression

procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

La garde (guard) Assigned compte. Un petit nombre de générateurs PDF écrivent des XObjects image avec des dimensions de pixels nulles ou d'autres données malformées ; dans ces cas, le composant renvoie nil plutôt qu'un bitmap vide. Traiter un retour nil comme une erreur et arrêter l'extraction est le mauvais réflexe : passez-le (skip it), enregistrez la page et l'index si vous avez besoin d'une piste d'audit (audit trail), et continuez. Le reste de la page peut encore produire (yield) des images valides

Notez que la boucle externe définit Pdf.PageNumber à chaque itération. C'est cette affectation qui charge la page dans l'état interne du composant et donne un sens à BitmapCount. Sautez-la et vous lisez le décompte (count) de la même page à plusieurs reprises. Le modèle semble redondant lorsque vous l'écrivez, mais c'est ainsi que l'API est conçue : la page est un curseur, pas une collection

Choix d'un format de sortie

BMP est sans perte (lossless) et toujours disponible sans unités supplémentaires, ce qui en fait un bon choix par défaut lorsque vous ne savez pas encore ce que contient l'image. Lorsque la taille du fichier compte, le format de pixel (pixel format) du TBitmap renvoyé vous indique quel codec est approprié. Un bitmap 32 bits porte un canal alpha ; PNG le préserve sans perte. Une grande image 24 bits à tons continus (continuous tone) est candidate pour JPEG. Il est généralement préférable de laisser les images plus petites ou celles dessinées avec une palette limitée sous forme de BMP plutôt que de les passer en JPEG, qui ajoute des artefacts de bloc à des paramètres de qualité faible et économise peu à des paramètres élevés

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP : sans perte, pas d'unités supplémentaires
  end;
end;

Dans la pratique, le choix du format dépend de Bmp.PixelFormat et des dimensions. Si PixelFormat = pf32bit, vous avez besoin d'un format qui transporte l'alpha ; PNG est le choix évident, bien qu'il nécessite l'unité PNGImage dans les anciennes versions de Delphi. Pour les images 24 bits plus larges qu'environ 300 pixels, JPEG à la qualité 85 donne une réduction de taille de trois pour un par rapport au BMP sans perte perceptible dans la plupart des contenus photographiques. En dessous de ce seuil, le BMP est de taille comparable et évite totalement toute décision de qualité

Ce que BitmapCount compte et ne compte pas

Le format PDF fait la distinction entre les XObjects image et les graphiques vectoriels dessinés avec des opérateurs de chemin (path operators). Une page qui semble visuellement complexe peut renvoyer un BitmapCount de zéro si chaque élément est vectoriel. Les pages numérisées (scanned pages) en renvoient presque toujours exactement un : le scanner écrit la numérisation (scan) entière sous la forme d'un seul XObject image pleine page, quelle que soit la résolution à laquelle le scanner a été défini. Les pages qui mélangent du texte composé (typeset text) avec des photographies intégrées renvoient une entrée par photographie. Les lignes de règle (rule lines) décoratives, les arrière-plans ombrés (shaded backgrounds) et les bordures de tableau n'apparaissent généralement pas du tout dans le décompte de bitmaps

Le décompte n'inclut pas non plus les images en ligne (inline images), une construction PDF rarement utilisée où les données d'image sont intégrées directement dans le flux de contenu de la page plutôt que sous la forme d'un XObject nommé. Celles-ci sortent du cadre de ce que cette API fait remonter (surfaces) ; elles sont suffisamment rares dans les documents réels pour que la plupart des outils d'extraction ne les gèrent tout simplement pas

Un détail qui mérite d'être gardé à l'esprit : le BitmapCount que vous lisez concerne la page courante au moment de la dernière affectation de PageNumber. Si votre code bifurque (branches) ou appelle une fonction qui modifie PageNumber entre le comptage (counting) et la récupération (fetching), vous risquez de lire moins d'images que l'espace alloué, ou d'indexer au-delà de la fin (index past the end). Gardez la lecture du décompte et la boucle Bitmap[] sur la même page sans toucher à PageNumber entre les deux

Utilisation de TPdfView dans une application de formulaire

Le composant TPdfView expose les mêmes propriétés BitmapCount et Bitmap[], mais la page à partir de laquelle il lit est la page actuellement affichée par la vue, et non TPdf.PageNumber. Les deux pointeurs de page sont indépendants ; régler l'un ne déplace pas l'autre. Dans une application de formulaire VCL avec une visionneuse en direct (live viewer), vous pouvez appeler Pdf.PageNumber := N pour piloter l'extraction via TPdf pendant que la visionneuse reste sur ce que l'utilisateur a fait défiler en dernier. Cette séparation est intentionnelle et maintient l'état d'affichage de la visionneuse propre pendant qu'une extraction en arrière-plan s'exécute

Mémoire et performances dans les traitements par lots (batch jobs)

Dans de grandes archives (archive), le budget de la mémoire est la chose principale à surveiller. Chaque appel Bitmap[] alloue un nouveau TBitmap sur le tas (heap), et sur une page numérisée à 300 DPI, cela représente facilement 25 Mo de données de pixels brutes avant tout encodage. Si vous traitez des pages dans une boucle serrée (tight loop) sans libérer (freeing) entre les itérations, l'ensemble de travail croît linéairement avec le nombre d'images. La forme correcte est toujours la suivante : récupérez un bitmap, faites ce dont vous avez besoin, libérez-le, récupérez le suivant. Si vous avez besoin de conserver des références à plusieurs bitmaps à la fois pour une étape de comparaison, comptez-les d'abord avec BitmapCount et allouez votre conteneur en conséquence, puis libérez chacun d'eux dès que vous en avez terminé plutôt que de reporter au nettoyage (cleanup) de fin de document. Sur un document de 500 pages numérisées, cette distinction peut faire la différence entre 25 Mo et 12 Go de RSS maximum (peak RSS)

Les propriétés BitmapCount et Bitmap[] présentées ici font partie du Composant PDFium pour Delphi et C++Builder