لقد أصبح RTF قديمًا بما يكفي ليظهر في أماكن لم يتوقعها أحد: مولدات التقارير القديمة، ومسارات دمج البريد، وأرشيفات المستندات القانونية التي سبقت معالجات النصوص الحديثة. تحويله إلى PDF عند الطلب مطلب يتكرر كثيرًا، والطريقة التي تعمل فعلا على Windows ليست محلل RTF مخصصًا، بل مسار العرض الذي توفره Windows نفسها عبر TRichEdit و EM_FORMATRANGE. إصدار DLL من losLab PDF Library يوفّر DC افتراضيًا يندمج مباشرة في هذه السلسلة.
الآلية: DC افتراضي و EM_FORMATRANGE
يمكن لعناصر Rich Edit تقسيم محتواها إلى صفحات لأي device context، وليس فقط لطابعة فعلية. ترسل رسالة EM_FORMATRANGE إلى العنصر لتطلب منه تنسيق نطاق من الأحرف داخل DC معين، ثم تعيد موضع آخر حرف تمكن من احتوائه. كرر ذلك مع تحريك cpMin في كل مرة، وستحصل على إخراج صفحة بصفحة. يوفّر GetCanvasDC في losLab PDF Library DC داخل الذاكرة بحجم أبعاد الصفحة التي تحددها؛ وبعد عرض صفحة بداخله، يلتقط LoadFromCanvasDc النتيجة كصفحة PDF. هذه هي السلسلة كاملة.
هناك نقطة يجب ضبطها من البداية: يجب أن يكون حجم TRichEdit مطابقًا لحجم الصفحة المستهدفة. إذا كان العنصر أصغر أو أكبر من أبعاد DC فلن يتطابق التقسيم إلى صفحات مع ما ينتهي به الأمر داخل PDF. لإخراج A4، الأسلوب المعتاد هو ضبط أبعاد العنصر بالبكسل لتطابق 210 x 297 mm عند 96 DPI قبل تحميل ملف RTF، باستخدام أدوات القياس نفسها التي ستستخدمها لتحديد حجم DC.
تنفيذ Delphi
المثال التالي يستخدم وحدة الاستيراد PDFlibAX_TLB التي تغلف إصدار DLL من المكتبة. النموذج يحتوي على TRichEdit وزر، ومعالج OnCreate في النموذج يضبط حجم العنصر ويحمّل ملف RTF، بينما يتولى نقر الزر حلقة التحويل.
unit MainUnit;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;
type
TForm1 = class(TForm)
RichEdit1: TRichEdit;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
FirstChar: Integer): Integer;
end;
var
Form1: TForm1;
PdfDoc: TPDFLibrary;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
PdfDoc := TPDFLibrary.Create(Self);
// Size the control to A4 at screen DPI so pagination matches the DC
RichEdit1.Width := Round(ScaleX(210, mmPixel));
RichEdit1.Height := Round(ScaleY(297, mmPixel));
RichEdit1.Lines.LoadFromFile(
ExtractFilePath(Application.ExeName) + 'document.rtf');
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Dc: HDC;
PageNumber, LastChar, PdfDocId: Integer;
begin
PageNumber := 1;
LastChar := 0;
repeat
// Obtain a virtual DC sized to A4
Dc := PdfDoc.GetCanvasDC(
Round(ScaleX(210, mmPixel)),
Round(ScaleY(297, mmPixel)));
// Render the next page of RTF content into the DC
LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
// Capture the DC contents as a PDF document
PdfDoc.LoadFromCanvasDc(96, 0);
PdfDocId := PdfDoc.SelectedPdfDocument;
PdfDoc.SaveToFile(
ExtractFilePath(Application.ExeName)
+ 'Output' + IntToStr(PageNumber) + '.pdf');
PdfDoc.RemovePdfDocument(PdfDocId);
Inc(PageNumber);
until LastChar = 0;
end;
function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
FirstChar: Integer): Integer;
var
RcDrawTo, RcPage: TRect;
Fr: TFormatRange;
NextCharPosition: Integer;
begin
RcPage.Left := 0;
RcPage.Top := 0;
RcPage.Right := rtfBox.Left + rtfBox.Width + 100;
RcPage.Bottom := rtfBox.Top + rtfBox.Height + 100;
RcDrawTo.Left := rtfBox.Left;
RcDrawTo.Top := rtfBox.Top;
RcDrawTo.Right := rtfBox.Left + rtfBox.Width;
RcDrawTo.Bottom := rtfBox.Top + rtfBox.Height;
Fr.hdc := hDc;
Fr.hdcTarget := hDc;
Fr.rc := RcDrawTo;
Fr.rcPage := RcPage;
Fr.chrg.cpMin := FirstChar;
Fr.chrg.cpMax := -1;
NextCharPosition :=
SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
if NextCharPosition < Length(rtfBox.Text) then
Result := NextCharPosition
else
Result := 0; // signals last page
end;
end.
ما الذي تفعله الحلقة
PrintRtfBox تملأ البنية TFormatRange وتمريرها إلى عنصر Rich Edit عبر SendMessage. يعرض العنصر الأحرف بدءًا من cpMin، ويتوقف عندما يمتلئ DC، ثم يعيد موضع أول حرف لم يتسع. عندما تساوي قيمة الإرجاع طول النص الكلي أو تتجاوزه، يكون كل حرف قد عُرض بالفعل وتعيد الدالة الصفر، وهذا ما ينهي حلقة repeat...until.
كل تكرار ينتج ملف PDF واحدًا باسم Output1.pdf و Output2.pdf وهكذا. إذا كنت تريد بدلًا من ذلك مستندًا واحدًا متعدد الصفحات، فإن واجهة page-append في المكتبة تتيح لك تجميعها بعد ذلك، أو يمكنك إعادة هيكلة الحلقة لاستدعاء AddPage داخل جلسة مستند واحدة. نمط SaveToFile في كل تكرار ثم RemovePdfDocument في الأعلى يحافظ على ذروة الذاكرة عند مقدار صفحة واحدة من المحتوى، وهذا مهم جدًا مع ملفات RTF الطويلة جدًا.
تفاصيل التحجيم التي تربك كثيرين
تخبر الوسيطة 96 DPI في LoadFromCanvasDc المكتبة بدقة الشاشة التي عُرض بها DC، حتى تتمكن من حساب التحويل الصحيح من النقطة إلى البكسل لصفحة PDF. إذا أخطأت هنا فسيظهر النص بالحجم الخطأ في الناتج حتى لو بدت الصورة صحيحة على الشاشة.
الإضافة +100 إلى RcPage.Right و RcPage.Bottom هي هامش صغير خارج الحافة المرئية للعنصر. يستخدم Rich Edit المستطيل rcPage لتحديد مكان تقسيم الصفحات، ومن دون هذا الهامش يمكن لسطح يقع تمامًا على الحد أن يتكرر عبر صفحتين. هذا ليس ثابتًا سحريًا، بل تحتاجه كبيرًا بما يكفي ليقع حد الصفحة داخل مساحة تنسيق العنصر بوضوح بدلًا من الوقوف على آخر بكسل.
أخيرًا، يجب أن يكون العنصر مرتبطًا بالفعل بنافذة نموذج مرئية عندما يعمل FormCreate حتى يكون واصف النافذة صالحًا قبل أول استدعاء لـ SendMessage. أما TRichEdit الذي يُنشأ ديناميكيًا وقت التشغيل فيحتاج إلى استدعاء صريح لـ HandleNeeded قبل بدء حلقة العرض إذا لم يكن النموذج قد ظهر بعد.
التعامل مع الخطوط وميزات RTF
بما أن العرض يتم بواسطة محرك Windows Rich Edit، فإن استبدال الخطوط يتبع القواعد نفسها التي يستخدمها للعرض والطباعة. الخطوط المشار إليها في ملف RTF والمثبتة على الجهاز ستُعرض كما ينبغي، أما الخطوط المفقودة فسيجري استبدالها بصمت، وهذا قد يغيّر طول السطر وتقسيم الصفحات. في التحويل الدفعي الإنتاجي يستحق هذا اختبارًا صريحًا: حمّل مستندًا بكل نوع خط تستخدمه ملفات RTF الخاصة بك، وتأكد من أن عدد الصفحات الناتج يطابق ما تتوقعه من معاينة طباعة يدوية.
الجداول والصور المضمّنة ومعظم خصائص تنسيق Rich Text تعمل من دون أي معالجة إضافية لأن Rich Edit يعرضها أصليًا. الجزء الذي قد يكون مفاجئًا هو النص الذي يستخدم تباعدًا مخصصًا للفقرات أو مسافات بادئة للسطر الأول معبرًا عنها بوحدة twips: نظام الإحداثيات الداخلي في Rich Edit هو twips (1/1440 inch)، بينما إحداثيات DC التي تضبطها في TFormatRange تكون بالبكسل عند DPI الحالي. يحول العنصر ذلك داخليًا، لكن إذا كنت تبني RTF برمجيًا فعليك التحقق من أن قيم الهوامش بوحدة القياس الصحيحة.
الوعي بـ DPI وشاشات عالية الكثافة
على شاشة تعمل عند تحجيم 150% (144 DPI)، ستعيد ScaleX(210, mmPixel) عدد بكسلات أكبر مما تعيده على شاشة 100%. تسجل PDF Library أي أبعاد بالبكسل تمررها إلى GetCanvasDC وتستخدم وسيطة DPI في LoadFromCanvasDc لإعادة حساب الحجم الفيزيائي للصفحة داخل PDF. ما دام مقدار DPI الذي تمرره يطابق DPI الذي يعمل عليه التطبيق، فسيكون حجم الصفحة الناتج صحيحًا مهما كان تحجيم العرض.
إذا كان تطبيقك غير واعٍ بـ DPI (السلوك الافتراضي القديم)، فإن Windows ستقوم بتحجيم DC الخاص بالشاشة وستكون حسابات البكسل خاطئة على الأجهزة عالية DPI. أبسط إصلاح هو إعلان الوعي بـ DPI في manifest الخاص بالتطبيق؛ عندها يتلقى التطبيق device pixels الفعلية، ويجب استبدال الرقم 96 الذي تمرره إلى LoadFromCanvasDc بـ DPI الفعلي للشاشة الذي تحصل عليه من GetDeviceCaps(GetDC(0), LOGPIXELSX). المثال أعلاه يثبت 96 لأنه مناسب لبيئة 100% ويُبقي المثال مختصرًا.
بنية الناتج: ملف لكل صفحة مقابل مستند واحد
الحلقة أعلاه تكتب كل صفحة إلى ملف PDF منفصل. ما إذا كان هذا هو المطلوب يعتمد على الاستخدام اللاحق. أنظمة توليد التقارير تحتاج غالبًا إلى الصفحات الفردية لأنها تجمع المستند النهائي لاحقًا عبر دمج الصفحات أو إعادة ترتيبها. إذا أردت ملف PDF واحدًا من البداية، فالمكتبة تتيح لك إنشاء مستند متعدد الصفحات في جلسة واحدة: أنشئ المستند مرة واحدة خارج الحلقة، واستدعِ طريقة إضافة الصفحة بدلًا من SaveToFile داخل الحلقة، ثم احفظ المستند الكامل بعد خروجها. هذا يتجنب الملفات الوسيطة، وهو الهيكل المناسب لمعظم حالات التحويل إلى مستند واحد.
ومع ملفات RTF الكبيرة يجدر إضافة بعض مؤشرات التقدم داخل الحلقة، لأن معدل التحويل يتناسب تقريبًا مع عدد الصفحات، وقد يستغرق مستند من 200 صفحة بضع ثوانٍ. من السهل توسيع بنية repeat...until: تتبع موضع الأحرف في تحديث شريط التقدم بعد كل تكرار، باستخدام LastChar مقسومًا على إجمالي عدد الأحرف من RichEdit1.GetTextLen.
الطرائق GetCanvasDC و LoadFromCanvasDc المعروضة هنا جزء من losLab PDF Library لـ Delphi و C++Builder.