Две жалби пристигат седмицата след като функция за компресия излезе: сканираният договор вече има стъпаловидни, окосмени буквени форми, а прозрачното лого на корицата седи в бледо хало. PDFiumPas отговаря на двете на едно място. TPdf.OptimizeImages измерва всяко изображение, преди да го свие, после избира ресемплиращо ядро и акумулира цвета в alpha-aware форма
Това не беше винаги така. Преди v3.100.0 същият метод намаляваше всяко не-двуниво изображение с фиксирана стъпка най-близък съсед, което е точно алгоритъмът, произвеждащ двете жалби: той точково семплира един изходен пиксел за изходен пиксел и третира RGB-то, седящо под напълно прозрачен пиксел, така сякаш читател някога би го видял. Преработката във v3.100.0 заменя този единствен път с пет ядра, измерено правило за избор и изричен бюджет за работна памет
Защо намаляването на разделителната способност прави сканирания текст назъбен?
Защото точковото семплиране отговаря на грешния въпрос. Когато скан от 300 DPI се пренасочва към 150 DPI, всеки целеви пиксел представлява блок от два на два изходни пиксела, а най-близкият съсед задържа един от четирите и отхвърля останалите. Кой оцелява зависи от закръглянето, така че ръб на щрих, който е бил гладко антиалиаснат в източника, се превръща в хвърляне на монета за пиксел. Резултатът е класическото алиасирано стълбище по ръбовете на глифовете, плюс муар върху полутонови области, където отхвърлените семпли случайно са носили шарката. Това тежи повече в PDF, отколкото на екрана, защото щетата е постоянна. Изображение XObject носи семпълните си данни заедно с /Width, /Height и /BitsPerComponent (ISO 32000-1 §8.9.5), а ресемплирането преписва и трите вътре във файла. Лошото мащабиране в зрител е кадър, който можете да пречертаете, а PDFiumPas има отделна машина за това в кеша на рендериране и производителността при мащабиране. Лошото надолу-семплиране е нов документ, който връчате на клиента
Как PDFiumPas измерва детайла и избира ядро
PDFiumPas решава за изображение, не за документ. Преди да избере ядро, той изчислява нормализиран резултат за луминантен детайл от ограничена семплираща мрежа: хоризонталната и вертикалната стъпка са (Width + 63) div 64 и (Height + 63) div 64, така че скан от 12000 пиксела и миниатюра от 300 пиксела струват почти едно и също 64-на-64 обхождане. На всяка семплирана позиция той сумира абсолютната разлика със съседа вдясно и съседа отдолу, през до три канала, после дели на броя семпли пъти 255. Резултатът каца между 0 и 1, където плоската бизнес графика седи близо до нула, а плътната фотографска текстура се изкачва
Стълбата за избор после върви в фиксиран ред. Ако ResampleFilter е нещо друго освен pirfAdaptive, този филтър се използва дословно. Иначе: съдържание от 1 бит взима pirfBilevel; ContentClass от piccLineArt взима pirfBox; мащабен коефициент 4 или повече също взима pirfBox, защото при такова свиване средното по площ е едновременно най-евтиният и най-коректният отговор; piccPhoto, резултат за детайл 0.08 или по-висок или PreferredQuality от 0.9 или по-високо взима pirfLanczos с неговото триделно ядро; мащаб 2 или повече или качество 0.7 или по-високо взима pirfBicubic с радиус 2; всичко останало взима pirfBilinear. Тъй като TPdfImageOptimizeOptions.Default задава PreferredQuality на 0.85, пускане по подразбиране никога не се връща към bilinear, освен ако свиването не е леко и съдържанието плоско
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// По подразбиране: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, качество 0.85, бюджет 64 MiB.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
Изображение се пипа само когато по-голямото от неговото хоризонтално и вертикално DPI на поставяне, разделено на TargetDpi, достига MinDpiRatio. Тази защита съществува, за да не се прекодира снимка от 160 DPI, насочена към цел 150 DPI, заради печалба от шест процента, която струва едно поколение качество. Изображения под MinDimension по която и да е ос, 8 по подразбиране, се пропускат като икони или линии
Защо прозрачните логота получават бял контур?
Защото цветът под напълно прозрачен пиксел е произволен, а обикновеното претеглено средно му позволява да гласува. Експортирате лого от инструмент за дизайн и невидимото поле често е бяло, или черно, или каквото и да е било платното; алфа каналът го скрива, а права сума върху отпечатъка на ядрото веднага го смесва обратно в видимия ръб. PDFiumPas избягва това, като акумулира BGRA семпли в premultiplied форма и отменя умножението едва на целевия пиксел
Конкретно, всеки допринасящ семпъл добавя channel * alpha * weight към акумулатора на цвят, alpha * weight към алфа акумулатор и weight към сумата тегла. Целевият цвят после се дели на алфа акумулатора, а не на сумата тегла, и това е стъпката, която има значение: деленето на сумата тегла би завлякло цвета към невидимите пиксели, докато деленето на акумулираната алфа реконструира цвета, за който видимите семпли действително са се споразумели. Целевата алфа е отделна величина, 255 * AlphaSum / WeightSum. Форматите без алфа делят на сумата тегла както обикновено, подложният байт на цел FPDFBitmap_BGRx се записва като константа 255, а всеки канал се ограничава в 0 до 255, преди да се съхрани. Тази алфа обикновено произхожда от запис на soft mask в речника на изображението (ISO 32000-1 §11.4), който PDFium вече е композирал в BGRA буфера, който ресемплерът получава
// Форма на вътрешния цикъл на акумулиране, за всеки допринасящ изходен семпъл
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... а на целевия пиксел умножението се отменя спрямо сумата алфа
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
Държане на 1-битовата щрихова графика извън сивата зона
Всяко непрекъснато ядро, приложено върху двуниво скан, произвежда сиво, а сивото е точно това, което изображение във факс стил няма право да съдържа. Затова PDFiumPas оставя 1-битовите изображения на мира по подразбиране: PreserveBilevel е True в TPdfImageOptimizeOptions.Default, а такива изображения кацат в SkippedCount непипнати. Зададете ли False, пътят pirfBilevel поема вместо изглаждащо ядро. Той обхожда точния изходен правоъгълник, покриващ всеки целеви пиксел, усреднява луминантността с тегла 0.114, 0.587 и 0.299 в BGR паметов ред и прагува резултата на 127.5 в плоско 0 или 255. Нищо междинно не може да се запише, така че ръбовете остават остри и не се образува сиво хало около тънките щрихи; алфа каналът на BGRA източник се усреднява нормално, а BGRx цел получава константата 255. Ако ви трябват лежащите отдолу пиксели, а не по-малък документ, извличането на изображения от PDF документи е отделният път
Какво се случва, когато изображение надхвърли бюджета за работна памет?
Оставя се точно такова, каквото е било, и се брои. MaxWorkingBytes е 64 MiB по подразбиране и се прилага два пъти. Преди целевият bitmap да бъде създаден, PDFiumPas отхвърля изображението, ако широчина по височина по байтове на пиксел надхвърля бюджета. След като FPDFBitmap_CreateEx успее, той проверява отново с реалния stride по височина, защото подложката на редовете може да избута алокация покрай ограничение, което наивното произведение е минало. Всяко от двете отхвърляния унищожава целта и не връща нищо. Бъдете ясни за деградацията, която това предполага: изображение над бюджета не се ресемплира в по-ниско качество и не се разцепва на плочки. Оригиналът остава в документа, BudgetExceededCount и SkippedCount и двата се увеличават, така че едно пускане може да докладва успех, докато документът е само частично оптимизиран. Това е нарочно поведение fail-safe, но означава, че отчетът не е по избор четиво. Съществува и отделен режим на провал: изображения, чийто bitmap PDFium изобщо не може да произведе, като CMYK, JPX, JBIG2 или маскирани източници, увеличават FailedCount вместо това и също остават непипнати
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // използай гласуването по площ за bilevel
Options.ContentClass := piccPhoto; // принуди Lanczos за фото набори
Options.MaxWorkingBytes := 256 * 1024 * 1024; // резерва за големи сканове
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
Прочитане на отчета, преди да предадете файла
TPdfImageOptimizeReport е изграден да бъде диагностициран, а не просто логван. До OptimizedCount, SkippedCount и FailedCount той излага по един брояч за ядро, така че BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount и BilevelFilterCount ви казват какво адаптивното правило реално е заключило за вашия корпус. Изцяло box резултат значи, че свиванията са били стръмни или съдържанието е класифицирано като щрихова графика; изцяло Lanczos резултат върху документ, за който сте вярвали, че е щрихова графика, е знак, че ContentClass трябва да се зададе изрично. AverageDetailScore е числото за сравнение с прага 0.08 за Lanczos при настройка на PreferredQuality, а PeakWorkingBytes показва колко от MaxWorkingBytes пускането реално е използвало. Невалидни опции се провалят шумно, а не тихо: не-положителен TargetDpi, MinDpiRatio под 1, PreferredQuality извън 0 до 1 или не-положителен MaxWorkingBytes хвърля EPdfError, преди която и да е страница да е пипната. А OptimizeImages редактира само документа в паметта; всяка променена страница се фикира с FPDFPage_GenerateContent, след което пак вие извиквате SaveAs. За да видите с просто око какво се е променило, рендерирайте документите преди и след към bitmap-и, както е описано в конвертирането на PDF страници към JPEG изображения, и ги сравнете на пълен мащаб
Адаптивното ресемплиране е една от онези функции, които са невидими, когато работят, и генерират заявки за поддръжка, когато не работят, което е причината измерването, обработката на алфата и паметовият бюджет да трябваше да кацнат заедно, а не като три отделни усъвършенствания. Ако оценявате това за продукт на Delphi, C++Builder или Lazarus, пълната API повърхност и детайлите за лицензиране са на страницата на PDFiumPas Delphi PDFium компонента