Technický článek

Vykreslování stránek PDF do JPEG obrázků v Delphi s komponentou PDFium

Vyrendrování stránky PDF do formátu JPEG se skládá ze dvou operací, které mají lidé tendenci spouštět společně a poté ladit odděleně. Nejprve rastrujete stránku do pixelové bitmapy v rozlišení, které si zvolíte. Potom předáte tuto bitmapu enkodéru JPEG a zvolíte si kvalitu. Komponenta PDFium vlastní první polovinu prostřednictvím RenderPage; druhá polovina je čisté VCL, TJPEGImage z Vcl.Imaging.jpeg. Šev mezi nimi je místem, kde se odehrávají ta zajímavá rozhodnutí, protože rozlišení, které vyberete na straně renderování, a kvalita, kterou zvolíte na straně kódování, tvoří kompromis navzájem proti sobě i proti velikosti souboru takovými způsoby, že je snadné to udělat špatně

Věc, kterou si musíte vrýt do paměti ještě před jakýmkoli kódem: stránka PDF nemá žádné pixely. Je popsána v bodech (points), kde jeden bod je 1/72 palce, a stránka je vektorová kresba měřená v těchto bodech. Když požádáte PDFium, aby kreslilo, volíte na kolik pixelů se tato kresba promítne, a touto volbou je DPI (bodů na palec). Pokud uděláte chybu v aritmetice, pak buď vykreslíte rozmazanou miniaturu, když jste chtěli originál pro tisk, nebo alokujete dvousetmegapixelovou bitmapu pro něco, co má sloužit jako stodvacetipixelový náhled

Od DPI k rozměrům v pixelech

RenderPage očekává celočíselnou šířku a výšku v pixelech (Width a Height), nikoli DPI. Takže prvním úkolem je převod. Stránka hlásí svou velikost v bodech prostřednictvím PageWidth a PageHeight (obojí jako Double) a převod je ten samý, který používá každý rasterizátor: pixely se rovnají bodům vynásobeným cílovým DPI děleno 72. Stránka formátu US Letter je 612 na 792 bodů. Při 150 DPI to bude 1275 na 1650 pixelů; při 72 DPI to zůstane 612 na 792, jeden pixel na bod, což je případ, na který lidé zapomínají, že to je prostě identita

// Pdf.PageNumber již musí ukazovat na stránku, kterou chcete.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... použijte Bitmap ...
Bitmap.Free;   // funkční podoba RenderPage vám předává vlastnictví

Dva detaily v těchto čtyřech řádcích rozhodují o tom, zda je kód správný. První je, že funkční podoba (function form) RenderPage vrací instanci TBitmap, kterou vlastníte vy. PDFium ji alokovalo a šlo dál; pokud ji v každé iteraci neuvolníte voláním Free, dávka nad několika sty stranami propustí několik set bitmap a proces bobtná až do chvíle, kdy něco spadne. Druhým detailem je argument Color, tady clWhite. Stránky PDF se obvykle kreslí s předpokladem neprůhledného bílého podkladu, a stránka s průhledností vyrendrovaná na špatnou barvu pozadí vyrobí blátivé hrany nebo zbloudilé tmavé aureoly. Bílá je správným výchozím nastavením pro téměř každý dokument; onen parametr existuje pro vzácné případy, kdy to tak není

Těch 0, 0 je posun Left a Top do stránky ve škálovaném souřadnicovém prostoru a ty necháte na nule, pokud zrovna neořezáváte (crop). ro0 je rotace: nechte ji na nule a PDFium bude respektovat jakoukoliv rotaci, kterou už stránka deklaruje ve svém záznamu /Rotate, takže stránka vytvořená na šířku vyjde na šířku, aniž byste s tím museli něco dělat

Zakódování bitmapy jako JPEG

Jakmile bitmapa existuje, JPEG je tou snazší částí, a to čistě v Delphi. TJPEGImage.Assign dovnitř zkopíruje bitmapu, CompressionQuality nastaví kvalitu na stupnici od 1 do 100 a SaveToFile zapíše soubor. Jediným pravidlem posloupnosti je, že kvalita se musí nastavit dříve, než provedete uložení, protože ta řídí kódování (encode), které volání SaveToFile spouští

uses
  Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;

procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
  const FileName: string);
var
  Bitmap: TBitmap;
  Jpeg: TJPEGImage;
begin
  Pdf.PageNumber := PageNumber;
  Bitmap := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Dpi / 72),
    Round(Pdf.PageHeight * Dpi / 72),
    ro0, [], clWhite);
  try
    Jpeg := TJPEGImage.Create;
    try
      Jpeg.Assign(Bitmap);
      Jpeg.CompressionQuality := Quality;   // 1..100
      Jpeg.SaveToFile(FileName);
    finally
      Jpeg.Free;
    end;
  finally
    Bitmap.Free;
  end;
end;

To vnořené try/finally vypadá až puntičkářsky pro pomocnou proceduru na jednu stránku, a přesně to je to, co je správně pro dávkové zpracování. Vnitřní blok uvolňuje enkodér, vnější blok uvolňuje bitmapu a buď jedno, nebo druhé se spustí při výjimce a pořád to uvolní to, co vlastní. Zhustěte je do jednoho a výjimka během kódování může nechat bitmapu na pospas. Během dlouhého běhu to tvoří rozdíl mezi konvertorem, který skončí, a tím, který zdechne na straně 300 s poškozeným souborem a chybovým dialogem "out of memory" (došla paměť)

Současný výběr DPI a kvality

Tyto dva knoflíky nejsou nezávislé na účelu výstupu a běžnou chybou je ze samé obezřetnosti otočit oběma naplno nahoru. Náhledovka na web vyrendrovaná ve 300 DPI a uložená s kvalitou 95 zabírá stovky kilobajtů předstírajících, že jde o stodvacetipixelový obrázek; prohlížeč z toho většinu zahodí při zmenšování (downscale). Slaďte rozlišení k pixelům, jaké výstup skutečně potřebuje, pak si vyberte kvalitu, která bez viditelných artefaktů přežije ztrátovou kompresi JPEG

VýstupDPIJPEG kvalita
Miniatura (thumbnail) v seznamu7260-70
Náhled (preview) na obrazovce96-15080-85
Prohlížení ve velkém detailu200-30085-95
Předloha pro tisk (print master)300-60090-100

Kvalita JPEG si sama o sobě zaslouží slovo varování. Není to lineární ciferník. Skok ze 70 na 85 kupuje opravdové vizuální vylepšení výměnou za skromný růst souboru; skok z 95 na 100 v hrubých rysech zdvojnásobí soubor pro rozdíl, který skoro nikdo nedokáže zahlédnout, protože kvalita 100 stále není bezztrátová (lossless), jenom přestane tolik zahazovat. U stránek s velkým podílem textu rozmaže komprese JPEGu (založená na blocích) ostré hrany glyfů do slabých zvonivých odlesků, a to je ten důvod, proč kvalita zhruba pod 80 udělá na textu s ambicí na ostrý výstup "oskenovaný" vzhled. Pokud jsou stránky většinově text a vy můžete změnit formátování, PNG vykreslí daný text bez oněch odlesků; JPEG si vysluhuje své místo pro fotografický a smíšený obsah, kde je zkrátka jeho komprese doopravdy menší

Rychlejší a menší miniatury

Když je vaším cílem miniatura (thumbnail) spíše než věrná reprodukce, můžete říci rendereru, aby udělal méně práce. Parametr Options bere sadu příznaků TRenderOption a několik z nich obětuje přesnost za rychlost přesně v tom smyslu, v jakém to malý náhled potřebuje. reGrayscale vypustí barvu, což jak vykreslí rychleji, tak k zakódování vytvoří menší bitmapu. reNoSmoothImage a reNoSmoothPath přeskočí anti-aliasing (vyhlazování), což je v měřítku malého náhledu beztak neviditelné

function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Přizpůsobit stránku dovnitř do MaxW x MaxH se zachováním poměru stran.
  Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
  Result := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Scale),
    Round(Pdf.PageHeight * Scale),
    ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;

Případ s miniaturou také ukazuje čistější způsob přemýšlení nad udáváním velikosti. Místo toho abyste to brali přes DPI, spočítáte jeden koeficient měřítka, jenž vecpá stránku do daného ohraničujícího rámečku (bounding box) a zachová přitom její poměr stran, což je vlastně to, co dělá Min nad oběma poměry. Jak stránka na výšku, tak stránka na šířku skončí uvnitř stejného rámečku bez toho, aniž by došlo ke zkreslení, a vy se nikdy nebudete muset potrápit s tím, jaké DPI odpovídá požadavku "nacpat do 200 x 280". S parametrem reGrayscale se pojí jeden háček: převede obsah rastrového obrázku na šeď, avšak vektorové výplně a text si uvnitř engine udrží své hodnoty barev, takže se stránka, kde z drtivé většiny převažuje vektorová kresba, může vrátit coby méně černobílá, než co napovídá název příznaku. Pro ryze celošedý výsledek je spolehlivou cestou nechat převést již vyrendrovanou bitmapu pomocí GrayscalePdfBitmap

Dávkové zpracování celého dokumentu

Když to poskládáte dohromady pro celý dokument, představuje to smyčku přes vlastnost PageCount, kde se s PageNumber posunujete o jednu stranu s každým krokem. Stránky se číslují od jedničky: první stránka je PageNumber := 1, a ta smyčka běží až do PageCount včetně, ne do PageCount - 1. Druhou věcí, co si dávka musí pohlídat, je smlouva ohledně tichého chování načítání. Krok Active := True na poškozeném souboru nebo při chybném hesle nikdy nevyvolá výjimku; jen ponechá hodnotu Active na False. Zkontrolujte ji předtím, než vyrendrujete jedinou stránku, anebo se to první volání RenderPage obrátí proti dokumentu, jenž nikdy nebyl otevřen

procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
  Pdf: TPdf;
  I, Digits: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := PdfPath;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.Create('Nepodařilo se otevřít ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // vycpání nulami pro správné třídění
    for I := 1 to Pdf.PageCount do
      SavePageAsJpeg(Pdf, I, Dpi, Quality,
        Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

To ucpávání (vyplňování) nulami skrze parametr Digits je tou drobností, co vám později pošetří klidně celé odpoledne. Pojmenujte ty soubory jako page_1.jpgpage_10.jpg a kterýkoliv nástroj, co je setřídí coby řetězce, postaví page_10 hned za page_1, čímž to rozbije dané pořadí. Doplnění na šířku čísla toho nejvyššího počtu stran, takže na třistastránkovém dokumentu dostanete page_001.jpg, uchová lexikografické setřídění i vlastní pořadí stránek jako navlas stejné kdekoliv dál po cestě (downstream)

Pro dokumenty tak velké, že konverze zabírá postřehnutelnou porci času, to spusťte mimo hlavní vlákno UI, případně napumpujte zpracování zpráv (messages) mezi zpracováním jednotlivých stran, aby vaše aplikace zůstala citlivá (responsive), a pro uživatele přidejte možnost to celé přerušit. Renderujete-li velmi rozsáhlé stránky a chystáte zrušení (cancel), jež se zakousne v polovině stránky spíše než výlučně až na hranicích mezi nimi, PDFium Component má pak cestu s postupným (progressive) vyrendrováním, podchycenou stornovacím tokenem (cancellation token); to se jedná o robustnější nástroj, než v jakém většina dávkových úloh pro export shledá své opodstatnění, ale ten mechanismus se tam schovává, když ta jediná strana ve vyšroubovaných 600 DPI představuje to, co samo osobě na blokování drhne dost

Jeden poslední pár stojí za zmínku. Když na stránce necháte proběhnout rasterizaci, zahodí se její vrstva textu: onen JPEG má jenom pixely, a už ta slova z něj nadále nesvedete hledat anebo do nich vtáhnout výběr. Kdybyste poptávali obraz i schovaný text naráz, vyrendrujte si pak grafiku a textový podklad si naberte vedle, to pokrýváme v sesterském materiálu zvaném extrahování textu z dokumentů PDF s komponentou PDFium. Ta volání funkce RenderPage s parametry pro možnost renderu (render options) tvoří dílek skládačky jménem PDFium Component stavěným primárně na ramena prostředí Delphi i C++Builder