Tehnički članak

Renderiranje PDF stranica u JPEG slike u Delphiju pomoću PDFium Component-a

Renderiranje PDF stranice u JPEG su dvije operacije koje ljudi obično provode zajedno, a zatim zasebno ispravljaju pogreške (debug). Prvo rasterizirate stranicu u bitmapu piksela (pixel bitmap) pri rezoluciji koju odaberete. Zatim predajete tu bitmapu JPEG koderu (encoder) i odabirete kvalitetu. PDFium Component posjeduje prvu polovicu kroz RenderPage; druga polovica je obični VCL, TJPEGImage iz Vcl.Imaging.jpeg. Šav (seam) između njih je mjesto gdje žive zanimljive odluke, jer rezolucija koju odaberete na strani renderiranja i kvaliteta koju odaberete na strani kodiranja međusobno se kompenziraju (trade off), ali i prema veličini datoteke, na načine koje je lako pogriješiti

Stvar koju treba internalizirati prije bilo kakvog koda: PDF stranica nema piksele. Opisana je u točkama (points), gdje je jedna točka 1/72 inča, a stranica je vektorski crtež mjeren u tim točkama. Kada tražite od PDFium-a renderiranje, vi birate na koliko piksela ćete projicirati taj crtež, a taj izbor je DPI. Ako pogriješite u aritmetici, ili renderirate mutnu sličicu (blurry thumbnail) kada ste željeli predložak za ispis (print master), ili dodijelite bitmapu od 200 megapiksela nečemu što je predodređeno za pregled od 120 piksela

Od DPI do dimenzija piksela

RenderPage želi cijeli broj piksela Width (Širina) i Height (Visina), a ne DPI. Dakle, prvi posao je pretvaranje (converting). Stranica izvještava o svojoj veličini u točkama kroz PageWidth i PageHeight (oboje Double), a pretvorba je ista ona koju koristi svaki rasterizator: pikseli su jednaki točkama puta ciljni (target) DPI podijeljeno sa 72. US Letter stranica je 612 sa 792 točke. Pri 150 DPI to postaje 1275 sa 1650 piksela; na 72 DPI ostaje 612 puta 792, jedan piksel po točki, što je slučaj za koji ljudi zaboravljaju da je samo identitet (identity)

// 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 odlučuju je li kod točan. Prvi je da oblik funkcije (function form) za RenderPage vraća TBitmap koji vi posjedujete. PDFium ga je dodijelio i otišao; ako ga ne oslobodite (Free) pri svakoj iteraciji, hrpa od nekoliko stotina stranica propušta (leaks) nekoliko stotina bitmape i proces nabubri (bloats) dok nešto ne padne (falls over). Drugi je argument Color, ovdje clWhite. PDF stranice obično se crtaju pod pretpostavkom neprozirne (opaque) bijele podloge, a stranica s prozirnošću (transparency) renderirana na krivu boju pozadine proizvodi mutne (muddy) rubove ili zalutale tamne aureole (stray dark halos). Bijela je prava zadana postavka (default) za gotovo svaki dokument; parametar postoji za rijetke slučajeve gdje nije

0, 0 su Left (lijevo) i Top (vrh) pomaci (offsets) unutar stranice, u skaliranom (scaled) koordinatnom prostoru, i ostavljate ih na nuli osim ako ne obrezujete (cropping). ro0 je rotacija: ostavite je na nuli i PDFium poštuje bilo koju rotaciju koju stranica već deklarira u svom /Rotate unosu, tako da stranica izrađena u pejzažnom obliku (landscape) ispadne pejzažno bez da išta poduzmete

Kodiranje bitmape kao JPEG

Nakon što bitmapa postoji, JPEG je lakši dio i to je čisti Delphi. TJPEGImage.Assign kopira bitmapu unutra, CompressionQuality postavlja kvalitetu na skali od 1 do 100, a SaveToFile zapisuje datoteku. Jedino pravilo za naručivanje (ordering rule) je da kvaliteta mora biti postavljena prije spremanja jer upravlja kodiranjem koje SaveToFile pokreće (triggers)

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 (nested) try/finally izgleda previše detaljno (fussy) za pomoćnika na jednoj stranici, a savršeno je ispravan za seriju (batch). Unutarnji (inner) blok oslobađa (frees) koder, vanjski (outer) blok oslobađa bitmapu, a pucanje (firing) bilo kojeg od njih zbog iznimke (exception) i dalje oslobađa ono što posjeduje. Srušite (collapse) ih u jedan i iznimka tijekom kodiranja može ostaviti bitmapu nasukanu (strand). Dugoročno (over a long run) gledano to je razlika između pretvarača (converter) koji završi i onog koji umre na stranici 300 s oštećenom (corrupt) datotekom i dijalogom o nedostatku memorije (out-of-memory)

Odabir DPI i kvalitete zajedno

Dva gumba (knobs) nisu neovisna o svrsi (purpose) izlaza (output), a uobičajena pogreška (common mistake) je paljenje oba iz opreza (caution). Web sličica (web thumbnail) prikazana na 300 DPI-a i spremljena uz kvalitetu 95 iznosi nekoliko stotina kilobajta i pretvara se da je slika od 120 piksela; preglednik odbacuje gotovo sve to prilikom smanjenja razmjera (downscale). Uskladite rezoluciju s pikselima koje izlaz (output) zapravo treba, a zatim odaberite kvalitetu koja preživljava JPEG-ovu kompresiju s gubicima bez vidljivih artefakata (artifacts)

IzlazDPIJPEG kvaliteta
Sličica popisa7260-70
Pregled na zaslonu96-15080-85
Pregled visokih detalja200-30085-95
Predložak za ispis300-60090-100

JPEG kvaliteta zaslužuje (is worth) riječ opreza (word of caution) sama po sebi. To nije linearni brojčanik (linear dial). Skok (jump) od 70 do 85 kupuje pravo vizualno poboljšanje (improvement) za skroman (modest) rast datoteke; skok s 95 na 100 otprilike udvostručuje (doubles) datoteku za razliku koju gotovo nitko ne vidi, jer kvaliteta 100 još uvijek nije bez gubitaka, nego se samo prestaje s prevelikim odbacivanjem (discarding much). Za stranice s velikim brojem teksta (text-heavy), JPEG-ova blokovska kompresija razmazuje (smears) oštre (sharp) rubove glifova u blijedo zujanje (faint ringing), zbog čega kvaliteta ispod oko 80 stvara tekst koji izgleda skenirano (scanned-looking) na onome što bi trebalo biti oštar izlaz (crisp output). Ako su stranice uglavnom tekstualne i možete mijenjati formate, PNG renderira (renders) taj tekst bez zujanja; JPEG zarađuje svoje mjesto na fotografskom (photographic) i mješovitom (mixed) sadržaju gdje je njegova kompresija istinski manja

Brže, manje sličice (thumbnails)

Kada je cilj sličica (thumbnail) umjesto vjerne reprodukcije (faithful reproduction), možete reći alatu za renderiranje da radi manje. Parametar Options zauzima skup zastavica (flags) TRenderOption, a neke od njih mijenjaju vjernost (fidelity) za brzinu točno onako kako to želi mali pregled (small preview). reGrayscale ispušta (drops) boju, što renderira brže i proizvodi manju bitmapu za kodiranje (encode). reNoSmoothImage i reNoSmoothPath preskaču ublažavanje neravnina (anti-aliasing) koje je ionako nevidljivo u mjerilu minijature (thumbnail scale)

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čica također (thumbnail case) pokazuje čišći (cleaner) način razmišljanja o veličini (sizing). Umjesto prolaska kroz DPI, izračunajte (compute) jedan faktor mjerila (scale factor) koji uklapa (fits) stranicu unutar graničnog okvira (bounding box) i zadržava omjer stranica (aspect ratio), što radi i Min od dva omjera. Obje stranice, (portrait i landscape), završavaju unutar istog okvira bez izobličenja (distortion), a vi nikada ne morate razmišljati (reason) o tome koji DPI odgovara na "stane u 200 x 280". Jedno upozorenje (caveat) s reGrayscale: on pretvara sadržaj rasterske (raster) slike u sivo, ali vektorske ispunjenosti (vector fills) i tekst zadržavaju svoje vrijednosti boja u stroju (engine), tako da se stranica koja je uglavnom vektorska umjetnost može vratiti manje jednobojna nego što to sugerira ime zastavice (flag). Za pravi potpuno sivi rezultat, pretvaranje renderirane bitmape s GrayscalePdfBitmap pouzdan je (reliable) put

Stavljanje u hrpu (batching) cijelog dokumenta

Sastavljanje za puni dokument je petlja (loop) preko PageCount, pri čemu se PageNumber pomiče za jednu po jednu stranicu. Stranice su bazirane na broju 1 (1-based): prva stranica je PageNumber := 1, a petlja ide do uključeno (inclusive) PageCount, a ne PageCount - 1. Druga stvar koju serija mora poštovati je ugovor o tihom opterećenju (silent-load contract). Postavljanje Active := True nikada se ne povećava (raises) na oštećenoj datoteci (damaged file) ili pogrešnoj lozinki (wrong password); samo ostavlja Active na False. Provjerite to prije nego što renderirate pojedinačnu stranicu ili će prvi RenderPage raditi protiv dokumenta koji nikada nije bio 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;

Dopuna nula (zero-padding) kroz Digits (znamenke) je sitnica (small thing) koja kasnije spašava poslijepodne. Imenujte datoteke page_1.jpg do page_10.jpg i svaki alat koji ih razvrstava (sorts) kao nizove znakova (strings) stavlja page_10 odmah iza page_1, miješajući (scrambling) redoslijed. Dopunjavanje (padding) do širine najvećeg broja stranice, tako da dokument od 300 stranica daje page_001.jpg, zadržava (keeps) leksički (lexical) redoslijed i redoslijed stranica identičnima svugdje niže u toku (downstream)

Za dokumente koji su dovoljno (enough) veliki (large) da pretvorba traje primjetno vrijeme (noticeable time), pokrenite je izvan UI niti (thread) ili ispumpajte (pump) poruke (messages) između stranica kako bi aplikacija ostala osjetljiva na odaziv (responsive) i dajte korisniku način da se zaustavi. Ako prikazujete (rendering) vrlo velike stranice i želite otkazivanje (cancellation) koje grize (bites) na sredini stranice, a ne samo između stranica, PDFium Component ima progresivni put (progressive path) renderiranja sa znakom otkazivanja (cancellation token); to je teži mehanizam od onoga što treba većina skupnih izvoza (batch exports), ali tu je kada je jedna stranica na 600 DPI sama po sebi dovoljno spora da se blokira

Zadnji par (pairing) za koji vrijedi (worth) znati. Rasteriziranjem stranice odbacuje se njen tekstualni sloj (text layer): JPEG se sastoji od piksela (pixels) i riječi u njemu se više ne mogu odabrati (selectable) ili pretraživati (searchable). Kada trebate i sliku i temeljni tekst (underlying text), napravite renderiranje za sliku i zasebno povucite (pull) tekst, što pokriva popratni članak na vađenje teksta iz PDF dokumenata pomoću PDFium Component-a. Preopterećenja (overloads) RenderPage i opcije renderiranja prikazane ovdje dio su PDFium Component komponente za Delphi i C++Builder