Technical Article

Renderiranje PDF stranica u JPEG u Delphiju pomoću PDFium VCL-a

Renderiranje PDF stranice u JPEG sastoji se od dvije operacije koje programeri obično spajaju, a zatim odvojeno ispravljaju pogreške. Najprije rasterizirate stranicu u slikovnu bitmapu u odabranoj rezoluciji. Zatim tu bitmapu predajete JPEG koderu i birate kvalitetu. PDFium VCL upravlja prvom polovinom procesa putem metode RenderPage; druga polovina je čisti VCL, odnosno TJPEGImage iz Vcl.Imaging.jpeg. Spoj između njih je mjesto gdje se donose zanimljive odluke, karena su rezolucija koju odaberete na strani renderiranja i kvaliteta koju odaberete na strani kodiranja u međusobnom omjeru i utječu na veličinu datoteke na načine koje je lako pogrešno procijeniti.

Stvar koju trebate usvojiti prije nego što napišete ijedan redak koda: PDF stranica nema piksele. Opisana je u točkama (points), pri čemu je jedna točka jednaka 1/72 inča, a stranica je vektorski crtež mjeren u tim točkama. Kada tražite od PDFium-a da renderira, vi zapravo birate na koliko piksela želite projicirati taj crtež, a taj izbor je DPI. Pogriješite li u izračunu, renderirat ćete mutnu minijaturu (thumbnail) kada ste željeli sliku visoke kvalitete za ispis, ili ćete dodijeliti bitmapu od 200 megapiksela za nešto što je namijenjeno pregledu od 120 piksela.

Od DPI-ja do dimenzija piksela

Metoda RenderPage zahtijeva cjelobrojne piksele za Width (širinu) i Height (visinu), a ne DPI. Dakle, prvi zadatak je pretvorba. Stranica javlja svoju veličinu u točkama putem svojstava PageWidth i PageHeight (oba tipa Double), a pretvorba je ista onoj koju koristi svaki rasterizator: pikseli su jednaki točkama pomnoženim s ciljnim DPI-jem i podijeljenim sa 72. Stranica formata US Letter ima 612 × 792 točaka. Pri 150 DPI to postaje 1275 × 1650 piksela; pri 72 DPI ostaje 612 × 792, odnosno jedan piksel po točki, što je slučaj za koji programeri zaboravljaju da je zapravo osnovni identitet.

// 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 detalja u ta četiri retka određuju je li kod ispravan. Prvi je da funkcijski oblik metode RenderPage vraća TBitmap čiji ste vi vlasnik. PDFium ga je alocirao i prepustio vama; ako ga ne oslobodite (Free) u svakoj iteraciji, skupna pretvorba od nekoliko stotina stranica procurit će nekoliko stotina bitmapa i proces će se napuhnuti dok se ne sruši. Drugi je argument Color, ovdje postavljen na clWhite. PDF stranice obično se crtaju pod pretpostavkom neprozirne bijele podloge, a stranica s prozirnošću renderirana na pogrešnu boju pozadine stvara nejasne rubove ili tamne aureole. Bijela je ispravna zadana vrijednost za gotovo svaki dokument; parametar postoji za rijetke slučajeve u kojima to nije tako.

Vrijednosti 0, 0 su pomaci Left i Top na stranici u skaliranom koordinatnom prostoru, i ostavljate ih na nuli osim ako ne obrezujete sliku. Vrijednost ro0 je rotacija: ostavite je na nuli i PDFium će poštovati rotaciju koju stranica već deklarira u svom /Rotate unosu, pa će stranica koja je kreirana vodoravno (landscape) ispasti vodoravno bez ikakve vaše intervencije.

Kodiranje bitmape u JPEG

Nakon što bitmapa postoji, JPEG je lakši dio posla i to je čisti Delphi. TJPEGImage.Assign kopira bitmapu, CompressionQuality postavlja kvalitetu na ljestvici od 1 do 100, a SaveToFile zapisuje datoteku. Jedino pravilo redoslijeda je da se kvaliteta mora postaviti prije spremanja, jer ona upravlja kodiranjem koje pokreće metoda SaveToFile.

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;

Taj ugniježđeni try/finally blok izgleda pedantno za pomoćnu funkciju od jedne stranice, ali je točno ono što je potrebno za skupnu obradu. Unutarnji blok oslobađa koder, vanjski blok oslobađa bitmapu, a ako se na bilo kojem od njih pojavi iznimka, i dalje će se osloboditi ono što posjeduju. Spojite li ih u jedno, iznimka tijekom kodiranja može ostaviti bitmapu zarobljenom u memoriji. Na dugim stazama to je razlika između pretvarača koji završi posao i onog koji umre na 300. stranici s oštećenom datotekom i dijalogom o nedostatku memorije.

Odabir DPI-ja i kvalitete zajedno

Ove dvije opcije nisu neovisne o svrsi izlaza, a uobičajena pogreška je pojačavanje obje iz opreza. Minijatura za web renderirana pri 300 DPI i spremljena u kvaliteti 95 ima nekoliko stotina kilobajta iako glumi sliku od 120 piksela; preglednik ionako odbacuje gotovo sve to prilikom smanjivanja. Uskladite rezoluciju s pikselima koje izlaz stvarno treba, a zatim odaberite kvalitetu koja preživljava JPEG kompresiju s gubitkom bez vidljivih artefakata.

IzlazDPIKvaliteta JPEG-a
Minijatura popisa7260-70
Pregled na zaslonu96-15080-85
Pregled s puno detalja200-30085-95
Glavna slika za ispis300-60090-100

Kvaliteta JPEG-a sama po sebi zahtijeva oprez. To nije linearna ljestvica. Skok sa 70 na 85 donosi stvarno vizualno poboljšanje uz skroman rast datoteke; skok s 95 na 100 otprilike udvostručuje veličinu datoteke za razliku koju gotovo nitko ne može vidjeti, jer kvaliteta 100 i dalje nije bez gubitaka, samo prestaje odbacivati previše informacija. Za stranice s puno teksta, JPEG kompresija temeljena na blokovima razmazuje oštre rubove glifova u slabe sjene (ringing), zbog čega kvaliteta ispod 80 pretvara tekst u nešto što izgleda kao loš sken. Ako su stranice uglavnom tekstualne i možete mijenjati formate, PNG renderira taj tekst bez tih sjena; JPEG zaslužuje svoje mjesto na fotografskom i mješovitom sadržaju gdje je njegova kompresija doista učinkovitija.

Brže i manje minijature

Kada je cilj minijatura (thumbnail) umjesto vjerne reprodukcije, možete reći rendereru da radi manje. Parametar Options prima skup zastavica TRenderOption, a nekoliko njih mijenja kvalitetu za brzinu na točno onaj način koji mali pregled zahtijeva. reGrayscale uklanja boju, što ubrzava renderiranje i stvara manju bitmapu za kodiranje. Zastavice reNoSmoothImage i reNoSmoothPath preskaču zaglađivanje (anti-aliasing) koje je na razini minijature ionako nevidljivo.

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;

Slučaj s minijaturama također pokazuje čišći način razmišljanja o veličini. Umjesto korištenja DPI-ja, izračunajte jedinstveni faktor skale koji stranicu prilagođava okviru i čuva omjer širine i visine, što radi funkcija Min s ta dva omjera. I okomita i vodoravna stranica završavaju unutar istog okvira bez izobličenja, i nikada ne morate razmišljati o tome koji DPI odgovara opciji "stisni u 200 × 280". Jedna napomena vezana uz reGrayscale: pretvara rasterski sadržaj slike u sivu, ali vektorska punjenja i tekst zadržavaju svoje vrijednosti boja u mehanizmu, pa stranica koja se uglavnom sastoji od vektorske grafike može ispasti manje monokromatska nego što to naziv zastavice sugerira. Za pravi crno-bijeli rezultat, post-render funkcija GrayscalePdfBitmap pouzdaniji je put.

Skupna obrada cijelog dokumenta

Spajanje svega za cijeli dokument svodi se na petlju kroz PageCount, pri čemu se PageNumber pomiče za jednu po jednu stranicu. Stranice su indeksirane od 1: prva stranica je PageNumber := 1, a petlja ide do PageCount uključivo, a ne do PageCount - 1. Druga stvar koju skupna obrada mora poštovati je ugovor o tihom učitavanju. Postavljanje Active := True nikada ne podiže iznimku u slučaju oštećene datoteke ili pogrešne lozinke; samo ostavlja Active na False. Provjerite to svojstvo prije nego što renderirate ijednu stranicu, inače će prvi poziv RenderPage pokušati raditi s dokumentom koji nikada nije 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;

Dodavanje vodećih nula kroz Digits mala je stvar koja vam kasnije može uštedjeti cijelo poslijepodne. Ako datoteke imenujete od page_1.jpg do page_10.jpg, bilo koji alat koji ih sortira kao tekst stavit će page_10 odmah iza page_1, narušavajući redoslijed. Dodavanje nula prema širini najvećeg broja stranice (tako da dokument od 300 stranica daje page_001.jpg) drži leksički redoslijed i redoslijedu stranica identičnima svuda.

Za dokumente koji su dovoljno veliki da pretvorba traje primjetno vrijeme, pokrenite je izvan glavne UI dretve ili osvježavajte poruke (pump messages) između stranica kako bi aplikacija ostala responzivna, te pružite korisniku mogućnost zaustavljanja. Ako renderirate vrlo velike stranice i želite otkazivanje koje se aktivira usred stranice, a ne samo između stranica, PDFium VCL ima progresivnu putanju renderiranja s tokenom otkazivanja; to je složeniji mehanizam nego što većina skupnih izvoza treba, ali je tu kada je renderiranje pojedinačne stranice pri 600 DPI samo po sebi dovoljno sporo da blokira rad.

Još jedna kombinacija koju vrijedi znati. Rasterizacija stranice odbacuje njezin tekstualni sloj: JPEG se sastoji od piksela, a riječi u njemu više se ne mogu označiti niti pretraživati. Kada trebate i sliku i tekst ispod nje, renderirajte sliku i zasebno izvucite tekst, što pokriva prateći članak o ekstrakciji teksta iz PDF dokumenata pomoću PDFium VCL-a. Preopterećenja metode RenderPage i opcije renderiranja prikazane ovdje dio su komponente PDFium VCL Component za Delphi i C++Builder.