Renderowanie strony PDF do formatu JPEG to dwie operacje, które ludzie mają tendencję wykonywać razem, a następnie osobno debugować. Najpierw rasteryzujesz stronę do pikselowej mapy bitowej w wybranej przez siebie rozdzielczości. Następnie przekazujesz tę mapę bitową do kodera JPEG i wybierasz jakość. Komponent PDFium obsługuje pierwszą połowę dzięki metodzie RenderPage; druga połowa to zwykły VCL, TJPEGImage z modułu Vcl.Imaging.jpeg. Złącze pomiędzy nimi to miejsce, gdzie kryją się interesujące decyzje, ponieważ rozdzielczość wybrana po stronie renderowania oraz jakość wybrana po stronie kodowania idą na kompromis ze sobą oraz z rozmiarem pliku, a na te kompromisy łatwo można wpłynąć w niewłaściwy sposób
To, co należy sobie przyswoić przed napisaniem jakiegokolwiek kodu: strona PDF nie ma pikseli. Została opisana w punktach, gdzie jeden punkt to 1/72 cala, a strona jest rysunkiem wektorowym mierzonym w tych punktach. Kiedy prosisz PDFium o wyrenderowanie strony, wybierasz liczbę pikseli, na które ten rysunek zostanie zrzutowany, i ten wybór to wartość DPI. Popełnij błąd w obliczeniach, a wyrenderujesz rozmytą miniaturę zamiast gotowego wydruku o wysokiej jakości lub wyalokujesz mapę bitową o rozdzielczości 200 megapikseli na potrzeby podglądu o wielkości zaledwie 120 pikseli
Od DPI do wymiarów w pikselach
Metoda RenderPage wymaga całkowitych wartości pikseli dla właściwości Width (szerokość) i Height (wysokość), a nie wartości DPI. Więc pierwszym krokiem jest konwersja. Strona zgłasza swój rozmiar w punktach poprzez właściwości PageWidth i PageHeight (obie typu Double), a konwersja jest tą samą, jakiej używa każdy rasteryzator: piksele są równe punktom pomnożonym przez docelowe DPI podzielone na 72. Strona US Letter ma 612 na 792 punkty. Przy 150 DPI daje to 1275 na 1650 pikseli; przy 72 DPI wartość ta zostaje przy 612 na 792 (jeden piksel na punkt), co jest przypadkiem, o którym ludzie często zapominają, że jest po prostu wartością tożsamości
// 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
Dwa szczegóły w tych czterech linijkach decydują o poprawności kodu. Pierwszy to fakt, że funkcyjna forma metody RenderPage zwraca obiekt TBitmap, który należy do ciebie. PDFium go wyalokował i zostawił; jeśli nie wykonasz Free przy każdej iteracji, partia ponad kilkuset stron doprowadzi do wycieku setek map bitowych, a proces będzie rósł, aż coś się zawali. Drugi to argument parametru Color, którym w tym przypadku jest clWhite. Strony PDF są najczęściej rysowane z założeniem użycia nieprzezroczystego białego podłoża, a strona z włączoną przezroczystością wyrenderowana na złym kolorze tła produkuje zabrudzone krawędzie lub ciemne obwódki. Biały to właściwe ustawienie domyślne dla niemal każdego dokumentu; ten parametr wprowadzono z myślą o wyjątkowych sytuacjach, w których tak nie jest
Wartości 0, 0 to przesunięcia dla właściwości Left (lewa) i Top (góra) w stronę, wewnątrz wyskalowanego układu współrzędnych, a zostawia się je wyzerowane pod warunkiem braku kadrowania. Wartość ro0 dotyczy rotacji: jeśli zostawisz w niej zero, PDFium uszanuje jakikolwiek obrót strony, który strona deklaruje już we wpisie /Rotate, więc jeśli na stronie wymuszono opcję poziomą, zostanie wyświetlona jako pozioma, nie wymagając od ciebie żadnych działań
Kodowanie mapy bitowej jako JPEG
Kiedy masz już mapę bitową, reszta z plikiem JPEG jest prosta, to wręcz czyste Delphi. Metoda TJPEGImage.Assign wkleja bitmape do wewnątrz, metoda CompressionQuality ustala jakość na skali od 1 do 100, a wywołanie SaveToFile powoduje zapis pliku. Jedyna zasada co do kolejności odnosi się do faktu, że jakość należy ustalić przed zapisem, ponieważ steruje ona operacją kodowania, którą wywołuje użycie opcji 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;
Zagnieżdżenie try/finally wygląda na fanaberię w przypadku pomocnika jednej strony, ale do masowych wdrożeń okazuje się idealne. Wewnętrzny blok zwalnia koder, a blok zewnętrzny uwalnia bitmape. Wystąpienie błędu (exception) i tak poskutkuje zwolnieniem zarezerwowanego zasobu. Spróbuj zmieścić je do jednego bloku, a problem z kodowaniem może uziemić mapę bitową. Na dłuższą metę to od tego zależy, czy praca konwertera dobiegnie pomyślnego zakończenia, czy program odmówi współpracy po przerobieniu 300 stron, wyświetlając tylko powiadomienie o braku pamięci (Out of Memory dialog) i zostawiając cię z uszkodzonym plikiem
Wspólny wybór wartości DPI oraz jakości
To nie jest tak, że nie ma związku między tymi dwoma wskaźnikami a ostatecznym przeznaczeniem pliku wyjściowego, jednak najpopularniejszym błędem jest maksymalne zwiększenie obu tych atrybutów "na wszelki wypadek". Jeżeli wyrenderujesz sieciową ikonkę przy rozdzielczości równej 300 DPI i ustawisz na koniec parametr "Jakość" (Quality) na poziomie równym 95 punktom (w 100-punktowej skali), uzyskasz plik o wadze rzędu kilkuset kilobajtów uchodzący za "niewinny" i mały pliczek złożony z raptem 120 pikseli; przeglądarka i tak odrzuci lwią część informacji na etapie zaniżania skali (downscale). Dopasuj rozdzielczość w taki sposób, aby liczba pikseli idealnie odpowiadała faktycznym potrzebom formatu docelowego, a dopiero na końcu skup się na dostrojeniu opcji odpowiedzialnej za ustawienia "Jakości", by przetrwać ubytki generowane przy stosowaniu stratnej metody kompresji charakterystycznej dla formatu JPEG przy uniknięciu rzucających się w oczy, niemiłych dla wzroku zniekształceń widoku (artifacts)
| Format wyjściowy | DPI | Jakość JPEG |
|---|---|---|
| Miniatura na liście | 72 | 60-70 |
| Podgląd na ekranie | 96-150 | 80-85 |
| Widok o wysokiej szczegółowości | 200-300 | 85-95 |
| Wersja główna do druku | 300-600 | 90-100 |
Jakość JPEG warta jest osobnego słowa ostrzeżenia. To nie jest skala liniowa. Skok z 70 do 85 kupuje nam autentyczną, zauważalną poprawę wizualną za cenę skromnego wzrostu pliku; skok z 95 do 100 z grubsza podwaja rozmiar pliku dla różnicy, której prawie nikt nie dostrzeże, ponieważ jakość 100 nadal nie jest bezstratna, ona po prostu przestaje wiele odrzucać. W przypadku stron mocno nasyconych tekstem oparta na blokach kompresja JPEG rozmywa ostre krawędzie glifów w słabe, przypominające echo artefakty kompresji (ringing), co jest powodem, dla którego jakość poniżej 80 sprawia, że tekst, który miał być wyrazisty, wygląda jak na słabym skanie. Jeśli strony to w większości tekst i możesz zmienić format, format PNG wyrenderuje ten tekst bez zjawiska dzwonienia krawędzi; JPEG ma uzasadnione racje bytu w przypadku materiałów fotograficznych oraz takich o mieszanej zawartości, gdzie wypracowana kompresja daje o wiele niższe gabaryty
Szybsze, mniejsze miniatury
Kiedy Twoim docelowym celem są miniatury, a wcale nie wierne odwzorowanie (reprodukcja), z powodzeniem poinstruujesz proces odpowiedzialny po stronie sprzętowo-programowej za przebieg renderingu, aby na chwilę "spuścił nieco z tonu" przy ilości zaplanowanej na rzecz wykonywania zadań włożonej pracy własnej. Parametr dla okna pod nazwą Opcje, czyli tak właściwie Options chłonie pulę poleceń flag z TRenderOption na zasadzie "całego zestawu reguł". Kilka z nich skutecznie wymieni się kosztem wierności za skok wydajności, czyli w tym przypadku mowa naturalnie o szybkości, oferując kompromisy dokładnie w takim wariancie, jakiego wymaga mały plik potrzebny wyłącznie dla rzutu podglądowego. Jeśli posłużysz się parametrem reGrayscale (skala szarości) system porzuci dla Ciebie odwzorowanie ze standardów pełnego obłożenia po stronie oddania gamy barw w ogólnym znaczeniu koloru. W efekcie rendering nabiera szalonego wręcz przyspieszenia podczas trwania swojej fazy u wyjścia, a tak zbudowana siatka po stronie kompresji wygeneruje dużo lżejszą oraz zminiaturyzowaną wersję pod mapę bitową na wypadek dalszego działania i pracy modułu w stronę fazy "kodowania obrazu". W celu pójścia o krok dalej, wyeliminuj opcję tak zwanego wygładzania w mechanice u podłoża "anty-aliasing" używając dwóch metod wywołania. reNoSmoothImage za jednym zamachem wyeliminuje nam niepotrzebny szlif przyporządkowany do pliku grafiki i to zaraz na samym starcie operacji z silnika u podstaw graficznego wyjścia (przy czym to reNoSmoothPath to w gruncie rzeczy porzucenie w obwodzie tak zwanego miękkiego prowadzenia krawędzi na samej docelowej powłoce i pod ścieżkami pętli na danym nośniku). Tego i tak absolutnie nigdzie i pod żadnym kątem nie dostrzeże ludzkie ani wprawne analityczne sprzętowe "oko", w momencie jak proces pomniejszania skalowania zaabsorbuje nam dany plik docelowy spłaszczając do poziomu zwykłej oraz mikroskopijnej formy miniaturki (ang. miniaturce scale level na rzędzie ułożenia dla zdefiniowanej uprzednio formy, tzw. rzut z lotu u góry "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;
Przypadek użycia miniaturki pokazuje także czystszy sposób myślenia o wymiarowaniu. Zamiast przechodzić przez DPI, oblicz pojedynczy współczynnik skali, który dopasowuje stronę do obwiedni z zachowaniem współczynnika proporcji, co osiąga się po przez użycie funkcji Min z dwóch wskaźników proporcji. Zarówno strona pionowa (portrait), jak i strona pozioma (landscape) lądują w tym samym pudełku bez zniekształceń, a ty nigdy nie musisz dedukować, jakie DPI odpowiada w układzie "dopasuj w wymiary 200 na 280". Jedno zastrzeżenie dotyczące reGrayscale: konwertuje ono zawartość obrazów rastrowych na odcienie szarości, ale wektorowe wypełnienia i tekst zachowują swoje wartości koloru w silniku, więc strona składająca się w większości ze sztuki wektorowej może wrócić mniej monochromatyczna, niż sugeruje nazwa samej flagi. W celu uzyskania wiarygodnego wyniku w pełnej skali szarości konwersja wyrenderowanej mapy bitowej z wywołaniem GrayscalePdfBitmap stanowi sprawdzoną, niezawodną ścieżkę
Przetwarzanie całego dokumentu w trybie wsadowym
Złożenie tego wszystkiego w całość dla pełnego dokumentu sprowadza się do pętli po PageCount, przy czym właściwość PageNumber jest przesuwana o jedną stronę naraz. Strony są oparte na indeksowaniu od 1: pierwsza strona to PageNumber := 1, a pętla wykonuje się łącznie z PageCount (inkluzywnie), a nie do PageCount - 1. Inną rzeczą, którą w trybie wsadowym należy mieć na uwadze, jest cicha natura założeń obwiązujących przy procedurze wczytywania zasobów. Ustawienie wartości przypisanej u zmiennej na Active := True w żadnej ze spraw wywoływanych u programisty na "tapecie przy pracy" absolutnie w żadnym toku akcji u siebie nie zaraportuje u góry "braku pomyślnego zakończenia przez głośne podniesienie alarmowego wywołania u komunikatu na widok zgłoszonego awarią" (ang. raise on fault) gdybyśmy u siebie posiadali wadliwe czy ułomne od zepsucia pliki na wejściu lub po drodze nie dopisało w dostępie ze względu na trefnie założone do dostępu w pliku, wpisane za złą w pisowni po stronie hasłowania, blokady opatrzone mylnie do klucza w parametrze "hasło". Moduł zostawi wtedy zaledwie wpis po swojej stronie w trybie roboczym o wartościach opisanych z wiersza, jako zostawienie od tak u parametru wezwania ze zmiany pod adresem Active przy obrocie u dołu bez echa po sztywnym progu rzędu do wzięcia pod zaprzeczenie oblanego ułożenia z zapisem jako odwrócony walor u stanu zaprzeczenia na ułomność trybu z etykietą przypisaną i zatwierdzoną wpisem "pod etykietę" u opisu wyjściowego wygenerowanego na False. Koniecznie przeprowadź odpowiednie rozpoznanie bojem pod sprawdzanie zanim postawisz na jeden choć wyrys do odpalenia renderowania o jednej czy też na podgląd po pierwszej stronie, w gorszym losie Twoje zadania dla zleconego odczytania powędrują przeciw muru pod żądania a pierwsze zlecenie przy podanym jako instruktaż zadaniu do podglądu, użyte pod komendą wiersz po wierszu pod wpisem u polecenia wpisanego przez Ciebie jako wywoływacz funkcji z rzędu pod kluczem RenderPage wykona potężny rozbieg bez namysłu pod adres ucelowany prosto na wylot u pustego lub w najlepszym przy zepsutym pod warstwę pliku jako operacja wyzwolona na dokument nigdy u programu nawet po samej instalacji po wciśnięciu u siebie jeszcze przy uruchomieniu nie otwarty i w dostępie nam zatrzasknięty całkowicie jako zamknięty do obróbki
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;
Dopełnianie zerami za pomocą Digits to mała rzecz, która później pozwala zaoszczędzić popołudnie. Nazwij pliki od page_1.jpg do page_10.jpg, a dowolne narzędzie, które sortuje je jako ciągi znaków, umieści page_10 zaraz po page_1, psując w ten sposób ich szyk i właściwe ułożenie przy podglądaniu numeracji dla zaplanowanych i obmyślanych uprzednio dokumentów po ułożeniu jako praca i kolejność podglądu z ułożenia ze względu na listę z pozycjami przy porządku u nas w plikach i aktach na biurku (order). Dopełnianie (Padding) wykonane do szerokości u wielkości wygenerowanej jako odpowiednio szeroki zarys przy ustaleniu u numeru powiązanego ze stroną o najwyższym u szczytu notowaniu, co sprawia, że mając dokument trzystustronicowy (300-stron) owocuje wynikiem u zapisu dla pierwszej z nich jako ułożony w podaniu pod wynik na ekran plik zwany w systemie roboczym jako i przy postaci wizualnej noszącej etykietę wpisu page_001.jpg. Zachowuje to porządek leksykalny obok porządku stron by w swej postaci z wyglądu były i stały przy zapisie jako ze wszech miar pod wszystkim u szczytów z zapisem i dołu tożsame ze strony na każdy jeden krok po zrzucie u podstawy operacyjnej dla dalszego użytku o spłaszczonej obróbce i drodze obróbki na wylocie bez ułomnej komplikacji na przepływ przez kanał rzutu wyłaniany na etapie generowania wyjściowego rzutu u procesu spłaszczonego przetwarzania dla wyników przy niższych strukturach potoków zrzuconego u dołu obłożenia, poddawanego w fazie kolejnej na etapie podanym "w strefie na wylocie, dalej u podłoża w przetworzeniu w dole procesu na obieg u dołu przy układzie strumienia w dół i pod spód po obniżonym podrzędnie cyklu na rzędzie" (zwanym na marginesie u ludzi pod anglojęzycznym pojęciem downstream)
Dla dokumentów na tyle dużych, że konwersja zajmuje zauważalną ilość czasu, uruchom ją poza głównym wątkiem interfejsu użytkownika lub pompuj komunikaty między stronami, aby aplikacja pozostała responsywna i dała użytkownikowi sposób na przerwanie. Jeśli renderujesz bardzo duże strony i chcesz mieć opcję przerwania, która łapie w połowie strony, a nie tylko pomiędzy stronami, Komponent PDFium ma ścieżkę progresywnego renderowania z tokenem przerwania (cancellation token); to cięższy mechanizm, niż zazwyczaj potrzebuje większość masowych eksportów, ale jest on na miejscu, gdy pojedyncza strona przy 600 DPI jest wystarczająco wolna, by zablokować aplikację
Ostatnie połączenie warte poznania. Rasteryzacja strony odrzuca jej warstwę tekstu: JPEG to piksele, a znajdujące się w nim słowa nie są już możliwe do zaznaczenia ani wyszukiwania. Kiedy potrzebujesz zarówno obrazu, jak i leżącego pod nim tekstu, wyrenderuj go, aby uzyskać obraz, i wyciągnij tekst osobno, co obejmuje materiał towarzyszący na temat wyodrębniania tekstu z dokumentów PDF za pomocą komponentu PDFium. Przeciążenia funkcji RenderPage oraz opcje renderowania pokazane w tym dokumencie są częścią Komponentu PDFium dla Delphi i C++Builder