Три растеризатори можуть прочитати той самий PDF і розійтися в тому, що саме він каже. Вбудований рушій у PDF Library for Delphi — це той, що постачається без жодних додаткових файлів і компетентно рендерить усе поспіль, тому саме він і посідає місце за замовчуванням. Cairo приносить інший конвеєр прозорості та згладжування і зазвичай є тим рушієм, до якого сягають, коли деінде криво виходять м'які маски або режими змішування. PDFium несе в собі код рендерингу Chrome, тож сторінка, яка виглядає правильно в браузері, зазвичай виглядає правильно й під PDFium — ціною чималої DLL і розрядності, яку він наполегливо вимагає узгодити. Жоден із трьох не є правильним в абстракції. Правильність визначається для кожного документа окремо, і єдиний чесний спосіб дізнатися, який рушій впорається з даним корпусом файлів, — прогнати цей корпус через кожен із них
Це і є аргумент на користь того, щоб трактувати рушій як вибір під час виконання, а не під час збирання. PDF Library for Delphi, PDF-бібліотека для Delphi та C++Builder від losLab, ховає всі три рушії за єдиною поверхнею рендерингу, тож рішення коштує одного цілого числа замість гілки коду. Решта статті зводиться до того, як безпечно обирати між ними, як переконатися, які рушії насправді несе в собі розгорнутий бінарник, і як не дати стану рендерингу тихцем отруїти наступне завдання
Три растеризатори за однією поверхнею викликів
Бібліотека нумерує свої рушії. Рушій 1 — це вбудований рендерер, за замовчуванням, із параметрами згладжування GDI+ у Windows. Рушій 2 — це Cairo, а рушій 3 — це PDFium, обидва обираються під час виконання через SelectRenderer. Два зовнішні рушії завантажуються з DLL, шляхи до яких ви передаєте через SetCairoFileName і SetPDFiumFileName перед тим, як їх обрати. Який би рушій не був активним, робота проходить через ті самі виклики: RenderPageToFile, RenderPageToStream, RenderDocumentToFile. Перемикання рушіїв зсуває лише одне число; решта вашого коду рендерингу цього взагалі не помічає
Модель призначення сягає далеко за межі растрових зображень. Клас рендерера також націлюється на метафайли (WMF, EMF, EMF+), EPS, прямі контексти пристроїв, принтери й HTML5, причому Cairo та PDFium з'являються як додаткові призначення лише тоді, коли їх було включено під час збирання. Саме растровий вивід — те місце, де три рушії розходяться найпомітніше, тому саме його й використовують приклади в цій статті
Ніколи не покладайтеся на наявність рушія: обстежуйте його при старті
Cairo та PDFium — це можливості умовної компіляції, а це означає, що бінарник можна зібрати повністю без них. Коли так стається, запит рушія 2 чи 3 не викликає жодного винятку. SelectRenderer просто повертає значення, відмінне від запитаного вами ID, і код, що ігнорує це повернене значення, спокійно продовжує рендерити тим рушієм, який уже був активним. Захист — це обстеження при старті, яке просить кожен рушій ідентифікувати себе й записує відповідь:
function ProbeEngines(PDF: TPDFlib): string;
begin
Result := 'built-in'; // рушій 1 присутній завжди
if (PDF.SetCairoFileName('cairo.dll') = 1) and (PDF.SelectRenderer(2) = 2) then
Result := Result + ', cairo';
if (PDF.SetPDFiumFileName('pdfium.dll') = 1) and (PDF.SelectRenderer(3) = 3) then
Result := Result + ', pdfium';
PDF.SelectRenderer(1); // відновлюємо значення за замовчуванням перед реальною роботою
end;
Запускайте це обстеження один раз при старті й записуйте його результат у журнал поруч із кожним завданням рендерингу. Найпоширеніше запитання, коли клієнт повідомляє про відмінність у рендерингу, — які рушії насправді має його інсталяція, а однорядкова відповідь у журналі закриває це питання без сеансу віддаленого робочого столу. Корисний побічний ефект: якщо сам SetPDFiumFileName повертає 0, ви вже знаєте, що проблема саме в DLL (неправильний шлях, неправильна розрядність, відсутня залежність), а не в бінарнику, зібраному без підтримки PDFium, бо виклик зі шляхом не розв'язав нічого ще до того, як взагалі запустився SelectRenderer
Десять вихідних форматів за одним цілим числом Options
Параметр Options у викликах рендерингу обирає вихідне кодування: 0 — це BMP, 1 — JPEG, 2 — WMF, 3 — EMF, 4 — EPS, 5 — PNG, 6 — GIF, 7 — TIFF, 8 — EMF+, а 9 — HTML5. PNG (5) — розумний варіант за замовчуванням для попереднього перегляду й архівних зображень сторінок. JPEG (1) у парі з SetJPEGQuality — кращий вибір для фотографічних сканів, де розмір файлу важить більше за чіткість країв
Один формат ховає вимогу до цільового потоку. Шлях BMP спершу записує дані зображення, а потім повертається назад на зсув 0x26, щоб виправити поля роздільної здатності в заголовку. Спрямуйте це на потік лише вперед, обгортку стиснення чи мережевий сокет — і виклик провалиться так, що виглядатиме як помилка рушія, хоч насправді нею не є. Коли неможливо уникнути цілі без підтримки позиціювання, рендерте натомість PNG, або проведіть BMP через потік у пам'яті й скопіюйте його далі, щойно він буде готовий
DPI, яке ви передаєте, — не те DPI, яке ви отримуєте
Кожен виклик рендерингу приймає аргумент DPI, але роздільна здатність, яку ви насправді отримуєте, — це те значення, помножене на глобальний масштаб рендерингу. SetRenderScale стартує зі значення 1.0, і щойно ви його зміните, новий коефіцієнт мовчки застосовується до кожного подальшого рендерингу на цьому екземплярі:
PDF.SetRenderScale(2.0); // кожен подальший рендеринг подвоюється
PDF.RenderPageToFile(150, 1, 5, 'p1.png'); // фактично 300 DPI
PDF.SetRenderScale(1.0); // скидання, інакше ваші мініатюри вийдуть величезними
Та сама липкість стосується і SetRenderCropType, і параметра якості JPEG. У сервісі, що виробляє мініатюри, попередні перегляди й зображення для друкарської роздільної здатності з одного спільного екземпляра, саме ці залишкові налаштування насправді ховаються за зверненнями на кшталт «мініатюри раптом стали по 40 МБ». Два чисті виходи: скидати відповідний стан на початку кожної операції або виділити окремий екземпляр під кожен вихідний профіль, щоб ніщо не просочувалося між ними
Налаштування рушія за замовчуванням, перш ніж сягати по інший
Дивовижна частка запитів «нам потрібен інший рушій» насправді виявляється проблемою налаштувань під маскуванням. Вбудований рендерер розкриває свою поведінку згладжування через SetGDIPlusOptions і ширшу родину SetRenderOptions, а SetGDIPlusFileName дає змогу націлити його на конкретний рантайм GDI+, коли середовище розгортання постачає нетиповий. Зубчаста лінійна графіка при низькому DPI, розмитий текст у мініатюрах, смугастість на градієнтах: усе це відгукується на ці регулятори, а їх обертання не коштує нічого в інсталяторі. Додавання Cairo чи PDFium, навпаки, означає постачання додаткових DLL, супровід другого чи третього варіанта розрядності й обов'язок їх оновлювати
Тож у скарги на якість є природний порядок дій. Спершу відтворіть її з точнісінько таким самим DPI і масштабом, як у клієнта, бо в половині випадків різниця випаровується, щойно ці значення збігаються. Далі спробуйте параметри згладжування вбудованого рушія. І лише тоді поставте сторінку поряд для всіх рушіїв, тримаючи решту змінних незмінними: відрендерте її в PNG через рушії 1, 2 і 3 при однаковому DPI і докладіть усі три. Зазвичай два з трьох збігаються, і ця більшість підказує, чи виняток пояснюється тим, що документ інтерпретують по-різному, чи тим, що збилося саме ваше базове очікування. Три конкретні зображення закривають суперечку «рендериться неправильно» набагато швидше, ніж абзац прикметників
Ланцюжок резервних варіантів, що сам себе пояснює
Коли обстеження та дисципліна стану вже на місці, сам ланцюжок резервних варіантів короткий. Виявлення збою спирається на LastRenderError, який тримає власний текст повідомлення рушія для найостаннішого рендерингу і порожній, коли рендеринг завершився успішно:
procedure RenderPageWithFallback(PDF: TPDFlib; Page: Integer; const OutFile: string);
begin
PDF.SelectRenderer(1); // спершу вбудований
PDF.RenderPageToFile(200, Page, 5, OutFile); // 5 = PNG
if PDF.LastRenderError = '' then Exit;
LogEngineFailure('built-in', Page, PDF.LastRenderError);
if PDF.SelectRenderer(3) = 3 then // PDFium як важкий резервний варіант
begin
PDF.RenderPageToFile(200, Page, 5, OutFile);
if PDF.LastRenderError = '' then Exit;
LogEngineFailure('pdfium', Page, PDF.LastRenderError);
end;
raise Exception.CreateFmt('Page %d failed on all available engines', [Page]);
end;
Тут вагу мають два проєктні рішення. Ланцюжок фіксує, чому сталося кожне перемикання, бо рядок журналу на кшталт «ця сторінка перейшла на резервний PDFium, починаючи з релізу 3.7» — це сигнал регресії, який ви хочете бачити в тренді моніторингу, а не втраченим. Сам порядок резервних варіантів — це політика, яку варто обирати під кожне навантаження окремо. Вбудований рушій розгортається без жодних додаткових DLL, що робить його правильною першою спробою в більшості інсталяцій, тоді як документи, перевантажені групами прозорості чи нетиповим шейдингом, зазвичай і є причиною, з якої команда взагалі підключає альтернативний рушій. Жоден рушій не є найшвидшим загалом, і саме тому вибір робиться на кожен виклик окремо: проженіть бенчмарк кожного на вибірці ваших реальних документів за вашого реального DPI і переглядайте цей замір щоразу, коли змінюються DLL рушіїв або склад документів. Корпус документів виграє суперечку щоразу
За межами окремих сторінок: пакети TIFF і живі контексти пристроїв
Набір інструментів довершують два сусіди викликів для окремих сторінок. RenderAsMultipageTIFFToFile рендерить вираз діапазону сторінок прямо в багатосторінковий TIFF — природну форму для архівної передачі системам керування документами, що існували ще до PDF. RenderPageToDC малює прямо на контексті пристрою Windows для елементів попереднього перегляду, і керує ним власна трійка липких налаштувань (SetRenderDCOffset, SetRenderDCErasePage, плюс тип обрізання), яка потребує тієї самої дисципліни скидання, що й коефіцієнт масштабу. Рендеринг попереднього перегляду на екрані та шляху друку несе достатньо власних пасток, щоб заслуговувати на окрему статтю, посилання на яку — нижче
Куди рухатися далі
Одна звичка, яку варто нести далі: оскільки SelectRenderer набуває чинності для кожного подальшого виклику на цьому екземплярі, одну вперту сторінку можна повторно спробувати на іншому рушії, поки решта документа лишається на рушії за замовчуванням. Про малювання попереднього перегляду, вибір принтера й роботу з DevMode читайте далі в статті про попередній перегляд друку та контекст пристрою. Коли рендеринг живить високооб'ємний конвеєр на дуже великих файлах, підхід на основі дескрипторів із посібника з прямого доступу природно поєднується з порядковим рендерингом сторінок через DARenderPageToFile
Пакування рушіїв, підтримувані формати й пробні збірки детально описані на сторінці продукту PDF Library for Delphi