HotPDF RenderCacheFolder перетворює кеш відрендерених сторінок у пам'яті компонента HotPDF для Delphi на постійний дисковий кеш сторінок: відрендерені сторінки пишуться як PNG-файли в папку на ваш вибір, і наступного разу, коли відкривається те саме джерело PDF, RenderLoadedPageToBitmapCached читає їх назад замість повторної растеризації. Порядок пошуку: пам'ять, потім диск, потім рендерер
Дисковий ярус живе в API з v2.416.0, але до v2.770.140 він жодного разу фактично не віддав сторінку для звичайного виклику LoadFromFile чи LoadFromStream. Виправлення змусило поставити питання, на яке мусить відповісти кожен постійний кеш: як зрозуміти, що файл, відкритий сьогодні, — це документ, який ви рендерили вчора, і що стається з кешованими сторінками, коли це не так? Нижче — відповіді, на яких зупинився HotPDF, зокрема де він свідомо відмовляється кешувати
Як працює дисковий рендер-кеш HotPDF?
Дисковий рендер-кеш HotPDF — це другий ярус за кешем растру в пам'яті, і він бере участь лише коли RenderCacheFolder — непорожній шлях. Виклик RenderLoadedPageToBitmapCached(PageIndex, DPI) спершу сканує записи в пам'яті, ключовані індексом сторінки, DPI і варіантом налаштувань рендерингу. На промаху він питає дисковий ярус; дисковий збіг декодує PNG, повертає його в пам'ять і повертає копію, якою володіє викликач. Лише коли обидва яруси промахнулися, сторінка йде через інтерпретатор content stream, описаний у рендерингу завантаженої сторінки PDF у TBitmap, і свіжий бітмап тоді також пишеться на диск
На диску компонування свідомо нудне. Кожен документ отримує підпапку, названу з 16-символьного hex ключа документа плюс 16-символьного hex варіанта рендерингу, кожна сторінка зберігається як <page>@<dpi>.png, а index.txt у корені тримає документи в порядку останнього використання за тегом схеми. Розбіжність схеми чистить папку при першому використанні. Запити йдуть спершу в тимчасовий файл і підміняються на місце атомарним заміщенням, тож падіння посеред запису лишає або стару сторінку, або нічого — ніколи половину PNG. PNG, що не декодується, видаляється і лічиться промахом
Три ліміти обмежують папку:
RenderCacheMaxDocuments(усталено 20) обмежує кількість підпапок документів; найменше нещодавно використана папка виселяється першоюRenderCacheMaxBytes(усталено 524288000, тобто 500 МБ) обмежує сумарний розмір усіх PNG-файлів під коренем- Кожна папка документа тримає щонайбільше 200 зображень сторінок; той на-документ обмежувач зафіксований у THotPDF і не є опублікованою властивістю
RenderCacheCapacity (усталено 8) — окрема ручка: вона ставить, скільки відрендерених сторінок тримає ярус у пам'яті, і не має нічого спільного з дисковим слідом
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Налаштуйте дисковий ярус перед першим кешованим рендером:
// папку та обидва ліміти прочитано, коли ярус уперше використовується
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // сторінки в пам'яті
if Pdf.LoadFromFile(FileName) > 0 then
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 96);
if Bmp <> nil then
try
// Віддайте копію стрічці мініатюр тут
finally
Bmp.Free; // кешований виклик завжди повертає копію, якою володіє викликач
end;
end;
finally
Pdf.Free; // з v2.770.140 це більше не видаляє дискові записи
end;
end;
Ганяйте ту саму процедуру двічі, і другий прогін жодного разу не растеризує сторінку, що вмістилася в кеш. Об'єкт дискового кешу створюється ліниво при першому кешованому рендері і живе до звільнення екземпляра THotPDF, тож зміна RenderCacheFolder, RenderCacheMaxDocuments чи RenderCacheMaxBytes після того моменту не пересуває і не змінює розмір уже відкритого кешу. Сторінки, завеликі для політики допуску в пам'ять (усталено один запис не може перевищувати 64 МіБ 32-бітових пікселів), також не персистуються, а дисковий ярус запитується лише доки RenderFallbackPolicy тримає усталений rfpIgnore, бо фолбек-діагностика не зберігається поруч із PNG
Чому RenderCacheFolder не працював до v2.770.140?
RenderCacheFolder не мав ефекту до v2.770.140, бо дисковий ярус ключував документи хешем байтів джерела, який звичайні завантаження ніколи не тримали. Ключ документа походив із SHA-256 над внутрішньою копією сирих байтів PDF, але LoadFromFile і LoadFromStream розбирають джерело на місці і не тримають такої копії; поле заповнювалося лише тимчасово на шляху шифрованого відновлення і чистилося одразу після. Без байтів ключ завжди був порожнім, а порожній ключ означає, що дисковий ярус обходиться. Без помилки, без попередження — просто папка, що лишалася порожньою
Зробити ключ непорожнім — і вилазить другий баг, який ховався за першим. Старий InvalidateRenderedPageCache видаляв дискову папку документа, а InvalidateRenderedPageCache запускається на початку кожного завантаження, при кожному редагуванні і всередині Free. Отже, щойно ключ запрацював би, кожна сесія переглядача знищила б власний кеш при виході, і наступна сесія все одно стартувала б холодною. Гірше: ключ перераховувався з того самого джерела після редагування, тож рендери відредагованого документа зберігалися б під ключем початкового файлу і віддавалися б наступній сесії, що відкрила немодифікований PDF. v2.770.140 полагоджує ідентичність та інвалідацію разом; полагодження лише одного з них випустило б або мертвий кеш, або брехливий
Як HotPDF ідентифікує PDF, не читаючи весь файл
HotPDF ідентифікує PDF, завантажений із локального файлу, відбитком його розміру, часу останнього запису та перших і останніх 64 КіБ, а стрім чи джерело з довільним доступом — SHA-256 усього його вмісту. Обидва захоплюються один раз, коли завантаження вдається, і перші 16 hex-символів дайджесту SHA-256 (64 біти) стають ключем документа
| Джерело | Ідентичність | Ціна | Коли захоплюється |
|---|---|---|---|
LoadFromFile | Розмір + LastWriteTime + перші й останні 64 КіБ, хешовані SHA-256 | Максимум 128 КіБ читання, незалежно від розміру файлу | Кожне успішне завантаження, навіть якщо RenderCacheFolder встановлено пізніше |
LoadFromStream | SHA-256 усього стріму | Один повний прохід по джерелу | Лише якщо RenderCacheFolder було встановлено до завантаження |
LoadFromRandomAccessSource | SHA-256 усього джерела | Один повний прохід по джерелу | Лише якщо папку встановлено спершу і весь діапазон доступний |
Будь-яке джерело із записом /Encrypt | Немає | Немає | Ніколи; дисковий ярус обходиться |
Відбиток файлу — свідомий компроміс. Повне хешування відсканованого архіву на 400 МБ при кожному відкритті може коштувати дорожче, ніж рендеринг двох сторінок, які користувач фактично дивиться. Вибрані регіони не випадкові: заголовок сидить на початку файлу, а трейлер і остання секція крос-посилань — в кінці (ISO 32000-1 §7.5). Інкрементальне оновлення додає нове тіло, секцію крос-посилань і трейлер (§7.5.6), тож воно змінює розмір і хвіст одночасно. Повне переписування будь-яким звичайним інструментом змінює час останнього запису. Для файлів до 128 КіБ дві вибірки покривають кожен байт, тож маленькі документи фактично хешуються повністю
Залишковий ризик — це зміна того самого розміру на місці, у середині великого файлу, чиїй письменник потім відновлює початковий часовий штамп. Це потребує інструменту, який свідомо зберігає час модифікації, редагуючи вміст, — рідкісний, але не неможливий, і в тому випадку кеш віддає застарілі сторінки. Зворотний бік добронамірений: копіювання файлу на Windows зазвичай зберігає час останнього запису, тож копія документа, який уже в кеші, влучає в ті самі записи, що коректно, бо байти ідентичні
У стрімів узагалі немає часу модифікації, тож єдина чесна ідентичність — вміст. HotPDF платить за той повний прохід SHA-256 лише коли ви попросили дисковий кеш до завантаження; кожен інший викликач LoadFromStream не бачить зайвої ціни. Тож порядок присвоєння властивостей стає несучим:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Неправильний порядок для стрімів: хеш вмісту обчислюється лише коли
// папку вже встановлено, тож цей документ обходив би дисковий ярус
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // встановлюємо першим
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Джерело з довільним доступом, що досі завантажується (деякі діапазони ще недоступні), не отримує ідентичності замість хешу часткового вмісту, а якщо обчислення ідентичності падає з будь-якої причини, завантаження все одно вдається; документ просто рендериться без дискового ярусу
Що інвалідує запис дискового кешу HotPDF?
Запис дискового кешу HotPDF ніколи не інвалідується видаленням при редагуванні; натомість редагування завантаженого документа скидає ідентичність документа, тож дисковий ярус обходиться до кінця того завантаження, а збережені сторінки лишаються валідними для немодифікованого джерела. Записи покидають диск лише через ліміти LRU та байтів, зіпсований PNG чи зміну схеми
Ключ описує джерело на диску, а не граф об'єктів у пам'яті. Щойно ви штампуєте сторінку чи змінюєте анотацію, документ більше не збігається з тим джерелом, тож і читання, і запис під його ключем були б неправильними. З v2.770.140 і документна, і сторінкова інвалідація чистять ідентичність замість того, щоб чіпати папку, а для редагувань, що не викликали InvalidateRenderedPageCache, є друга захисна лінія: перед використанням дискового ярусу THotPDF перевіряє, чи є брудні завантажені об'єкти, і трактує брудний документ як такий, що не має ідентичності
Налаштування рендерингу працюють у зворотний бік. Перемикання PageRenderBackend (чи виклик UseNativeGDIRenderBackend), а також виклики ConfigureRenderICCWorkflow чи ClearRenderICCWorkflow змивають сторінки в пам'яті, але тримають ідентичність, бо документ досі збігається зі своїм джерелом. Ті налаштування змінюють пікселі, не будучи частиною варіанта в пам'яті, тож дисковий ключ вплітає ім'я бекенда, прапорець компенсації чорної точки та SHA-256-дайджести ICC-профілів proof і виходу. Сам варіант уже покриває колірний намір, дизеринг виходу, прев'ю overprint, режим маски світності, політику фолбеку та видимість кожної групи необов'язкового вмісту, тож перемикання шару рендерить в іншу папку замість перезаписування усталеного вигляду
Що повернути відредагований документ на дисковий ярус — дайте йому нову ідентичність джерела, зберігши його та завантаживши результат:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Після редагування завантаженого документа: оновіть сторінки в пам'яті.
// Ідентичність джерела вже зникла, тож нічого не читається з
// дискової папки початкового документа і не пишеться в неї
Pdf.InvalidateRenderedPageCache;
// Збережений файл має новий розмір і новий час останнього запису, отже нову
// ідентичність; рендери після цього завантаження кешуються під новим ключем
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
Папка початкового документа лишається в спокої та старіє через RenderCacheMaxDocuments і RenderCacheMaxBytes, як будь-який інший запис. Якщо користувач перевідкриє нередактований оригінал, його сторінки досі там
Межі безпеки: зашифровані джерела та пов'язані папки
Дисковий рендер-кеш HotPDF свідомо відмовляє двом видам входу: він ніколи не пише сторінки зашифрованого PDF на диск і ніколи не слідує за підпапкою документа, що є junction чи іншим reparse point. Обидва правила торгують кеш-збігами за не-витік даних чи не-видалення чужих файлів
Зашифровані PDF ніколи не кешуються на диску
Відрендерена сторінка — це розшифрований вміст. Записати її звичайним PNG у папку кешу — залишити читабельну копію документа, захищеного паролем, на диску, поза захистом, який обрав автор (ISO 32000-1 §7.6). Тому HotPDF не захоплює ідентичність для жодного джерела, чиїй трейлер несе запис /Encrypt, зокрема файлів, відкритих з паролем чи з порожнім паролем користувача. Ті документи досі користуються ярусом у пам'яті, який вмирає з процесом
Junction-підпапки відхиляються з v2.770.173
Корінь кешу — ваш вибір, і націлити його на junction дозволено. Підпапки документів під ним — інша справа: кеш створює, читає, торкає і видаляє їх самотужки, під час стартового відновлення (яке прибирає залишилися тимчасові файли), пошуку (який оновлює часові штампи), запису, інвалідації та трьох меж виселення. Якщо хтось із правом запису в корінь кешу замінить папку документа на junction до іншої директорії, кожен із тих шляхів пішов би за нею, і виселення видаляло б файли десь, чим кеш ніколи не володів. З v2.770.173 кожен із тих вхідних пунктів перевіряє атрибут reparse point і пропускає пов'язану папку документа: пошук лічить промах, запис лічить невдачу запису, а виселення лишає її в спокої
Unicode-шляхи та спільні корені
Дві суміжні виправлення важать, якщо ви деплоїте в профілі користувачів. До v2.770.135 RenderCacheFolder був AnsiString, тож папка поза системною кодовою сторінкою (китайське ім'я користувача на англійській установці Windows, скажімо) конвертувалася з втратами, перш ніж кеш її бачив; властивість тепер — Unicode string, а атомарне заміщення користується широким Windows API. З v2.770.52 кілька екземплярів THotPDF в одному процесі, що вказують на той самий корінь (після розширення шляху, порівняно без врахування регістру), ділять один індекс і блокування з лічильником посилань. Раніше кожен екземпляр перезаписував index.txt власною копією і застосовував ліміти проти свого часткового погляду, тож папка могла вирости в кілька разів понад бюджет
Те спільне володіння зупиняється на межі процесу. Два окремі процеси на тому самому корені досі тримають окремі індекси в пам'яті, тож дайте кожній одночасно запущеній програмі власний корінь кешу. Переглядачі, що рендерять у робочих потоках, у порядку в межах одного процесу: PrefetchLoadedPages і черга, описана в фоновому рендерингу з чергою запитів, обидва йдуть тим самим кешованим шляхом і тим самим блокуванням
Шпаргалка: чекліст RenderCacheFolder
- Встановіть
RenderCacheFolder,RenderCacheMaxDocumentsіRenderCacheMaxBytesдо першого викликуRenderLoadedPageToBitmapCached; для завантажень зі стрімів і довільного доступу встановіть папку до завантаження - Оновіться до v2.770.140 чи новішої, якщо покладаєтеся на дисковий ярус; старіші версії приймають властивість, але жодного разу не віддають сторінку з диска для звичайних завантажень
- Очікуйте відсутності дискового кешування для зашифрованих PDF, для документів, відредагованих після завантаження, і поки
RenderFallbackPolicyне дорівнюєrfpIgnore - Звільняйте екземпляр THotPDF звичайно; з v2.770.140 ні
Free, ніInvalidateRenderedPageCacheне видаляють дискові записи - Зміна
PageRenderBackendчи ICC-воркфлоу тримає документ на дисковому ярусі під іншим ключем - Використовуйте один корінь кешу на запущену програму; екземпляри в одному процесі ділять індекс з v2.770.52
- Тримайте корінь кешу в на-користувача розташуванні; підпапки документів, що є junction, пропускаються з v2.770.173
Постійний кеш сторінок окупується найбільше в переглядачі, що перевідкриває ті самі документи цілий день, — а це точно форма архітектури власного PDF-переглядача в Delphi, описаної в іншому місці цього блогу. RenderCacheFolder, растровий кеш у пам'яті та рендерер сторінок постачаються з компонентом HotPDF Delphi PDF для Delphi і C++Builder