مقال تقني

عارض PDF بتمرير متواصل في Delphi

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

وهذا التوتر هو المشكلة كلها هنا. ويحلّها PDFium Component داخل TPdfView، فمعظم العمل هو اختيار وضع العرض الصحيح وفهم ما يفعله المكوّن نيابةً عنك. أما الأجزاء التي لا يؤديها عنك، أي تحجيم الصفحات لتدفّق القراءة وإبقاء التمرير السريع متجاوبًا، فهي حيث يستحق القليل من الكود مكانه. وإذا كنت لا تزال تجمّع الإطار المحيط (شريط الأدوات، والمصغّرات، ومربع البحث)، فإن جولة العارض الغني بالميزات تغطي تلك الأرضية؛ أما هنا فالموضوع هو التمرير نفسه

التخطيط وضع عرض، لا لوحة من الصور النقطية

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

كيف تخطّط خاصية DisplayMode في TPdfView عارض PDF بتمرير متواصل في Delphi: عمود صفحة واحدة، أو صفحتان متقابلتان، أو صفحتان متقابلتان مع صفحة غلاف مستقلة
إسناد واحد لـ DisplayMode يبدّل سطح التمرير المتواصل نفسه بين عمود واحد، أو صفحات متقابلة على طراز الكتب، أو صفحات متقابلة مع غلاف مستقل
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;

PdfView.DisplayMode := dmSingleContinuous;   // بعرض صفحة واحدة، يمرّر رأسيًا

Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
  ShowMessage('Could not open the document');

هذا هو إعداد التمرير المتواصل بأكمله. فـ dmSingleContinuous يرتّب الصفحات في عمود رأسي واحد مع معالجة الفجوات بينها داخليًا، ويمرّر العرض عبر ذلك العمود كسطح واحد. ولا يوجد عنصر تحكم لكل صفحة تحتاج إلى توصيله ولا معالج تمرير تكتبه للتنقّل العادي. ولاحظ الفحص على Pdf.Active بعد الإسناد: ففتح مستند لا يرفع استثناءً أبدًا، ولذا يترك الملف التالف أو المحمي بكلمة مرور Active عند False دون أي استثناء تلتقطه، والعارض الذي يتخطى هذا الفحص يصيّر لوحة فارغة ويلوم نفسه

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

الصفحات المرئية وحدها تُنقَّط

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

PDF: التنقيط الافتراضي في عارض PDF بتمرير متواصل: تُصيَّر الصفحات داخل منفذ العرض مع هامش جلب مسبق صغير فقط بينما يبقى باقي مستند من 400 صفحة كبنية شجرة صفحات
عمود الصفحات افتراضي: تُنقَّط الصفحات الداخلة إلى منفذ العرض مع هامش صغير بكسل، وتحرّر الصفحات الخارجة صورها النقطية

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

حجّم الصفحات على العرض، ثم اترك التكبير وشأنه

يريد عمود القراءة صفحات محجّمة على عرض اللوحة، لا مثبَّتة على تكبير مطلق. وFitMode يفعل هذا ويواصل فعله مع تغيّر حجم النافذة

PdfView.FitMode := pfmFitWidth;   // كل صفحة تملأ عرض العمود؛ والارتفاع يتبع

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

ولعناصر تحكم التكبير المطلق التي تُقرأ بشكل طبيعي، يكشف العرض تكبيرات الملاءمة كقيم يمكنك تطبيقها أو عرضها: فـ PageWidthZoom[PageNumber] تُعيد التكبير الذي يلائم تلك الصفحة على العرض، وPageZoom المقابلة تلائم الصفحة كاملة. وقراءة هاتين هي طريقة ملء قائمة "ملاءمة العرض" / "ملاءمة الصفحة" دون ترميز نسب مئوية سحرية تُخطئ على الصفحات الأفقية أو كبيرة الحجم

حافظ على تجاوب التمرير السريع بالتصيير التدريجي

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

تصيّر RenderPageProgressive على دفعات وتفحص رمز إلغاء عند كل حدّ دفعة، ولذا يمكن إسقاط تصيير جارٍ لصفحة مرّرت بعيدًا للتو بدلًا من تشغيله حتى النهاية

تدفّق حالة RenderPageProgressive: يعطي الإلغاء عند حدود الدفعات نتائج prsDone أو prsCancelled أو prsFailed لتمرير سريع متجاوب في عارض PDF في Delphi
يُبقي التصيير التدريجي القابل للإلغاء التمرير الخاطف متجاوبًا: تُسقَط التصييرات المتجاوَزة عند حدّ الدفعة التالي بدلًا من حجب الطابور
type
  TFormMain = class(TForm)
    // ...
  private
    FRenderCancel: IPdfCancellationTokenSource;
    procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
  end;

procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
  Status: TPdfProgressiveStatus;
begin
  // ألغِ أي تصيير كان جاريًا؛ الرمز القديم أصبح مُشارًا إليه الآن.
  if Assigned(FRenderCancel) then
    FRenderCancel.Cancel;
  FRenderCancel := TPdfCancellationTokenSource.New;

  Pdf.PageNumber := PageNo;
  Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
    FRenderCancel.Token);

  case Status of
    prsDone:      ;                    // الصورة النقطية مكتملة، ارسمها
    prsCancelled: Exit;                // تم تجاوزه، تجاهل هذه النتيجة
    prsFailed:    ShowMessage('Render failed for page ' + IntToStr(PageNo));
  end;
end;

والشكل الذي يهم هو القيمة المُعادة. فـ prsDone تعني أن الصورة النقطية مرسومة بالكامل وتستحق النقل إلى الشاشة؛ وprsCancelled تعني أن موضع تمرير أحدث تجاوز هذه الصفحة، فترمي النتيجة الجزئية بدلًا من عرضها؛ وprsFailed خطأ حقيقي في تلك الصفحة. ويُستطلَع الإلغاء عند حدود الدفعات لا استباقيًا، فتوقّع عشرات الميلي ثوانٍ من الكمون بين استدعاء Cancel وتوقف التصيير فعليًا. وهذا لا يزال أرخص بكثير من ترك تصيير صفحة كاملة بائت يحجب الطابور. وتمرير nil بوصفه الرمز يصيّر مباشرة حتى الاكتمال، وهو الخيار الصحيح لتصيير لمرة واحدة مثل معاينة الطباعة حيث لا شيء يُلغى في مقابله

وحين تستدعي بدلًا من ذلك صورة الدالة من RenderPage، تلك التي تُعيد TBitmap جديدة، فتذكّر أن المستدعي يملكها ويجب أن يحرّرها بـ Free. ففي حلقة تمرير تخصّص صورة نقطية لكل صفحة، يكون نسيان هذا تسريبًا ينمو مع كل صفحة يمرّ بها المستخدم، وهو بالضبط فشل الذاكرة غير المحدودة الذي كان من المفترض أن يتجنّبه التصميم المتواصل. صيّر في صورة نقطية معاد استخدامها حيثما استطعت

ما يبقى لك

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

واجهات TPdfView وDisplayMode وRenderPageProgressive البرمجية المبيّنة هنا جزء من PDFium Component لـ Delphi وLazarus