Tehnički članak

Ekstrakcija slika iz PDF-a pomoću PDFium Component-a u Delphi-ju

PDF čuva slike kao objekte prvog reda unutar svojih tokova sadržaja (content streams). Kada stranica referencira fotografiju, skeniranu sliku ili dijagram, podaci o pikselima žive u XObject rečniku pored geometrije same stranice. PDFium Component to izlaže preko dva svojstva na TPdf komponenti: BitmapCount, koje vraća koliko se ugrađenih bitmapa nalazi na trenutnoj stranici, i Bitmap[Index], koje dekodira jednu od njih u TBitmap nad kojim preuzimate vlasništvo i koji morate osloboditi. To je ceo model ekstrakcije. Petlja ima četiri reda; ono što zahteva procenu jeste prateća infrastruktura

Otvaranje dokumenta

Prva stvar koju treba znati o TPdf komponenti jeste da postavljanje Active := True nikada ne podiže izuzetke. Neuspesi učitavanja, pogrešne lozinke, oštećene datoteke: sve se to guta interno i komponenta jednostavno ostaje neaktivna. Morate sami da proverite tu zastavicu nakon dodele, ili ćete nastaviti u petlju stranica sa PageCount koji vraća nulu i pitati se zašto ništa nije ekstrahovano

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;

Datoteke zaštićene lozinkom prate isti šablon: dodelite vrednost svojstvu Pdf.Password pre nego što postavite Active := True. Ako je lozinka pogrešna, Active ostaje False i ne dobijate izuzetak koji biste mogli da uhvatite. U paketnom alatu koji obrađuje stotine datoteka, to tiho ponašanje je zapravo korisno: akumulirate neuspehe u listu umesto da odmotavate pozivni stek (call stack) za svaki pojedinačni neuspeh

Prolazak kroz stranice i izvlačenje bitmapa

BitmapCount je po stranici, tako da podešavate Pdf.PageNumber pre nego što ga pročitate. Brojevi stranica počinju od 1; podrazumevana vrednost je 0, što znači da nijedna stranica nije učitana. Svojstvo Bitmap[Index] je indeksirano od nule i vraća TBitmap čiji je vlasnik pozivaoc. Morate ga osloboditi. Zanemarite oslobađanje unutar duge petlje na velikom dokumentu i memorija će brzo rasti, jer svaka bitmapa može predstavljati nekoliko megabajta sirovih podataka o pikselima pre bilo kakve kompresije

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;

Provera pomoću Assigned je važna. Mali broj generatora PDF-a upisuje slikovne XObject-e sa nultim dimenzijama piksela ili na drugi način neispravnim podacima; u tim slučajevima komponenta vraća nil umesto prazne bitmape. Tretiranje nil povrata kao greške i zaustavljanje ekstrakcije je pogrešan refleks: preskočite ga, zabeležite stranicu i indeks ako vam je potreban trag revizije i nastavite dalje. Ostatak stranice i dalje može dati ispravne slike

Primetite da spoljna petlja postavlja Pdf.PageNumber u svakoj iteraciji. Ta dodela je ono što učitava stranicu u interno stanje komponente i čini BitmapCount smislenim. Preskočite to i čitaćete broj slika iste stranice više puta. Šablon izgleda redundantno kada ga pišete, ali tako je API dizajniran: stranica je kursor, a ne kolekcija

Izbor izlaznog formata

BMP je bez gubitaka (lossless) i uvek je dostupan bez dodatnih biblioteka, što ga čini dobrim podrazumevanim izborom kada još uvek ne znate šta slika sadrži. Kada je veličina datoteke važna, format piksela vraćenog TBitmap-a vam govori koji je kodek odgovarajući. 32-bitna bitmapa nosi alfa kanal; PNG to čuva bez gubitaka. Velika 24-bitna slika sa neprekidnim tonovima je kandidat za JPEG. Manje slike ili one nacrtane sa ograničenom paletom boja je generalno bolje ostaviti kao BMP nego ih provlačiti kroz JPEG, koji dodaje blokovske artefakte na postavkama niskog kvaliteta, a štedi malo na visokim postavkama

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;

U praksi, izbor formata diktiraju Bmp.PixelFormat i dimenzije. Ako je PixelFormat = pf32bit, potreban vam je format koji prenosi alfa kanal; PNG is očigledan izbor, iako zahteva PNGImage jedinicu u starijim verzijama Delphi-ja. Za 24-bitne slike šire od otprilike 300 piksela, JPEG sa kvalitetom 85 daje trostruko smanjenje veličine u odnosu na BMP bez primetnog gubitka u većini fotografskih sadržaja. Ispod tog praga, BMP je uporediv po veličini i u potpunosti izbegava bilo kakvu odluku o kvalitetu

Šta BitmapCount broji, a šta ne

PDF razlikuje slikovne XObject-e i vektorsku grafiku nacrtanu operatorima putanje. Stranica koja izgleda vizuelno složeno može vratiti BitmapCount jednak nuli ako je svaki element vektor. Skenirane stranice skoro uvek vraćaju tačno jedan: skener upisuje celo skeniranje kao jedan XObject slike cele stranice u rezoluciji na koju je skener bio podešen. Stranice koje mešaju otkucani tekst sa ugrađenim fotografijama vraćaju po jedan unos za svaku fotografiju. Dekorativne linije, osenčene pozadine i okviri tabela obično se uopšte ne pojavljuju u broju bitmapa

Broj takođe ne uključuje inline slike, retko korišćeni PDF konstrukt gde su podaci o slici ugrađeni direktno u tok sadržaja stranice, a ne kao imenovani XObject. To izlazi van okvira onoga što ovaj API prikazuje; one su neuobičajene u stvarnim dokumentima i većina alata za ekstrakciju ih jednostavno ne obrađuje

Jedan detalj koji vredi imati na umu: BitmapCount koji čitate odnosi se na trenutnu stranicu od poslednje dodele svojstva PageNumber. Ako se vaš kod grana ili poziva bilo koju funkciju koja menja PageNumber između brojanja i preuzimanja, možete pročitati manje slika nego što ste alocirali prostora, ili indeksirati van opsega. Držite čitanje broja i petlju Bitmap[] na istoj stranici bez dodirivanja svojstva PageNumber u međuvremenu

Korišćenje TPdfView u aplikaciji sa formom

Memorija i performanse u paketnim poslovima

U celoj arhivi, budžet memorije je glavna stvar na koju treba paziti. Svaki poziv Bitmap[] alocira novi TBitmap na hipu (heap), a na skeniranoj stranici od 300 DPI to je lako 25 MB sirovih podataka o pikselima pre bilo kakvog kodiranja. Ako obrađujete stranice u tesnoj petlji bez oslobađanja između iteracija, radni skup raste linearno sa brojem slika. Ispravan oblik je uvek: preuzmite jednu bitmapu, uradite šta vam treba, oslobodite je, preuzmite sledeću. Ako treba da držite reference na nekoliko bitmapa odjednom radi koraka poređenja, prvo ih prebrojte pomoću BitmapCount i u skladu sa tim alocirajte svoj kontejner, a zatim oslobodite svaku čim završite sa njom, umesto da odlažete čišćenje za kraj dokumenta. Na dokumentu sa 500 skeniranih stranica ta razlika može značiti razliku između 25 MB i 12 GB vršne memorije (peak RSS)

Komponenta TPdfView izlaže ista svojstva BitmapCount i Bitmap[], ali stranica sa koje čita je trenutno prikazana stranica prikaza, a ne TPdf.PageNumber. Dva pokazivača stranica su nezavisna; postavljanje jednog ne pomera drugi. U VCL aplikaciji sa formom sa prikazom uživo, možete pozvati Pdf.PageNumber := N da biste pokrenuli ekstrakciju preko TPdf dok prikaz ostaje na onome što je korisnik poslednje skrolovao. To razdvajanje je namerno i održava stanje prikaza čitača čistim dok se u pozadini odvija ekstrakcija

Svojstva BitmapCount i Bitmap[] prikazana ovde su deo PDFium Component komponente za Delphi i C++Builder