Technical Article

ملفات PDF المرفقة في Delphi باستخدام PDFium VCL: القراءة والإضافة والحذف

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

قراءة المرفقات من مستند مفتوح

تعطيك AttachmentCount عدد الملفات المضمنة التي يعلن عنها المستند. وهي تقرأ مباشرة من استدعاء PDFium الأساسي، لذلك فهي تعكس ما يحتويه PDF فعليًا فقط. بعد ذلك، ترجع AttachmentName[Index] الاسم المعروض على شكل WString، وتسلّم Attachment[Index] البايتات الخام على هيئة مصفوفة TBytes. كلاهما يبدأ من الصفر. يجب أن يكون المستند مفتوحًا (Pdf.Active = True) قبل أن تستعلم عن أي من الخاصيتين؛ واستدعاؤهما على مستند مغلق يعيد صفرًا أو نتيجة فارغة من دون استثناء.

هناك أمر يجب تذكره: Attachment[Index] يحجز ويعيد الحمولة الكاملة للملف في كل قراءة. بالنسبة إلى مستند يحمل أصلًا مضمّنًا كبيرًا، فإن المرور على كل المرفقات لبناء قائمة عرض يعني دفع كلفة هذا الحجز في كل استدعاء. إذا كنت تحتاج إلى الأسماء فقط لأغراض العرض، فاقرأ AttachmentName أولًا وأخّر جلب البايتات حتى يطلب المستخدم الملف فعليًا.

procedure ListAttachments(Pdf: TPdf);
var
  I: Integer;
  Data: TBytes;
begin
  if not Pdf.Active then
    Exit;

  for I := 0 to Pdf.AttachmentCount - 1 do
  begin
    Data := Pdf.Attachment[I];
    Writeln(Format('%d: %s (%d bytes)',
      [I, Pdf.AttachmentName[I], Length(Data)]));
  end;
end;

استخراج مرفق إلى القرص

لا توجد أداة مساعدة باسم SaveAttachment. أنت تقرأ البايتات وتكتبها حيثما تحتاج، وهذا يجعل إنشاء المسار وتنظيفه مسؤوليتك بالكامل. هذا مهم عندما تأتي أسماء المرفقات من مستندات غير موثوقة. أسماء مرفقات PDF هي سلاسل مخزنة داخل الملف؛ ويمكن أن تحتوي على فواصل مسار، أو أشكال Unicode متشابهة، أو أحرف أخرى ستنتج نتائج غير متوقعة إذا مررتها مباشرة إلى TFileStream.Create. مرّر الاسم دائمًا عبر ExtractFileName قبل بناء أي مسار إخراج، وفكّر في رفض الأسماء التي تبدأ بنقطة أو تحتوي أحرفًا خارج ما يتوقعه نظامك.

مصفوفة البايتات التي تعيدها Attachment[Index] يملكها المستدعي. اكتبها بوساطة TFileStream عادي، وستكون ملكك لتفعل بها ما تشاء، بما في ذلك فحص البايتات الأولى للتأكد من تنسيق الملف الفعلي بدلًا من الوثوق بالاسم المعلن.

procedure ExtractAttachment(Pdf: TPdf; Index: Integer; const OutputDir: string);
var
  SafeName: string;
  OutPath: string;
  Data: TBytes;
  FS: TFileStream;
begin
  SafeName := ExtractFileName(Pdf.AttachmentName[Index]);
  if SafeName = '' then
    SafeName := Format('attachment_%d', [Index]);

  OutPath := IncludeTrailingPathDelimiter(OutputDir) + SafeName;
  Data := Pdf.Attachment[Index];

  FS := TFileStream.Create(OutPath, fmCreate);
  try
    if Length(Data) > 0 then
      FS.WriteBuffer(Data[0], Length(Data));
  finally
    FS.Free;
  end;
end;

إضافة المرفقات والكتابة على خطوتين

إنشاء مرفق يتطلب استدعاءين، لا استدعاءً واحدًا. يسجل CreateAttachment(Name) خانة جديدة في شجرة الملفات المضمنة ويعيد True عند النجاح. وتبدأ تلك الخانة فارغة. ثم تعيّن الحمولة بكتابة Attachment[AttachmentCount - 1]، أي أحدث إدخال تم إنشاؤه. إذا أعاد CreateAttachment القيمة False، فلن تُنشأ الخانة، وسيؤدي الإسناد إلى إتلاف المرفق عند الفهرس الأخير صدفةً.

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

procedure AddFileAttachment(Pdf: TPdf; const FilePath: string);
var
  FS: TFileStream;
  Data: TBytes;
  AttachName: string;
begin
  if not Pdf.Active then
    Exit;

  FS := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
  try
    SetLength(Data, FS.Size);
    if FS.Size > 0 then
      FS.ReadBuffer(Data[0], FS.Size);
  finally
    FS.Free;
  end;

  AttachName := ExtractFileName(FilePath);
  if Pdf.CreateAttachment(AttachName) then
    Pdf.Attachment[Pdf.AttachmentCount - 1] := Data;
end;

معلومات نوع المرفق

إلى جانب الاسم والحمولة البايتية، ترجع AttachmentType[Index] سلسلة نوع MIME المخزنة في قاموس الملف المضمّن في PDF، إذا كانت قد سُجلت عند إرفاق الملف أصلًا. كثير من المولدات تترك هذا الحقل فارغًا أو تضبطه على قيمة عامة مثل application/octet-stream، لذلك لا يمكنك الاعتماد عليه لاكتشاف التنسيق في خط إنتاج حقيقي. من أجل تعريف موثوق، اقرأ البايتات الأولى من الحمولة وتحقق من التواقيع المعروفة: %PDF لملف PDF مضمّن، ورأس ملف ZIP المحلي PK\x03\x04 لمستندات Office Open XML، و\xD0\xCF\x11\xE0 لملفات compound القديمة. معلومات النوع من القاموس جيدة للعرض في تسمية واجهة مستخدم، لكنها لا ينبغي أن تقود قرارات المعالجة عندما تكون البايتات الفعلية متاحة لديك.

حذف المرفقات

تزيل DeleteAttachment(Index) الإدخال في ذلك الموضع وتعيد True عند النجاح. بعد الحذف، تنتقل الإدخالات المتبقية إلى الأسفل، لذلك إذا كنت تحذف عدة مرفقات داخل حلقة فعليك التكرار من آخر فهرس إلى أول فهرس، لا بالعكس، حتى لا تتخطى إدخالات بعد كل إزاحة. التغيير يبقى في الذاكرة حتى تستدعي SaveAs.

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

procedure StripAllAttachments(Pdf: TPdf);
var
  I: Integer;
begin
  for I := Pdf.AttachmentCount - 1 downto 0 do
    Pdf.DeleteAttachment(I);
end;

أين تظهر مرفقات PDF عمليًا

تعمل واجهة المرفقات مع أي PDF يستطيع PDFium فتحه، لكن المستندات التي تصادف فيها الملفات المضمّنة فعليًا تتركز حول بضع حالات محددة. PDF/A-3 ‏(ISO 19005-3) يسمح صراحة بالملفات المضمّنة المطابقة كآلية لضم بيانات المصدر إلى جانب النسخة الأرشيفية؛ وتعتمد ZUGFeRD و Factur-X للفواتير الإلكترونية بالضبط على هذا لإخفاء حمولة XML منظمة داخل تخطيط PDF المقروء بشريًا. بعض ملفات PDF المشتقة من البريد الإلكتروني تحمل أحيانًا مرفقات الرسالة الأصلية وقد أُعيد توجيهها إلى شجرة الملفات المضمنة. كما أن الوثائق التقنية التي نشأت في أنظمة التأليف المنظم قد تضم أحيانًا أصولًا مساعدة بالطريقة نفسها.

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

خصائص المرفقات المعروضة هنا جزء من مكوّن PDFium VCL لـ Delphi و C++Builder.