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