مقاله فنی

اعتبارسنجی PDFهای فشرده: استریم‌های شیء و xref

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

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

PDF 1.5 چه چیزی را در دُم فایل تغییر داد

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

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

نموداری که دُم کلاسیک فایل PDF با جدول xref متن آشکار و کلمهٔ کلیدی trailer را با استریم ارجاع متقاطع PDF 1.5 مقایسه می‌کند که دیکشنری‌اش ‎/Root، /Size و /ID را برای یک اعتبارسنج دلفی به شکل متن آشکار نگه می‌دارد
دُم کلاسیک فایل یک جدول xref متن آشکار و کلمهٔ کلیدی trailer حمل می‌کند، در حالی که دُم PDF 1.5 هر دو را با یک شیء استریم ارجاع متقاطع جایگزین می‌کند که دیکشنری‌اش هنوز ‎/Root، /Size و /ID را آشکار عرضه می‌کند

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

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

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

استریم‌های شیء: یک سرآیند، سپس یک تودهٔ Flate

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

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

نمودار یک استریم شیء ObjStm در PDF فشرده: مدخل‌های دیکشنری ‎/N و /First، گام بادکردن Flate از راه System.ZLib در دلفی، و بار باد‌کرده شامل جفت‌های سرآیند به‌علاوهٔ بدنه‌های پشت‌سرهم اشیا
بادکردن یک ObjStm با System.ZLib سرآیندی از N جفت شمارهٔ شیء و افست را آشکار می‌کند که بدنه‌های پشت‌سرهم در پی آن می‌آیند و مستقیم به جدول اشیای اعتبارسنج می‌ریزند

آنچه لازم نیست پیاده کنید

دست‌بالا گرفتن این کار آسان است. خواندن کلیدهای تریلر از فایل فشرده به رمزگشایی مدخل‌های باینری استریم ارجاع متقاطع نیاز ندارد. استریم ارجاع متقاطع §7.5.8 سه نوع مدخل به کار می‌برد، و مدخل نوع 2، همان که می‌گوید این شیء درون استریم شیء N در ایندکس i زندگی می‌کند، همان چیزی است که برای ساختن نقشهٔ کامل افست‌ها رمزگشایی‌اش می‌کردید. آن نقشه را برای حل دلخواه اشیا با شماره لازم دارید. برای خواندن /Root، /Size و /ID که در دیکشنری متن آشکارند لازمش ندارید، و برای گشودن استریم‌های شیء هم لازمش ندارید، چون هر /ObjStm محتوای خود را از راه /N و /First اعلام می‌کند

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

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

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

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

نموداری که یک اعتبارسنج PDF/A و PDF/X در دلفی را نشان می‌دهد که پیش از اجرای بررسی‌های پروفایل هر ObjStm را می‌گشاید تا مقصدهای خروجی کاتالوگ، فرادادهٔ XMP و فونت‌های توکار یافته شوند نه اینکه به‌دروغ گم‌شده گزارش گردند
گشودن باید پیش از بررسی‌های پروفایل اجرا شود، چون هر بررسی فرض می‌گیرد می‌تواند شیئی را با شماره برسد؛ رد کردن آن روی فایل‌های فشردهٔ سالم تخلف کاذب می‌سازد

بگذارید 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;            // استریم‌های xref و شیء را هنگام بارگذاری تجزیه می‌کند
    Result := Pdf.ValidatePdfA;    // جدول گشوده‌شدهٔ اشیا را می‌بیند
  finally
    Pdf.Free;
  end;
end;

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

function PdfXConformanceName(C: TPdfXConformance): string;
begin
  case C of
    pxc1a: Result := 'PDF/X-1a';
    pxc3 : Result := 'PDF/X-3';
    pxc4 : Result := 'PDF/X-4';
  else
    Result := 'none';
  end;
end;

var
  Pdf: TPdf;
  R  : TPdfXValidationResult;
  Issue: TPdfXValidationIssue;
  IssueCount: Integer;
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: ', PdfXConformanceName(R.Conformance))
    else
    begin
      IssueCount := 0;
      for Issue in R.Issues do   // Issues یک مجموعه است: اعضایش را بشمارید
        Inc(IssueCount);
      Writeln('Not conformant; issue count = ', IssueCount);
    end;
  finally
    Pdf.Free;
  end;
end;

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

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