HotPDF RenderCacheFolder превръща кеша на рендерирани страници в паметта на HotPDF Delphi компонента в постоянен дисков кеш на страници: рендираните страници се записват като PNG файлове в папка по ваш избор, а при следващото отваряне на същия PDF източник RenderLoadedPageToBitmapCached ги прочита обратно вместо да растеризира отново. Редът на търсене е памет, после диск, после рендерерът
Дисковото ниво е в API-я от v2.416.0 насам, но до v2.770.140 никога действително не е сервирало страница при нормален LoadFromFile или LoadFromStream разговор. Поправката принуди един въпрос, на който всеки постоянен кеш трябва да отговори: откъде знаете, че файлът, отворен днес, е документът, рендиран вчера, и какво става с кешираните страници, когато не е? По-долу са отговорите, на които HotPDF се спря, включително къде нарочно отказва да кешира
Как работи дисковият render кеш на HotPDF?
Дисковият render кеш на HotPDF е второ ниво зад растерния кеш в паметта и участва само когато RenderCacheFolder е непразен път. Разговор към RenderLoadedPageToBitmapCached(PageIndex, DPI) първо сканира записите в паметта, ключувани по индекс на страница, DPI и вариант на настройките за рендиране. При пропуск пита дисковото ниво; попадение на диска декодира PNG, връща го обратно в паметта и връща копие, собственост на извикващия. Само когато и двете нива пропуснат, страницата минава през интерпретатора на content-stream, описан в рендирането на заредена PDF страница в TBitmap, а пресният bitmap се записва и на диска
На диска подредбата е нарочно скучна. Всеки документ получава подпапка, назована от 16-символен шестнадесетичен ключ на документа плюс 16-символен шестнадесетичен render вариант, всяка страница се съхранява като <page>@<dpi>.png, а index.txt в корена държи документите в реда най-скоро ползвани зад таг на схемата. Несъответствие на схемата изчиства папката при първа употреба. Записите отиват първо във временен файл и се разменят на място с атомарна замяна, така че срив по средата на запис оставя или старата страница, или нищо, никога половин PNG. PNG, който се провали да декодира, се изтрива и брои като пропуск
Три лимита ограничават папката:
RenderCacheMaxDocuments(по подразбиране 20) ограничава броя подпапки на документи; най-отдавна неползваната папка се изтласква пръвRenderCacheMaxBytes(по подразбиране 524288000, тоест 500 MB) ограничава общия размер на всички 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 MiB 32-битови пиксела), също не се персистират, а дисковото ниво се пита само докато RenderFallbackPolicy пази дефолта си rfpIgnore, защото fallback диагностиката не се съхранява до PNG-а
Защо RenderCacheFolder никога не работеше преди v2.770.140?
RenderCacheFolder нямащ ефект преди v2.770.140, защото дисковото ниво ключуваше документите по hash на входни байтове, които обикновените зареждания никога не пазеха. Ключът на документа идваше от SHA-256 над вътрешно копие на суровите PDF байтове, но LoadFromFile и LoadFromStream парсват източника на място и не задържат такова копие; полето се попълваше временно само на път за възстановяване при шифроване и се изчистваше веднага след това. Без байтове ключът беше винаги празен, а празен ключ значи, че дисковото ниво се заобикаля. Нито грешка, нито предупреждение — просто папка, която оставаше празна
Правенето на ключа непразен извади наяве втори бъг, криел се зад първия. Старият InvalidateRenderedPageCache изтриваше дисковата папка на документа, а InvalidateRenderedPageCache се пуска в началото на всяко зареждане, при всяка редакция и вътре в Free. Така в момента, в който ключът започнеше да работи, всяка сесия на прегледач би унищожила собствения си кеш при изход, а следващата сесия пак щеше да стартира студена. По-лошото: ключът се пресмяташе от същия източник след редакция, така че рендираните страници на редактирания документ щяха да се съхранят под ключа на оригиналния файл и да се сервират на следващата сесия, отворила непроменения PDF. v2.770.140 оправя идентичността и инвалидацията заедно; оправянето само на едната щеше да пусне или мъртъв кеш, или лъжлив
Как HotPDF разпознава PDF без да чете целия файл
HotPDF разпознава PDF, зареден от локален файл, по отпечатък от размера му, времето на последно записване и първите и последните 64 KiB, а stream или random-access източник — по SHA-256 на цялото му съдържание. И двете се улавят веднъж, при успешен товар, а първите 16 hex символа на SHA-256 дайджеста (64 бита) стават ключът на документа
| Източник | Идентичност | Цена | Улавя се кога |
|---|---|---|---|
LoadFromFile | Размер + LastWriteTime + първи и последни 64 KiB, хеширани със SHA-256 | Най-много 128 KiB четене, независимо от размера на файла | Всяко успешно зареждане, дори RenderCacheFolder да е зададен по-късно |
LoadFromStream | SHA-256 на целия stream | Един пълен проход по източника | Само ако RenderCacheFolder е бил зададен преди товара |
LoadFromRandomAccessSource | SHA-256 на целия източник | Един пълен проход по източника | Само ако папката е била зададена пръв и целият диапазон е наличен |
Всеки източник с запис /Encrypt | Нищо | Нищо | Никога; дисковото ниво се заобикаля |
Отпечатъкът на файла е нарочен компромис. Пълното хеширане на сканиран архив от 400 MB при всяко отваряне може да струва повече от рендирането на двете страници, които потребител действително гледа. Извадковите области не са произволни: header-ът седи в началото на файла, а trailer-ът и последната секция с кръстосани препратки — в края му (ISO 32000-1 §7.5). Инкрементална обновка добавя ново тяло, секция с кръстосани препратки и trailer (§7.5.6), така че мени размера и опашката наведнъж. Пълно пренаписване от всеки нормален инструмент променя времето на последно записване. За файлове до 128 KiB двете извадки покриват всеки байт, така че малки документи на практика се хешират изцяло
Остатъчният риск е промяна на място, със същия размер, в средата на голям файл, чийто writer после възстановява оригиналния timestamp. Това изисква инструмент, който нарочно пази времената на промяна, докато редактира съдържание — рядкост, но не и невъзможност — и в този случай кешът сервира остарели страници. Обратната страна е доброкачествена: копирането на файл под Windows обикновено пази времето на последно записване, така че копие на документ, вече в кеша, уцелва същите записа, което е вярно, защото байтовете са идентични
Stream-овете нямат изобщо време на промяна, така че единствената честна идентичност е съдържанието. HotPDF плаща за този пълен SHA-256 проход само когато сте поискали дисков кеш преди товара; всеки друг извикващ LoadFromStream не вижда допълнителна цена. Това прави редът на задаване на свойствата съществен:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Грешен ред за stream-ове: content hash-ът се смята само когато
// папката вече е зададена, така че този документ би заобиколил дисковото ниво
// 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;
Random-access източник, който още се тегли (някои диапазони още недостъпни), не получава идентичност вместо hash на частично съдържание, а ако смятането на идентичността провали по каквато и да е причина, товарът пак успява; документът просто рендира без дисковото ниво
Какво инвалидира запис в дисковия кеш на HotPDF?
Запис в дисковия кеш на HotPDF никога не се инвалидира чрез изтриване при редакция; вместо това редакция на заредения документ изпуска идентичността на документа, така че дисковото ниво се заобикаля до края на този товар, а съхранените страници остават валидни за непроменения източник. Записите напускат диска само чрез LRU и байтовите лимити, повреден PNG или смяна на схемата
Ключът описва източник на диска, не обектния граф в паметта. Щом подпечатате страница или смените анотация, документът вече не съответства на този източник, така че нито четенето, нито писането под неговия ключ би било вярно. От v2.770.140 инвалидация и на ниво документ, и на ниво страница изчиства идентичността вместо да пипа папката, а има и втори предпазател за редакции, които не са извикали InvalidateRenderedPageCache: преди употреба на дисковото ниво THotPDF проверява дали някой зареден обект е мръсен и третира мръсен документ като без идентичност
Настройките за рендиране работят обратно. Смяна на PageRenderBackend (или викане на UseNativeGDIRenderBackend), както и викане на ConfigureRenderICCWorkflow или ClearRenderICCWorkflow, изпразва страниците в паметта, но пази идентичността, защото документът все още съответства на източника си. Тези настройки менят пикселите, без да са част от варианта в паметта, така че дисковият ключ сгъва вътре името на backend-а, флага за black-point компенсация и SHA-256 дайджестите на ICC proof и изходните профили. Самият вариант вече покрива цветовото намерение, изходното dithering, preview на overprint, режима на luminosity mask, fallback политиката и видимостта на всяка група optional content, така че превключването на слой рендира в друга папка вместо да презапише дефолтния изглед
За да върнете редактиран документ на дисковото ниво, дайте му нова идентичност на източника, като го запишете и заредите резултата:
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 като всеки друг запис. Ако потребителят преотвори нередактирания оригинал, страниците му все още са там
Граници за сигурността: шифровани източници и свързани папки
Дисковият render кеш на HotPDF отказва два вида вход нарочно: никога не пише страници на шифрован PDF на диска и никога не следва подпапка на документ, която е junction или друг reparse point. И двете правила разменят попадения в кеша срещу това да не изтичат данни или да не се изтрият грешни файлове
Шифровани PDF-и никога не се кешират на диска
Рендирана страница е декриптирано съдържание. Записът ѝ като обикновен PNG в кеш папка би оставил четимо копие на документ, защитен с парола, на диска, извън защитата, избрана от автора (ISO 32000-1 §7.6). Затова HotPDF не улавя идентичност за всеки източник, чийто trailer носи запис /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 в един процес, сочащи един и същ корен (след разширяване на пътя, сравнявани без разлика на регистъра), споделят един reference-брояен индекс и заключване. Преди това всяка инстанция презаписваше index.txt със собственото си копие и налагаше лимитите спрямо частичния си изглед, така че папката можеше да порасне няколко пъти над бюджета си
Това споделяне спира на границата на процеса. Два отделни процеса на същия корен все още държат отделни индекси в паметта, така че дайте на всяко едновременно работещо приложение собствен корен на кеша. Прегледачи, рендиращи на работни нишки, са наред в рамките на един процес: PrefetchLoadedPages и опашката, разгледана в фоновото рендиране с опашка от заявки, и двете минават през същия кеширан път и същото заключване
Бърза справка: контрольен списък за RenderCacheFolder
- Задайте
RenderCacheFolder,RenderCacheMaxDocumentsиRenderCacheMaxBytesпреди първия разговор къмRenderLoadedPageToBitmapCached; при товар от stream или random-access източник задайте папката преди товара - Надградете към v2.770.140 или по-нова, ако разчитате на дисковото ниво; по-стари версии приемат свойството, но никога не сервират страница от диска при нормални товари
- Не очаквайте дисково кеширане за шифровани PDF-и, за документи, редактирани след товара, или докато
RenderFallbackPolicyне еrfpIgnore - Освобождавайте инстанцията THotPDF нормално; от v2.770.140 нито
Free, нитоInvalidateRenderedPageCacheизтриват дискови записи - Смяна на
PageRenderBackendили на ICC workflow държи документа на дисковото ниво под друг ключ - Ползвайте един корен на кеша на работещо приложение; инстанции в един процес споделят индекса от v2.770.52
- Дръжте корена на кеша в място за потребителя; подпапки на документи, които са junction-и, се прескачат от v2.770.173
Постоянен кеш на страници се изплаща най-много в прегледач, който преотворя едни и същи документи цял ден — точно формата на архитектурата на персонализиран PDF прегледач в Delphi, описана другаде в този блог. RenderCacheFolder, растерният кеш в паметта и рендерерът на страници идват с HotPDF Delphi PDF компонент за Delphi и C++Builder