مقاله فنی

پردازش فایل‌های PDF حجیم در دلفی با HotPDF Direct File API

شمارش تعداد صفحات در یک بایگانی اسکن‌شده به حجم 1.4 گیگابایت باید کاری کم‌هزینه باشد. اما فراخوانی LoadFromFile روی آن فایل باعث می‌شود که این فرآیند دیگر ارزان نباشد: HotPDF داده‌های ارجاع متقابل (cross-reference) را تجزیه کرده و برای هر یک از چند صد هزار شیء غیرمستقیم سند، یک شیء درون حافظه ایجاد می‌کند. در این فرآیند، یک Worker 32-بیتی در میانه‌ی راه با سقف ۲ گیگابایتی فضای آدرس‌دهی مواجه می‌شود. عملیاتی که شما نیاز داشتید یعنی شمارش صفحات، هرگز به هیچ‌یک از این اشیاء نیاز نداشت. فقط به درخت صفحات (page tree) نیاز داشت و نه چیز دیگر. این فاصله، بین چیزی که یک درخواست می‌خواهد و چیزی که یک بارگذاری کامل ارائه می‌دهد، تمام دلیلی است که Direct File API ایجاد شده است

مجموعه Direct File API به دلفی و C++Builder دسترسی سطح-فایل به یک PDF می‌دهد: شمارش صفحات، کپی‌ها، رمزگشایی، الحاق‌های افزایشی، و تمام خواندن‌ها از دیسک دقیقاً متناسب با چیزی است که واقعاً به آن نیاز است، به جای آنکه کل مدل سند را در حافظه RAM بازسازی کند. مهارت اصلی این است که هر کار را با سبک‌ترین سطح ممکنی که می‌تواند پاسخگوی آن باشد، تطبیق دهیم. با تطبیق صحیح، یک سرویس می‌تواند مصرف حافظه را در هر اندازه ورودی ثابت نگه دارد. در صورت اشتباه، اولین فایل بسیار بزرگ باعث از کار افتادن Worker خواهد شد

هزینه‌ای که یک بارگذاری کامل برای شما دارد

تابع LoadFromFile دشمن شما نیست. این تابع ارزش حافظه‌ای را که مصرف می‌کند دارد: وقتی درخت سند در RAM قرار گرفت، شما دسترسی تصادفی به هر صفحه و هر شیء دارید، که این دقیقاً همان چیزی است که توابعی مانند InsertPagesFromDocument، MovePage و سریال‌سازی مجدد از طریق SaveLoadedDocument به آن نیاز دارند. هیچ میانبری برای تغییر ساختار واقعی وجود ندارد؛ برای جابجایی عناصر سند، باید آن را به طور کامل در اختیار داشته باشید

مشکل از زمانی آغاز می‌شود که حجم فایل‌های ورودی در کنترل شما نیست. آپلودهای کاربران، خروجی‌های اسکنر و آرشیوهای مربوط به یک دهه پیش، از هر فرضی که شما در داده‌های آزمایشی خود داشته‌اید، پیروی نمی‌کنند. اگر هر ورودی را به طور بی‌قیدوشرط بارگیری کنید، سقف حافظه شما توسط بزرگترین فایلی که یک فرد ارسال می‌کند تعیین می‌شود. زمان تجزیه (parse time) به تعداد اشیاء بستگی دارد و پس از محاسبه ساختارهای شیء و جریان‌های کدگشایی شده، حافظه مقیم (resident memory) چندین برابر اندازه فایل اصلی می‌شود. بنابراین یک گیگابایت روی دیسک می‌تواند به معنای چندین گیگابایت فضای مقیم در حافظه باشد

کامپایل مجدد برای معماری ۶۴-بیت سقف فضای آدرس را بالا می‌برد اما هزینه را دست‌نخورده باقی می‌گذارد. Worker همچنان ثانیه‌هایی از پردازنده و مضربی از حجم فایل را در RAM هدر می‌دهد تا به سؤالی پاسخ دهد که ساختار خود فایل می‌توانست در چند میلی‌ثانیه به آن پاسخ دهد. تحت شرایط هم‌زمانی (concurrency)، محاسبات پیچیده‌تر می‌شود: چهار بارگذاری بزرگ که همزمان اجرا می‌شوند، از یک بودجه حافظه مشترک استفاده می‌کنند، و دقیقاً زمانی که صف پردازش در عمیق‌ترین حالت است و شما کمترین توانایی را برای مقابله دارید، توان عملیاتی به شدت کاهش می‌یابد

خواندن یک فایل از طریق هندل (Handle)

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

var
  Pdf: THotPDF;
  Handle, PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Handle := Pdf.DAOpenFileReadOnly('archive-2026-06.pdf', '');
    if Handle > 0 then
    try
      PageCount := Pdf.DAGetPageCount(Handle);
      RouteByPageCount('archive-2026-06.pdf', PageCount);
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;

سه عادت به شما کمک می‌کند که این سطح را پایدار نگه دارید. اول، مقدار بازگشتی را بررسی کنید. اگر هندل مثبت نباشد، به معنای باز نشدن فایل است، و ارسال DAGetPageCount به یک هندل مرده، نوعی باگ است که تا روزی که مشتری یک فایل معیوب ارسال کند، پنهان باقی می‌ماند. دوم، هر بار باز شدن موفق را با DACloseFile در داخل یک بلوک finally همراه کنید؛ سرویسی که در آن نشت هندل رخ دهد ممکن است متلاشی (crash) نشود، بلکه دچار فرسایش می‌شود که وضعیتی بدتر است. سوم، به کارکرد واقعی پارامتر رمز عبور دقت کنید. DAOpenFileReadOnly یک رمز عبور می‌پذیرد، اما برای ورودی‌های رمزنگاری شده، برای خواندن تعداد صفحات به طور خاموش به بارگیری کامل متوسل می‌شود و تضمین حافظه ثابت از بین می‌رود. برای جلوگیری از این مشکل، فایل‌های محافظت‌شده را ابتدا از طریق DecryptFile عبور دهید تا بقیه خط لوله کم‌هزینه باقی بماند

همان کاوش به عنوان یک دروازه تریاژ نیز عمل می‌کند. فایل‌ها ممکن است با نام‌گذاری اشتباه، آپلود ناقص، یا تغییر نام‌یافته از یک فرمت دیگر ظاهر شوند، و یک بررسی DAOpenFileReadOnly همه آن‌ها را در ورودی سیستم در چند میلی‌ثانیه رد می‌کند و خطا را به فایلی که مشکل دارد، اختصاص می‌دهد. جایگزین این است که یک فایل نامعتبر را به عمق یک worker صف هدایت کنید و بگذارید در آنجا خراب شود، جایی که پیدا کردن اینکه کدام ورودی باعث ایجاد مشکل شده، ممکن است ساعت‌ها وقت ببرد

کپی، رمزگشایی و رمزگذاری فایل‌های کامل

لایه دوم بدون اینکه نیازی به افشای ساختارهای داخلی داشته باشد، فایل‌های کامل را جابه‌جا کرده و تغییر می‌دهد. این‌ها فراخوانی‌هایی هستند که خطوط لوله ورودی بیشتر روی آن‌ها حساب می‌کنند

// Structural copy: validate-and-move without parsing the object tree
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Decrypt while copying: the Direct File route into protected inputs
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Encrypt while copying: protect an output without a full load
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

هر فراخوانی جایگاه خود را به دست می‌آورد. DACopyFile یک کپی اعتبارسنجی شده از یک دایرکتوری قرنطینه به حافظه مدیریت شده است: در حین انجام کار، ساختار PDF را باز کرده و فهرست‌بندی می‌کند، در نتیجه یک ورودی ناقص یا غیر PDF فوراً متوقف می‌شود، نه در مراحل بعدی کار. DecryptFile یک کپی رمزگشایی شده را از طریق مسیر بازنویسی مستقیم AES-256 ذخیره می‌کند که هرجا امکان داشته باشد از درخت شیء عبور نمی‌کند؛ این معادل مسیر بارگیری و ذخیره‌سازی مجدد برای فایل‌های بزرگ است که در مقاله رمزنگاری AES-256 پوشش داده شده است. EncryptFile همان حرکت را به صورت معکوس اجرا می‌کند و محافظت با رمز عبور را در حین کپی در سطح فایل اعمال می‌کند، و این کار را با استفاده از نوع کلید و پارامترهای دسترسی که در مسیر مبتنی بر حافظه استفاده می‌شود، انجام می‌دهد

افزودن تغییرات به جای بازنویسی کامل

به‌روزرسانی افزایشی، که در استاندارد ISO 32000-1 §7.5.6 تعریف شده، لایه سوم است. بایت‌های اولیه فایل دست‌نخورده روی دیسک باقی می‌مانند، و هر شیء جدید یا تغییریافته‌ای به انتهای فایل متصل (append) می‌شود. در ادامه یک بخش جدید از ارجاع متقابل اضافه می‌شود که زنجیره‌وار به بخش اصلی بازمی‌گردد. برای افزودن یک صفحه به یک بایگانی 900 مگابایتی، هزینه نوشتن تنها مقدار تغییرات است، نه کل حجم فایل

// Append an audit page to a large archive without rewriting it
Pdf.BeginIncrementalUpdate('archive-2026-06.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Processed by intake service 2026-06-11');
Pdf.SaveIncrementalUpdate('archive-2026-06-stamped.pdf');  // original bytes + delta

دو نکته مهم درباره نظم و انضباط در اینجا وجود دارد. BeginIncrementalUpdate باید به فایل اصلی اشاره کند، زیرا داده‌های پیوست‌شده مرجع متقابل (cross-reference) به آفست‌های (offsets) بایت داخل آن متصل می‌شوند. ثانیاً این مدل از نظر طراحی به صورت افزودنی است (append-only): هر ذخیره افزایشی باعث رشد فایل می‌شود و هرگز آن را کوچک نمی‌کند. سندی که شبانه مهر می‌شود بدون محدودیت بزرگ خواهد شد تا زمانی که از طریق سریال‌سازی مجدد با دستور SaveLoadedDocument (بارگیری و نوشتن مجدد) متراکم شود. همین ماهیت افزودنی است که به‌روزرسانی‌های افزایشی را به تنها روش ایمن برای تعامل با اسناد امضا شده دیجیتالی تبدیل می‌کند، محدودیتی که در مقاله امضاهای دیجیتال و PAdES مورد بررسی قرار گرفته است. مکانیسم‌های زیرین ارجاع متقابل در مقاله‌ای درباره جریان اشیا و به‌روزرسانی‌های افزایشی توضیح داده شده است

یک تله در ذخیره‌سازی‌های افزودنی وجود دارد که در بسیاری از بررسی‌ها از قلم می‌افتد. بایت‌های اصلی در فایل باقی می‌مانند و هر کسی که مایل به بررسی آن‌ها باشد می‌تواند آن‌ها را بخواند. به‌روزرسانی افزایشی‌ای که یک صفحه را "جایگزین" می‌کند، در واقع صفحه قدیمی را حذف نمی‌کند؛ بلکه صفحه را در نسخه فعلی جایگزین کرده و نسخه قبلی به صورت دست‌نخورده باقی می‌ماند که کاملاً قابل بازیابی است. بنابراین به‌روزرسانی‌های افزایشی ابزار مناسبی برای پاک کردن محتواهای حساس نیستند. برای از بین بردن تاریخچه‌ای که دریافت‌کننده نباید آن را ببیند، شما به یک سریال‌سازی مجدد کامل نیاز دارید: LoadFromFile و در پی آن SaveLoadedDocument، که تنها وضعیت فعلی را نوشته و نسخه‌های قدیمی مدفون شده را پشت سر می‌گذارد

تطبیق سطح با عملیات

منطق انتخاب آن‌قدر کوتاه است که در ذهن بماند، و بهتر است آن را به عنوان یک تصمیم مسیریابی مشخص در بالای یک خط لوله برنامه‌نویسی لحاظ کرد تا اینکه هر کار خود مسیر مستقلی انتخاب کند. عملیات مورد نیاز شما، لایه مناسب را تعیین می‌کند:

  • شمارش، بازرسی یا طبقه‌بندی یک هندل را باز می‌کند: DAOpenFileReadOnly، DAGetPageCount، DACloseFile
  • انتقال، رمزگشایی یا رمزگذاری یک فایل کامل با دستورات DACopyFile، DecryptFile، یا EncryptFile در سطح فایل می‌ماند
  • تغییر ساختار صفحات یا ادغام اسناد به بارگیری کامل نیاز مه دارد: LoadFromFile، سپس InsertPagesFromDocument یا MovePage، و سپس SaveLoadedDocument
  • افزودن تغییرات کوچک به یک فایل حجیم یا امضا شده دستور BeginIncrementalUpdate و سپس ذخیره‌سازی را فرا می‌خواند

خطوط لوله ترکیبی، وقتی خوب عمل می‌کنند که محدودیت اندازه‌ای در جلوی مسیر بارگذاری کامل اعمال کنند. هر چیزی که بیش از چند صد مگابایت باشد را از طریق لایه‌های Direct File ارسال کنید، و بارگیری کامل را برای تغییرات ساختاری واقعی روی یک Worker 64-بیتی با تخصیص بودجه حافظه واقعی، نگه دارید. این آستانه، خرابی خارج از حافظه (out-of-memory) را به یک تصمیم مسیریابی تبدیل می‌کند که می‌توانید آن را مشاهده و تنظیم کنید

هر لایه‌ای که کار را بر عهده بگیرد، باید خروجی را در یک نام موقت بنویسد و تنها پس از اینکه نتیجه اعتبارسنجی شد، آن را به جای اصلی منتقل (rename) کند. یک فایل نیمه‌کاره در صورت ذخیره در نام نهایی از نظر مرحله بعدی خط لوله به عنوان یک فایل بی‌عیب تلقی می‌شود، در حالی که فراخوانی‌های Direct File این کار را ارزان‌تر می‌کند: تأیید یک خروجی نیازمند بررسی ساده هندل در یک خط از برنامه است

Direct File API به عنوان بخشی از کامپوننت HotPDF برای دلفی و C++Builder در دسترس است. صفحه این محصول شامل راهنمای عملکرد کامل و همچنین فراخوانی‌های مربوط به به‌روزرسانی افزایشی که در اینجا به آن‌ها اشاره شد، می‌باشد