Technical Article

Ekstrakcija slik iz PDF dokumentov s PDFium VCL v Delphiju

PDF shranjuje slike kot objekte prvega razreda (first-class objects) znotraj svojih vsebinskih strug. Ko se stran sklicuje na fotografijo, skenirano sliko ali diagram, slikovni podatki živijo v slovarju XObject poleg geometrije strani. PDFium VCL to izpostavlja prek dveh lastnosti v TPdf: BitmapCount, ki vrne število vgrajenih bitnih slik na trenutni strani, in Bitmap[Index], ki eno izmed njih dekodira v objekt TBitmap, ki je v vaši lasti in ga morate sami sprostiti. To je celoten model ekstrakcije. Zanka obsega le štiri vrstice, več premisleka pa zahteva okoljska koda.

Odpiranje dokumenta

Prva stvar, ki jo morate vedeti o TPdf, je, da nastavitev Active := True nikoli ne sproži izjeme. Napake pri nalaganju, napačna gesla, poškodovane datoteke: vse to se ujame interno, komponenta pa preprosto ostane neaktivna. Po dodelitvi morate sami preveriti to zastavico, sicer boste nadaljevali v zanko strani, kjer bo PageCount vrnil nič, vi pa se boste spraševali, zakaj se nič ni izvleklo.

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;

Z geslom zaščitene datoteke sledijo enakemu vzorcu: določite Pdf.Password pred nastavitvijo Active := True. Če je geslo napačno, ostane lastnost Active na False in ne prejmete nobene izjeme, ki bi jo lahko ulovili. Pri paketnem orodju, ki obdeluje stotine datotek, je to tiho delovanje dejansko uporabno: napake preprosto zberete na seznamu, namesto da bi za vsako datoteko posebej prekinjali izvajanje.

Pomikanje po straneh in pridobivanje bitnih slik

Vrednost BitmapCount velja za posamezno stran, zato morate pred njenim branjem nastaviti Pdf.PageNumber. Številke strani se začnejo z 1; privzeta vrednost je 0, kar pomeni, da ni naložena nobena stran. Lastnost Bitmap[Index] ima indekse z začetkom pri 0 in vrne objekt TBitmap v vaši lasti. To bitno sliko morate sami sprostiti. Če sprostitev opustite znotraj dolge zanke čez obsežen dokument, bo poraba pomnilnika hitro narasla, saj lahko vsaka bitna slika pred stiskanjem predstavlja več megabajtov surovih slikovnih podatkov.

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;

Preverjanje z metodo Assigned je pomembno. Nekaj generatorjev PDF zapisuje slikovne objekte XObject z ničelnimi dimenzijami ali kako drugače poškodovanimi podatki; v teh primerih komponenta vrne nil namesto prazne bitne slike. Obravnavanje vrednosti nil kot napake in zaustavitev ekstrakcije je napačen odziv: preprosto jo preskočite, po potrebi zabeležite stran in indeks ter nadaljujte. Preostali del strani lahko še vedno vsebuje veljavne slike.

Upoštevajte, da zunanja zanka v vsakem koraku nastavi Pdf.PageNumber. Ta dodelitev naloži stran v interno stanje komponente, s čimer BitmapCount pridobi smisel. Če ta korak izpustite, boste nenehno brali število slik na isti strani. Vzorec se ob pisanju zdi odveč, vendar je API tako zasnovan: stran deluje kot kazalec in ne kot zbirka.

Izbira izhodne oblike zapisa

BMP je brezizguben format in je vedno na voljo brez dodatnih enot, zato je zanesljiva privzeta izbira, ko še ne veste, kaj slika vsebuje. Ko je velikost datoteke pomembna, vam format slikovnih pik vrnjene slike TBitmap pove, kateri kodek je primeren. 32-bitna bitna slika vsebuje kanal alfa (alpha channel); PNG to ohrani brez izgub. Velika 24-bitna slika z neprekinjenimi toni je primerna za format JPEG. Manjše slike ali tiste z omejeno paleto barv je na splošno bolje pustiti v formatu BMP kot pa jih pretvoriti v JPEG, ki pri nizkih nastavitvah kakovosti doda popačenja (artifacts), pri visokih pa prinaša malo prihranka pri velikosti.

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;

V praksi izbiro formata narekujeta lastnost Bmp.PixelFormat in dimenzije. Če velja PixelFormat = pf32bit, potrebujete format, ki podpira kanal alfa; PNG is the obvious choice, though it requires the PNGImage unit in older Delphi versions. Za 24-bitne slike, širše od približno 300 slikovnih pik, JPEG s kakovostjo 85 ponuja trikratno zmanjšanje velikosti v primerjavi z BMP brez opazne izgube kakovosti pri večini fotografskih vsebin. Pod to mejo je BMP po velikosti primerljiv in se v celoti izogne odločanju o stopnji kakovosti.

Kaj BitmapCount šteje in česa ne

PDF razlikuje med slikovnimi objekti XObject in vektorsko grafiko, narisano s potmi. Stran, ki je vizualno kompleksna, lahko vrne vrednost BitmapCount enako nič, če so vsi elementi vektorski. Skenirane strani skoraj vedno vrnejo natanko ena: skener zapiše celoten sken kot en sam celostranski slikovni objekt XObject pri tisti ločljivosti, na katero je bil skener nastavljen. Strani, ki mešajo besedilo z vgrajenimi fotografijami, vrnejo po en vnos za vsako fotografijo. Okrasne črte, osenčena ozadja in obrobe tabel se običajno sploh ne pojavijo v številu bitnih slik.

Število prav tako ne vključuje vrstičnih slik (inline images), redko uporabljene strukture PDF, kjer so slikovni podatki vgrajeni neposredno v vsebinsko strugo strani namesto v poimenovan XObject. Ti elementi padejo izven tistega, kar ta API ponuja; v realnih dokumentih so tako redki, da jih večina orodij za ekstrakcijo preprosto ne obravnava.

Pozorni morate biti na naslednjo podrobnost: prebrana vrednost BitmapCount velja za trenutno stran glede na zadnjo dodelitev PageNumber. Če se vaša koda veja ali kliče funkcije, ki spreminjajo PageNumber med štetjem in pridobivanjem, lahko preberete manj slik, kot ste predvideli, ali pa prekoračite indeks. Štetje in zanko Bitmap[] izvajajte na isti strani brez vmesnega spreminjanja PageNumber.

Pomnilnik in zmogljivost pri paketnih opravilih

Pri obdelavi velikega arhiva je poraba pomnilnika glavna stvar, na katero morate paziti. Vsak klic Bitmap[] alocira nov TBitmap na kopici (heap), pri skenirani strani z 300 DPI pa to pred kakršnim koli kodiranjem zlahka predstavlja 25 MB surovih slikovnih podatkov. Če strani obdelujete v tesni zanke brez vmesnega sproščanja pomnilnika, bo delovni sklop naraščal linearno s številom slik. Pravilen pristop je vedno: pridobite eno bitno sliko, opravite delo, jo sprostite in šele konto pridobite naslednjo. Če morate zaradi primerjave obdržati reference na več slik hkrati, jih najprej preštejte z BitmapCount, ustrezno alocirajte vsebnik in nato sprostite vsako sliko takoj, ko jo končate uporabljati, namesto da sproščanje odlagate na konec dokumenta. Pri dokumentu s 500 skeniranimi stranmi lahko ta razlika pomeni razkorak med 25 MB in 12 GB največje porabe pomnilnika (peak RSS).

Komponenta TPdfView ponuja enaki lastnosti BitmapCount in Bitmap[], vendar se stran, ki jo bere, nanaša na trenutno prikazano stran pogleda in ne na lastnost TPdf.PageNumber. Kazalca strani sta neodvisna; nastavitev enega ne premakne drugega. V aplikaciji z obrazci VCL in prikazovalnikom v živo lahko pokličete Pdf.PageNumber := N za izvajanje ekstrakcije prek TPdf, medtem ko bralnik ostane na strani, na katero se je uporabnik nazadnje pomaknil. Ta ločitev je namerna in ohranja čisto stanje prikaza bralnika med izvajanjem ekstrakcije v ozadju.

Lastnosti BitmapCount in Bitmap[], prikazane tukaj, so del komponente PDFium VCL Component za Delphi in C++Builder.