Technical Article

بارگذاری PDFهای با ارجاع ترکیبی از Word و Excel در Delphi

یک PDF تولیدشده توسط Microsoft Word یا Excel را باز کنید، صفحات آن را ورق بزنید، و هیچ چیز غیرعادی به نظر نمی‌رسد. آن را در یک برنامه Delphi بارگذاری کنید، تعداد صفحات را بخوانید، و این تعداد درست است. سپس آن را با فعال کردن رمزگذاری مجدداً ذخیره کنید؛ کار با یک EListError مواجه می‌شود یا خروجی با هشدار خراب بودن جدول ارجاع متقابل (Cross-reference) باز می‌شود. فایل هرگز خراب نبوده است. این یک فایل با ارجاع ترکیبی (Hybrid-reference) است و دقیقاً همان ساختاری که به یک نمایشگر پانزده‌ساله اجازه می‌دهد آن را باز کند، ساختاری است که بارگذارنده‌ای را که خیلی زود خواندن را متوقف می‌کند، شکست می‌دهد

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

آنچه Word و Excel در واقع می‌نویسند

استاندارد ISO 32000-1 طرح‌بندی ارجاع ترکیبی را در بخش ۷.۵.۸.۴ توصیف می‌کند. برنامه‌ای که ویژگی‌های PDF 1.5 مانند جریان‌های شیء (Object streams) را می‌خواهد، در حالی که همچنان به یک خواننده PDF 1.4 اجازه می‌دهد فایل را باز کند، اطلاعات ارجاع متقابل را دو بار می‌نویسد. یک جدول ارجاع متقابل کلاسیک وجود دارد، یعنی همان ردیف‌های ASCII با عرض ثابت که به هر PDF تا نسخه ۱.۴ خاتمه می‌داد، و یک جریان ارجاع متقابل نیز وجود دارد که بقیه را نمایه می‌کند. تریلر (Trailer) بخش کلاسیک شامل یک ورودی /XRefStm است که مقدار آن آفست بایت آن جریان است

این تقسیم کار عمدی است. اشیایی که یک خواننده قدیمی باید به آن‌ها دسترسی داشته باشد، از جمله کاتالوگ و درخت صفحات، از جدول کلاسیک قابل آدرس‌دهی هستند. اشیایی که در جریان‌های شیء فشرده‌شده قرار گرفته‌اند، در جدول کلاسیک با یک ورودی نوع f به عنوان آزاد علامت‌گذاری می‌شوند تا یک خواننده ۱.۴ مستقیماً از روی آن‌ها عبور کند و هرگز با ساختاری که نمی‌تواند تجزیه کند برخورد نکند. مکان‌های واقعی آن‌ها فقط در جریان ارجاع متقابل قرار دارد. نشانه چنین فایلی انتهای آن است: یک بخش کلاسیک کوتاه، که اغلب چیزی بیش از xref نیست که با یک هدر زیربخش 0 0 دنبال می‌شود و تریلر آن به /XRefStm اشاره می‌کند، جایی که داده‌های بازیابی واقعی در آن قرار دارند

چرا تعداد صفحات درست هیچ چیز را ثابت نمی‌کند

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

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

تله، آشکارسازی است که xref را می‌بیند و متوقف می‌شود

راه ساده برای تصمیم‌گیری در مورد نحوه نمایه شدن یک فایل، دنبال کردن startxref و بررسی اولین بایت‌هایی است که به آن‌ها اشاره می‌کند. کلمه کلیدی xref به معنای یک جدول کلاسیک است؛ یک شیء جریان به معنای جریان ارجاع متقابل است. این تست برای هر فایلی که به یک طرح متعهد است درست است. اما برای یک فایل ترکیبی اشتباه است، فایلی که startxref آن صرفاً برای راضی کردن خوانندگان قدیمی به یک بخش کلاسیک اشاره می‌کند، در حالی که /XRefStm در تریلر آن بخش جایی است که بیشتر سند در واقع نمایه شده است. آشکارسازی که با دیدن اولین xref مقدار "classic" را برمی‌گرداند، هرگز /XRefStm را نمی‌خواند و تمام اشیایی که فقط در جریان زندگی می‌کنند نامرئی می‌شوند

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // count is correct
    // inspect or edit the loaded document here
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // walks every object
  finally
    Pdf.Free;
  end;
end;

با وجود آشکارساز خروج زودهنگام، بارگذاری درست به نظر می‌رسد و ذخیره مجدد جایی است که اشیاء غایب خود را نشان می‌دهند. راه حل این نیست که بایت‌های بیشتری را در ابتدا بخوانیم؛ بلکه این است که تریلر ترکیبی را شناسایی کرده و قبل از تصمیم‌گیری درباره اتمام فایل، /XRefStm را دنبال کنیم

ترتیب ادغام قابل مذاکره نیست

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

همین انضباط از احیای یک شیء حذف‌شده توسط یک نسخه قدیمی‌تر جلوگیری می‌کند. به‌روزرسانی‌های تدریجی به عقب و از طریق /Prev زنجیره می‌شوند و یک ورودی آزاد نوع ۰ یک نگهبان است که نشان می‌دهد یک بخش جدیدتر یک شماره شیء را بازنشسته کرده است. نباید به یک بخش بعدی و قدیمی‌تر در زنجیره اجازه داد که آن نگهبان را با یک مکان قدیمی بازنویسی کند. اگر اولین مشاهده را برای نشانگرهای آزاد معتبر بدانید، شیء حذف‌شده، حذف‌شده باقی می‌ماند؛ در غیر این صورت، تاریخچه خود فایل محتوایی را که آخرین نسخه حذف کرده است دوباره زنده می‌کند

این در HotPDF چه معنایی دارد

موتور فایل‌های با ارجاع ترکیبی را برای شما حل می‌کند، و این کار را در هر مسیری که باید داده‌های ارجاع متقابل را تجزیه کند انجام می‌دهد. یک سند را با LoadFromFile یا LoadFromStream بارگذاری کنید، تغییرات خود را اعمال کنید و SaveLoadedDocument را فراخوانی کنید؛ یا یک عملیات یک‌باره مانند EncryptFile را اجرا کنید که یک ورودی را می‌خواند و یک خروجی می‌نویسد. در هر صورت، بازیابی مقدار /XRefStm را می‌خواند، بخش جریان را قبل از ورودی‌های کلاسیک ادغام می‌کند و اشیایی را که در جریان‌ها زندگی می‌کنند قبل از اینکه عمل نوشتن آن‌ها را شمارش کند، حل می‌کند. مسیر رمزگذاری AES-256 جایی بود که مشکل برای اولین بار خود را نشان داد، زیرا رمزگذاری یک سند، هر شیء را بازنویسی می‌کند و بنابراین نیاز دارد که هر شیء قبلاً مکان‌یابی شده باشد

// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

جزئیاتی که ارزش به خاطر سپردن دارد در بالای زنجیره API قرار دارد. فایل‌هایی که از Word، Excel، PowerPoint و لیست طولانی خطوط لوله "ذخیره به عنوان PDF" می‌رسند، به طور معمول ترکیبی هستند، بنابراین بارگذاری که فقط با خروجی تولیدکننده خودتان تست می‌کنید، ممکن است هرگز در طول آزمایش با یکی از آن‌ها مواجه نشود. محیط‌های آزمایشی خود را با اسناد صادرشده از برنامه‌های واقعی Office پر کنید، نه فقط با فایل‌هایی که کدهای خودتان تولید کرده‌اند

بررسی فایلی که به آن مشکوک هستید

دو بازرسی این سوال را به سرعت حل می‌کنند. فایل را در یک نمای هگز باز کنید و بایت‌های پس از آخرین startxref را بخوانید؛ یک فایل ترکیبی یک بخش کلاسیک کوتاه را نشان می‌دهد که دیکشنری تریلر آن شامل /XRefStm است. یا تعداد اشیاء گزارش‌شده توسط یک تجزیه کامل را با بالاترین شماره شیء که /Size در تریلر اعلام می‌کند مقایسه کنید. یک شکاف بزرگ به این معنی است که اشیاء در جریان‌هایی پنهان شده‌اند که بارگذار باز نکرده است، که این همان کمبودی است که بعداً به خرابی در زمان ذخیره تبدیل می‌شود

سمت نویسنده این داستان، یعنی نحوه تولید جریان‌های شیء و ارجاع‌های متقابل فشرده در درجه اول، در مقاله ما در مورد جریان‌های شیء و به‌روزرسانی‌های تدریجی پوشش داده شده است. هنگامی که فایل ترکیبی مورد نظر بسیار بزرگ است، تکنیک‌های بارگذاری در راهنمای Direct File API برای گردش‌های کاری بزرگ PDF به شما اجازه می‌دهد آن را بدون خواندن کل فایل در حافظه بررسی کنید. هر دو به طور طبیعی با بازیابی توضیح داده شده در اینجا جفت می‌شوند، که به عنوان بخشی از کامپوننت HotPDF برای Delphi و C++Builder همراه با APIهای بارگذاری، ویرایش، رمزگذاری و امضا که در بخش‌های دیگر این وبلاگ پوشش داده شده‌اند، عرضه می‌شود