مقال تقني

تقارير PDF في Delphi مع HotPDF: TextOut والخطوط والصور

يتلخّص توليد تقرير في وضع ثلاثة أشياء على الصفحة وجعلها تتفق على مواضعها: نص عند إحداثيات معلومة، وخطوط تُصيَّر على الخادم كما تُصيَّر على سطح مكتبك، وصور بحجم يناسب مكانها. وكل ما تفعله مكتبة تقارير غير ذلك مرتَّب حول هذه الثلاثة. ويمنحك HotPDF، مكتبة توليد PDF من losLab لـ Delphi و C++Builder، كلًا منها كاستدعاء مباشر على كائن الصفحة، والاحتكاك الحقيقي الوحيد هو نظام الإحداثيات الكامن تحتها، الذي يسير في الاتجاه المعاكس لقماش VCL الذي اعتدته. سوِّ مسألة الاتجاه هذه أولًا فيتوقف بقية عمل التخطيط عن مقاومتك

وضع النص ونقطة الأصل في الزاوية السفلية اليسرى

يخرج أول تقرير لدى الجميع تقريبًا مقلوبًا رأسًا على عقب. فيحطّ العنوان قرب الحافة السفلية ويتسلّق كل سطر تحته نحو الأعلى. ولا شيء معطّل. ففضاء مستخدم PDF، المعرَّف في ISO 32000-1 §8.3، يضع نقطة الأصل في الزاوية السفلية اليسرى مع تزايد Y نحو الأعلى، وهو الصورة المرآتية لقماش GDI حيث يتزايد Y نحو الأسفل من الزاوية العلوية اليسرى. وخمس دقائق تقضيها في التصالح مع ذلك توفّر عليك تخطيطًا كنت ستعيد كتابته حين تكفّ الأرقام عن أن يكون لها معنى

مخطط HotPDF يقارن نقطة أصل إحداثيات VCL في الزاوية العلوية اليسرى بنقطة أصل PDF في الزاوية السفلية اليسرى، حيث تضع TextOut عنوانًا على بعد 50 نقطة من أعلى صفحة Letter عند Y يساوي 792 ناقص 50
يعكس فضاء مستخدم PDF قماش VCL، لذا فالعنوان الواقع على بعد 50pt من أعلى صفحة Letter هو TextOut(50, 792 - 50, 0, 'INVOICE') والتحويل نفسه يُبقي كل إحداثيات التقرير بديهية

الاستدعاء المحوري في كائن الصفحة هو TextOut(X, Y, Angle, Text). فتحدّد X و Y موضع النص بالنقاط من الزاوية السفلية اليسرى، وتدوّره Angle بالدرجات، وهكذا يُرسم ختم DRAFT أو COPY القطري دون أي دعم خاص. والحيلة التي تُبقي الحدس المتدرّب على VCL يعمل هي التعبير عن Y بارتفاع الصفحة ناقص المسافة التي تريدها من الأعلى:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // 50pt من أعلى صفحة Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // ختم مدوَّر
    Pdf.AddPage;                                                // CurrentPage تشير الآن إلى هنا
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // حالة الخط لا تنتقل تلقائيًا
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

السلوكان ذوا الحالة في ذلك الكود مسؤولان عن معظم العلل التي لا تظهر إلا في الصفحة الثانية. فـAddPage تعيد توجيه CurrentPage إلى الصفحة التي أنشأتها للتو، فلا يعود مرجع الصفحة الذي خزّنته سابقًا يرسم حيث تتوقع. واختيار الخط أيضًا لكل صفحة لا لكل مستند. فإن أغفلت SetFont بعد AddPage، عاد أول TextOut على الصفحة الجديدة إلى الافتراضي الذي بدأت به الصفحة، لا إلى خط العنوان العريض الذي ضبطته قبل ثلاث صفحات. والعادة الآمنة هي التعامل مع «بدء صفحة جديدة» و«إعادة تأسيس حالة النص» كخطوة واحدة لا تنفصل في حلقة التقرير

خطوط موجودة على الخادم لا على سطح مكتبك فحسب

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

مخطط لمشكلة نشر خطوط PDF في Delphi: يستبدل خادم الإنتاج بصمت خطًا مفقودًا، بينما تحمّل RegisterUnicodeTTF ملف TTF من ملف منشور وتضمّنه في PDF
الاعتماد على دليل خطوط نظام التشغيل ينكسر حين يفتقر حساب خدمة الإنتاج إلى الخط، بينما يضمّن تحميل TTF من ملف منشور الأحرف الرسومية فيصيّر كل مضيف النتيجة نفسها
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));

تقبل TextOut سلسلة WideString مباشرةً، وهو ما يهم أكثر مما يبدو للوهلة الأولى. فاسم عميل بحرف معلَّم، وشارع ألماني، ومدينة بولندية: هذه ليست حالات حدّية، بل المحتوى الطبيعي لجدول عملاء، وتمر عبر الاستدعاء نفسه الذي تمر به تسميات ASCII التي تكتبها صراحةً في الكود، ما دام الخط المسجَّل يحتوي فعلًا على الأحرف الرسومية. ويرافق الخطوط المضمَّنة قيد إصدار واحد: يجب أن يكون المستند PDF 1.5 أو أحدث، فإن كان متطلب آخر لا علاقة له يثبّتك على إصدار أقدم، فذلك ما سينكسر بهدوء. أما الكتابات من اليمين إلى اليسار مثل العربية والعبرية فتحتاج إلى تشكيل حقيقي لا إلى بحث مباشر عن الأحرف الرسومية، ولذلك خط أنابيبه الخاص؛ انظر مقالتنا عن تشكيل نصوص الكتابات المعقّدة مع HotPDF

وحين لا يستطيع أي خط مثبَّت التعبير عمّا تحتاجه، فكّر في أحرف MICR على شيك أو مجموعة رموز مملوكة، تسدّ خطوط Type 3 الفجوة. فتعرّف كل حرف رسومي كتدفق محتوى صغير عبر RegisterType3Font وAddType3Glyph. وهي زاوية متخصصة من API لن تلجأ إليها إلا نادرًا، لكنها أنظف بكثير من نثر مئات الصور النقطية الصغيرة للرموز عبر الصفحة

الصور: الوسيطان الأوسطان عرض وارتفاع لا زاوية

تنقسم معالجة الصور إلى خطوتين، وإبقاؤهما منفصلتين هو بيت القصيد. فتأخذ AddImage صورة TBitmap أو TJPEGImage، وتضمّنها مرة واحدة، وتعيد فهرسًا. ويجب فكّ ترميز رسوم PNG إلى صورة نقطية قبل وصولها إلى هناك. ثم ترسم ShowImage ذلك الفهرس حيثما تشاء وبقدر ما تشاء من المرات. وترتيب الوسائط في ShowImage هو الموضع الوحيد الذي يستحق التمهّل لقراءته:

مخطط لخط أنابيب الصور في HotPDF حيث تضمّن AddImage الصورة النقطية مرة واحدة وتعيد فهرسًا، وتضعها ShowImage بعرض وارتفاع، وترتيب الوسائط ليس زوج زوايا
تضمّن AddImage البكسلات مرة واحدة ويعيد كل استدعاء ShowImage استخدام ذلك الفهرس، والوسيطان الأوسطان عرض وارتفاع لا إحداثيات الزاوية المقابلة
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // فكّ ترميز PNG إلى صورة نقطية
    LogoIdx := Pdf.AddImage(Logo, icFlate); // بلا فقد للرسوم ذات الألوان المسطّحة
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): وليس (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

الرقمان التاليان للموضع عرض وارتفاع. وليسا إحداثيات الزاوية المقابلة، والوسيط الأخير زاوية دوران بالدرجات. اقرأ التوقيع على أنه صندوق X1/Y1/X2/Y2 فيمتد شعار بقياس 120 في 40 موضوع عند (50, 700) من هناك إلى (120, 40) بدلًا من ذلك، متمدّدًا عبر معظم الصفحة. وتجعل المخرجات الخطأ واضحًا بينما يبدو الكود المصدري معقولًا تمامًا، وهذا ما يجعله يضيّع عصرًا كاملًا. وتأخذ KeepImageAspectRatio القيمة True افتراضيًا، لذا فالصندوق ذو النسب الخاطئة يُحيط الصورة بشرائط بدلًا من تشويهها؛ ولا تقلبها إلى False إلا حين تقصد التمديد فعلًا

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

واختيار الترميز هو رافعة الحجم الأخرى. فالمحتوى الفوتوغرافي، كالمرفقات الممسوحة ضوئيًا وما شابهها، مكانه JPEG: مرّر icJpeg إلى AddImage وأنزل JpegQuality إلى نحو 85، لأن الخاصية تبدأ عند 100 والفرق عند 85 غير مرئي على صفحة مطبوعة. أما الرسوم ذات الألوان المسطّحة كالشعارات والمخططات والرسوم الخطية فمكانها icFlate، حيث الضغط بلا فقد مضغوط أصلًا بينما كان JPEG سيلطّخ رنينًا مرئيًا حول الحواف الحادة. وتشغيل كشوف يدفع صورة فوتوغرافية بجودة كاملة على كل صفحة قد ينتفخ إلى غيغابايتات؛ والمحتوى نفسه عند JPEG 85 يحطّ عند نحو عُشر الحجم، ولا يستطيع أي قارئ أن يميّز الفرق

الخطوط الفاصلة والصناديق والتظليل بأوليات المسارات

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

// خط فاصل أفقي تحت ترويسة الجدول
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// صندوق إجماليات مظلَّل: X و Y والعرض والارتفاع
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

والترتيب ليس اختياريًا: اضبط حالة الرسم، وابنِ المسار، ثم استدعِ Stroke أو Fill. فالمسار الذي تبنيه ولا ترسمه أبدًا لا يسهم بشيء في الصفحة، وهذا دائمًا تقريبًا هو الجواب حين «لا يظهر» خط فاصل. وتأخذ SetRGBFillColor قيمة TColor واحدة، لذا تدخل ثوابت VCL المألوفة مثل clNavy وclBlack مباشرةً، وتستخدم Rectangle وسيطي العرض والارتفاع نفسيهما المستخدمين في وضع الصور لا زاويتين. وتحذير واحد بشأن الخطوط الرفيعة: أي شيء دون نصف نقطة تقريبًا قد يبدو أنيقًا على الشاشة ثم يختفي على طابعة مكتبية بدقة 600 dpi، لذا فإن 0.75pt حد أدنى معقول لأي خط فاصل يجب أن ينجو من الطباعة

تقسيم الصفحات على بيانات حقيقية لا بيانات نموذجية

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

خطر تقسيم الصفحات أنك تكتبه على مجموعة البيانات التجريبية، حيث تتسع عشرة صفوف قصيرة في صفحة واحدة ولا تضطر الحلقة أبدًا إلى الانقطاع. ثم يسلّمك الإنتاج عميلًا يمتد اسم شركته 140 حرفًا وكشفًا من 4,000 بند، وعندها يجب أن تنقطع الحلقة على نحو صحيح في كل مرة. والنمط الذي يصمد هو مؤشر Y واحد يتحرك نزولًا كلما طرحت ارتفاع كل صف، وفحص يبدأ صفحة جديدة لحظة يوشك المؤشر على تجاوز الهامش السفلي. والنزول هنا يعني تناقص Y، وهو الموضع الوحيد الذي تبقى فيه نقطة الأصل السفلية اليسرى مخالفة للحدس. أبقِ كل ذلك في إجراء واحد يعيد أيضًا إصدار SetFont ويعيد رسم الترويسة الجارية على الصفحة الجديدة، فلا تجد علل الانزياح بصفحة واحدة موطئ قدم أبدًا. وحين يجب أن تفي التقارير نفسها أيضًا بقواعد الأرشفة أو إمكانية الوصول، فإن الخيارات التي تتخذها هنا بالذات، أي الخطوط التي تضمّنها، وما إذا كانت المخرجات موسومة، وفضاءات الألوان التي تستخدمها، هي ما تراقبه تلك المعايير؛ ودليل HotPDF لـ PDF/A و PDF/X و PDF/UA جدير بالقراءة قبل أن يتصلّب القالب

كل استدعاء معروض هنا، من تموضع النص وتسجيل الخطوط وتضمين الصور ورسم المسارات، يُشحن ضمن مكوّن HotPDF لـ Delphi لـ Delphi و C++Builder، الذي يوثّق مرجعه واجهة برمجة المخرجات كاملةً إلى جانب ميزات النماذج والتشفير والتوقيع التي تجاورها