Technisch artikel

Afbeeldingen extraheren uit PDF-documenten met PDFium VCL in Delphi

PDF slaat afbeeldingen op als volwaardige objecten binnen de inhoudsstromen. Wanneer een pagina verwijst naar een foto, een scan of een diagram, bevindt de pixeldata zich in een XObject-dictionary naast de paginageometrie. PDFium VCL maakt dit toegankelijk via twee eigenschappen op TPdf: BitmapCount, dat teruggeeft hoeveel ingebedde bitmaps er op de huidige pagina staan, en Bitmap[Index], dat een daarvan decodeert naar een TBitmap die u bezit en moet vrijgeven. Dat is het volledige extractiemodel. De lus bestaat uit vier regels; wat inzicht vereist is de omliggende structuur.

Het document openen

Het eerste dat u moet weten over TPdf is dat Active := True nooit een uitzondering genereert. Fouten bij het laden, onjuiste wachtwoorden en corrupte bestanden worden intern onderdrukt, waardoor de component simpelweg inactief blijft. U moet de statusindicator zelf controleren na de toewijzing, anders gaat u door naar de paginalus terwijl PageCount nul retourneert, en vraagt u zich af waarom er niets is geëxtraheerd.

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // proceed to extraction
  finally
    Pdf.Free;
  end;
end;

Wachtwoordbeveiligde bestanden volgen hetzelfde patroon: wijs Pdf.Password toe voordat u Active := True instelt. Als het wachtwoord onjuist is, blijft Active op False staan en krijgt u geen uitzondering om op te vangen. In een batchtool die honderden bestanden verwerkt, is dit stille gedrag eigenlijk nuttig: u verzamelt de fouten in een lijst in plaats van dat u voor elk bestand de callstack moet afwikkelen.

Pagina's doorlopen en bitmaps ophalen

BitmapCount werkt per pagina, dus u stelt Pdf.PageNumber in voordat u deze uitleest. Paginanummers zijn 1-based; de standaardwaarde is 0, wat betekent dat er geen pagina is geladen. De eigenschap Bitmap[Index] is 0-based en retourneert een TBitmap waarvan de aanroeper de eigenaar is. U moet deze zelf vrijgeven. Als u de vrijgave vergeet binnen een lange lus over een groot document, loopt het geheugengebruik snel op, omdat elke bitmap enkele megabytes aan ruwe pixeldata kan bevatten nog voordat er compressie is toegepast.

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;

De controle met Assigned is belangrijk. Een klein aantal PDF-generators schrijft afbeelding-XObjects met nulpixels of andere misvormde gegevens; in die gevallen retourneert de component nil in plaats van een lege bitmap. Een nil-waarde als fout beschouwen en de extractie stoppen is de verkeerde reflex: sla deze over, log eventueel de pagina en index voor controle, en ga verder. De rest van de pagina kan nog steeds geldige afbeeldingen bevatten.

Merk op dat de buitenste lus bij elke iteratie Pdf.PageNumber instelt. Deze toewijzing laadt de pagina in de interne status van de component en zorgt ervoor dat BitmapCount de juiste waarde heeft. Als u dit overslaat, leest u herhaaldelijk de telling van dezelfde pagina. Dit patroon voelt misschien overbodig wanneer u het schrijft, maar zo is de API ontworpen: de pagina fungeert als een cursor, niet als een collectie.

Een uitvoerformaat kiezen

BMP is verliesvrij (lossless) en altijd beschikbaar zonder extra units, wat het een logische standaard maakt wanneer u nog niet weet wat de afbeelding bevat. Als bestandsgrootte belangrijk is, laat het pixelformaat van de geretourneerde TBitmap zien welke codec geschikt is. Een 32-bit bitmap bevat een alfakanaal; PNG behoudt dit zonder kwaliteitsverlies. Een grote 24-bit afbeelding met vloeiende kleurovergangen is geschikt voor JPEG. Kleinere afbeeldingen of afbeeldingen met een beperkt kleurenpalet kunnen over het algemeen beter als BMP worden opgeslagen in plaats van via JPEG te worden verwerkt. JPEG veroorzaakt blokartefacten bij lage kwaliteitsinstellingen en levert bij hoge instellingen nauwelijks winst op.

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: lossless, no extra units
  end;
end;

In de praktijk wordt de formaatselectie bepaald door Bmp.PixelFormat en de afmetingen. Als PixelFormat = pf32bit, heeft u een indeling nodig die transparantie ondersteunt; PNG is dan de logische keuze, hoewel dit in oudere Delphi-versies de unit PNGImage vereist. Voor 24-bit afbeeldingen die breder zijn dan ongeveer 300 pixels, biedt JPEG met een kwaliteit van 85 een verkleining van drie op één ten opzichte van BMP, zonder waarneembaar kwaliteitsverlies bij de meeste foto's. Onder die grens is BMP vergelijkbaar in grootte en vermijdt u elke kwaliteitsafweging.

Wat BitmapCount wel en niet telt

PDF maakt onderscheid tussen afbeelding-XObjects en vectorafbeeldingen die met pad-operators zijn getekend. Een pagina die er visueel complex uitziet, kan een BitmapCount van nul retourneren als elk element een vector is. Gescande pagina's retourneren bijna altijd precies één: de scanner schrijft de volledige scan als een enkele paginagrote afbeelding-XObject op de ingestelde scannerresolutie. Pagina's die tekst combineren met ingebedde foto's retourneren één item per foto. Decoratieve lijnen, gekleurde achtergronden en tabelranden verschijnen meestal helemaal niet in de bitmap-telling.

De telling omvat ook geen inline-afbeeldingen, een zelden gebruikte PDF-constructie waarbij de afbeeldingsgegevens rechtstreeks in de inhoudsstroom van de pagina zijn ingebed in plaats van als een benoemd XObject. Deze vallen buiten het bereik van deze API; ze komen zo weinig voor in echte documenten dat de meeste extractietools ze simpelweg niet ondersteunen.

Een belangrijk detail om te onthouden: de uitgelezen BitmapCount is gekoppeld aan de huidige pagina van de laatste toewijzing aan PageNumber. Als uw code vertakt of een functie aanroept die PageNumber wijzigt tussen het tellen en ophalen, leest u mogelijk minder afbeeldingen dan waarvoor u ruimte heeft gereserveerd, of indexeert u buiten het bereik. Zorg dat het lezen van de telling en de Bitmap[]-lus op dezelfde pagina plaatsvinden, zonder tussendoor PageNumber te wijzigen.

TPdfView gebruiken in een formulierapplicatie

Geheugen en prestaties bij batchverwerking

Bij een groot archief is het geheugenbudget het belangrijkste om in de gaten te houden. Elke aanroep van Bitmap[] alloceert een nieuwe TBitmap op de heap, en bij een gescande pagina van 300 DPI is dat al snel 25 MB aan ruwe pixeldata nog voor eventuele codering. Als u pagina's verwerkt in een strakke lus zonder vrijgave tussen de iteraties, groeit de werkset lineair met het aantal afbeeldingen. De juiste methode is altijd: haal één bitmap op, voer de nodige bewerkingen uit, geef deze vrij en haal de volgende op. Als u verwijzingen naar meerdere bitmaps tegelijk moet vasthouden voor een vergelijkingsstap, tel ze dan eerst met BitmapCount en alloceer uw container dienovereenkomstig. Geef vervolgens elke bitmap vrij zodra u ermee klaar bent, in plaats van de opruiming uit te stellen tot het einde van het document. Bij een document met 500 gescande pagina's kan dit verschil betekenen tussen 25 MB en 12 GB piek-RSS.

De component TPdfView biedt dezelfde eigenschappen BitmapCount en Bitmap[], maar de pagina die deze leest is de momenteel weergegeven pagina van de weergave, niet TPdf.PageNumber. De twee paginapointer-variabelen zijn onafhankelijk van elkaar; het instellen van de ene verplaatst de andere niet. In een VCL-formulierapplicatie met een live viewer kunt u Pdf.PageNumber := N aanroepen om de extractie via TPdf uit te voeren, terwijl de viewer blijft staan op de pagina waarnaar de gebruiker het laatst heeft gescrold. Die scheiding is opzet en houdt de weergavestatus van de viewer schoon terwijl er op de achtergrond een extractie loopt.

De hier getoonde eigenschappen BitmapCount en Bitmap[] maken deel uit van de PDFium VCL Component voor Delphi en C++Builder.