Две оплаквания съпътстват почти всеки вграден PDF визуализатор: големите документи се рендират отново от нулата при всяко стартиране, а текстът се размива в момента, в който прозорецът бъде преместен на монитор с висока разделителна способност (high-DPI). losLab PDF Library решава и двата проблема в своя компонент TPDFlibViewer: свойството DiskCacheFolder позволява дисков кеш за страници между сесиите, индексиран по отпечатък (fingerprint) на документа, а свойството 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 показва колко рендирания на страници е спестил дисковият кеш в текущата сесия, което прави ползата измерима, а не просто пожелателна. При документ, който сте затворили и отворили отново, всяка страница, която се появява на екрана без преминаване през рендиране, увеличава този брояч
Защо ключът за кеширане е отпечатък (fingerprint), а не номер на версия
Отпечатъкът на документа е 64-битов FNV-1a хеш върху низа path|size|last write time, форматиран като шестнадесет шестнадесетични цифри. Тази тройка стойности представлява цялата стратегия за инвалидиране: когато файлът се редактира, неговият размер или време на запис се променя, хешът се променя и визуализаторът просто отваря различна подпапка в кеша. Остарялата папка никога повече не се проверява и в крайна сметка бива изтрита при LRU изчистването. Няма протокол за версии, който да се поддържа, няма логика за сравняване на времеви отпечатъци, която да се обърка, и няма начин старо растерно изображение да бъде сервирано за променен документ
Това е същата идея „идентичност на съдържанието като ключ за кеширане“, която използват компилационните системи, и тя доказва стойността си чрез проблемите, които предотвратява. Кеш, който съхранява файлове под обикновеното име на документа, трябва да открива промените изрично, а всяка изрична проверка е предпоставка за бъг при гранични случаи: файл, възстановен от архив със старо време, запис, който запазва същия размер, или път, сравняван с чувствителност към регистъра (case-sensitive) на една машина, но не и на друга. Хеширането на трите сигнала в името на папката прави остаряването на кеша структурно невъзможно, вместо процедурно проверявано
Защо завъртането е умишлено изключено от дисковия ключ
Името на файла в кеша кодира номера на страницата и мащабирането, и нищо друго – ъгълът на завъртане на изгледа умишлено отсъства. Дисковият кеш винаги съхранява неразгърнатите байтове от рендирането; когато кеширана страница се зареди при завъртян изглед, пикселите се преориентират в паметта след декодирането. Ако завъртането беше част от ключа на диска, потребител, който преминава през четирите ъгъла на изглед, би записал една и съща страница на диска четири пъти, което би учетворило растежа на кеша за нулева допълнителна информация, тъй като завъртането е евтина операция за пренареждане на пиксели в сравнение с пълно рендиране
Кешът за растерни изображения в паметта наистина използва съставен ключ: Round(Zoom * 1000) * 4 + Rotation div 90, защото там завъртяното изображение е точно това, от което се нуждае манипулаторът за изчертаване (paint handler). Фоновата нишка за предварително извличане (prefetch thread) следва същата дисциплина: тя винаги рендира незавъртени страници и предава обикновени ключове за мащабиране, а основната нишка прилага завъртането при обработка на резултатите – същото правило „основната нишка притежава кеша“, обсъдено в статията за паралелно рендиране на страници и безопасност на нишките. Изключването на завъртането от всичко под слоя на паметта означава едно канонично рендиране на страница и мащабиране навсякъде, където съхранението е скъпо
Как изчистването по LRU ограничава размера на папката с кеша?
Кешът съхранява до десет папки с документи и изчиства най-отдавна използваните (least recently used), когато пристигнат нови документи. Времето на използване се проследява от файл last-used.marker във всяка папка за документ, чийто времеви отпечатък се обновява както при запис, така и при четене от кеша, така че документ, който само четете отново, остава активен без да се презаписва. Папката, принадлежаща на текущо отворения документ, е защитена от изтриване, така че изчистването никога не може да премахне страници под активния в момента изглед
Всяка дискова операция в кеша – хеширане, създаване на папки, промяна на маркери, четене и запис на страници, изчистване – улавя и игнорира собствените си изключения. Това е умишлено поведение, а не небрежност: кешът е оптимизатор с най-добри намерения и пълен диск, папка само за четене или блокиране от антивирусна програма трябва просто да върнат визуализатора към режим „рендиране в реално време“, но никога да не водят до „неуспех при показване на страницата“. Най-лошият резултат, който може да причини входно-изходна грешка в кеша, е просто липса на съвпадение в кеша (cache miss)
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-зависимо изчисление през едно единствено поле и този дизайн с единствена точка на промяна прави поддръжката на DPI за всеки монитор лесно управляема. Кодът за оформление, предварително извличане и рендиране чете една стойност FScreenDPI; при 100% мащаб една PDF точка се картографира към ScreenDPI / 72 пиксела. Публичният метод за задаване на свойството ScreenDPI изчиства кеша на изображенията, преизчислява оформлението, актуализира диапазоните на превъртане и преначертава прозореца, така че промяната на DPI е просто едно присвояване, което всички потребители следват автоматично. Същата пикселна математика е важна, когато рендирате за принтер вместо за монитор – тема, разгледана в статията за предварителен преглед при печат и рендиране в контекст на устройство
Разпознаването се случва в предефинирания метод CreateWnd, а не в конструктора, тъй като GetDpiForWindow изисква валиден дескриптор на прозорец (window handle), а конструкторът се изпълнява много преди такъв да съществува. Докато не се задейства CreateWnd, компонентът държи класическата стойност по подразбиране от 96, за да остане математиката на оформлението правилна. Самият метод GetDpiForWindow е Windows 10 1607+ API, затова компонентът го обвързва динамично чрез 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 съвместимост в опциите на приложението. Процес със системно DPI съвместимост никога не получава това съобщение и компонентът тогава рендира при DPI, открит при създаването на прозореца. Второ, дисковият кеш обменя дисково пространство за скорост – растерните изображения за десет документа при различни нива на мащабиране изискват реално място за съхранение, затова насочете DiskCacheFolder към папка, която позволява растеж, а не към мобилен потребителски профил (roaming profile). Трето, папката с кеша се нуждае от разрешения за запис за текущия потребител; без тях визуализаторът все още работи, но безшумно и с нула попадения в кеша – проверявайте DiskCacheHits по време на тестване, ако подозирате, че кешът не работи
Взети заедно, тези две функции променят усещането за качество на приложение, работещо интензивно с документи, повече от повечето оптимизации за рендиране: документите, които вашите потребители отварят често, се зареждат незабавно, а изгледът остава остър на всеки монитор, на който се окаже прозорецът. И двете се доставят като обикновени свойства на TPDFlibViewer в losLab PDF Library, такъв че внедряването им е въпрос на няколко настройки, а не на сложен проект по пренаписване на двигател за изчертаване