Рендирането на PDF страница в JPEG са две операции, които хората са склонни да изпълняват заедно и след това да отстраняват грешки поотделно. Първо растеризирате страницата в пикселна растерна графика (bitmap) при резолюция, която изберете. След това предавате тази растерна графика на JPEG енкодер и избирате качество. PDFium Component притежава първата половина чрез RenderPage; втората половина е обикновен VCL, TJPEGImage от Vcl.Imaging.jpeg. Шевът между тях е мястото, където живеят интересните решения, защото резолюцията, която избирате от страната на рендирането, и качеството, което избирате от страната на кодирането, се компенсират едно друго и спрямо размера на файла по начини, които лесно могат да се объркат
Нещото, което трябва да осъзнаете преди какъвто и да е код: PDF страницата няма пиксели. Тя е описана в точки, където една точка е 1/72 инча, а страницата е векторна рисунка, измерена в тези точки. Когато поискате от PDFium да рендира, вие избирате върху колко пиксела да се проектира тази рисунка, и този избор е DPI. Сгрешете аритметиката и или ще рендирате размазана миниатюра, когато сте искали оригинален мастър за печат (print master), или ще заделите 200-мегапикселова растерна графика за нещо, предназначено да бъде 120-пикселов преглед
От DPI до размери в пиксели
RenderPage иска цяло число за пиксели Width и Height, а не DPI. Така че първата задача е конвертирането. Една страница отчита размера си в точки чрез PageWidth и PageHeight (и двете Double), и конвертирането е същото, което използва всеки растеризатор: пикселите са равни на точките по целевия DPI, разделено на 72. Страница US Letter е 612 на 792 точки. При 150 DPI това става 1275 на 1650 пиксела; при 72 DPI тя остава 612 на 792, по един пиксел на точка, което е случаят, за който хората забравят, че е просто идентичността
// 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
Две подробности в тези четири реда решават дали кодът е правилен. Първата е, че функционалната форма на RenderPage връща TBitmap, който вие притежавате. PDFium го е заделил и си е тръгнал; ако не го освободите (Free) на всяка итерация, партида от няколкостотин страници изпуска няколкостотин растерни графики и процесът се издува, докато нещо не падне. Втората е аргументът Color, clWhite тук. PDF страниците обикновено се рисуват с предположението за непрозрачен бял субстрат (substrate), и страница с прозрачност, рендирана върху грешен цвят на фона, произвежда мътни ръбове или блуждаещи тъмни ореоли. Бялото е правилната настройка по подразбиране за почти всеки документ; параметърът съществува за редкия случай, когато не е
0, 0 са отместванията Left и Top в страницата, в мащабираното координатно пространство, и ги оставяте на нула, освен ако не изрязвате (cropping). ro0 е ротация: оставете го на нула и PDFium ще спази каквато и ротация страницата вече декларира в своя запис /Rotate, така че страница, създадена като пейзаж (landscape), излиза като пейзаж, без да правите нищо
Кодиране на растерната графика като JPEG
След като растерната графика съществува, JPEG е лесната част, и това е чист Delphi. TJPEGImage.Assign копира растерната графика вътре, CompressionQuality задава качеството по скала от 1 до 100, а SaveToFile записва файла. Единственото правило за подреждане е, че качеството трябва да бъде зададено, преди да запазите, защото то управлява кодирането, което 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;
Този вложен try/finally изглежда превзет за помощник за една страница, но е точно правилен за пакетна обработка. Вътрешният блок освобождава енкодера, външният блок освобождава растерната графика, и задействането на който и да е от тях при изключение все пак освобождава това, което притежава. Свийте ги в един и изключение по време на кодирането може да изостави растерната графика. При продължителна работа това е разликата между конвертор, който завършва, и такъв, който умира на страница 300 с повреден файл и диалог за липса на памет
Избор на DPI и качество заедно
Двете копчета не са независими от целта на изхода и честата грешка е и двете да се увеличат от предпазливост. Уеб миниатюра, рендирана на 300 DPI и запазена с качество 95, е няколкостотин килобайта, преструващи се на 120-пикселово изображение; браузърът изхвърля почти цялото от него при намаляването на мащаба. Съобразете резолюцията с пикселите, от които изходът действително се нуждае, след което изберете качество, което оцелява при компресията със загуби на JPEG без видими артефакти
| Изход | DPI | JPEG качество |
|---|---|---|
| Миниатюра за списък | 72 | 60-70 |
| Преглед на екран | 96-150 | 80-85 |
| Преглед с висока детайлност | 200-300 | 85-95 |
| Оригинал за печат (Print master) | 300-600 | 90-100 |
JPEG качеството си струва да бъде отбелязано с една дума за внимание само по себе си. То не е линеен циферблат. Скокът от 70 на 85 купува реално визуално подобрение при скромен растеж на файла; скокът от 95 на 100 грубо удвоява файла за разлика, която почти никой не може да види, защото качество 100 все още не е без загуби, то просто спира да изхвърля много. За страници с много текст базираната на блокове компресия на JPEG размазва острите ръбове на глифовете в бледо звънене (ringing), ето защо качество под около 80 прави текст, изглеждащ като сканиран, върху това, което би трябвало да е отчетлив изход. Ако страниците са предимно текст и можете да променяте формати, PNG рендира този текст без звъненето; JPEG печели мястото си при фотографско и смесено съдържание, където компресията му е неподправено по-малка
По-бързи, по-малки миниатюри
Когато целта е миниатюра, а не вярно възпроизвеждане, можете да кажете на рендерера да върши по-малко работа. Параметърът Options приема набор от флагове TRenderOption и няколко от тях разменят вярност (fidelity) за скорост точно по начина, по който малък преглед иска. reGrayscale премахва цвета, което едновременно рендира по-бързо и произвежда по-малка растерна графика за кодиране. reNoSmoothImage и reNoSmoothPath пропускат заглаждането (anti-aliasing), което така или иначе е невидимо в мащаба на миниатюра
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;
Случаят с миниатюрата показва и по-чистия начин да се мисли за оразмеряването. Вместо да минавате през DPI, изчислете единствен коефициент на мащабиране, който побира страницата вътре в ограничаваща кутия (bounding box) и запазва съотношението, което е това, което Min от двете съотношения прави. Портретна страница и пейзажна страница се озовават вътре в една и съща кутия без изкривяване, и никога не ви се налага да разсъждавате на какъв DPI съответства „побиране в 200 на 280“. Едно предупреждение с reGrayscale: той конвертира съдържанието на растерни изображения в сиво, но векторните запълвания и текстът запазват своите цветови стойности в енджина, така че страница, която е предимно векторно изкуство, може да се върне по-малко монохромна, отколкото подсказва името на флага. За истински резултат в пълна скала на сивото (full-grayscale), конвертирането на рендираната растерна графика с GrayscalePdfBitmap е надеждният път
Пакетна обработка на цял документ
Сглобяването на всичко за пълен документ е цикъл върху PageCount, като PageNumber се премества по една страница наведнъж. Страниците са базирани на 1: страница едно е PageNumber := 1, и цикълът работи до PageCount включително, а не PageCount - 1. Другото нещо, което партидата трябва да спазва, е договорът за мълчаливо зареждане. Задаването на Active := True никога не хвърля изключение при повреден файл или грешна парола; то просто оставя Active на False. Проверете го, преди да рендирате дори една страница, иначе първият RenderPage работи срещу документ, който никога не е бил отварян
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;
Допълването с нули чрез Digits е малко нещо, което спестява един следобед по-късно. Наименувайте файловете от page_1.jpg до page_10.jpg и всеки инструмент, който ги сортира като низове, поставя page_10 веднага след page_1, разбърквайки реда. Допълването до ширината на най-високия номер на страница, така че документ с 300 страници да дава page_001.jpg, запазва лексикалния ред и реда на страниците идентични навсякъде по веригата
За документи, достатъчно големи, за да може конвертирането да отнеме забележимо време, изпълнявайте го извън нишката на потребителския интерфейс (UI thread) или изпомпвайте съобщения между страниците, така че приложението да остане отзивчиво, и дайте на потребителя начин да спре. Ако рендирате много големи страници и искате отмяна, която хваща по средата на страницата, а не само между страниците, PDFium Component има прогресивен път за рендиране с токен за отмяна; това е по-тежък механизъм, отколкото повечето пакетни експорти се нуждаят, но е там, когато една страница при 600 DPI сама по себе си е достатъчно бавна, за да блокира
Едно последно сдвояване, което си струва да знаете. Растеризирането на страница изхвърля нейния текстов слой: JPEG-ът е пиксели, а думите в него вече не могат да бъдат избирани или търсени. Когато имате нужда както от изображение, така и от лежащия отдолу текст, рендирайте за изображението и изтеглете текста отделно, което е покрито в съпътстващата статия за извличане на текст от PDF документи с PDFium Component. Претоварванията (overloads) на RenderPage и опциите за рендиране, показани тук, са част от PDFium Component за Delphi и C++Builder