Technical Article

Extrahovanie obrázkov z PDF pomocou PDFium VCL v Delphi

PDF ukladá obrázky ako plnohodnotné objekty vo svojich prúdoch obsahu (content streams). Keď stránka odkazuje na fotografiu, sken alebo diagram, pixelové dáta žijú v slovníku XObject vedľa geometrie stránky. PDFium VCL to sprístupňuje prostredníctvom dvoch vlastností v triede TPdf: vlastnosti BitmapCount, ktorá vracia počet vložených bitmapových obrázkov na aktuálnej stránke, a vlastnosti Bitmap[Index], ktorá jeden z nich dekóduje do objektu TBitmap, ktorý vlastníte a musíte ho sami uvoľniť. To predstavuje celý model extrakcie. Cyklus má len štyri riadky; to, čo vyžaduje správny úsudok, sú okolité operácie.

Otvorenie dokumentu

Prvou vecou, ktorou je potrebné o triede TPdf vedieť, je, že priradenie Active := True nikdy nevyvolá výnimku. Zlyhanie načítania, nesprávne heslá, poškodené súbory – to všetko sa spracuje interne a komponent jednoducho zostane neaktívny. Po priradení musíte sami skontrolovať tento príznak, inak postúpite do cyklu stránok s tým, že PageCount vráti nulu, a budete sa diviť, prečo sa nič neextrahovalo.

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;

Heslom chránené súbory nasledujú rovnaký vzor: priraďte hodnotu Pdf.Password pred nastavením Active := True. Ak je heslo nesprávne, vlastnosť Active zostane na hodnote False a nevyvolá sa žiadna výnimka na zachytenie. V hromadnom nástroji spracúvajúcom stovky súborov je toto tiché správanie vlastne užitočné: zlyhania hromadíte v zozname namiesto zložitého spracovávania výnimiek pre každý jednotlivý súbor.

Prechádzanie stránok a získavanie bitmap

Vlastnosť BitmapCount sa vzťahuje na konkrétnu stránku, takže pred jej prečítaním musíte nastaviť Pdf.PageNumber. Stránky sú indexované od 1; predvolená hodnota je 0, čo znamená, že nie je načítaná žiadna stránka. Vlastnosť Bitmap[Index] je indexovaná od 0 a vracia objekt TBitmap, ktorý vlastní volajúci. Musíte ho sami uvoľniť. Ak na uvoľnenie v dlhom cykle nad veľkým dokumentom zabudnete, pamäť bude rýchlo rásť, pretože každá bitmapa môže pred akoukoľvek kompresiou predstavovať niekoľko megabajtov surových pixelových dát.

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;

Kontrola pomocou Assigned je dôležitá. Malý počet PDF generátorov zapisuje obrazové objekty XObject s nulovými rozmermi pixelov alebo inak poškodenými dátami; v týchto prípadoch komponent vracia hodnotu nil namiesto prázdnej bitmapy. Považovať vrátenie hodnoty nil za chybu a zastaviť extrakciu by bolo chybou: preskočte ju, ak potrebujete audit, zaznamenajte stránku a index, a pokračujte. Zvyšok stránky môže stále obsahovať platné obrázky.

Všimnite si, že vonkajší cyklus nastavuje vlastnosť Pdf.PageNumber pri každej iterácii. Toto priradenie načíta stránku do interného stavu komponentu a dá vlastnosti BitmapCount zmysel. Ak to vynecháte, budete opakovane čítať počet z rovnakej stránky. Vzor vyzerá pri zápise nadbytočne, ale takto je API navrhnuté: stránka je ukazovateľ (cursor), nie kolekcia.

Výber výstupného formátu

Formát BMP je bezstratový a vždy dostupný bez potreby ďalších jednotiek, čo z neho robí spoľahlivú predvolenú voľbu, keď ešte neviete, čo obrázok obsahuje. Keď záleží na veľkosti súboru, formát pixelov vráteného objektu TBitmap vám napovie, ktorý kodek je vhodný. 32-bitová bitmapa nesie alfa kanál; formát PNG ho zachováva bez straty. Veľký 24-bitový obrázok s plynulými tónmi je kandidátom na JPEG. Menšie obrázky alebo obrázky nakreslené s obmedzenou paletou farieb je vo všeobecnosti lepšie ponechať ako BMP, než ich komprimovať do JPEG, ktorý pri nízkej kvalite pridáva blokové artefakty a pri vysokej prináša len malú úsporu veľkosti.

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 praxi sa výber formátu riadi vlastnosťou Bmp.PixelFormat a rozmermi obrázka. Ak platí PixelFormat = pf32bit, potrebujete formát, ktorý podporuje alfa kanál; PNG je jasnou voľbou, hoci v starších verziách Delphi vyžaduje jednotku PNGImage. Pre 24-bitové obrázky širšie ako približne 300 pixelov prináša JPEG s kvalitou 85 trojnásobné zmenšenie veľkosti oproti BMP bez viditeľnej straty kvality pri väčšine fotografického obsahu. Pod touto hranicou je veľkosť BMP porovnateľná a vyhnete sa akémukoľvek rozhodovaniu o kvalite.

Čo BitmapCount počíta a čo nie

PDF rozlišuje medzi obrazovými objektmi XObject a vektorovou grafikou vykreslenou pomocou čiar a ciest. Stránka, ktorá vyzerá vizuálne zložito, môže vrátiť hodnotu BitmapCount rovnú nule, ak sú všetky prvky vektorové. Naskenované stránky takmer vždy vracajú presne jeden obrázok: skener zapíše celý sken ako jeden celostránkový objekt XObject v rozlíšení, na ktoré bol nastavený. Stránky kombinujúce tlačený text s vloženými fotografiami vracajú jeden záznam na každú fotografiu. Dekoratívne čiary, tieňované pozadia a okraje tabuliek sa v počte bitmapových obrázkov zvyčajne vôbec neobjavia.

Tento počet nezahŕňa ani priame (inline) obrázky, čo je zriedkavo používaný konštrukt PDF, kedy sú dáta obrázka vložené priamo v prúde obsahu stránky a nie ako samostatný pomenovaný objekt XObject. Tie stoja mimo toho, čo toto API sprístupňuje; v reálnych dokumentoch sú také neobvyklé, že ich väčšina extrakčných nástrojov jednoducho nespracováva.

Detail, ktorý si treba pamätať: hodnota BitmapCount, ktorú čítate, sa vzťahuje na aktuálnu stránku podľa posledného priradenia PageNumber. Ak sa váš kód vetví alebo volá akúkoľvek funkciu, ktorá mení PageNumber medzi spočítaním a načítaním, môžete prečítať menej obrázkov, než pre koľko ste alokovali priestor, alebo indexovať mimo povolený rozsah. Prečítanie počtu aj cyklus cez Bitmap[] držte na rovnakej stránke bez toho, aby ste medzitým menili PageNumber.

Použitie TPdfView vo formulárovej aplikácii

Pamäť a výkon pri hromadných úlohách

Pri veľkom archíve je hlavnou vecou, ktorú treba sledovať, spotreba pamäte. Každé volanie Bitmap[] alokuje nový objekt TBitmap na halde a pri naskenovanej stránke s rozlíšením 300 DPI to môže byť aj 25 MB surových pixelových dát pred akýmkoľvek kódovaním. Ak spracovávate stránky v rýchlom cykle bez uvoľňovania pamäte medzi iteráciami, pracovná pamäť rastie lineárne s počtom obrázkov. Správny postup je vždy: získať jednu bitmapu, urobiť s ňou to potrebné, uvoľniť ju a až potom získať ďalšiu. Ak pedagóg potrebuje držať odkazy na viacero bitmap naraz (napríklad kvôli porovnávaniu), najprv zistite ich počet pomocou BitmapCount a podľa toho alokujte svoj kontajner. Každú bitmapu uvoľnite hneď, ako s ňou skončíte, a neodkladajte to na koniec celého dokumentu. Pri dokumente s 500 naskenovanými stránkami môže tento rozdiel znamenať rozdiel medzi 25 MB a 12 GB špičkovej alokácie pamäte.

Komponent TPdfView sprístupňuje rovnaké vlastnosti BitmapCount a Bitmap[], ale stránka, z ktorej číta, je aktuálne zobrazená stránka v zobrazení, nie vlastnosť TPdf.PageNumber. Tieto dva ukazovatele stránok sú nezávislé; nastavenie jedného nepresunie druhý. V aplikácii s VCL formulárom a živým náhľadom môžete nastaviť Pdf.PageNumber := N na riadenie extrakcie cez TPdf, zatiaľ čo prehliadač zostane na stránke, na ktorú používateľ naposledy prešiel. Toto oddelenie je zámerné a udržiava stav zobrazení prehliadača čistý aj počas behu extrakcie na pozadí.

Vlastnosti BitmapCount a Bitmap[] popísané v tomto článku sú súčasťou komponentu PDFium VCL Component pre Delphi a C++Builder.