مقال تقني

عارض PDFium في Delphi: ذاكرة التصيير المؤقتة وتكبير سلس

اضغط باستمرار على زر التكبير في عارض PDF ساذج وراقب رسم استخدام المعالج. فضغطة واحدة على عنصر تحكم تكبير ذي تكرار تلقائي تطلق اثنتي عشرة خطوة تكبير أو أكثر في الثانية، وإذا كانت كل خطوة تبدأ إعادة تصيير بجودة كاملة للصفحة المرئية، فإن عمليات التصيير تتراكم أسرع مما تكتمل. تُنقَّط الصفحة جيدًا بمعزل عن غيرها، ربما في 180 مللي ثانية لمسح ضوئي بحجم A4، لكنك الآن تشغّل اثنتي عشرة عملية تصيير من 180 مللي ثانية على عمل تجاوزه المستخدم بالفعل. يتجمد العارض، وتُثبَّت نواة معالج عند 100%، وبحلول الوقت الذي تلحق فيه الشاشة بالركب يكون المستخدم قد توقف عند مستوى تكبير سبق بأربع عمليات تصيير. والعلاج ليس منقّطًا أسرع. بل ذاكرة مؤقتة تعيد الصفحات المكتملة فورًا وحلقة تصيير مستعدة للتخلي عن العمل لحظة يصبح قديمًا

يسلّمك PDFium Component مكوّنات الأمرين معًا ويبقى بعيدًا عن السياسة. فتحصل على صور نقطية يملكها المستدعي، ومصيّر تدريجي يأخذ رمز إلغاء، وأوضاع ملاءمة تعيد حساب التكبير عند تغيير الحجم، واستدعاء تبليط للصفحات الأكبر من أن تُنقَّط دفعة واحدة. أما ما لا يوفره عمدًا فهو الذاكرة المؤقتة نفسها، لأن سياسة الإخلاء الصحيحة تعتمد على إطار العرض لديك، وسقف الذاكرة في منصتك، وكيفية تمرير مستخدميك. فذلك القرار قرارك أنت لتصيبه، وعواقب الخطأ فيه هي بالضبط التجمد والتسرب

أين تذهب المللي ثواني والميجابايتات

ضع أرقامًا على التكلفة قبل أن تصمم أي شيء. فصفحة A4 عند 96 DPI تبلغ تقريبًا 794 في 1123 بكسل، أي نحو 3.5 MB كصورة نقطية بعمق 32 بت. كبّر إلى 200% فيتضاعف ذلك أربع مرات. وعند 400% على شاشة عالية الكثافة تخصص وتملأ صورة نقطية لصفحة واحدة بحجم 50 إلى 60 MB، وعارض التمرير المستمر يبقي عدة صفحات حية في آن واحد. وتتبع تكلفة التنقيط عدد بكسلات الخرج، ولذلك فإن كل مضاعفة للتكبير تضاعف زمن التصيير والذاكرة معًا نحو أربع مرات

تنبثق نتيجتان مباشرة من هذا الحساب. فالذاكرة المؤقتة التي يتجاهل مفتاحها مستوى التكبير عديمة القيمة، لأن الإيماءة ذاتها التي يُفترض أن تسرّعها، أي التكبير، تنتج صورة نقطية جديدة في كل مرة. والذاكرة المؤقتة غير المحدودة ستستنفد فضاء العناوين لعملية 32 بت على وجه التحديد في المستندات التي يكبّر فيها الناس بأقصى قدر: مسوحات سندات الملكية الكثيفة، والرسومات الهندسية، والخرائط ذات التنسيق الكبير. يجب أن تُفهرَس الذاكرة المؤقتة بمفتاح صحيح وأن تُقيَّد بسقف صارم، ولا شيء منهما اختياري

ما الذي ينتمي إلى مفتاح الذاكرة المؤقتة

لا تكون إعادة استخدام صورة نقطية مخزنة مؤقتًا آمنة إلا عندما يظل كل مدخل شكّل بكسلاتها مطابقًا. وهذا يعني رقم الصفحة، والتكبير الفعلي (أو بالمثل أبعاد بكسلات الخرج)، والدوران، وكثافة DPI للشاشة، وخيارات التصيير التي كانت سارية عند إنتاجها. فالصفحة المصيّرة مع reAnnotations صورة مختلفة عن الصفحة نفسها من دونها، والتمريرة بتدرج رمادي عبر reGrayscale مختلفة مرة أخرى. أسقط أيًا من هذه من المفتاح فتصبح الأخطاء متوقعة: طبقة تعليق توضيحي تبقى بعد أن يحذف المراجع التعليق، أو صفحة تصير ضبابية لحظة يسحب المستخدم النافذة من لوحة حاسوب محمول إلى شاشة 4K خارجية فتتغير كثافة DPI تحت صورة نقطية قديمة

بحث في ذاكرة التصيير المؤقتة لـ PDFium في عارض Delphi حيث يجمع مفتاح الذاكرة المؤقتة الصفحة والتكبير والدوران وكثافة DPI للشاشة وخيارات التصيير، وتعيد الإصابة الصورة النقطية في ميكروثوانٍ، ويحرر الإخلاء كل صورة نقطية يسقطها
يغطي مفتاح الذاكرة المؤقتة كل مدخل يشكّل البكسلات، ويحرر الإخلاء الصور النقطية التي يسقطها
function TPageCache.Acquire(Pdf: TPdf; PageNo: Integer; ZoomPct: Single;
  Rotation: TRotation; Opts: TRenderOptions): TBitmap;
var
  Key: string;
begin
  Key := Format('%d|%.0f|%d|%d|%d',
    [PageNo, ZoomPct, Ord(Rotation), Screen.PixelsPerInch, OptionsMask(Opts)]);
  if FBitmaps.TryGetValue(Key, Result) then
    Exit;

  Pdf.PageNumber := PageNo;
  Result := Pdf.RenderPage(0, 0, OutputWidth(PageNo, ZoomPct),
    OutputHeight(PageNo, ZoomPct), Rotation, Opts);
  FBitmaps.Add(Key, Result);   // الذاكرة المؤقتة تملك هذه الصورة النقطية الآن
end;

عند الإصابة يعود هذا في ميكروثوانٍ، وهذا هو المقصود كله. أما السؤال الأصعب فهو ما يحدث للصور النقطية التي تسقط من الذاكرة المؤقتة، ويتبين أنه سؤال عمّن يملكها

من يحرر الصورة النقطية

يعيد الشكل الدالّي لـ RenderPage كائن TBitmap يملكه المستدعي. وفي تصدير لمرة واحدة تكون تلك الملكية واضحة ويسهل احترامها. أما داخل ذاكرة مؤقتة فتصبح أكثر التسربات شيوعًا على الإطلاق في عارضات PDF المكتوبة بـ Delphi، لأن القاموس يحمل الآن المرجع الوحيد لكل صورة نقطية، وTDictionary العادي لا يحرر المفاتيح والقيم نيابةً عنك إلا إذا كانت أنواعًا مُدارة. وTBitmap ليس كذلك. أخلِ مدخلًا دون استدعاء Free فتبقى البكسلات مخصصة ولا شيء يشير إليها

وسبب تسلل هذا الخطأ هو التوقيت. فاختبار دخان مدته عشر دقائق لا يكبّر ما يكفي من الصفحات المتمايزة ليلاحظ؛ ولا يظهر التسرب إلا بعد أن يمرر أحدهم ويكبّر مستندًا طويلًا لساعتين، وعندها تكون العملية ممسكة بمئات من الصور النقطية اليتيمة للصفحات وتبدأ الآلة في الترحيل. ولهذا ينتمي الإخلاء إلى النسخة الأولى من الذاكرة المؤقتة، لا إلى نسخة لاحقة. قيّد الذاكرة المؤقتة بالبايتات المقدرة، محسوبة كالعرض في الارتفاع في أربعة، وأخلِ الصفحات الأقل استخدامًا مؤخرًا التي تقع خارج إطار العرض ونافذة الجلب المسبق، وحرر كل صورة نقطية عند إزالتها. أما للرسومات العابرة فعلًا، فإن التحميلات الزائدة التي تصيّر في TBitmap يوفره المستدعي أو مباشرة على HDC تتيح لك تخطي رقصة الملكية بالكامل. ومعاينة الطباعة هي الحالة البديهية، إذ تصيّر كل ورقة مرة واحدة ولا يجلب تخزينها المؤقت أي فائدة

التصيير التدريجي والإلغاء الصادق

تحجب التحميلات الزائدة العادية لـ RenderPage التنفيذ حتى تكتمل الصفحة، وهو بالضبط السلوك الذي لا تريده بينما المستخدم لا يزال يحرك عنصر تحكم التكبير. ولذلك تلجأ إلى RenderPageProgressive. فهي تأخذ IPdfCancellationToken وتعيد إحدى القيم prsDone أو prsCancelled أو prsFailed. والتفصيل السلوكي الذي يوقع الناس في الفخ أن الإلغاء ليس فوريًا. فالرمز يُستطلع عند حدود المقاطع داخل التصيير، ولذلك فإن الرمز الذي تشير إليه في منتصف مقطع لا يسري إلا عند انتهاء ذلك المقطع. وعلى صفحة معقدة يصل التأخير بين الطلب والتوقف إلى عشرات المللي ثواني. صمّم حول تلك الفجوة بدلًا من تمني زوالها: ألغِ الرمز السابق لحظة وصول قيمة تكبير جديدة، لكن لا تفترض أن التصيير القديم يتوقف لحظة تطلب منه ذلك

خط زمني للتصيير التدريجي في PDFium بـ Delphi حيث يلغي كل طلب تكبير جديد الرمز السابق، ويحط الإلغاء عند حدود مقطع، وتعيد عمليات التصيير المتجاوَزة prsCancelled، وتعيد المحاولة الأخيرة prsDone
يلغي كل طلب تكبير جديد رمز التصيير السابق، ويحط الإلغاء عند حدود مقطع
procedure TViewerForm.RequestRender(TargetZoom: Single);
var
  Status: TPdfProgressiveStatus;
begin
  if FTokenSource <> nil then
    FTokenSource.Cancel;           // تخلَّ عن التصيير السابق الجاري
  FTokenSource := TPdfCancellationTokenSource.New;  // الوحدة FPdfAsync

  Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
    FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
    ro0, [reAnnotations]);

  case Status of
    prsDone:      PresentBackBuffer;
    prsCancelled: ;                // تجاوزه طلب أحدث: أسقطه بصمت
    prsFailed:    ShowRenderFailure;
  end;
end;

أثناء التفاعل، تكون prsCancelled هي النتيجة الطبيعية لا الاستثنائية. فمعظم عمليات التصيير التي تبدؤها إيماءة تكبير ستُتجاوز قبل أن تنتهي، ولذلك عامل الإلغاء كأمر روتيني وأسقط النتيجة بصمت. وطابور التصيير الذي يسجل كل إلغاء كتحذير سيدفن الفشل الوحيد المهم فعلًا تحت آلاف الأسطر من الضجيج. ولمنع الشاشة من أن تبدو ميتة بينما يعمل التصيير الحقيقي، اقرن المسار التدريجي ببديل رخيص: غيّر مقياس الصورة النقطية السابقة المخزنة مؤقتًا إلى التكبير الجديد واعرضها فورًا. تبدو ناعمة لمئة مللي ثانية أو مئتين، لكنها تُقرأ كأنها فورية، وتكسب التصيير بالجودة الكاملة الوقت الذي يحتاجه إما لينتهي أو ليُلغى بالإيماءة التالية

وضع الملاءمة الذي يطفئه التكبير بصمت

تعيد خاصية FitMode في العارض، عند ضبطها على pfmFitPage أو pfmFitWidth، حساب التكبير عند كل تغيير للحجم لتظل الصفحة ملائمة مع تغير النافذة. والمأخذ أن إسناد قيمة إلى Zoom مباشرة يعيد FitMode إلى pfmNone. وهذا صحيح كسلوك افتراضي: فالمستخدم الذي كتب 150% عمدًا لا يريد أن يطيح به تغيير الحجم التالي للنافذة. لكنه يفاجئ كل من يوصّل زر تكبير على شكل Zoom := Zoom * 1.25 ثم يعجز عن فهم سبب توقف الملاءمة للعرض عن الاستجابة بعد النقرة الأولى. فإذا كان شريط الأدوات لديك يقدم التكبير الصريح وأوضاع الملاءمة معًا، فعليك أن تتذكر آخر اختيار ملاءمة للمستخدم بنفسك وأن تعيد إسناده عندما يضغط زر الملاءمة مرة أخرى. فالمكوّن لن يستعيد وضعًا مسحه إسناد تكبير للتو، وليس من المفترض أن يفعل

ميزانية ذاكرة يمكنك الدفاع عنها

الميزانية التي يمكنك كتابتها ميزانية يمكنك المجادلة عنها في مراجعة كود، فابدأ من سيناريو ملموس. لنقل إن التمرير المستمر يبقي الصفحة المرئية إضافةً إلى صفحة مجلوبة مسبقًا فوقها وأخرى تحتها، إلى جانب شريط صور مصغرة. عند 100% على شاشة 96 DPI تبلغ تلك الصور النقطية الثلاث بالحجم الكامل نحو 3.5 MB لكل منها، وهذا لا شيء. وعند 300% على شاشة 4K تبلغ الصور النقطية الثلاث نفسها نحو 30 MB لكل منها، وذلك قبل أن تحتفظ الذاكرة المؤقتة بصفحة تاريخية واحدة. فالنمو في الإيماءة، لا في المستند

حساب ذاكرة الصور النقطية في PDFium لعارض Delphi حيث تضاعف كل مضاعفة للتكبير ذاكرة الصفحة أربع مرات، ويبقي التمرير المستمر ثلاث صفحات حية، وتدافع ميزانية LRU المقيدة عن الذاكرة المؤقتة، ويتولى RenderTile الرسومات كبيرة الحجم
تضاعف كل مضاعفة للتكبير ذاكرة الصور النقطية أربع مرات، ولذلك تحتاج الذاكرة المؤقتة إلى سقف صارم وإلى بلاطات للصفحات كبيرة الحجم

الافتراض السليم لعملية Delphi بـ 32 بت هو ميزانية صور نقطية بحجم 256 MB تحت إخلاء LRU. وعلى 64 بت يمكنك التوسع مع ذاكرة RAM الفعلية، لكن أبقِ سقفًا صارمًا بصرف النظر عن ذلك، لأن الفشل الذي تحترس منه ليس انهيار عمليتك. بل تخبط الآلة كلها في ملف الترحيل بينما يواصل عارضك العمل من الناحية التقنية ويتساءل المستخدم لماذا تباطأ كل شيء آخر. فالسقف الصارم يفشل بشكل متوقع؛ أما الذاكرة المؤقتة غير المحدودة فتفشل آخذةً سطح المكتب معها. وتستحق الصور المصغرة معاملة خاصة بها: صيّر كل واحدة مرة واحدة بحجمها المستهدف الصغير واحتفظ بها في تجمّع منفصل لا يلمسه منطق LRU أبدًا. فإعادة توليد صورة مصغرة بعرض 120 بكسل بتصغير صورة نقطية لصفحة كاملة بحجم 60 MB هي أكثر الطرق الممكنة إهدارًا لإنتاج طابع بريد

بعض الصفحات المفردة تهزم أي ميزانية. فرسم هندسي بحجم E أو خريطة كبيرة تُصيَّر كاملة عند 400% تخصيص بمئات الميجابايتات، ولا توجد سياسة إخلاء تجعل ذلك مقبولًا. والجواب هناك هو التوقف عن تصيير الصفحات كاملة. فـ RenderTile تنقّط فقط المنطقة عند إزاحة البكسل (Left, Top) داخل صفحة مقيسة نظريًا إلى PageWidth في PageHeight، فتصيّر المستطيل المرئي فقط إضافةً إلى هامش ببلاطة واحدة حوله لتحريك سلس، وتدمج إزاحات البلاطات في مفتاح الذاكرة المؤقتة إلى جانب التكبير. وأبقِ أبعاد البلاطة ثابتة عبر المستند. فالبلاطات الثابتة تعني أن تغيير DPI يبطل الشبكة كلها بنظافة، بينما تتركك البلاطات المتغيرة تطارد درزات مرئية بين مناطق صُيّرت بمقاييس مختلفة قليلًا

ميزتان مجاورتان تضيفان بصمت إلى كل هذا. فتمريرات مرشحات الألوان مثل التدرج الرمادي أو العكس تعمل بعد التصيير وتنتج صورة نقطية ثانية بالحجم الكامل في كل مرة، فتضاعف البصمة لكل صفحة في أي عرض يستخدمها؛ وتلك التكلفة موضوع مقالة ترشيح الألوان لضعاف البصر في عارضات PDF بـ Delphi. والعارض الذي يبرز الكلمات أثناء تحويل النص إلى كلام يبطل العرض المصيّر مع كل كلمة منطوقة، ولذلك فإن التفاعل بين إعادة رسم الإبراز ومعدل الكلام أهم مما يبدو للوهلة الأولى، كما تغطيه مقالة إبراز TTS كلمةً بكلمة

التحميلات الزائدة للتصيير، ورموز حالة التصيير التدريجي، ومكوّن العارض نفسه موثقة في صفحة منتج PDFium Component