Technický článek

Extrahování obrázků z PDF pomocí komponenty PDFium v Delphi

PDF ukládá obrázky jako objekty první třídy (first-class objects) uvnitř svých obsahových proudů. Když stránka odkazuje na fotografii, sken nebo diagram, data pixelů žijí ve slovníku XObject vedle geometrie stránky. Komponenta PDFium to odhaluje prostřednictvím dvou vlastností na TPdf: BitmapCount, která vrací, kolik vložených bitmap je na aktuální stránce, a Bitmap[Index], která dekóduje jednu z nich do objektu TBitmap, který vlastníte a musíte uvolnit. To je celý model extrakce. Smyčka má čtyři řádky; to, co vyžaduje úsudek, je okolní infrastruktura

Otevření dokumentu

První věc, kterou byste měli vědět o TPdf, je ta, že přiřazení Active := True nikdy nevyvolá výjimku. Selhání načtení, chybná hesla, poškozené soubory: to vše se interně spolkne a komponenta jednoduše zůstane neaktivní. Po přiřazení musíte tento příznak zkontrolovat sami, jinak budete pokračovat do smyčky stránek s tím, že PageCount vrátí nulu, a budete se divit, proč se nic 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;

Soubory chráněné heslem sledují stejný vzor: před nastavením Active := True přiřaďte heslo přes Pdf.Password. Pokud je heslo nesprávné, Active zůstane na hodnotě False a nemáte žádnou výjimku k odchycení. V dávkovém nástroji, který zpracovává stovky souborů, je toto tiché chování vlastně užitečné: místo rozmotávání zásobníku volání u každého z nich pouze nahromadíte případná selhání do nějakého seznamu

Průchod stránkami a vytahování bitmap

Hodnota BitmapCount platí vždy pro konkrétní stránku, takže před jejím přečtením nastavíte vlastnost Pdf.PageNumber. Čísla stránek začínají od jedničky; výchozí hodnota je 0, což znamená, že není načtena žádná stránka. Vlastnost Bitmap[Index] se indexuje od nuly a vrací TBitmap, který je ve vlastnictví volajícího. Musíte ho uvolnit z paměti. Pokud zapomenete zavolat uvolnění uvnitř dlouhé smyčky nad velkým dokumentem, spotřeba paměti rychle poroste, protože každá bitmapa může představovat několik megabajtů surových pixelových dat před jakoukoliv kompresí

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 přes Assigned je důležitá. Menší počet generátorů PDF zapisuje obrazové XObjects s nulovými rozměry pixelů nebo s jinak chybně formátovanými daty; v takových případech komponenta vrací nil spíše než prázdnou bitmapu. Považovat návratovou hodnotu nil za chybu a zastavit kvůli tomu extrakci je nesprávný reflex: přeskočte to, zapište si stránku a index, pokud potřebujete auditní stopu, a pokračujte. Zbytek stránky může stále poskytovat platné obrázky

Všimněte si, že vnější smyčka nastavuje Pdf.PageNumber při každé iteraci. Toto přiřazení je přesně tím, co načítá stránku do vnitřního stavu komponenty a dává význam hodnotě BitmapCount. Pokud to přeskočíte, budete opakovaně číst počet bitmap z té samé stránky. Ačkoliv se vám tento vzor může zdát při psaní nadbytečný, takto je API navrženo: stránka funguje jako kurzor, ne jako kolekce

Výběr výstupního formátu

BMP je bezeztrátový a vždy dostupný bez přidávání dalších unit, což z něj činí rozumnou výchozí volbu v momentě, kdy ještě nevíte, co obrázek obsahuje. Pokud záleží na velikosti souboru, pixelový formát (pixel format) vrácené komponenty TBitmap vám napoví, jaký kodek je vhodný. 32bitová bitmapa obsahuje alfa kanál; ten PNG zachová beze ztrát. Velký 24bitový obrázek s plynulými tóny je pak kandidátem pro JPEG. U menších obrázků nebo u těch, které jsou nakreslené s omezenou paletou, je obecně lepší ponechat formát BMP, než je prohánět přes JPEG. Ten při nastavení na nízkou kvalitu přidává blokové artefakty a při nastavení vysoké kvality ušetří jen málo místa

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 se výběr formátu řídí hodnotou Bmp.PixelFormat a rozměry. Pokud je PixelFormat = pf32bit, potřebujete formát, který nese alfa kanál; v tomto případě je jasnou volbou PNG, i když ve starších verzích Delphi to bude vyžadovat unit PNGImage. Pro 24bitové obrázky širší než zhruba 300 pixelů poskytuje JPEG při kvalitě 85 v porovnání s BMP trojnásobné zmenšení velikosti bez patrné ztráty u většiny fotografického obsahu. Pod touto hranicí je BMP srovnatelně velký a pomůže vám zcela se vyhnout jakémukoliv rozhodování ohledně kvality

Co BitmapCount počítá a co naopak ne

PDF rozlišuje mezi obrazovými XObjects a vektorovou grafikou vykreslenou pomocí operátorů cesty. Stránka, která vypadá vizuálně složitě, tak může vrátit hodnotu BitmapCount nula, pokud jsou naprosto všechny prvky vektorové. Naskenované stránky téměř vždy vracejí hodnotu jedna: skener totiž celou takovou stránku zapíše jako jeden obrazový XObject zabírající celou stránku, v rozlišení, na které byl skener právě nastaven. Stránky, jež míchají sázený text s vloženými fotografiemi, vrátí vždy jeden záznam pro každou fotografii. Zdobné dělící čáry, stínovaná pozadí ani okraje tabulek se pak do počtu bitmap obvykle vůbec nepromítnou

Tento počet také nezahrnuje takzvané vložené obrázky (inline images) – to je poměrně zřídka využívaný PDF konstrukt, kdy se obrazová data integrují přímo do samotného datového proudu stránky a nejsou zapsána jako samostatně pojmenovaný XObject. Ty se nacházejí mimo dosah tohoto API; u reálných dokumentů se nicméně vyskytují tak vzácně, že většina nástrojů určených k extrakci s nimi zkrátka vůbec nepočítá

Jeden detail, který stojí za to si pamatovat: načtená hodnota BitmapCount se vztahuje k aktuální stránce podle jejího posledního přiřazení do proměnné PageNumber. Pokud se váš kód mezi započítáním a samotným načtením obrázků rozdělí nebo zavolá jakoukoliv funkci měnící PageNumber, může se stát, že přečtete méně obrázků, než pro kolik jste vyhradili místo, nebo budete indexovat až někam za samotný konec. Ujistěte se, že obě tyto operace – zjištění počtu prvků i provádění smyčky pro volání pole Bitmap[] – provádíte na téže stránce, aniž byste mezi nimi měnili PageNumber

Použití TPdfView ve formulářové aplikaci

Komponenta TPdfView vystavuje ty samé vlastnosti BitmapCount a Bitmap[], ovšem stránka, ze které načítá, je právě ta stránka zobrazená v prohlížeči, nikoliv TPdf.PageNumber. Tyto dva ukazatele na stránky jsou vzájemně nezávislé; nastavení jednoho neposune ten druhý. Ve VCL formulářové aplikaci s živým prohlížečem můžete extrakci komponentou TPdf pohánět voláním Pdf.PageNumber := N a prohlížeč tak nechat nastavený na to, k čemu uživatel naposledy scrolloval. Toto oddělení je navrženo zcela záměrně, přičemž zajišťuje, že stav zobrazení zůstane bez problémů uchován i během toho, co na pozadí poběží extrakce

Paměť a výkon v dávkových úlohách

Při práci s rozsáhlými archivy je nejdůležitějším bodem ke sledování právě velikost dostupné paměti. Každé zavolání metody Bitmap[] alokuje novou vrstvu TBitmap na haldě (heap) a na stránce naskenované v rozlišení 300 DPI tak může bez problémů vzniknout i 25 MB zcela nezpracovaných obrazových pixelových dat, a to ještě před jakýmkoliv jejich dalším kódováním. Pokud tyto stránky zpracováváte v těsné smyčce a mezi jednotlivými iteracemi je nijak neuvolňujete, velikost zabrané paměti (working set) tak začne lineárně stoupat ruku v ruce se samotným množstvím obrázků. Ten správný postup tedy musí mít vždycky stejnou podobu: vyvolej si bitmapu, zařiď si s ní to, co právě potřebuješ, smaž ji a hned nato zavolej tu další. Jakmile pro určitý srovnávací proces začnete potřebovat držet v jednu a tu samou chvíli vícero odkazů na různé bitmapy, tak si ty obrázky prostě nejdřív hezky spočítejte s využitím nástroje BitmapCount. Následně odpovídajícím způsobem vyčleňte dostatek místa do paměťového bloku (container) a každý jednotlivý z nich raději vždy smažte hned potom, co jste daný krok zvládli dodělat do konce. Rozhodně toto uvolňování nenechávejte až úplně na samotný konec celého úklidového kolečka (end-of-document cleanup). Když byste například zrovna pracovali nad souborem, který v sobě obsahuje na pět stovek naskenovaných dokumentových stran, ten nepatrný rozdíl v daném rozhodnutí bude najednou znamenat naprostý extrém. A to sice rozdíl v podobě toho, že můžete za pomyslným okrajem té největší zátěže (peak RSS) sahat po číslech rovnajících se třeba jenom 25 megabajtům, nebo úplně v klidu dosáhnete až k neskutečně vysokým 12 gigabajtům

Vlastnosti BitmapCount a Bitmap[] uvedené výše jsou součástí komponenty PDFium Component pro Delphi a C++Builder