Technisch artikel

Afbeeldingen extraheren uit PDF's met PDFium-component in Delphi

PDF slaat afbeeldingen op als eersteklas objecten (first-class objects) binnen de content streams (inhoudsstromen). Wanneer een pagina verwijst naar een foto, een scan of een diagram, bevinden de pixelgegevens zich in een XObject dictionary (woordenboek) naast de paginageometrie. De PDFium-component brengt dat naar de oppervlakte (surfaces that) via twee eigenschappen op TPdf: BitmapCount, dat retourneert hoeveel ingesloten (embedded) bitmaps er op de huidige pagina staan, en Bitmap[Index], die een ervan decodeert naar een TBitmap waarvan u de eigenaar bent en die u moet vrijmaken (free). Dat is het gehele extractiemodel. De loop (lus) beslaat vier regels; wat beoordelingsvermogen (judgment) vergt, is het omliggende loodgieterswerk (plumbing)

Het document openen

Het eerste dat u moet weten over TPdf is dat Active := True nooit exception opwerpt (raises). Laadfouten (load failures), verkeerde wachtwoorden, beschadigde (corrupted) bestanden: ze worden allemaal intern ingeslikt en de component blijft simpelweg inactief. U moet de flag (vlag) zelf controleren na de toewijzing, anders gaat u door naar de pagina-lus waarbij PageCount nul retourneert en u zich afvraagt 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;

Met een wachtwoord beveiligde 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 exceptie (exception) om af te vangen. In een batchtool die honderden bestanden verwerkt, is dat stille gedrag eigenlijk nuttig: u verzamelt de fouten in een lijst in plaats van de call stack (aanroepstapel) voor elk bestand af te wikkelen (unwinding)

Door pagina's itereren en bitmaps ophalen

BitmapCount is per pagina. U stelt dus Pdf.PageNumber in voordat u dit uitleest. Paginanummers zijn 1-gebaseerd (1-based); de standaardwaarde is 0, wat betekent dat er geen pagina is geladen. De eigenschap Bitmap[Index] is 0-gebaseerd en retourneert een door de aanroeper (caller-owned) in eigendom gehouden TBitmap. U moet deze vrijgeven (free). Verwaarloos de vrijgave (free) binnen een lange loop (lus) over een groot document en het geheugengebruik stijgt snel, omdat elke bitmap enkele megabytes aan ruwe pixelgegevens kan bevatten voorafgaand aan enige compressie

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 Assigned-beveiliging (guard) is van belang. Een klein aantal PDF-generatoren schrijft image XObjects met pixelafmetingen van nul of anderszins onjuist gevormde (malformed) gegevens; in die gevallen retourneert de component nil in plaats van een lege bitmap. Het behandelen van een nil-retour als een fout en het stoppen van de extractie is de verkeerde reflex: sla het over (skip it), log de pagina en index als u het audittraject nodig hebt, en ga verder. De rest van de pagina kan nog steeds geldige afbeeldingen opleveren

Let op dat de buitenste loop (outer loop) bij elke iteratie Pdf.PageNumber instelt. Die toewijzing is wat de pagina in de interne status van de component laadt en BitmapCount betekenisvol maakt. Als u dit overslaat, leest u herhaaldelijk de telling van dezelfde pagina. Het patroon voelt overbodig wanneer u het schrijft, maar zo is de API ontworpen: de pagina is een cursor, geen collectie

Kiezen van een uitvoerformaat (output format)

BMP is verliesvrij (lossless) en altijd beschikbaar zonder extra units (eenheden), wat het een degelijke standaard (sound default) maakt wanneer u nog niet weet wat de afbeelding bevat. Wanneer de bestandsgrootte van belang is, vertelt het pixelformaat van de geretourneerde TBitmap u welke codec geschikt is. Een 32-bits bitmap draagt een alfakanaal; PNG behoudt dit zonder verlies. Een grote 24-bits afbeelding met continue tonen (continuous tone) is een kandidaat voor JPEG. Kleinere afbeeldingen of afbeeldingen getekend met een beperkt palet kunt u over het algemeen beter als BMP laten in plaats van ze door JPEG te halen. JPEG voegt blokkerige artefacten (blocking artifacts) toe bij lage kwaliteitsinstellingen en bespaart weinig bij hoge instellingen

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 formaatselectie (format selection) gestuurd door Bmp.PixelFormat en afmetingen. Als PixelFormat = pf32bit, hebt u een formaat nodig dat alpha draagt; PNG is dan de logische keuze, al vereist het de unit PNGImage in oudere Delphi-versies. Voor 24-bits afbeeldingen die breder zijn dan ruwweg 300 pixels, geeft JPEG bij een kwaliteit van 85 een groottereductie van drie-op-een ten opzichte van BMP, zonder waarneembaar verlies in de meeste fotografische inhoud. Onder die drempelwaarde is BMP vergelijkbaar in grootte en vermijdt het elke beslissing over kwaliteit volledig

Wat BitmapCount wel en niet telt

PDF maakt onderscheid tussen image XObjects en vector graphics die getekend zijn met pad-operatoren (path operators). 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 exact één: de scanner schrijft de gehele scan als een enkel paginagroot image XObject op de resolutie waarop de scanner was ingesteld. Pagina's die gezette tekst (typeset text) mengen met ingesloten foto's, retourneren één vermelding per foto. Decoratieve lijnen (rule lines), gearceerde achtergronden en tabelranden verschijnen doorgaans helemaal niet in de bitmap-telling

De telling omvat ook geen inline images (inline-afbeeldingen), een zelden gebruikte PDF-constructie waarbij afbeeldingsgegevens rechtstreeks in de content stream van de pagina worden ingesloten (embedded) in plaats van als een benoemd XObject. Deze vallen buiten wat deze API naar de oppervlakte brengt (surfaces); ze zijn dusdanig ongebruikelijk in echte documenten dat de meeste extractietools ze simpelweg niet afhandelen

Eén detail dat de moeite van het onthouden waard is: de BitmapCount die u leest is voor de huidige pagina, gebaseerd op de laatste PageNumber-toewijzing. Als uw code vertakt (branches) of een functie aanroept die PageNumber wijzigt tussen het tellen en ophalen (fetching), leest u mogelijk minder afbeeldingen dan waarvoor u ruimte hebt toegewezen, of indexeert u voorbij het einde. Behoud het lezen van de telling (count read) en de Bitmap[]-loop op dezelfde pagina, zonder tussendoor aan PageNumber te komen

TPdfView gebruiken in een form-applicatie

De TPdfView-component legt dezelfde eigenschappen voor BitmapCount en Bitmap[] bloot (exposes), maar de pagina waaruit het leest is de momenteel in de view weergegeven pagina, niet TPdf.PageNumber. De twee pagina-pointers (aanwijzers) zijn onafhankelijk; het instellen van de één verplaatst de ander niet. In een VCL-formapplicatie met een live viewer, kunt u Pdf.PageNumber := N aanroepen om extractie te sturen (drive) via TPdf, terwijl de viewer blijft staan op datgene waar de gebruiker het laatst naar is gescrold. Die scheiding is opzettelijk en houdt de weergavestatus (display state) van de viewer schoon terwijl er op de achtergrond een extractie draait (background extraction runs)

Geheugen en prestaties in batch jobs

Over een groot archief heen is het geheugenbudget het belangrijkste om in de gaten te houden. Elke Bitmap[]-aanroep wijst een nieuwe TBitmap toe op de heap, en op een gescande pagina van 300 DPI is dat gemakkelijk 25 MB aan ruwe pixelgegevens (raw pixel data) vóór enige codering (encoding). Als u pagina's in een krappe lus (tight loop) verwerkt zonder ze tussen iteraties vrij te maken (freeing), groeit de working set lineair met het aantal afbeeldingen. De juiste vorm is altijd: haal één bitmap op, doe wat u moet doen, geef hem vrij, haal de volgende op. Als u meerdere bitmaps tegelijk moet vasthouden voor een vergelijkingsstap (comparison step), tel ze dan eerst met BitmapCount en wijs uw container dienovereenkomstig (accordingly) toe. Geef er vervolgens telkens één vrij zodra u ermee klaar bent, in plaats van het uit te stellen (deferring) tot het opschonen aan het eind van het document. Bij een document met 500 gescande pagina's kan dat onderscheid het verschil betekenen tussen 25 MB en 12 GB piekgrootte van de resident set (peak RSS)

De BitmapCount- en Bitmap[]-eigenschappen die hier worden getoond, maken deel uit van de PDFium-component voor Delphi en C++Builder