Будь-який вбудований переглядач PDF зазвичай супроводжують дві скарги: великі документи рендеряться з нуля при кожному запуску, а текст стає розмитим, щойно вікно перетягують на монітор із високою роздільною здатністю (High-DPI). losLab PDF Library вирішує обидві проблеми у своєму компоненті TPDFlibViewer: властивість DiskCacheFolder активує міжсесійний дисковий кеш сторінок на основі відбитка документа, а властивість ScreenDPI разом із викликом GetDpiForWindow та обробником повідомлення WM_DPICHANGED забезпечують компоненту повноцінну підтримку DPI для кожного монітора окремо
Обидві ці проблеми варто сприймати серйозно, оскільки користувачі помічають їх одразу. Відсканований договір на 400 сторінок, прокручування якого вчора ставало доступним за вісім секунд, сьогодні знову вимагає тих самих восьми секунд, хоча у файлі нічого не змінилося. А на робочому столі з різними моніторами переглядач, який рендерить сторінки із системним значенням 96 DPI первинного монітора, виглядає добре на ньому, але стає помітно розмитим на сусідній 4K-панелі. Ця стаття детально описує, як TPDFlibViewer вирішує кожну з цих проблем і, що важливіше, чому архітектура склалася саме так. Якщо ви тільки починаєте знайомство з цим компонентом, почніть із огляду інтерактивного компонента переглядача PDF, а потім повертайтеся до налаштувань продуктивності
Як кешувати відрендерені сторінки PDF між сеансами в Delphi?
Призначте папку властивості TPDFlibViewer.DiskCacheFolder, і компонент зберігатиме відрендерені бітові карти сторінок на диск, завдяки чому нещодавно відкритий документ завантажуватиметься миттєво замість повторного рендерингу. На диску структура має такий вигляд: <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin — по одній підпапці на кожен документ і по одному файлу на кожну комбінацію сторінки та масштабу. Усе інше відбувається автоматично; немає потреби викликати API кешування чи примусово щось очищати
uses
PDFlibViewer;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FViewer := TPDFlibViewer.Create(Self);
FViewer.Parent := Self;
FViewer.Align := alClient;
// One line turns on the cross-session page cache
FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;
Лічильник DiskCacheHits показує, скільки операцій рендерингу сторінок вдалося уникнути завдяки дисковому кешу в поточному сеансі, що робить вигоду цілком вимірною, а не просто теоретичною. Для документа, який ви закрили та знову відкрили, кожна сторінка, яка потрапляє у видиму область без рендерингу, збільшує це значення
Чому ключем кешу є відбиток, а не номер версії
Відбиток документа — це 64-бітний хеш FNV-1a від рядка path|size|last write time (шлях, розмір та час останньої зміни), представлений шістнадцятьма шістнадцятковими цифрами. Ця тріада є основою всієї стратегії інвалідації: коли файл редагується, змінюється його розмір або час запису, відповідно змінюється хеш, і переглядач просто відкриває іншу підпапку кешу. Застаріла папка більше ніколи не використовується і з часом видаляється механізмом LRU. Не потрібно підтримувати протокол версій, немає логіки порівняння часових міток, яка може збитися, і стара бітова карта ніколи не буде показана замість зміненого документа
Це та сама ідея «ідентифікації вмісту як ключа кешу», яку використовують системи збирання, і вона повністю виправдовує себе завдяки усуненню потенційних помилок. Кеш, який зберігає файли під звичайним іменем документа, повинен явно виявляти зміни, а кожна така перевірка загрожує помилками в граничних випадках: відновлення файлу з резервної копії зі старою міткою часу, збереження зі збереженням розміру, порівняння шляхів із чутливістю до регістру на одній машині та без неї на іншій. Хешування всіх трьох параметрів в ім'я папки робить застарілість структурно неможливою, а не просто процедурно перевіреною
Чому поворот навмисно виключено з ключа на диску
Ім'я файлу кешу кодує лише номер сторінки та масштаб — кут повороту вигляду навмисно відсутній. Дисковий кеш завжди зберігає нерозгорнуті байти рендерингу; коли кешована сторінка завантажується при повернутому вигляді, пікселі переорієнтуються в пам'яті вже після декодування. Якби поворот був частиною ключа на диску, користувач, перемикаючи чотири кути огляду, записував би одну й ту саму сторінку на диск чотири рази, збільшуючи розмір кешу в чотири рази без жодної користі, оскільки поворот — це просте переставлення пікселів у пам'яті порівняно з повним рендерингом сторінки
При цьому кеш бітових карт у пам'яті використовує складений ключ — Round(Zoom * 1000) * 4 + Rotation div 90, оскільки повернута бітова карта — це саме те, що потрібно обробнику малювання. Фоновий потік попередньої вибірки дотримується того ж принципу: він завжди рендерить нерозгорнуті сторінки та повідомляє прості ключі масштабування, а головний потік застосовує поворот при отриманні результатів — відповідно до правила «головний потік володіє кешем», яке описано в статті про паралельний рендеринг сторінок та безпеку потоків. Виключення повороту з усіх рівнів нижче рівня пам'яті дозволяє мати один канонічний рендеринг на сторінку та масштаб скрізь, де збереження даних коштує дорого
Як очищення LRU обмежує розмір папки кешу?
Кеш зберігає папки до десяти документів і видаляє найменш використовувані (LRU), коли з'являються нові документи. Час використання відстежується за допомогою файлу-маркера last-used.marker у папці кожного документа, мітка часу якого оновлюється як при записі, так і при читанні з кешу, тому документ, який ви просто перечитуєте, залишається в кеші без повторного запису. Папка поточного відкритого документа захищена від видалення, тому очищення ніколи не видалить сторінки з-під активного перегляду
Кожна дискова операція в кеші — створення відбитка, створення папок, оновлення маркера, читання та запис сторінок, видалення застарілих файлів — ігнорує власні винятки. That is a deliberate posture, not sloppiness: the cache is a best-effort accelerator, and a full disk, a read-only folder or an antivirus lock must degrade the viewer to "renders like before", never to "fails to display the page". Найгірше, що може зробити помилка введення-виведення кешу — це просто пропущений хіт кешу
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// How many render passes did the disk cache save this session?
Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;
Як зробити переглядач PDF чутливим до DPI в Delphi?
Компонент TPDFlibViewer спрямовує всі розрахунки, залежні від DPI, через одне єдине поле, і така архітектура з однією точкою змін робить підтримку кожного монітора дуже зручною. Код розмітки, попередньої вибірки та рендерингу зчитує одне значення FScreenDPI; при масштабі 100% одна точка PDF відповідає ScreenDPI / 72 пікселям. Сетер публічної властивості ScreenDPI очищає кеш бітових карт, перебудовує розмітку, оновлює діапазони прокрутки та інвалідує вікно, тому зміна DPI — це лише одне присвоєння, а всі інші залежні елементи оновлюються автоматично. Та сама піксельна математика важлива і при рендерингу для принтера замість монітора, що детально описано у статті про попередній перегляд та рендеринг у контексті пристрою
Визначення значення відбувається в перевизначеному методі CreateWnd, а не в конструкторі, оскільки GetDpiForWindow потребує дійсного дескриптора вікна, якого в конструкторі ще немає. Поки не викликано CreateWnd, компонент тримає стандартне значення 96, щоб математика розмітки залишалася коректною. Сам метод GetDpiForWindow є API Windows 10 1607+, тому компонент в'яже його динамічно через GetProcAddress, а в старіших системах повертається до GetDeviceCaps(LOGPIXELSY) — модуль завантажується скрізь, і старіші версії Windows просто повідомляють системний DPI
Що відбувається, коли вікно переміщується на 4K-монітор?
Windows надсилає повідомлення WM_DPICHANGED, коли чутливе до DPI вікно переходить на монітор з іншим коефіцієнтом масштабування, і TPDFlibViewer обробляє його безпосередньо. Обробник зчитує нове значення DPI з молодшого слова wParam, присвоює його через SetScreenDPI (що очищає кеш бітових карт і перераховує розмітку сторінок), а потім застосовує запропонований прямокутник вікна, який Windows передає в lParam за допомогою SetWindowPos, що зберігає логічний розмір клієнтської області стабільним під час переміщення. Користувач бачить, як документ чітко перемальовується у новому масштабі, замість розтягнутої операційною системою бітової карти
// Normally you never touch ScreenDPI: CreateWnd detects the host monitor
// and WM_DPICHANGED tracks moves. Override it only for special targets,
// e.g. rendering the layout as if for a 150% display:
FViewer.ScreenDPI := 144; // clears the bitmap cache and rebuilds layout
Обмеження, про які варто знати перед релізом
Три практичні обмеження заслуговують на увагу при розробці. По-перше, повідомлення WM_DPICHANGED надходить лише тоді, коли процес увімкнув відповідний режим: маніфест вашого додатка повинен декларувати підтримку DPI для кожного монітора (режим per-monitor v2 на сучасній Windows), що в Delphi IDE налаштовується в параметрах додатка (DPI awareness). Процес із підтримкою системного DPI ніколи не отримує це повідомлення, і компонент у такому разі рендерить із тим DPI, який було визначено при створенні вікна. По-друге, дисковий кеш обмінює дисковий простір на швидкість — бітові карти сторінок для десяти документів на кількох рівнях масштабування займають чимало місця, тому вказуйте DiskCacheFolder туди, де є достатньо простору, а не в профіль користувача (roaming profile). По-третє, папка кешу повинна мати дозвіл на запис для поточного користувача; без цього переглядач все одно працюватиме, але тихо і без влучань у кеш — перевіряйте значення DiskCacheHits під час тестування, якщо підозрюєте, що кеш не працює
Разом ці дві функції значно покращують суб'єктивне сприйняття якості роботи додатка з великою кількістю документів: сторінки, які користувачі відкривають повторно, завантажуються миттєво, а вигляд залишається чітким на будь-якому моніторі. Обидві функції постачаються як звичайні властивості TPDFlibViewer у складі losLab PDF Library, тому їх впровадження — це скоріше кілька годин налаштувань, ніж складний проект із розробки движка рендерингу