مقاله فنی

پیوست‌های PDF در دلفی با استفاده از کامپوننت PDFium: خواندن، افزودن، حذف

فایل‌های پیوست PDF در ساختار درختیِ فایل‌های تعبیه‌شده (embedded-file tree) سند ذخیره می‌شوند، ساختاری که بیشتر برنامه‌های نمایشگر آن را به صورت پنل گیره کاغذ یا نوار کناری پیوست‌ها نمایش می‌دهند. از دید کدهای دلفی، کامپوننت PDFium آن درخت را از طریق مجموعه‌ای کوچک از ویژگی‌های ایندکس‌شده روی کلاس TPdf ارائه می‌دهد: شما بر اساس ایندکس عددی پیمایش می‌کنید، نام‌ها و بایت‌های محتوا را می‌خوانید، فضاهای جدید ایجاد می‌نمایید، و موارد موجود را حذف می‌کنید. این API بسیار جمع‌وجور است; فقط چند محدودیت در ترتیب اجرا و یک قانون پاک‌سازی وجود دارد که بهتر است قبل از نوشتن کد اصلی برنامه با آنها آشنا شوید

خواندن پیوست‌ها از یک سند باز شده

ویژگی AttachmentCount تعداد فایل‌های تعبیه‌شده اعلام‌شده در سند را برمی‌گرداند. این مقدار مستقیماً از فراخوانی پایه‌ای پی‌دی‌اف‌یوم خوانده می‌شود، بنابراین تنها چیزی را که واقعاً در 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 رشته‌هایی هستند که در داخل فایل ذخیره می‌شوند؛ آنها می‌توانند شامل جداکننده‌های مسیر، کاراکترهای مشابه یونیکد، و سایر کاراکترهایی باشند که در صورت ارسال مستقیم به متد 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 از ذخیره‌سازی مجدد روی همان فایلِ باز شده پشتیبانی نمی‌کند، زیرا موتور یک هندل خواندن فعال روی فایل منبع دارد. الگوی استاندارد برای به‌روزرسانی محلی، ذخیره در یک مسیر موقت، بستن سند، حذف یا تغییر نام فایل اصلی، و سپس تغییر نام فایل موقت به فایل اصلی و باز کردن مجدد آن است

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 برای فایل‌های باینری قدیمی مایکروسافت. نمایش اطلاعات نوع دریافت شده از دیکشنری در برچسب‌های رابط کاربری بلامانع است، اما وقتی بایت‌های واقعی در دسترس هستند، نباید فرآیند تصمیم‌گیری پردازش را هدایت کنند

حذف پیوست‌ها

متد 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

API پیوست‌ها روی هر فایل PDF که پی‌دی‌اف‌یوم بتواند باز کند کار می‌کند، اما اسنادی که در آنها با فایل‌های تعبیه‌شده مواجه می‌شوید، در چند مورد خاص خلاصه می‌شوند. استاندارد PDF/A-3 (ISO 19005-3) به طور صریح فایل‌های تعبیه‌شده سازگار را به عنوان مکانیزمی برای بسته‌بندی داده‌های منبع در کنار نسخه آرشیوی مجاز می‌داند; فاکتورهای الکترونیکی ZUGFeRD و Factur-X دقیقاً به همین موضوع برای تعبیه ساختار XML در داخل چیدمان PDF قابل خواندن توسط انسان تکیه می‌کنند. فایل‌های PDF دریافت شده از ایمیل‌ها گاهی اوقات پیوست‌های اصلی پیام را در درخت فایل‌های تعبیه‌شده به همراه دارند. مستندات فنی تولید شده در سیستم‌های نویسندگی ساختاریافته نیز گاهی اوقات دارایی‌های پشتیبان را به همین صورت بسته‌بندی می‌کنند

هنگامی که برنامه شما فایل‌های PDF ورودی را از خارج از سازمان پردازش می‌کند، بررسی AttachmentCount به عنوان بخشی از فرآیند دریافت سند به دو دلیل مستقل ارزش انجام دارد. اول اینکه، فایل‌های تعبیه‌شده ممکن است حاوی داده‌هایی باشند که می‌خواهید آنها را استخراج و پردازش کنید، مانند فایل XML داخل PDF فاکتور. دوم اینکه، فایل‌های تعبیه‌شده می‌توانند حاوی محتوای اجرایی دلخواه باشند، بنابراین دانستن اینکه چه چیزی وجود دارد اهمیت دارد حتی زمانی که قصد استخراج آن را ندارید. هیچ‌یک از این دلایل نیاز به کارهای پیچیده ندارند: خواندن تعداد، بررسی نام‌ها، و تصمیم‌گیری درباره بایت‌ها کافی است

ویژگی‌های پیوست نشان داده شده در اینجا بخشی از PDFium Component برای دلفی و سی‌پلاس‌پلاس‌بیلدر هستند