Odborný článok

Prevod PDF stránok na JPEG obrázky v Delphi pomocou PDFium Component

Prevod stránky PDF na obrázok JPEG predstavuje dve operácie, ktoré vývojári často spájajú dohromady a následne ladia oddelene. Najprv musíte stránku vykresliť (rasterizovať) do bitmapy v pixeloch pri zvolenom rozlíšení. Potom túto bitmapu odovzdáte kódovaču JPEG a zvolíte kvalitu. PDFium Component zabezpečuje prvú polovicu prostredníctvom metódy RenderPage; druhá polovica je čistou záležitosťou VCL, konkrétne triedy TJPEGImage z jednotky Vcl.Imaging.jpeg. Prepojenie medzi nimi je miestom, kde sa robia tie najzaujímavejšie rozhodnutia, pretože rozlíšenie zvolené pri vykresľovaní a kvalita vybraná pri kompresii sa navzájom ovplyvňujú a majú priamy vplyv na veľkosť súboru, v čom sa dá ľahko urobiť chyba

Pred napísaním akéhokoľvek kódu si treba uvedomiť: stránka PDF nemá žiadne pixely. Je popísaná v bodoch (points), kde jeden bod predstavuje 1/72 palca, a stránka je vlastne vektorový nákres meraný v týchto bodoch. Keď požiadate PDFium o vykreslenie, vyberáte si, na koľko pixelov sa má tento nákres premietnuť, a touto voľbou je hodnota DPI. Ak urobíte chybu v aritmetike, buď vykreslíte rozmazanú miniatúru namiesto tlačovej predlohy, alebo alokujete 200-megapixelovú bitmapu pre niečo, čo malo byť len náhľadom s veľkosťou 120 pixelov

Od DPI k rozmerom v pixeloch

Metóda RenderPage vyžaduje celočíselnú šírku (Width) a výšku (Height) v pixeloch, nie jednu hodnotu DPI. Prvou úlohou je teda prepočet. Stránka hlási svoju veľkosť v bodoch prostredníctvom vlastností PageWidth a PageHeight (obe typu Double) a prepočet je rovnaký, aký používa každý rasterizátor: pixely sa rovnajú bodom vynásobeným cieľovým DPI a vydeleným číslom 72. Stránka formátu US Letter má rozmery 612 × 792 bodov. Pri 150 DPI to predstavuje 1275 × 1650 pixelov; pri 72 DPI to zostáva 612 × 792, teda jeden pixel na jeden bod, čo je prípad, na ktorý ľudia často zabúdajú, že ide o identitu

// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free;   // the function-form RenderPage hands you ownership

Dva detaily v týchto štyroch riadkoch rozhodujú o tom, či je kód správny. Prvým je, že funkčná forma metódy RenderPage vracia objekt TBitmap, ktorý vlastníte vy. PDFium ho alokovalo a uvoľnilo; ak ho neuvoľníte (Free) v každej iterácii, hromadné spracovanie stoviek stránok spôsobí únik stoviek bitmap a proces sa bude nafukovať, kým nespadne. Druhým detailom je parameter Color, v tomto prípade clWhite. Stránky PDF sa zvyčajne kreslia s predpokladom nepriehľadného bieleho podkladu a vykreslenie stránky s priehľadnosťou na nesprávnu farbu pozadia vytvorí nepekné okraje alebo tmavé haló efekty. Biela farba je správnou predvolenou voľbou pre takmer každý dokument; tento parameter existuje pre zriedkavé prípady, keď to tak nie je

Hodnoty 0, 0 predstavujú odsadenie Left a Top na stránke v prepočítanom súradnicovom priestore a ponechávate ich na nule, pokiaľ nerobíte výrez. Hodnota ro0 predstavuje otočenie: ponechajte ju na nule a PDFium rešpektuje akékoľvek otočenie, ktoré stránka deklaruje vo svojom zázname /Rotate, takže stránka vytvorená na šírku vyjde na šírku bez toho, aby ste museli čokoľvek robiť

Kódovanie bitmapy do formátu JPEG

Keď už bitmapa existuje, kódovanie do JPEG je tá jednoduchšia časť a ide o čisté Delphi. Metóda TJPEGImage.Assign skopíruje bitmapu, CompressionQuality nastaví kvalitu na stupnici od 1 do 100 a SaveToFile zapíše súbor. Jediným pravidlom pre poradie krokov je, že kvalita musí byť nastavená pred uložením, pretože riadi kódovanie, ktoré SaveToFile spúšťa

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;

Tento vnorený blok try/finally vyzerá pre pomocnú funkciu pre jednu stránku prehnane zložito, ale pri hromadnom spracovaní je úplne na mieste. Vnútorný blok uvoľňuje kódovač, vonkajší blok uvoľňuje bitmapu a ktorýkoľvek z nich pri výskyte výnimky uvoľní to, čo vlastní. Zlúčte ich do jedného a výnimka počas kódovania môže zanechať bitmapu visieť v pamäti. Pri dlhom behu je to rozdiel medzi prevodníkom, ktorý úspešne dokončí prácu, a tým, ktorý zlyhá na 300. stránke s poškodeným súborom a chybovým hlásením o nedostatku pamäte

Spoločný výber DPI a kvality

Tieto dva ovládacie prvky nie sú nezávislé od účelu výstupu a častou chybou je nastavenie oboch hodnôt na maximum z opatrnosti. Miniatúra pre web vykreslená pri 300 DPI a uložená s kvalitou 95 má veľkosť niekoľko stoviek kilobajtov, hoci sa tvári ako obrázok s veľkosťou 120 pixelov; prehliadač pri zmenšení takmer všetky tieto dáta zahodí. Prispôsobte rozlíšenie pixelom, ktoré výstup skutočne potrebuje, a potom zvoľte kvalitu, ktorá prežije stratovú kompresiu JPEG bez viditeľných artekaktov

VýstupDPIKvalita JPEG
Miniatúra zoznamu7260-70
Náhľad na obrazovke96-15080-85
Detailné zobrazenie200-30085-95
Tlačová predloha300-60090-100

Kvalita JPEG si zaslúži samostatné varovanie. Nie je to lineárne nastavenie. Skok zo 70 na 85 prináša skutočné vizuálne zlepšenie pri miernom nraste veľkosti súboru; skok z 95 na 100 zhruba zdvojnásobí veľkosť súboru pri rozdiele, ktorý takmer nikto neuvidí, pretože kvalita 100 stále nie je bezstratová, iba prestáva zahadzovať toľko dát. Pri stránkach s veľkým množstvom textu kompresia JPEG založená na blokoch rozmazáva ostré hrany glyfov do slabých „duchov“ (ringing), preto kvalita pod približne 80 robí text neostrým. Ak sú stránky prevažne textové a môžete zmeniť formát, PNG vykreslí tento text bez týchto artefaktov; JPEG si obháji svoje miesto pri fotografickom a zmiešanom obsahu, kde je jeho kompresia naozaj efektívnejšia

Rýchlejšie a menšie miniatúry

Ak je cieľom miniatúra a nie verná reprodukcia, môžete vykresľovaciemu jadru prikázať, aby robilo menej práce. Parameter Options prijíma sadu príznakov TRenderOption, pričom niektoré z nich vymieňajú vernosť za rýchlosť presne tak, ako to malý náhľad vyžaduje. Príznak reGrayscale odstráni farby, čo urýchli vykresľovanie a vytvorí menšiu bitmapu pre kódovanie. Príznaky reNoSmoothImage a reNoSmoothPath vynechajú vyhladzovanie, ktoré je pri veľkosti miniatúry aj tak nepostrehnuteľné

function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Fit the page inside MaxW x MaxH while preserving aspect ratio.
  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;

Prípad s miniatúrou tiež ukazuje čistejší spôsob uvažovania o veľkosti. Namiesto prepočtu cez DPI vypočítajte jeden mierkový faktor, ktorý prispôsobí stránku do vymedzeného rámca pri zachovaní pomeru strán, čo robí funkcia Min z oboch pomerov. Stránka na výšku aj stránka na šírku tak skončia vo vnútri rovnakého rámca bez skreslenia a vy nemusíte uvažovať o tom, aké DPI zodpovedá rozmeru „prispôsobiť rozmeru 200 × 280“. Jedno upozornenie k príznaku reGrayscale: konvertuje rastrový obsah obrázkov na odtiene sivej, ale vektorové výplne a text si v jadre ponechávajú svoje farebné hodnoty, takže stránka, ktorá obsahuje prevažne vektorovú grafiku, môže vyjsť menej monochromatická, než napovedá názov príznaku. Pre skutočne plnohodnotný prevod do odtieňov sivej je spoľahlivou cestou metóda GrayscalePdfBitmap z prehľadu tipov

Hromadné spracovanie celého dokumentu

Spojenie všetkých častí pre celý dokument predstavuje cyklus cez PageCount, pričom sa hodnota PageNumber mení po jednej stránke. Stránky sú indexované od 1: prvá stránka má PageNumber := 1 a cyklus beží do PageCount vrátane, nie do PageCount - 1. Ďalšou vecou, ktorou musí hromadné spracovanie rešpektovať, je zmluva o tichom načítaní. Nastavenie Active := True nikdy nevyvolá výnimku pri poškodenom súbore alebo nesprávnom hesle; iba ponechá Active na hodnote False. Overte túto vlastnosť pred tým, než začnete vykresľovať čo i len jednu stránku, inak bude prvé volanie RenderPage pracovať s dokumentom, ktorý neobol nikdy úspešne otvorený

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('Could not open ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // zero-pad so files sort right
    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;

Doplnenie núl na začiatku pomocou premennej Digits is a small thing that saves an afternoon later. Pomenujte súbory ako page_1.jpgpage_10.jpg, a akýkoľvek nástroj, ktorý ich usporadúva ako reťazce, zaradí page_10 hneď za page_1, čím pomieša poradie strán. Doplnenie núl na šírku najvyššieho čísla stránky (takže 300-stránkový dokument vygeneruje názvy od page_001.jpg) udrží lexikálne poradie a poradie stránok všade zhodné

Pri dokumentoch, ktoré sú dostatočne veľké na to, aby konverzia trvala dlhšie, spustite proces mimo hlavného vlákna používateľského rozhrania (UI thread) alebo spracovávajte správy systému medzi stránkami, aby aplikácia nezamŕzala, a poskytnite používateľovi možnosť proces zastaviť. Ak vykresľujete veľmi veľké stránky a vyžadujete možnosť zrušenia priamo uprostred vykresľovania stránky, PDFium Component ponúka cestu progresívneho vykresľovania s tokenom zrušenia; to je síce zložitejší mechanizmus než bežný hromadný export, ale v prípade potreby je k dispozícii

Posledné užitočné prepojenie. Rasterizácia stránky zahodí jej textovú vrstvu: výsledný JPEG sú len pixely a slová v ňom už nie je možné vyberať ani vyhľadávať. Keď potrebujete obrázok aj podkladový text, vykreslite obrázok a text vytiahnite samostatne, čomu sa venuje sprievodný článok o extrahovaní textu z PDF dokumentov pomocou PDFium Component. Preťaženia metódy RenderPage a možnosti vykresľovania uvedené v tomto článku sú súčasťou komponentu PDFium Component pre Delphi a C++Builder