Technical Article

اعتبارسنجی PDFهای فشرده شده: جریان‌های Object و XRef

شما یک اعتبارسنج کوچک می‌نویسید. این برنامه یک PDF را باز می‌کند، به انتها می‌رود، startxref را پیدا می‌کند، افست را می‌خواند و انتظار دارد روی کلمه کلیدی xref فرود آید با یک جدول ارجاع متقاطع با عرض ثابت در زیر آن. از آن جدول افست‌های شیء را جمع‌آوری می‌کند، سپس برای یادگیری /Root و /Size به عقب اسکن می‌کند تا کلمه کلیدی trailer را بیابد. این برنامه روی هر فایلی که برای آزمایش تولید کرده‌اید عالی کار می‌کند. سپس فایلی که توسط نسخه فعلی Word یا کتابخانه‌ای که PDF 1.5 را هدف قرار داده ساخته شده می‌رسد و اعتبارسنج آن را خراب اعلام می‌کند. هیچ کلمه کلیدی xref در جایی که افست اشاره می‌کند وجود ندارد، هیچ دیکشنری trailer در هیچ کجا نیست و جدول اشیایی که اعتبارسنج ساخته تقریباً خالی است. فایل معتبر است؛ اعتبارسنج دارد آن را از یک عینک پانزده ساله می‌خواند.

این تک دلیل متداول شکست بررسی PDF در سطح بایت است که در برابر چیدمان کلاسیک روی اسناد مدرن نوشته شده است. ساختاری که به آن وابسته است، یعنی جدول ارجاع متقاطع متنی ساده و کلمه کلیدی trailer، در PDF 1.5 اختیاری شد و اغلب وجود ندارد. دو ویژگی جایگزین آن شدند: جریان ارجاع متقاطع (cross-reference stream) و جریان شیء فشرده (compressed object stream). هر دو در ISO 32000-1 توصیف شده‌اند و اعتبارسنجی که از آن‌ها خبر ندارد، یک فایل سالم را به عنوان تلی از اشیای مفقود می‌بیند.

آنچه PDF 1.5 در مورد انتهای فایل تغییر داد

بخش ۷.۵.۸ استاندارد ISO 32000-1 جریان ارجاع متقاطع را تعریف می‌کند و بخش ۷.۵.۷ جریان شیء از نوع /ObjStm را تعریف می‌نماید. آن‌ها با هم به یک نویسنده اجازه می‌دهند دو ساختاری را که یک پارسر کلاسیک روی آن‌ها کلید می‌کند رها کند. یک فایل PDF 1.5 ممکن است اصلاً بدون جدول xref به پایان برسد. در جای آن، شیئی که startxref به آن اشاره می‌کند یک شیء جریان معمولی است که دیکشنری آن /Type /XRef را حمل می‌کند و آن جریان داده‌های ارجاع متقاطع را به صورت باینری فشرده نگه می‌دارد. همچنین هیچ کلمه کلیدی trailer وجود ندارد، زیرا تریلر اکنون دیکشنری خود جریان است. کلیدهایی که یک پارسر کلاسیک به دنبالشان بود، یعنی /Root ،/Size و /ID، در داخل آن دیکشنری زندگی می‌کنند.

تغییر دوم خود اشیاء را جابه‌جا می‌کند. نویسنده به جای نوشتن هر شیء غیرمستقیم در افست بایت خود، می‌تواند بسیاری از اشیای کوچک، دیکشنری‌های صفحه، دیکشنری‌های حاشیه‌نویسی و درخت ساختار را در یک جریان شیء واحد بسته‌بندی کند و کل ظرف را با Flate فشرده نماید. اشیای فردی دیگر افست بایتی در فایل ندارند. آن‌ها موقعیتی در داخل یک بلاک فشرده دارند. اعتبارسنجی که بایت‌های خام را برای 1 0 obj اسکن می‌کند هرگز آن‌ها را پیدا نمی‌کند، زیرا آن متن تنها پس از باد شدن (inflation) وجود دارد. برای یک پارسر کلاسیک، نیمی از سند به سادگی ناپدید شده است.

کلیدهای تریلر متنی ساده هستند، حتی در یک فایل فشرده

بخش اطمینان‌بخش این است که خواندن تریلر یک جریان ارجاع متقاطع نیازی به باد کردن هیچ چیزی ندارد. یک شیء جریان به صورت یک دیکشنری و به دنبال آن کلمه کلیدی stream و سپس بایت‌های فشرده نوشته می‌شود. دیکشنری متنی ساده است. بنابراین وقتی startxref به یک جریان ارجاع متقاطع اشاره می‌کند، بایت‌های بلافاصله پس از شماره شیء مانند یک دیکشنری معمولی به نظر می‌رسند و /Root ،/Size و /ID در آنجا به صورت واضح قرار دارند، قبل از اینکه کلمه کلیدی stream و داده‌های Flate شروع شوند.

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

جریان‌های شیء: یک هدر، سپس یک بلاک Flate

یک جریان شیء (object stream) یک ظرف است. دیکشنری آن /Type /ObjStm، یک ورودی /N که تعداد اشیای بسته‌بندی شده در داخل را نشان می‌دهد و یک ورودی /First که افست بایت را در داخل داده‌های باد شده نشان می‌دهد، جایی که بدنه اولین شیء شروع می‌شود. محموله فشرده پس از باد شدن، با هدر کوچکی از جفت‌های عدد صحیح /N شروع می‌شود. هر جفت یک شماره شیء و افست بدنه آن شیء نسبت به /First است. پس از هدر، بدنه‌های خود اشیاء به صورت به هم پیوسته می‌آیند.

گسترش یکی پس از باد شدن بایت‌ها مکانیکی است. شما دیکشنری را می‌خوانید تا /N و /First را بگیرید، جریان را با یک روتین Flate باد می‌کنید، جفت‌های پیشرو /N را می‌پیمایید تا یاد بگیرید کدام شماره شیء در کدام افست زندگی می‌کند و سپس هر بدنه را بیرون می‌کشید گویی یک شیء غیرمستقیم معمولی است. تنها وابستگی واقعی روتین دکپرس است و شما از قبل یکی را دارید: Delphi واحد System.ZLib را ارسال می‌کند و Free Pascal واحد zstream را ارسال می‌نماید، که هر دو zlib را بسته‌بندی کرده و جریان خام Flate را بدون هیچ کد شخص ثالثی باد می‌کنند. روتینی که هر شیء استخراج‌شده را به جدول شیء اعتبارسنج اضافه می‌کند، باعث می‌شود بقیه اعتبارسنج، یعنی بخشی که /Root را می‌پیماید و درخت صفحه را بررسی می‌کند، دقیقاً مانند رفتار روی یک فایل کلاسیک عمل کند.

آنچه نیازی به پیاده‌سازی آن ندارید

تخمین بیش از حد کار آسان است. خواندن کلیدهای تریلر از یک فایل فشرده نیازی به رمزگشایی ورودی‌های باینری جریان ارجاع متقاطع ندارد. جریان ارجاع متقاطع بخش ۷.۵.۸ از سه نوع ورودی استفاده می‌کند و ورودی نوع ۲، همان که می‌گوید this object lives inside object stream N at index i، چیزی است که برای ساخت یک نقشه افست کامل رمزگشایی می‌کنید. شما برای حل اشیای دلخواه بر اساس شماره به آن نقشه نیاز دارید. اما برای خواندن /Root ،/Size و /ID که در دیکشنری متنی ساده قرار دارند نیازی به آن ندارید و برای گسترش جریان‌های شیء نیز به آن نیازی ندارید، زیرا هر /ObjStm محتویات خود را از طریق /N و /First اعلام می‌کند.

شما همچنین مجبور نیستید توابع پیش‌بینی‌کننده PNG و TIFF را که یک جریان ارجاع متقاطع ممکن است از طریق /DecodeParms خود فقط برای دریافت کلیدهای تریلر اعمال کند، مدیریت کنید. پیش‌بینی‌کننده‌ها ردیف‌های ارجاع متقاطع باینری را فیلتر می‌کنند تا بهتر فشرده شوند؛ آن‌ها هیچ کاری با دیکشنری که قبل از جریان می‌آید ندارند. ارتقای حداقلی که یک اعتبارسنج کلاسیک را با PDF مدرن سازگار می‌کند کوچک است: وقتی startxref به جای کلمه کلیدی xref روی یک جریان فرود می‌آید، دیکشنری جریان را برای کلیدهای تریلر پارس کنید و هر شیء /ObjStm را که با آن مواجه می‌شوید گسترش دهید تا محتویات آن‌ها وارد جدول اشیاء شود. رمزگشایی ورودی‌های نوع ۲ و پیش‌بینی‌کننده‌ها کار جداگانه و بزرگ‌تری است که می‌توانید تا زمانی که واقعاً به حل تصادفی اشیاء نیاز پیدا کنید، به تعویق بیندازید.

چاه بررسی انطباق باید ابتدا جریان‌ها را گسترش دهد

این موضوع به محض اجرای بررسی پروفایل از حالت آکادمیک خارج می‌شود. یک اعتبارسنج PDF/A یا PDF/X اشیای خاصی را بررسی می‌کند: کاتالوگ سند برای یک آرایه /OutputIntents، جریان /Metadata برای یک پکت XMP با شناسه مناسب، هر توصیف‌کننده فونت برای یک فایل فونت تعبیه‌شده، تریلر برای یک /ID. در یک فایل فشرده، بیشتر آن اشیاء در داخل جریان‌های شیء قرار دارند. اعتبارسنجی که جریان‌های شیء را گسترش نداده است نمی‌تواند کلیدهای کاتالوگ را ببیند، نمی‌تواند متادیتا را پیدا کند و نمی‌تواند فونت‌ها را فهرست نماید. این برنامه یک سند کاملاً منطبق را به عنوان فاقد قصد خروجی، فاقد XMP و فاقد نیمی از ساختار گزارش می‌دهد، زیرا شواهدی که نیاز دارد هنوز در یک بلاک Flate نشسته است که هرگز آن را باد نکرده است.

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

سپردن کار پارس کردن به PDFium

کامپوننت PDFium جریان‌های ارجاع متقاطع و جریان‌های شیء را به عنوان بخشی از بارگذاری یک سند پارس می‌کند که راه عملی برای جلوگیری از ساخت دستی مرحله باد کردن و گسترش است. وقتی فایلی را با کامپوننت TPdf بارگذاری می‌کنید، اشیای بسته‌بندی شده در ظروف /ObjStm از قبل حل شده‌اند و نقاط ورود اعتبارسنجی سند کاملاً گسترش‌یافته را می‌بینند. ValidatePdfA یک رکورد TPdfAValidationResult را برمی‌گرداند که فیلد Conformance آن یک مقدار TPdfAConformance مانند pac1b یا pacNone است، فیلد Issues آن مجموعه‌ای از مشکلات خاص یافت شده است و متد IsCompliant آن تنها زمانی درست است که یک سطح انطباق شناسایی شده و مجموعه مشکلات خالی باشد. از آنجا که اشیاء در طول بارگذاری گسترش یافته‌اند، یک آرایه /OutputIntents یا یک فونت تعبیه‌شده که در داخل یک جریان شیء زندگی می‌کرده پیدا می‌شود، نه اینکه مفقود گزارش شود.

uses
  PDFium, FPdfPdfa;

function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    Result := Pdf.ValidatePdfA;
  finally
    Pdf.Free;
  end;
end;

همین امر در مورد ValidatePdfX نیز صدق می‌کند که یک TPdfXValidationResult با همان شکل برمی‌گرداند. نکتهِ مسیریابی از طریق PDFium این است که فشرده‌سازی ساختاری توصیف‌شده در بالا یک بار به طور صحیح در داخل بارگذار اتفاق می‌افتد، بنابراین کد اعتبارسنجی شما هرگز تفاوت بین یک فایل کلاسیک و یک فایل کاملاً فشرده را نمی‌بیند. هر دو به عنوان یک مجموعه حل شده از اشیاء به اعتبارسنج می‌رسند.

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Press_Ready.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePdfX;
    if R.IsCompliant then
      Writeln('PDF/X conformance: ', Ord(R.Conformance))
    else
      Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
  finally
    Pdf.Free;
  end;
end;

اگر بایت‌ها از قبل در حافظه قرار دارند تا روی دیسک، همان توالی بارگذاری و سپس اعتبارسنجی از طریق اضافه بار (overload) متد LoadDocument(const Data: TBytes) کار می‌کند که محتوای خام فایل را می‌گیرد و جریان‌های ارجاع متقاطع و شیء آن را به همان روش مسیر فایل پارس می‌کند. نتیجه برای یک اعتبارسنج دست‌نویس، قانون ساختاری است، نه API: کلیدهای تریلر را از دیکشنری جریان به صورت متنی ساده بخوانید، هر /ObjStm را با یک روتین Flate قبل از پیمایش سند گسترش دهید و رمزگشایی ورودی‌های ارجاع متقاطع باینری را به عنوان کار اختیاری و بزرگ‌تری که هست در نظر بگیرید.

هنگامی که ساختار گسترش یافت، یک اعتبارسنج می‌تواند بقیه گردش کار را روی آن هدایت کند. برای یک ابزار preflight خط فرمان که انطباق را در یک پوشه از ورودی‌ها گزارش می‌دهد، به راهنمای ما برای ساخت CLI گزارش preflight بچ مراجعه کنید. هنگامی که اعتبارسنجی دروازه‌ای قبل از تقسیم یک سند بزرگ است، تکنیک‌های موجود در راهنمای ما برای تقسیم اسناد PDF به چندین فایل به طور طبیعی با الگوی بارگذاری و بررسی نشان داده شده در اینجا جفت می‌شوند. هر دو بر روی سطح بارگذاری و اعتبارسنجی کامپوننت PDFium برای Delphi و C++Builder ساخته شده‌اند.