Egy PDF-oldal JPEG-be történő renderelése két olyan művelet, amelyet az emberek hajlamosak egybefuttatni, majd külön-külön hibakeresésnek (debug) alávetni. Először raszterizálja az oldalt egy pixel bittérképpé egy ön által választott felbontáson. Ezután átadja ezt a bittérképet egy JPEG kódolónak, és kiválaszt egy minőséget. A PDFium Component a RenderPage révén birtokolja az első felét; a második fele tiszta VCL, a Vcl.Imaging.jpeg-ből származó TJPEGImage. A kettő közötti varratnál (seam) születnek az érdekes döntések, mivel a renderelési oldalon választott felbontás és a kódolási oldalon választott minőség kompromisszumot (trade off) köt egymással és a fájlmérettel olyan módokon, amiket könnyű elrontani
A dolog, amit bármilyen kódolás előtt tudatosítania kell magában: egy PDF-oldalnak nincsenek pixelei. Pontokban (points) van leírva, ahol egy pont 1/72 hüvelyk (inch), és az oldal egy vektoros rajz, amelyet ezekben a pontokban mérnek. Amikor arra kéri a PDFiumot, hogy rendereljen, ön választja meg, hogy hány pixelre vetítse (project) azt a rajzot, és ez a választás a DPI. Ha elrontja a számítást (arithmetic), akkor vagy egy homályos bélyegképet renderel, amikor nyomdai mesterpéldányt (print master) szeretett volna, vagy pedig egy 200 megapixeles bittérképet foglal le olyasvalaminek, ami egy 120 pixeles előnézetnek készült
A DPI-től a pixelméretekig
A RenderPage egész (integer) Width (szélesség) és Height (magasság) pixeleket kér, nem pedig DPI-t. Tehát az első feladat az átváltás. Egy oldal a méretét pontokban a PageWidth és a PageHeight (mindkettő Double) segítségével adja meg, és a konverzió megegyezik azzal, amit minden raszterizáló használ: a pixelek száma egyenlő a pontok száma szorozva a cél DPI-vel, osztva 72-vel. Egy US Letter méretű oldal 612-szer 792 pont. 150 DPI mellett ez 1275-ször 1650 pixellé válik; 72 DPI-nél marad 612-szer 792, egy pixel pontonként, ami az az eset, amiről az emberek elfelejtik, hogy csak maga az azonosság (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
Két részlet abban a négy sorban dönti el, hogy a kód helyes-e. Az első az, hogy a RenderPage függvényes (function) formája egy olyan TBitmap-et ad vissza, ami az öné (you own). A PDFium lefoglalta, aztán elsétált; ha nem szabadítja fel (Free) minden iterációban, akkor egy néhány száz oldalas kötegelt művelet (batch) során néhány száz bittérkép szivárog el (leaks), és a folyamat addig duzzad (bloats), amíg valami össze nem omlik (falls over). A második a Color argumentum, jelen esetben clWhite. A PDF-oldalakat általában egy átlátszatlan (opaque) fehér hordozót feltételezve rajzolják, és egy átlátszóságot (transparency) tartalmazó oldal, ha rossz háttérszínre renderelik, sárba hajló (muddy) éleket vagy kósza sötét udvarokat (halos) eredményez. A fehér a megfelelő alapértelmezés majdnem minden dokumentumhoz; a paraméter arra a ritka esetre létezik, amikor nem az
A 0, 0 a Left és a Top eltolások (offsets) az oldalon, a skálázott koordinátatérben, és ezeket nullán hagyja, kivéve, ha vágást (cropping) végez. Az ro0 a forgatás: hagyja nullán, és a PDFium tiszteletben tartja azt a forgatást, amit az oldal eleve deklarál a /Rotate bejegyzésében, így egy tájképként (landscape) írt oldal anélkül is tájképként jelenik meg, hogy ön bármit is tenne
A bittérkép kódolása JPEG-ként
Amint a bittérkép létezik, a JPEG a könnyebb rész, és ez tiszta Delphi. A TJPEGImage.Assign bemásolja a bittérképet, a CompressionQuality beállítja a minőséget egy 1-től 100-ig terjedő skálán, a SaveToFile pedig kiírja a fájlt. Az egyetlen sorrendi szabály, hogy a minőséget a mentés előtt kell beállítani, mert ez vezérli a kódolást, amit a SaveToFile indít el
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;
Ez az egymásba ágyazott try/finally nyűgösnek (fussy) tűnhet egy egyoldalas segédfüggvény számára, de pont így megfelelő egy kötegelt feladathoz (batch). A belső blokk felszabadítja a kódolót, a külső blokk a bittérképet, és ha bármelyik kivételt (exception) dob, még mindig felszabadítja azt, ami az övé. Vonja össze őket egybe, és egy kódolás közbeni kivétel cserbenhagyhatja (strand) a bittérképet. Egy hosszú futás során ez a különbség a között, hogy a konvertáló befejezi a dolgát, vagy a 300. oldalon elhalálozik egy korrupt fájllal és egy "out-of-memory" (elfogyott a memória) dialógussal
A DPI és a minőség együttes megválasztása
Ez a két szabályozó (knobs) nem független a kimenet céljától, és a gyakori hiba az, hogy óvatosságból mindkettőt feltekerik. Egy 300 DPI-vel renderelt és 95-ös minőséggel mentett webes bélyegkép több száz kilobájtnyi fájl, amely 120 pixeles képnek adja ki magát; a böngésző szinte az egészet eldobja a lekicsinyítéskor (downscale). Igazítsa a felbontást (resolution) ahhoz a pixelszámhoz, amire a kimenetnek valójában szüksége van, majd válasszon egy olyan minőséget, amely látható műtermékek (artifacts) nélkül túléli a JPEG veszteséges tömörítését
| Kimenet | DPI | JPEG minőség |
|---|---|---|
| Lista bélyegkép | 72 | 60-70 |
| Képernyős előnézet | 96-150 | 80-85 |
| Részletgazdag megtekintés | 200-300 | 85-95 |
| Nyomdai mesterpéldány | 300-600 | 90-100 |
A JPEG minőség önmagában is megér egy figyelmeztető szót (word of caution). Ez nem egy lineáris skála (dial). A 70-ről 85-re ugrás valódi vizuális javulást hoz mérsékelt fájlnövekedés mellett; a 95-ről 100-ra ugrás nagyjából megduplázza a fájlt egy olyan különbségért, amit szinte senki sem lát, mivel a 100-as minőség még mindig nem veszteségmentes, pusztán felhagy az adatok nagyarányú eldobásával. Erősen szöveges oldalaknál a JPEG blokk alapú tömörítése a glifák (glyphs) éles széleit halvány "csengésbe" (ringing / kontúrhibába) keni el, ezért a 80 alatti minőség szkennelt kinézetűvé teszi a szöveget, aminek élesnek (crisp) kellene lennie. Ha az oldalak leginkább szöveget tartalmaznak, és meg tudja változtatni a formátumot, a PNG a szöveget ez a csengés nélkül rendereli; a JPEG a fényképeken és a vegyes tartalmakon érdemli ki a helyét, ahol a tömörítése valóban kisebb méretet eredményez
Gyorsabb, kisebb bélyegképek
Amikor a cél egy bélyegkép (thumbnail), nem pedig egy hű reprodukció, megmondhatja a renderelőnek, hogy kevesebbet dolgozzon. Az Options paraméter egy TRenderOption jelzőhalmazt (flags) vesz fel, és ezek közül néhány éppen úgy cseréli el a hűséget a sebességre, ahogyan egy kis előnézet igényli. A reGrayscale eldobja a színeket, ami egyrészt gyorsabb renderelést, másrészt kisebb kódolandó bittérképet eredményez. A reNoSmoothImage és a reNoSmoothPath kihagyják az élsimítást (anti-aliasing), amely bélyegkép méretben úgyis láthatatlan
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;
A bélyegkép esete a méretezésen való gondolkodás tisztább módját is megmutatja. Ahelyett, hogy a DPI-n menne keresztül, számítson ki egyetlen léptékezési tényezőt (scale factor), amely az oldalt egy határolókeretbe (bounding box) illeszti, és megőrzi a képarányt (aspect ratio) – pontosan ezt teszi a két arány minimuma (Min). Egy portré oldal és egy tájkép oldal egyaránt ugyanabban a dobozban végzi torzítás nélkül, és önnek soha nem kell azon agyalnia, hogy milyen DPI felel meg a "200-szor 280-as dobozba illesztésnek". Egy fenntartás (caveat) a reGrayscale kapcsolóval: a raszteres képtartalmat szürkére konvertálja, de a vektoros kitöltések (vector fills) és a szövegek megtartják a színértékeiket a motorban (engine), így egy javarészt vektorgrafikát (vector art) tartalmazó oldal kevésbé lehet monokróm, mint amit a jelző (flag) neve sugall. Egy valódi, teljesen szürkeárnyalatos (full-grayscale) eredményhez a renderelt bittérkép konvertálása a GrayscalePdfBitmap segítségével a megbízható út
Egy teljes dokumentum kötegelt feldolgozása
Ennek az egésznek az összerakása egy teljes dokumentumra annyit tesz, mint végighaladni egy cikluson a PageCount hosszan, ahol a PageNumber egyszerre csak egy oldalt mozog. Az oldalak 1-alapúak: az első oldal a PageNumber := 1, és a ciklus a PageCount-ig (azt is beleértve) fut, nem pedig PageCount - 1-ig. A másik dolog, amit a kötegelt műveletnek tiszteletben kell tartania, az a csendes-betöltés (silent-load) szerződés. Az Active := True beállítása soha nem dob kivételt (raises) egy sérült fájl vagy egy rossz jelszó esetén; egyszerűen False értéken hagyja az Active-t. Ellenőrizze le, mielőtt egyetlen oldalt is renderelne, különben az első RenderPage egy olyan dokumentum ellen fog dolgozni, amely soha nem is volt megnyitva
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;
A nullákkal való kiegészítés (zero-padding) a Digits segítségével apróság, ami később megspórol egy egész délutánt. Ha a fájlokat page_1.jpg-től page_10.jpg-ig nevezi el, bármilyen eszköz, amelyik sztringként (strings) rendezi sorba őket, a page_10-et rögtön a page_1 utánra teszi, összekuszálva (scrambling) a sorrendet. A legmagasabb oldalszám szélességéhez igazított kiegészítés – így egy 300 oldalas dokumentum esetén a page_001.jpg forma – megtartja a lexikális és az oldalrendi azonosságot (identical) a folyamat későbbi szakaszaiban (downstream) mindenhol
Azoknál a dokumentumoknál, amelyek elég nagyok ahhoz, hogy a konverzió észrevehető időt vegyen igénybe, futtassa azt a UI szálon (UI thread) kívül, vagy pumpáljon üzeneteket (pump messages) az oldalak között, hogy az alkalmazás válaszkész (responsive) maradjon, és adjon a felhasználónak egy lehetőséget a leállításra (stop). Ha nagyon nagy oldalakat renderel, és olyan megszakítást (cancellation) szeretne, ami menet közben, nem pedig csak az oldalak között csíp (bites mid-page), a PDFium Component rendelkezik egy progresszív renderelési útvonallal, megszakítási tokennel (cancellation token); ez nehézkesebb mechanizmus, mint amire a legtöbb kötegelt exportálásnak szüksége van, de rendelkezésre áll, amikor egyetlen oldal 600 DPI-n önmagában is elég lassú ahhoz, hogy blokkoljon
Még egy utolsó megjegyzés, amit érdemes tudni a párosításról. Az oldal raszterizálása eldobja annak szövegrétegét: a JPEG csak pixelek, és a benne lévő szavak többé nem jelölhetők ki vagy kereshetők. Ha egy képre és a mögöttes szövegre egyaránt szüksége van, renderelje a képet, a szöveget pedig külön húzza ki, amit a Szöveg kinyerése PDF dokumentumokból a PDFium Component segítségével című kísérő írás (companion piece) mutat be. Az itt bemutatott RenderPage túlterhelések (overloads) és renderelési beállítások a Delphihez és C++Builderhez készült PDFium Component részét képezik