Tehnički članak

Renderovanje PDF stranica u JPEG slike u Delphi-ju pomoću PDFium Component-a

Pretvaranje PDF stranice u JPEG predstavlja dve operacije koje ljudi imaju tendenciju da spoje, a zatim da ih otklanjaju odvojeno. Prvo rasterizujete stranicu u piksel bitmapu u rezoluciji koju odaberete. Zatim tu bitmapu predajete JPEG enkoderu i birate kvalitet. PDFium Component upravlja prvom polovinom procesa preko metode RenderPage; druga polovina je običan VCL, konkretno TJPEGImage iz Vcl.Imaging.jpeg. Spoj između njih je mesto gde leže zanimljive odluke, jer rezolucija koju izaberete na strani renderovanja i kvalitet koji izaberete na strani kodiranja utiču jedno na drugo, kao i na veličinu datoteke na načine koje je lako pogrešno shvatiti

Stvar koju treba usvojiti pre bilo kakvog koda jeste sledeća: PDF stranica nema piksele. Ona je opisana u tačkama (points), gde je jedna tačka 1/72 inča, a stranica je vektorski crtež meren u tim tačkama. Kada tražite od PDFium-a da renderuje, vi birate na koliko piksela želite da projektujete taj crtež, a taj izbor je DPI. Pogrešite u aritmetici i dobićete ili mutnu sličicu (thumbnail) kada ste želeli master za štampanje, ili ćete alocirati bitmapu od 200 megapiksela za nešto što je namenjeno da bude pregled od 120 piksela

Od DPI-ja do dimenzija u pikselima

RenderPage zahteva celobrojnu širinu (Width) i visinu (Height) u pikselima, a ne DPI. Dakle, prvi zadatak je konverzija. Stranica prijavljuje svoju veličinu u tačkama preko svojstava PageWidth i PageHeight (oba tipa Double), a konverzija je ista ona koju koristi svaki rasterizator: pikseli su jednaki tačkama pomnoženim sa ciljnim DPI-jem i podeljenim sa 72. Stranica formata US Letter ima 612 sa 792 tačke. Na 150 DPI to postaje 1275 sa 1650 piksela; na 72 DPI ostaje 612 sa 792, jedan piksel po tački, što je slučaj identiteta koji ljudi često zaboravljaju

// 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 ova četiri reda odlučuju da li je kod ispravan. Prvi je taj što funkcijski oblik metode RenderPage vraća TBitmap nad kojim vi preuzimate vlasništvo. PDFium ga je alocirao i prepustio vama; ako ga ne oslobodite (Free) u svakoj iteraciji, paketna obrada preko nekoliko stotina stranica će procuriti nekoliko stotina bitmapa i proces će rasti sve dok se program ne sruši. Drugi detalj je argument Color, ovde postavljen na clWhite. PDF stranice se obično crtaju pretpostavljajući neprozirnu belu podlogu, a stranica sa transparentnošću renderovana na pogrešnu boju pozadine proizvodi zamućene ivice ili neželjene tamne oreole. Bela je ispravna podrazumevana vrednost za skoro svaki dokument; parametar postoji za retke slučajeve kada to nije slučaj

Vrednosti 0, 0 su Left i Top ofseti na stranici, u skaliranom koordinatnom prostoru, i ostavljate ih na nuli osim ako ne vršite isecanje (cropping). ro0 označava rotaciju: ostavite je na nuli i PDFium poštuje rotaciju koju stranica već deklariše u svom unosu /Rotate, tako da stranica kreirana u pejzažnoj (landscape) orijentaciji izlazi kao pejzažna bez ikakvog vašeg učešća

Kodiranje bitmape kao JPEG slike

Kada bitmapa postoji, JPEG je lakši deo i to je čist Delphi. TJPEGImage.Assign kopira bitmapu unutra, CompressionQuality podešava kvalitet na skali od 1 do 100, a SaveToFile upisuje datoteku. Jedino pravilo redosleda jeste da kvalitet mora biti podešen pre nego što sačuvate datoteku, jer on upravlja kodiranjem koje pokreće 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 ugnježđeni try/finally blok izgleda previše detaljno za pomoćnu funkciju od jedne stranice, ali je potpuno ispravan za paketnu obradu. Unutrašnji blok oslobađa enkoder, spoljašnji blok oslobađa bitmapu, i bilo koji od njih koji se pokrene usled izuzetka i dalje oslobađa ono što poseduje. Spojite ih u jedan i izuzetak tokom kodiranja može ostaviti bitmapu zarobljenu u memoriji. Tokom dugog izvršavanja, to je razlika između konvertora koji završava posao i onog koji umire na 300. stranici sa oštećenom datotekom i dijalogom o nedostatku memorije

Zajednički izbor DPI-ja i kvaliteta

Dva kontrolna parametra nisu nezavisna od svrhe izlaza, a uobičajena greška je povećanje oba parametra iz predostrožnosti. Veb sličica renderovana na 300 DPI i sačuvana sa kvalitetom 95 iznosi nekoliko stotina kilobajta pretvarajući se da je slika od 120 piksela; pregledač odbacuje skoro sve to prilikom smanjenja razmere. Uskladite rezoluciju sa pikselima koji su izlazu stvarno potrebni, a zatim izaberite kvalitet koji preživljava JPEG kompresiju sa gubitkom bez vidljivih artefakata

IzlazDPIJPEG kvalitet
Sličica liste7260-70
Pregled na ekranu96-15080-85
Prikaz visokih detalja200-30085-95
Master za štampu300-60090-100

JPEG kvalitet zaslužuje reč opreza sam po sebi. To nije linearna skala. Skok sa 70 na 85 donosi stvarno vizuelno poboljšanje uz umeren rast datoteke; skok sa 95 na 100 otprilike duplira veličinu datoteke za razliku koju skoro niko ne može da primeti, jer kvalitet 100 i dalje nije bez gubitaka, već samo prestaje da odbacuje mnogo podataka. Za stranice bogate tekstom, JPEG kompresija zasnovana na blokovima zamućuje oštre ivice glifova u slabe prstenaste artefakte (ringing), zbog čega kvalitet ispod oko 80 čini da tekst izgleda loše na onome što bi trebalo da bude jasan izlaz. Ako su stranice uglavnom tekstualne i možete da promenite format, PNG renderuje taj tekst bez tih artefakata; JPEG zaslužuje svoje mesto na fotografskom i mešovitom sadržaju gde je njegova kompresija zaista efikasnija

Brže, manje sličice

Kada je cilj sličica, a ne verna reprodukcija, možete reći rendereru da radi manje posla. Parametar Options prima skup TRenderOption zastavica, a nekoliko njih menja vernost za brzinu na način koji mali pregled upravo i zahteva. reGrayscale odbacuje boju, što renderuje brže i proizvodi manju bitmapu za kodiranje. reNoSmoothImage i reNoSmoothPath preskaču anti-aliasing koji je ionako nevidljiv na skali sličice

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 sličice takođe pokazuje čistiji način razmišljanja o veličini. Umesto da idete preko DPI-ja, izračunajte jedinstveni faktor skale koji uklapa stranicu unutar graničnog okvira i čuva odnos širine i visine, što je upravo ono što Min od ta dva odnosa radi. Portretna i pejzažna stranica na kraju završavaju unutar istog okvira bez deformacija, i nikada ne morate da razmišljate o tome koji DPI odgovara uklapanju u "200 sa 280". Jedna napomena u vezi sa reGrayscale: on pretvara rasterski sadržaj slike u sivu, ali vektorske ispune i tekst zadržavaju svoje vrednosti boja u mehanizmu, tako da se stranica koja je uglavnom vektorska umetnost može vratiti manje jednobojna nego što naziv zastavice sugeriše. Za pravi puni grayscale rezultat, funkcija GrayscalePdfBitmap sa cheat sheet-a je pouzdan put nakon renderovanja

Paketna obrada celog dokumenta

Spajajući sve to za ceo dokument dobija se petlja preko PageCount, sa PageNumber koji se pomera za po jednu stranicu. Stranice su indeksirane od 1: prva stranica je PageNumber := 1, a petlja ide do PageCount uključujući nju, a ne do PageCount - 1. Druga stvar koju paketna obrada mora poštovati jeste ugovor o tihom učitavanju. Postavljanje Active := True nikada ne podiže izuzetak na oštećenoj datoteci ili pogrešnoj lozinki; samo ostavlja Active na False. Proverite ga pre nego što renderujete ijednu stranicu, inače prva metoda RenderPage radi sa dokumentom koji zapravo 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;

Popunjavanje nulama (zero-padding) preko vrednosti Digits je sitnica koja štedi celo popodne kasnije. Nazovite datoteke od page_1.jpg do page_10.jpg i bilo koji alat koji ih sortira kao stringove staviće page_10 odmah iza page_1, narušavajući redosled. Popunjavanje do širine najvećeg broja stranice, tako da dokument od 300 stranica daje page_001.jpg, održava leksički redosled i redosled stranica identičnim svuda nizvodno

Za dokumente koji su dovoljno veliki da konverzija zahteva primetno vreme, pokrenite je van niti korisničkog interfejsa (UI thread) ili pumpajte poruke između stranica kako bi aplikacija ostala responzivna, i dajte korisniku način da je zaustavi. Ako renderujete veoma velike stranice i želite otkazivanje koje deluje usred stranice, a ne samo između stranica, PDFium Component ima progresivnu putanju renderovanja sa tokenom otkazivanja; to je teži mehanizam od onog koji je potreban većini paketnih izvoza, ali je tu kada je jedna stranica na 600 DPI sama po sebi dovoljno spora da blokira izvršavanje

Još jedno uparivanje koje vredi znati. Rasterizacija stranice odbacuje njen tekstualni sloj: JPEG se sastoji od piksela, a reči u njemu se više ne mogu selektovati niti pretraživati. Kada su vam potrebni i slika i tekst ispod nje, renderujte sliku i izvucite tekst odvojeno, što je pokriveno u pratećem članku o ekstrakciji teksta iz PDF dokumenata pomoću PDFium Component-a. Preopterećenja metode RenderPage i opcije renderovanja prikazane ovde deo su PDFium Component komponente za Delphi i C++Builder