يحوّل 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 وترقّيه عائداً إلى الذاكرة وتعيد نسخة يملكها المستدعي. وفقط حين تخفق الطبقتان معاً تمر الصفحة عبر مفسّر تدفق المحتوى الموصوف في عرض صفحة PDF محمّلة إلى TBitmap، ثم يُكتب الـ bitmap الجديد إلى القرص هو أيضاً
وعلى القرص التخطيط ممل عمداً. لكل مستند مجلد فرعي مسمى من مفتاح مستند بستة عشر محرفاً سدسياً عشرياً زائد صيغة عرض بستة عشر محرفاً سدسياً، وكل صفحة مخزنة كملف <page>@<dpi>.png، وملف index.txt عند الجذر يحفظ المستندات بترتيب الأحدث استخداماً خلف وسم schema. وعدم تطابق الـ schema يمسح المجلد عند أول استخدام. والكتابة تذهب إلى ملف مؤقت أولاً وتُبدَّل إلى مكانها باستبدال ذري، فانهيار أثناء الكتابة يترك إما الصفحة القديمة أو لا شيء، لا نصف 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 الافتراضية، لأن تشخيصات التراجع لا تُخزن بجوار الـ 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 فيه وآخرها، وتحدد تياراً أو مصدر وصول عشوائي بـ SHA-256 لمحتواه بالكامل. وكلاهما يُلتقط مرة واحدة حين ينجح تحميل، وتصير أول 16 محرفاً سدسياً من بصمة SHA-256 (64 بت) مفتاح المستند
| المصدر | الهوية | التكلفة | متى تُلتقط |
|---|---|---|---|
LoadFromFile | الحجم + LastWriteTime + أول 64 KiB وآخرها، موهَّشة بـ SHA-256 | قراءة 128 KiB على الأكثر، مستقلة عن حجم الملف | كل تحميل ناجح، حتى لو ضُبطت RenderCacheFolder لاحقاً |
LoadFromStream | SHA-256 للتدفق كله | تمريرة كاملة واحدة على المصدر | فقط إذا ضُبطت RenderCacheFolder قبل التحميل |
LoadFromRandomAccessSource | SHA-256 للمصدر كله | تمريرة كاملة واحدة على المصدر | فقط إذا ضُبط المجلد أولاً وكان المدى كله متاحاً |
أي مصدر بمدخل /Encrypt | لا شيء | لا شيء | أبداً؛ تُتجاوز الطبقة القرصية |
بصمة الملف مقايضة مقصودة. تهشير أرشيف ممسوح بحجم 400 MB بالكامل عند كل فتح قد يكلف أكثر من عرض الصفحتين الذي ينظر إليهما المستخدم فعلاً. والمناطق المسحوبة ليست اعتباطية: الترويسة عند بداية الملف، والـ trailer وآخر قسم مراجع متبادلة عند نهايته (ISO 32000-1 §7.5). والتحديث التدريجي يُلحق جسماً جديداً وقسم مراجع متبادلة و trailer (§7.5.6)، فيغيّر الحجم والذيل دفعة واحدة. وإعادة الكتابة الكاملة بأي أداة عادية تغيّر وقت آخر كتابة. وللملفات حتى 128 KiB يغطي السحبان كل بايت، فالمستندات الصغيرة تُهشَّر فعلياً بالكامل
والمخاطرة المتبقية تغيير بنفس الحجم في موضع وسطي من ملف كبير يستعيد كاتبه بعدها الطابع الزمني الأصلي. يحتاج ذلك أداة تحفظ أوقات التعديل عمداً وهي تحرر المحتوى، وهذا نادر لكنه غير مستحيل، وفي تلك الحالة تخدم الذاكرة صفحات متقادمة. والوجه الآخر حميد: نسخ ملف على Windows يحفظ عادة وقت آخر كتابة، فنسخة مستند موجود في الذاكرة تصل إلى المدخلات نفسها، وهذا صحيح لأن البايتات متطابقة
والتيارات بلا وقت تعديل إطلاقاً، فالهوية الصادقة الوحيدة هي المحتوى. ولا تدفع HotPDF ثمن تمريرة SHA-256 الكاملة تلك إلا إذا طلبت ذاكرة قرصية قبل التحميل؛ وكل مستدعي آخر لـ LoadFromStream لا يرى تكلفة زائدة. وهذا يجعل ترتيب إسناد الخصائص حاملاً للمعنى:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// ترتيب خاطئ للتيارات: تُحسب 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;
ومصدر وصول عشوائي ما زال يُنزَّل (بعض المدوات غير متاحة بعد) لا يحصل على هوية بدل hash لمحتوى جزئي، وإن فشل حساب الهوية لأي سبب يظل التحميل ناجحاً؛ فالمستند يُعرض ببساطة دون الطبقة القرصية
ما الذي يُبطِل مدخل ذاكرة HotPDF القرصية؟
لا يُبطَل مدخل ذاكرة HotPDF القرصية أبداً بحذفه عند التحرير؛ بدلاً من ذلك يُسقط تحرير المستند المحمَّل هوية المستند، فتُتجاوز الطبقة القرصية لبقية ذلك التحميل وتبقى الصفحات المخزنة صالحة للمصدر غير المعدل. وتغادر المدخلات القرص عبر حدود LRU والبايتات، أو PNG تالف، أو تغيير schema
المفتاح يصف مصدراً على القرص، لا مخطط الكائنات في الذاكرة. ما إن تختم صفحة أو تغيّر annotation حتى يتوقف المستند عن مطابقة ذلك المصدر، فلا يكون القراءة تحت مفتاحه ولا الكتابة تحت صحيحتين. ومنذ v2.770.140 يمسح كل من الإبطال على مستوى المستند والإبطال على مستوى الصفحة الهوية بدل لمس المجلد، وهناك حارس ثانٍ للتحريرات التي لم تستدعِ InvalidateRenderedPageCache: قبل استخدام الطبقة القرصية تفحص THotPDF هل أي كائن محمّل متسخ وتعامل المستند المتسخ كأنه بلا هوية
وإعدادات العرض تعمل بالاتجاه المعاكس. تبديل PageRenderBackend (أو استدعاء UseNativeGDIRenderBackend)، واستدعاء ConfigureRenderICCWorkflow أو ClearRenderICCWorkflow، يفرغ صفحات الذاكرة لكنه يبقي الهوية، لأن المستند ما زال يطابق مصدره. تلك الإعدادات تغيّر البكسلات دون أن تكون جزءاً من صيغة الذاكرة، فيضم مفتاح القرص اسم المحرك وعلم تعويض النقطة السوداء وبصمات SHA-256 لملفَي ICC للبرهان والمخرج. وصيغة العرض نفسها تغطي أصلاً مقصد اللون و dithering المخرج ومعاينة 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 أو نقطة إعادة تحليل أخرى. وكلا القاعدتين تداول إصابات الذاكرة بعدم تسريب بيانات أو حذف ملفات خاطئة
PDFs المشفرة لا تُخزَّن على القرص أبداً
الصفحة المعروضة محتوى مفكوك التشفير. كتابتها PNG عادياً في مجلد ذاكرة كانت ستترك نسخة مقروءة من مستند محمي بكلمة سر على القرص، خارج الحماية التي اختارها المؤلف (ISO 32000-1 §7.6). لذلك لا تلتقط HotPDF أي هوية لأي مصدر يحمل الـ trailer الخاص به مدخل /Encrypt، بما في ذلك الملفات المفتوحة بكلمة سر أو بكلمة سر مستخدم فارغة. وتلك المستندات ما زالت تستخدم طبقة الذاكرة، وهي تموت مع العملية
المجلدات الفرعية junction تُرفض منذ v2.770.173
جذر الذاكرة اختيارك، وتوجيهه إلى junction مسموح. أما المجلدات الفرعية للمستندات تحته فأمر مختلف: الذاكرة تنشئها وتقرأها وتلمسها وتحذفها بنفسها، أثناء استرجاع بدء التشغيل (الذي يزيل الملفات المؤقتة المتبقية) والبحث (الذي يحدّث الطوابع الزمنية) والتخزين والإبطال وحدود الطرد الثلاثة. لو استبدل ذو صلاحية كتابة على جذر الذاكرة مجلدَ مستند بـ junction إلى دليل آخر لاتبعت كل واحدة من تلك المسارات ذلك الـ junction، وحذف الطرد ملفات في مكان لم تملكه الذاكرة قط. ومنذ v2.770.173 يفحص كل مدخل من تلك المدخلات خاصية نقطة إعادة التحليل ويتخطى مجلد مستند مرتبطاً: البحث يعدّه إخفاقاً، والتخزين يعدّه فشل كتابة، والطرد يتركه وشأنه
المسارات Unicode والجذور المشتركة
إصلاحان مرتبطان يهمان إن كنت تنشر في ملفات تعريف المستخدمين. قبل v2.770.135 كانت RenderCacheFolder من نوع AnsiString، فمجلد خارج صفحة كود النظام (اسم مستخدم صيني على تركيب Windows إنجليزي مثلاً) حُوّل بفقد قبل أن تراه الذاكرة؛ والخاصية الآن string من نوع Unicode، والاستبدال الذري يستخدم الـ Windows API الواسع. ومنذ v2.770.52 تتشارك عدة نظائر THotPDF في عملية واحدة تشير إلى الجذر نفسه (بعد توسيع المسار وبمقارنة غير حساسة لحالة الأحرف) فهرساً واحداً مقفولاً بعدد مراجع. قبل ذلك كان كل نظير يكتب index.txt فوقه بنسخته الخاصة ويطبق الحدود على مشهدته الجزئية، فكان المجلد يستطيع أن ينمو عدة أضعاف فوق ميزانيته
ويتوقف ذلك التشارك عند حد العملية. عمليتان منفصلتان على الجذر نفسه ما زالتا تحملان فهارس ذاكرة منفصلة، فامنح كل تطبيق يعمل آنياً جذر ذاكرة خاصاً به. والعارضات التي تعرض على خيوط عاملية سليمات داخل عملية واحدة: PrefetchLoadedPages وطابور الطلبات المغطى في العرض في الخلفية بطابور طلبات كلاهما يمشي عبر المسار المخزَّن نفسه والقفل نفسه
مرجع سريع: قائمة تحقق RenderCacheFolder
- اضبط
RenderCacheFolderوRenderCacheMaxDocumentsوRenderCacheMaxBytesقبل أول استدعاء لـRenderLoadedPageToBitmapCached؛ ولمحملات التيارات والوصول العشوائي اضبط المجلد قبل التحميل - رقِّ إلى v2.770.140 أو أحدث إن كنت تعتمد على الطبقة القرصية؛ الإصدارات الأقدم تقبل الخاصية لكنها لا تخدم صفحة من القرص للتحميلات العادية أبداً
- توقع لا ذاكرة قرصية لـ PDFs مشفرة، أو لمستندات حُررت بعد التحميل، أو ما دامت
RenderFallbackPolicyليستrfpIgnore - حرّر نظير THotPDF بشكل طبيعي؛ فمنذ v2.770.140 لا
FreeولاInvalidateRenderedPageCacheيحذفان مدخلات القرص - تغيير
PageRenderBackendأو سير عمل ICC يبقي المستند على الطبقة القرصية تحت مفتاح مختلف - استخدم جذر ذاكرة واحداً لكل تطبيق يعمل؛ والنظائر داخل عملية واحدة تتشارك الفهرس منذ v2.770.52
- أبقِ جذر الذاكرة في موقع لكل مستخدم؛ ومجلدات المستندات الفرعية التي هي junction تُتخطى منذ v2.770.173
ذاكرة الصفحات الدائمة تؤتي أثمرها أكثر في عارض يعيد فتح المستندات نفسها طوال اليوم، وهو بالضبط شكل معمارية عارض PDF مخصص في Delphi الموصوفة في مكان آخر من هذه المدونة. وتبعث RenderCacheFolder وذاكرة التنقيط داخل الذاكرة ومحرك عرض الصفحات مع مكوّن HotPDF Delphi PDF component لـ Delphi و C++Builder