Technical Article

Ekstrakcija slika iz PDF-ova pomoću PDFium VCL-a u Delphiju

PDF pohranjuje slike kao objekte prve klase (first-class objects) unutar svojih tokova sadržaja (content streams). Kada stranica referencira fotografiju, skenirani dokument ili dijagram, pikselni podaci žive u rječniku XObject (XObject dictionary) uz geometriju stranice. PDFium VCL to izlaže kroz dva svojstva na TPdf: BitmapCount, koje vraća koliko je ugrađenih bitmapa na trenutnoj stranici, i Bitmap[Index], koje dekodira jednu od njih u TBitmap čiji ste vi vlasnik i kojeg morate osloboditi. To je cijeli model ekstrakcije. Petlja ima svega četiri retka; ono što zahtijeva pažnju je prateća struktura.

Otvaranje dokumenta

Prva stvar koju treba znati o TPdf jest da Active := True nikada ne podiže iznimku. Neuspjesi učitavanja, pogrešne lozinke, oštećene datoteke: sve se to interno guta i komponenta jednostavno ostaje neaktivna. Morate sami provjeriti zastavicu nakon dodjele, inače ćete nastaviti u petlju stranica s time da PageCount vraća nulu i pitati se zašto ništa nije izvučeno.

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 slijede isti uzorak: dodijelite Pdf.Password prije postavljanja Active := True. Ako je lozinka pogrešna, Active ostaje False i ne dobivate iznimku koju biste mogli uhvatiti. U skupnim alatima koji obrađuju stotine datoteka, to tiho ponašanje zapravo je korisno: neuspjehe akumulirate u popis umjesto da odmatate stog poziva za svaki neuspjeli pokušaj.

Prolazak kroz stranice i preuzimanje bitmapa

Svojstvo BitmapCount se odnosi na pojedinu stranicu, pa prije njegova čitanja morate postaviti Pdf.PageNumber. Brojevi stranica počinju od 1; zadana vrijednost je 0, što znači da nijedna stranica nije učitana. Svojstvo Bitmap[Index] je indeksirano od 0 i vraća TBitmap koji je u vlasništvu pozivatelja. Morate ga osloboditi. Zanemarite li to oslobađanje unutar duge petlje nad velikim dokumentom, potrošnja memorije brzo će rasti jer svaka bitmapa može predstavljati nekoliko megabajta sirovih pikselnih podataka prije ikakve 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;

Provjera Assigned je važna. Mali broj generatora PDF-a zapisuje slikovne XObject-e s nultim dimenzijama piksela ili na drugi način neispravnim podacima; u tim slučajevima komponenta vraća nil umjesto prazne bitmape. Tretiranje nil vrijednosti kao pogreške i prekidanje ekstrakcije pogrešan je refleks: preskočite taj unos, zabilježite stranicu i indeks ako trebate trag revizije te nastavite. Ostatak stranice i dalje može dati valjane slike.

Primijetite da vanjska petlja postavlja Pdf.PageNumber u svakoj iteraciji. Ta je dodjela ono što učitava stranicu u interno stanje komponente i čini BitmapCount smislenim. Preskočite to i čitat ćete broj slika iste stranice iznova. Uzorak se čini redundantnim kada ga pišete, ali tako je API dizajniran: stranica je kursor, a ne kolekcija.

Odabir izlaznog formata

BMP je format bez gubitaka i uvijek je dostupan bez dodatnih programskih jedinica, što ga čini sigurnim zadanim izborom kada još ne znate što slika sadrži. Kada je veličina datoteke važna, format piksela vraćenog TBitmap-a govori vam koji je kodek prikladan. 32-bitna bitmapa nosi alfa kanal; PNG ga čuva bez gubitka kvalitete. Velika 24-bitna slika s neprekidnim tonovima kandidat je za JPEG. Manje slike ili one nacrtane s ograničenom paletom boja općenito je bolje ostaviti u BMP formatu nego ih provlačiti kroz JPEG, koji dodaje blok-artefakte pri niskim postavkama kvalitete, a malo toga uštedi pri visokim.

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 je odabir formata određen svojstvom Bmp.PixelFormat i dimenzijama. Ako je PixelFormat = pf32bit, trebate format koji podržava alfa kanal; PNG is the obvious choice, though it requires the PNGImage unit in older Delphi versions. Za 24-bitne slike šire od otprilike 300 piksela, JPEG s kvalitetom 85 pruža trostruko smanjenje veličine u odnosu na BMP bez primjetnog gubitka u većini fotografskog sadržaja. Ispod tog praga BMP je usporediv po veličini i potpuno izbjegava donošenje odluke o kvaliteti.

Što BitmapCount broji, a što ne

PDF razlikuje slikovne XObject-e od vektorske grafike nacrtane operatorima putanja. Stranica koja izgleda vizualno složeno može vratiti BitmapCount nula ako je svaki njezin element vektor. Skenirane stranice gotovo uvijek vraćaju točno jednu sliku: skener zapisuje cijeli skenirani dokument kao jedan slikovni XObject preko cijele stranice na rezoluciji na koju je skener bio postavljen. Stranice koje miješaju složeni tekst s ugrađenim fotografijama vraćaju po jedan unos za svaku fotografiju. Ukrasne linije, osjenčane pozadine i obrubi tablica obično se uopće ne pojavljuju u broju bitmapa.

Broj također ne uključuje ugrađene slike (inline images), rijetko korištenu PDF strukturu u kojoj su slikovni podaci ugrađeni izravno u tok sadržaja stranice umjesto kao imenovani XObject. One ispadaju iz onoga što ovaj API izlaže; dovoljno su rijetke u stvarnim dokumentima da ih većina alata za ekstrakciju jednostavno ne obrađuje.

Jedan detalj koji vrijedi imati na umu: vrijednost BitmapCount koju čitate odnosi se na trenutnu stranicu definiranu zadnjom dodjelom svojstva PageNumber. Ako se vaš kod grana ili poziva bilo koju funkciju koja mijenja PageNumber između brojanja i dohvaćanja, možete pročitati manje slika nego što ste alocirali prostora ili indeksirati izvan granica polja. Držite čitanje broja i petlju Bitmap[] na istoj stranici bez dodirivanja svojstva PageNumber u međuvremenu.

Memorija i performanse u skupnim poslovima

U velikoj arhivi proračun memorije je glavna stvar na koju treba paziti. Svaki poziv Bitmap[] dodjeljuje novi TBitmap na hrpi (heap), a na skeniranoj stranici rezolucije 300 DPI to je lako 25 MB sirovih pikselnih podataka prije ikakvog kodiranja. Ako stranice obrađujete u tijesnoj petlji bez oslobađanja memorije između iteracija, radni skup memorije raste linearno s brojem slika. Ispravna struktura je uvijek: dohvatite jednu bitmapu, obavite što trebate, oslobodite je, a zatim dohvatite sljedeću. Ako trebate držati reference na nekoliko bitmapa odjednom zbog koraka usporedbe, najprije ih prebrojite pomoću BitmapCount i prema tome alocirajte svoj spremnik, a zatim oslobodite svaku čim završite s njom, umjesto da odgađate čišćenje do kraja rada s dokumentom. Na dokumentu od 500 skeniranih stranica ta razlika može značiti granicu između 25 MB i 12 GB vršne memorije.

Komponenta TPdfView izlaže ista svojstva BitmapCount i Bitmap[], ali stranica s koje čita je stranica koja je trenutno prikazana u prikazu, a ne TPdf.PageNumber. Ova dva pokazivača stranice su neovisna; postavljanje jednog ne pomiče drugo. U VCL aplikaciji s aktivnim preglednikom, možete pozvati Pdf.PageNumber := N kako biste upravljali ekstrakcijom kroz TPdf, dok preglednik ostaje na onome što je korisnik zadnje pomaknuo. To razdvajanje je namjerno i održava stanje prikaza preglednika čistim dok se u pozadini izvodi ekstrakcija.

Svojstva BitmapCount i Bitmap[] ovdje prikazana dio su komponente PDFium VCL Component za Delphi i C++Builder.