هناك شكويان ترافقان كل عارض PDF مدمج: يعاد عرض المستندات الكبيرة من الصفر عند كل تشغيل، ويتحول النص إلى ضبابي في اللحظة التي يتم فيها سحب النافذة إلى شاشة ذات DPI عالٍ. تجيب مكتبة losLab PDF Library على كلتيهما في عنصر تحكم TPDFlibViewer الخاص بها: تتيح الخاصية DiskCacheFolder ذاكرة تخزين مؤقت لصفحات القرص عبر الجلسات ومفهرسة ببصمة المستند، بينما تمنح الخاصية ScreenDPI، المدعومة بـ GetDpiForWindow ومعالج WM_DPICHANGED، عنصر التحكم وعياً حقيقياً بـ DPI لكل شاشة
يستحق كلا المشكلتين أخذ الملاحظة بجدية لأن المستخدمين يلاحظونهما على الفور. فالعقد الممسوح ضوئياً المكون من 400 صفحة والذي استغرق ثماني ثوانٍ ليصبح قابلاً للتمرير بالأمس يستغرق ثماني ثوانٍ أخرى اليوم، على الرغم من عدم تغيير أي شيء في الملف. وعلى سطح مكتب ذي DPI مختلط، فإن العارض الذي يعرض بدقة 96 DPI للشاشة الرئيسية يبدو جيداً هناك بينما يظهر ناعماً بشكل واضح على لوحة 4K المجاورة لها. تستعرض هذه المقالة كيفية حل TPDFlibViewer لكل من هذه المشاكل، وبشكل أكثر فائدة، لماذا استقر التصميم حيث استقر. إذا كنت جديداً على عنصر التحكم نفسه، فابدأ بـ نظرة عامة على عنصر تحكم عارض PDF التفاعلي ثم عد إلى طبقة الأداء
كيف تقوم بتخزين صفحات PDF المعروضة مؤقتاً عبر الجلسات في دلفي؟
قم بتعيين مجلد لـ TPDFlibViewer.DiskCacheFolder وسيقوم عنصر التحكم بحفظ الصور النقطية للصفحات المعروضة على القرص، بحيث يعاد فتح المستند الذي تم عرضه مؤخراً على الفور بدلاً من إعادة العرض. التخطيط على القرص هو <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: مجلد فرعي واحد لكل مستند، وملف واحد لكل تركيبة صفحة وتكبير. كل شيء آخر تلقائي; لا توجد واجهة برمجة تطبيقات لذاكرة التخزين المؤقت لاستدعائها ولا يوجد شيء لإخلائه
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 عن عدد عمليات عرض الصفحات التي وفرتها ذاكرة التخزين المؤقت للقرص في الجلسة الحالية، مما يجعل الفوز قابلاً للقياس بدلاً من كونه مجرد حكايات. في المستند الذي أغلقته وأعدت فتحه، تزيد كل صفحة يتم تمريرها في العرض دون تمريرة عرض من هذا العداد
لماذا مفتاح ذاكرة التخزين المؤقت هو بصمة وليس رقم إصدار
بصمة المستند هي تجزئة (hash) FNV-1a بقوة 64 بت على السلسلة path|size|last write time، المنسقة كستة عشر رقماً ست عشرياً. هذا الثلاثي يمثل إستراتيجية الإبطال بأكملها: عندما يتم تحرير الملف، يتغير حجمه أو وقت كتابته، فتتغير التجزئة، ويفتح العارض ببساطة مجلداً فرعياً مختلفاً لذاكرة التخزين المؤقت. لا يتم الرجوع إلى المجلد القديم مرة أخرى ويخضع في النهاية لتقليم LRU. لا يوجد بروتوكول لعلامات الإصدار للمحافظة عليه، ولا يوجد منطق لمقارنة الطوابع الزمنية يمكن أن ينحرف، ولا توجد طريقة لتقديم صورة نقطية قديمة مقابل مستند معدل
هذه هي نفس فكرة "هوية المحتوى كمفتاح لذاكرة التخزين المؤقت" التي تستخدمها أنظمة البناء، وهي تثبت جدارتها في أوضاع الفشل التي تزيلها. ذاكرة التخزين المؤقت التي تخزن الملفات تحت الاسم البسيط للمستند يتعين عليها اكتشاف التعديلات صراحة، وكل فحص صريح يمثل خطأ ينتظر حالة خاصة: ملف مسترجع من نسخة احتياطية بطابع زمني قديم، أو حفظ يحافظ على الحجم، أو مسار تتم مقارنته مع مراعاة حالة الأحرف على جهاز ودون مراعاتها على جهاز آخر. تجزئة الإشارات الثلاث في اسم المجلد تجعل التقادم مستحيلاً من الناحية الهيكلية بدلاً من التحقق منه إجرائياً
لماذا يتم استبعاد التدوير عمداً من مفتاح القرص
يرمز اسم ملف ذاكرة التخزين المؤقت رقم الصفحة والتكبير، ولا شيء غير ذلك — زاوية تدوير العرض غائبة عمداً. تخزن ذاكرة التخزين المؤقت للقرص دائماً بايتات العرض غير المدورة؛ وعند تحميل صفحة مخزنة مؤقتاً أثناء تدوير العرض، يتم إعادة توجيه البكسلات في الذاكرة بعد فك التشفير. إذا كان التدوير جزءاً من مفتاح القرص، فإن المستخدم الذي يتنقل عبر زوايا العرض الأربع سيكتب نفس الصفحة على القرص أربع مرات، مما يضاعف نمو ذاكرة التخزين المؤقت أربع مرات مقابل صفر معلومات إضافية، نظراً لأن التدوير هو عملية تبديل بكسلات رخيصة مقارنة بتمريرة عرض كاملة
تستخدم ذاكرة التخزين المؤقت للصور النقطية في الذاكرة مفتاحاً مركباً، وهو Round(Zoom * 1000) * 4 + Rotation div 90، لأن الصورة النقطية المدورة هناك هي بالضبط ما يحتاجه معالج الرسم. يتبع خيط الجلب المسبق في الخلفية نفس النظام: فهو يعرض دائماً صفحات غير مدورة ويبلغ عن مفاتيح تكبير واضحة، ويطبق الخيط الرئيسي التدوير عند تصريف النتائج — وهي نفس قاعدة "الخيط الرئيسي يمتلك ذاكرة التخزين المؤقت" الموضحة في العرض المتوازي للصفحات وسلامة الخيوط. الحفاظ على التدوير خارج كل شيء تحت طبقة الذاكرة يعني عرضاً قانونياً واحداً لكل صفحة وتكبير، في كل مكان يكون من المكلف تخزينه
كيف يحافظ تقليم LRU على مجلد ذاكرة التخزين المؤقت محدوداً؟
تحتفظ ذاكرة التخزين المؤقت بما يصل إلى عشرة مجلدات مستندات وتقوم بتقليم المجلدات الأقل استخداماً مؤخراً عند وصول مستندات جديدة. يتم تتبع الحداثة بواسطة ملف last-used.marker في كل مجلد مستند، حيث يتم لمس طابعه الزمني في كل من عمليات كتابة وقراءة ذاكرة التخزين المؤقت، لذا فإن المستند الذي تعيد قراءته فقط يظل دافئاً دون إعادة كتابته. المجلد الذي ينتمي إلى المستند المفتوح حالياً مستثنى من الحذف، لذا لا يمكن للتقليم أبداً سحب صفحات من تحت العرض النشط
تتجاهل كل عملية قرص في ذاكرة التخزين المؤقت — مثل البصمات، وإنشاء المجلدات، ولمس العلامات، وقراءة وكتابة الصفحات، والتقليم — استثناءاتها الخاصة. هذا موقف متعمد وليس إهمالاً: ذاكرة التخزين المؤقت هي مسرع يبذل قصارى جهده، ويجب أن يؤدي القرص الممتلئ أو المجلد المخصص للقراءة فقط أو قفل مكافح الفيروسات إلى خفض العارض إلى "يعرض كما كان من قبل"، وليس إلى "يفشل في عرض الصفحة". أسوأ نتيجة يمكن أن ينتجها خطأ إدخال/إخراج لذاكرة التخزين المؤقت هي فوات الإصابة (missed hit)
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 في دلفي؟
يوجه عنصر TPDFlibViewer كل عملية حسابية تعتمد على DPI عبر حقل واحد، وهذا التصميم ذو نقطة التغيير الواحدة هو ما يجعل دعم كل شاشة قابلاً للحل. تقرأ أكواد التخطيط والجلب المسبق والعرض جميعاً قيمة FScreenDPI واحدة؛ وعند تكبير 100%, تقابل نقطة PDF واحدة ScreenDPI / 72 بكسل. يقوم واضع خاصية ScreenDPI العامة بمسح ذاكرة التخزين المؤقت للصور النقطية، وإعادة بناء التخطيط، وتحديث نطاقات التمرير، وإبطال النافذة، وبالتالي فإن تغيير DPI هو تعيين واحد يتبعه كل مستهلك تلقائياً. تهم نفس رياضيات البكسل عند العرض للطابعة بدلاً من الشاشة، وهو موضوع مغطى في معاينة الطباعة وعرض سياق الجهاز
يحدث الاكتشاف في تجاوز CreateWnd بدلاً من المنشئ (constructor)، لأن GetDpiForWindow تحتاج إلى مقبض نافذة صالح والمنشئ يعمل قبل وقت طويل من وجود أحدها. حتى يتم إطلاق CreateWnd، يحتفظ عنصر التحكم بالافتراضي القديم 96 لكي تظل رياضيات التخطيط سليمة. تعتبر GetDpiForWindow نفسها واجهة برمجة تطبيقات لنظام التشغيل Windows 10 1607+، لذا يربطها عنصر التحكم ديناميكياً بـ GetProcAddress ويتراجع إلى GetDeviceCaps(LOGPIXELSY) في الأنظمة القديمة — لا تزال الوحدة تتحمل في كل مكان، ويقوم Windows الأقدم ببساطة بالإبلاغ عن DPI للنظام
ماذا يحدث عندما تنتقل النافذة إلى شاشة 4K؟
يرسل Windows الرسالة WM_DPICHANGED عندما تعبر نافذة واعية بـ DPI لكل شاشة إلى شاشة ذات عامل مقياس مختلف، ويتعامل معها TPDFlibViewer مباشرة. يقرأ المعالج الـ DPI الجديد من الكلمة المنخفضة (low word) لـ 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 فقط إذا اختارت العملية ذلك: يجب أن يعلن بيان تطبيقك (manifest) عن الوعي بـ DPI لكل شاشة (per-monitor v2 على نظام Windows الحالي)، والذي يتمثل في بيئة تطوير Delphi IDE في إعداد الوعي بـ DPI ضمن خيارات التطبيق. لا تتلقى العملية الواعية بـ DPI للنظام هذه الرسالة أبداً، ويعرض عنصر التحكم عندئذٍ عند DPI الذي اكتشفه عند إنشاء النافذة. ثانياً، تبادل ذاكرة التخزين المؤقت للقرص مساحة القرص بالسرعة — حيث يمثل تخزين الصور النقطية لصفحات عشرة مستندات عبر عدة مستويات تكبير مساحة تخزين حقيقية، لذا وجه DiskCacheFolder إلى موقع تشعر بالارتياح لنموه، وليس ملف تعريف متنقل (roaming profile). ثالثاً، يحتاج مجلد ذاكرة التخزين المؤقت إلى إذن كتابة للمستخدم الذي يقوم بالتشغيل؛ وبدونه يظل العارض يعمل بصمت مع صفر نجاحات — تحقق من DiskCacheHits في الاختبار إذا كنت تشك في عدم تفعيل ذاكرة التخزين المؤقت
تغير الميزتان معاً الجودة المتصورة لتطبيق كثيف المستندات أكثر من معظم تحسينات العرض: فالمستندات التي يعيد مستخدموك زيارتها تفتح على الفور، ويظل العارض حاداً على أي شاشة يهبط عليها. يتم شحن كلتيهما كخصائص عادية في TPDFlibViewer في losLab PDF Library، لذا فإن تبنيهما هو مجرد عمل بعد ظهر يوم من التوصيل بدلاً من مشروع محرك عرض