Technical Article

إنشاء ملفات PDF من الصفر باستخدام PDFium VCL في Delphi

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

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

الحد الأدنى الذي ينتج ملفاً

ثلاثة استدعاءات تفصل بين TPdf فارغ وملف PDF محفوظ: أنشئ المستند، أضف صفحة، اكتب الملف. كل ما عدا ذلك هو محتوى تضيفه بين هذه الخطوات.

uses
  Vcl.Graphics,   // for clBlack and TColor
  PDFium;         // TPdf lives here

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // empty in-memory document
    Pdf.AddPage(0, 595, 842);           // A4 portrait, in points
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialize to disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

تفصيلة واحدة تربك من رأى أمثلة أقدم: لا تُسند Pdf.Active := True بعد CreateDocument. الخاصية Active تعرض ما إذا كان مقبض المستند موجوداً، وCreateDocument أنشأه بالفعل، لذلك تكون الخاصية True لحظة عودة الاستدعاء. إعادة إسنادها ليست أكثر من عملية بلا أثر في أحسن الأحوال، ومضللة للقارئ التالي في أسوأها. تظهر قيمة Active عند الإنهاء: إسناد False يحرر المستند الأساسي قبل Free، وهذا هو ترتيب الإنهاء النظيف. تعامل مع CreateDocument والفتح من ملف بوصفهما حالتين متنافيتين. فالمكتبة ترفض إنشاء مستند جديد على TPdf لديه مستند مفتوح بالفعل، لذا فإن إعادة الاستخدام تعني إغلاق المستند الحالي أولاً.

الإحداثيات تبدأ من أسفل اليسار

الوسيطان الثانيان إلى AddText، وإلى كل استدعاء تموضع، هما نقطة في فضاء المستخدم الخاص بـ PDF. يقع الأصل في الزاوية السفلية اليسرى من الصفحة، ويمتد X إلى اليمين، ويمتد Y إلى الأعلى. الوحدة الواحدة هي نقطة واحدة، أي 1/72 من البوصة، لذلك تكون صفحة A4 بحجم 595 في 842 وحدة، وتكون US Letter بحجم 612 في 792. هذا الاتجاه الصاعد في Y هو أكثر مصدر شائع للارتباك من نوع "النص خرج خارج الصفحة"، لأن إحداثيات الشاشة والبيكسلات تضع الأصل في الأعلى مع ازدياد Y إلى الأسفل. على صفحة ارتفاعها 842 نقطة، يكون العنوان قرب الأعلى عند Y يساوي نحو 780، لا 60. عندما تظهر النتيجة في مكان غير متوقع، فإن ارتفاع الصفحة ناقص قيمة Y هو غالباً الرقم الذي قصدته فعلاً.

AddPage يأخذ موضع الإدراج بوصفه وسيطه الأول، بصيغة تبدأ من واحد، مع 0 بوصفه اختصاراً مناسباً لمعنى "بداية المستند". مرر 0 أو 1 للصفحة الأولى فتُدرج الصفحة في المقدمة، ومرر القيمة المطابقة للعدد الذي تلحقه إذا أردت الإضافة في النهاية. تصبح الصفحة المضافة حديثاً أيضاً الصفحة الحالية، أي الصفحة التي تستهدفها استدعاءات الرسم اللاحقة، لذلك لا توجد خطوة منفصلة من نوع "حدد هذه الصفحة" بعد إضافتها. إذا أضفت عدة صفحات ثم احتجت لاحقاً إلى الرسم على صفحة سابقة، فاضبط PageNumber لتحريك المؤشر، أما أثناء ملء الصفحات بالترتيب الذي تنشئها به، فيمكنك تركه كما هو.

كتابة النص، وقاعدة الخط التي تخطئ بصمت

يحمل توقيع AddText كل ما يحتاجه تمرير نص واحد: السلسلة النصية، واسم الخط، والحجم بالنقاط، ونقطة الارتكاز X وY، ثم اللون الاختياري، وبايت ألفا للشفافية، وزاوية الدوران بالدرجات.

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Title in black, default opacity, no rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // A lighter byline 24 points below it
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // A faint diagonal draft stamp across the page
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

يتراوح بايت ألفا من $00 (غير مرئي) إلى $FF (معتم)، وهذا ما يجعل ختم المسودة علامة مائية لا كتلة صلبة: فالقيمة $30 تعادل تقريباً شفافية بنسبة تسعة عشر في المئة، تكفي للقراءة من خلالها. تدور الزاوية عكس اتجاه عقارب الساعة حول نقطة الارتكاز، لذا تمنحك 45 درجة الختم التقليدي من زاوية إلى زاوية. لا يحتاج أي من هذا إلى ميزة علامة مائية منفصلة. فالعلامة المائية ليست إلا استدعاء AddText كبيراً وشبه شفاف ومائلاً، وتحديد ما إذا كنت ترسمه قبل المتن أو بعده هو ما يقرر إن كان خلف المحتوى أو فوقه.

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

الأشكال المتجهة: أنشئ مساراً ثم اعتمده

تمر الخطوط والمستطيلات والمناطق المعبأة عبر مسار. تفتحه بـ CreatePath، الذي يحدد نقطة البداية وكل أنماط التنسيق دفعة واحدة، ووضع التعبئة، وألوان التعبئة والحدود مع بايتات ألفا الخاصة بها، وعرض الحد، وأغطية الخطوط ووصلاته. ثم توسع المسار بـ LineTo وBezierTo وClosePath، وأخيراً يعتمد AddPath المسار المكتمل على الصفحة. خطوة الاعتماد سهلة النسيان، ولا ينتج عنها شيء إذا تجاوزتها.

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // A thin horizontal rule. The rectangle overload sets a box directly:
  // X, Y, Width, Height, then fill mode and colors.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Point overload: start at the first vertex, line to the rest, close.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nothing is drawn until this runs
end;

يغطي الشكلان الاستخدامات الشائعة. فالشكل رباعي الإحداثيات يأخذ X وY والعرض والارتفاع ويمنحك مستطيلاً بمحاذاة المحاور في استدعاء واحد، وهو ما تلجأ إليه لرسم خط فاصل أو حد خلية أو لوحة خلفية مملوءة. أما الشكل ثنائي الإحداثيات فيحدد نقطة بداية فقط، ثم ترسم بقية المخطط بنفسك باستخدام LineTo وBezierTo. تتحكم أوضاع التعبئة في كيفية طلاء المناطق المتداخلة: fmWinding، أي winding غير الصفري، يناسب معظم الأشكال الصلبة، وfmAlternate، أي even-odd، يتعامل مع الفتحات والمخططات المتقاطعة ذاتياً، وfmNone يترك المسار بحد فقط من دون تعبئة، وهو ما يستخدمه الفاصل أعلاه.

الجداول هي مسارات ونصوص، مجمعة يدوياً

لأنه لا توجد بنية أصلية للجداول، فكل جدول هو حلقة. تحدد إزاحات X للأعمدة وارتفاع الصف، وتكتب كل خلية باستخدام AddText، وترسم الخطوط بمسارات مستطيلة. الحسابات عليك، لكنها بسيطة، وما إن تُكتب حتى يمكن تعميمها على أي شبكة تحتاجها.

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // column offsets
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Header row
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Rule under the header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Data rows, stepping Y downward each iteration
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

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

الصور وصفحات متعددة

تدخل المحتويات النقطية عبر أدوات الصور. يستقبل AddPicture كائناً TPicture محملاً ويضعه عند نقطة، مع عرض وارتفاع اختياريين لتحجيمه؛ ويقبل AddImage مسار ملف أو TBitmap مباشرة، بينما يبث AddJpegImage بيانات JPEG من دون المرور مرة أخرى عبر Bitmap. وكما في كل شيء آخر، تكون إحداثيات التموضع هي الزاوية السفلية اليسرى للصورة في فضاء المستخدم، ويكون العرض والارتفاع بالحجم على الصفحة بالنقاط، لا بأبعاد البكسل للمصدر.

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; the new page becomes current
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer near the bottom edge
      // ... draw this page's body here ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

المستند متعدد الصفحات هو النمط الأحادي الصفحة داخل حلقة. كل AddPage يلحق صفحة ويجعلها الحالية، لذلك يقع المتن والتذييل اللذان ترسمهما بعده على الصفحة التي أضفتها للتو. لا تعيد إسناد PageNumber داخل هذه الحلقة، لأن إضافة الصفحة نقلت المؤشر إليها بالفعل؛ ولا تحتاج إلى PageNumber إلا عندما تعود إلى صفحة خارج ترتيب الإنشاء. استدعِ SaveAs مرة واحدة في النهاية، بعد امتلاء الصفحة الأخيرة. وإذا كنت تحتاج إلى ملف أرشيفي بدلاً من ملف عادي، فالكائن نفسه يعرض SaveAsPdfA ومتغيرات المطابقة الأخرى، لذا فإن اختيار معيار الإخراج هو استدعاء حفظ مختلف، لا مسار بناء مختلف.

أين يندرج هذا

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

استدعاءات الإنشاء الموصوفة هنا هي جزء من مكوّن PDFium VCL الخاص بـ Delphi، الذي يجمع هذا المسار التأليفي مع ميزات العرض واستخراج النص التي اشتهر بها PDFium أكثر.