HotPDF виставляє три ядра зменшення зображень через властивість ImageDownsampleKernel і окремий прохід Floyd-Steinberg через RenderOutputDither. Перше керує тим, як виглядають фотографії після того, як ви зменшили їх, щоб вписатися в бюджет розміру, друге — тим, як вони виглядають після того, як сторінку зведено до чорного й білого. Ні те, ні інше не ввімкнене за замовчуванням, і обидва — opt-in з тієї самої причини: вони коштують справжнього часу
Тиск, який приводить людей сюди, знайомий. Сканований контракт на 60 МБ мусить вийти через поштовий шлюз, який відхиляє все понад 10 МБ, або пачка виписок має приземлитися на монохромний пристрій у стилі факсу, який рендерить кожен сірий піксель або як папір, або як тонер. Обидві проблеми — проблеми ресемплінгу, і в обох є швидка відповідь, яка виглядає погано, і повільна, яка виглядає правильно
Чим три ядра справді відрізняються
THPDFResampleKernel має три значення, і вони сидять у справді різних точках кривої швидкість-якість. rkHalftone делегує історичному GDI-шляху StretchBlt з режимом HALFTONE, який, попри назву, є фільтрацією класу bilinear: швидкий, достатній для лінійної графіки й скріншотів і схильний до грубих країв, які ви миттєво впізнаєте на зменшених фотографіях. rkBicubic виконує сепарабельне ядро Catmull-Rom, а rkLanczos3 — сепарабельний віконний sinc із трьохлопасевою підтримкою
Обидва сепарабельні ядра працюють двома проходами, горизонтальним потім вертикальним, з 6 до 12 тапів на цільовий піксель у чистому Паскалі. Це приблизно на порядок повільніше за GDI-шлях, і саме тому rkHalftone лишається дефолтом. У нічній пачці з тисяч сторінок різниця — це рішення планувальника, а не вподобання. На одному документі, якого чекає користувач, Lanczos3 майже безкоштовний і наочний кращий
Дві властивості реалізації варто знати, бо вони визначають, що вивід може і чого не може. Межі стискаються реплікацією краю, а не загортанням чи згасанням, і ваги нормалізуються на цільовий піксель. Разом ці дві речі означають, що результат ніколи не дзвенить нижче чорного чи вище білого, тож класичний гало-переліт Lanczos навколо жорсткого краю не з'являється як обрізані артефакти в закодованому зображенні
var
Pdf: THotPDF;
Info: THPDFLoadedResourceOptimizationInfo;
Changed: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-contract.pdf');
Pdf.ImageDownsampleKernel := rkLanczos3; // задати до виклику
Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
if Changed > 0 then
begin
Writeln('resampled images: ', Info.DownsampledImageCount);
Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
Pdf.SaveToFile('scanned-contract-150dpi.pdf');
end;
finally
Pdf.Free;
end;
end;
Аргумент MinimumSavingsBytes, 4096 вище, — це захист, який тримає операцію чесною. Перекодування зображення, яке вже було ефективно стиснуте, може дати більший потік, ніж оригінал, і ресемплер, який сліпо замінює кожне зображення, час від часу виростить файл, який його просили зменшити. Поріг каже: підтверджуйте заміну, лише коли вона економить принаймні стільки байтів. PreservedCalibratedImageCount повідомляє інше консервативне рішення — зображення, які лишилися недоторканими, бо несуть калібрований кольоровий простір, що його ресемплінг скомпрометував би
Чому неправильний коефіцієнт полінома так важко помітити?
Бо зламане інтерполяційне ядро не падає і не кидає виключень, воно просто продукує зображення, яке виглядає ледь помітно неправильно так, що ніхто не може це атрибутувати. Ядро Catmull-Rom — кусково-кубічне, і його зовнішня гілка у вкладеній формі Горнера — ((-0.5t + 2.5)t - 4)t + 2. Напишіть той середній коефіцієнт як -5 замість -4, і функція все ще обчислюється, все ще повертає числа в правдоподібному діапазоні і все ще продукує зображення
Шкода проявляється як W(1), що дорівнює -1, коли мусить бути 0. Від'ємні ваги накопичуються, сума стискається до нуля, і видимий симптом — градієнт, чий лівий кінець іде в чорне, і ступінчастий край, який втрачає проміжні тони. Ніщо в цій відмові не вказує на поліном. Перевірка, що ловить це за секунди, — арифметична, а не візуальна: інтерполяційне ядро мусить задовольняти W(0) = 1 і W(±1) = W(±2) = 0, і будь-яке ядро, що промахується повз ці три точки, має помилку коефіцієнта, крапка. Стверджуйте про ці три значення в юніт-тесті — і весь клас дефектів-опечаток зникає
Дизеринг Floyd-Steinberg і його місце в конвеєрі
Прохід дизерингу — інша проблема, ніж ресемплінг, і живе в іншій точці конвеєра. RenderOutputDither застосовує дифузію помилки Floyd-Steinberg після композиції сторінки, і це єдине розташування, яке має сенс для монохромного попереднього перегляду друку чи експорту в стилі факсу: операція про те, щоб звести готовий растр до одного біта на піксель, а не про те, як окремі зображення масштабувалися на вході
Сам алгоритм короткий. Яскравість пороговується на 50 відсотків, а помилка квантування дифундується до чотирьох сусідів із класичними вагами 7/16, 3/16, 5/16 і 1/16 — вправо, вниз-ліворуч, вниз і вниз-праворуч. Вихідний піксель — 0 або 255 у кожному каналі. А наївна альтернатива — жорсткий поріг без дифузії — перетворює фотографію на силует і втрачає кожен середній тон, який ніс вміст
// Дизеринг під час рендеру для монохромного пристрою перегляду
Pdf.RenderOutputDither := True;
// Або застосуйте той самий прохід до бітмапу, який уже маєте. Бітмап
// мусить бути pf24bit; функція повертає False, а не вгадує
if not HPDFFloydSteinbergDitherBitmap(Preview) then
raise Exception.Create('dither expects a 24-bit bitmap');
// Прямий доступ до ядра, коли ресемплите поза документним конвеєром
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
Small.SaveToFile('thumb.bmp');
finally
Small.Free;
end;
Є одна деталь реалізації в дифузії помилки, яка кусає кожного один раз. Буфер помилки між рядками мусить накопичувати. Кожен піксель наступного рядка отримує внески від трьох різних пікселів поточного рядка — тапи 3/16, 5/16 і 1/16, — і якщо код присвоює замість додавання, кожен запис викидає попередній внесок, і лише останній тап виживає. Зображення все ще виглядає дизерингованим, чим і небезпечно — важко помітити, — але текстура неправильна, і тональне відтворення дрейфує. Тест, який це ловить, кількісний: дизеруйте однорідне середньо-сіре поле і вимагайте, щоб внутрішнє покриття впало між 40 і 60 відсотками
Яку комбінацію має використовувати конвеєр зменшення розміру?
Підбирайте ядро під те, чим зображення справді є, а дизеринг вважайте питанням пристрою, а не стиснення. Для фотографічних сканів, які мусять вижити в бюджеті розміру, rkLanczos3 на 150 чи 200 DPI зберігає деталі, які люди помічають, різючи кількість пікселів у чотири рази й більше. Для скріншотів, діаграм і лінійної графіки rkHalftone справді годиться і значно швидший, бо в тих зображень мало тональних градієнтів, які варто зберігати. Для змішаної пачки, яку ви не можете роздивитися по одному, rkBicubic — розумна середина: кращий за bilinear і приблизно вдвічі менша кількість тапів, ніж у Lanczos3
Зменшення — один із кількох важелів, і не завжди найбільший. Двобітрові скани зазвичай набагато краще відгукуються на кодувальник, розглянутий у нативному стисненні JBIG2 у Delphi, де виграш іде від символьних словників, а не від кількості пікселів. Перш ніж вирішувати, корисно знати, що справді в файлі, — для того й існує витяг зображень та їхніх фільтрів декодування: інвентаризація об'єктів зображень і їхнього наявного стиснення підкаже, чи має ресемплінг що вигравати
Якщо ви будуєте поверхню перегляду, яка показує результат, той самий шлях рендерингу, задокументований у рендерингу сторінки PDF у бітмап, — це те місце, де діє RenderOutputDither, тож дизерингований перегляд і дизерингований вивід ідуть з одного кодового шляху, а не з двох реалізацій, які роз'їжджаються
Широкий принцип за обома можливостями: налаштування якості мають бути явними й оборотними. HotPDF тримає історичну поведінку дефолтом, щоб наявний застосунок оновився без несподіваної зміни виводу чи часу, а кращі на вигляд, повільніші шляхи — на відстані одного присвоювання властивості. Обидва — частина HotPDF Delphi PDF component, поруч із механікою оптимізації ресурсів і рендерингу, на якій вони будуються