اگر سندی را از Microsoft Word یا Excel با Save as PDF صادر کنید، فایل روی دیسک در بیشتر مواقع یک hybrid-reference file است. این فایل اطلاعات cross-reference خود را دو بار حمل می کند: یک بار به شکل جدول کلاسیک با عرض ثابت که تا PDF 1.4 پایان هر فایل بود، و یک بار به شکل cross-reference stream فشرده که بخش عمده سند واقعاً به آن وابسته است. یک trailer key به نام /XRefStm این دو نما را به هم می دوزد و اینکه یک ابزار کل سند را ببیند یا نه، به این برمی گردد که آیا آن کلید را دنبال می کند یا نه
این مقاله به فایل های hybrid از سمت مصرف کننده نگاه می کند: بایت های انتهای فایل چه شکلی هستند، این دو نما زیر ویرایش چطور از هم فاصله می گیرند، و یک pipeline در Delphi چطور می تواند ورودی های hybrid را تشخیص دهد و route کند. اینکه loader چطور این دو نما را با هم ادغام می کند و چرا ترتیب آن قابل مذاکره نیست، موضوع مقاله HotPDF ما درباره بارگذاری فایل های hybrid-reference است. این مقاله روی تشخیص دادن layout در همان ابتدا تمرکز دارد
چرا خروجی های Office فهرست را دو بار می نویسند
PDF 1.5 دو قابلیت را معرفی کرد که شکل فایل را عوض کرد: cross-reference stream که فهرست object ها را به جای جدول متن ساده به صورت داده باینری فشرده نگه می دارد، و object stream که تعداد زیادی object کوچک را در یک ظرف Flate-compressed جمع می کند. نویسنده ای که از این قابلیت ها استفاده کند فایل کوچک تری می سازد، اما یک reader مربوط به PDF 1.4 نمی تواند نتیجه را باز کند، چون ساختارهایی که به آن ها تکیه دارد، یعنی کلیدواژه xref و دیکشنری trailer، دیگر وجود ندارند
ISO 32000-1 §7.5.8.4 راه مصالحه را تعریف می کند. یک hybrid-reference file هر دو را می نویسد: یک جدول cross-reference کلاسیک برای object هایی که reader قدیمی باید به آن ها برسد، از جمله catalog و page tree، و یک cross-reference stream که بقیه چیزها را index می کند. object هایی که داخل object stream تا شده اند در جدول کلاسیک آزاد علامت می خورند تا reader نسخه 1.4 بدون اعتراض از آن ها رد شود؛ مکان واقعی آن ها فقط در stream وجود دارد. trailer کلاسیک بعداً یک کلید /XRefStm حمل می کند که byte offset همان stream را نگه می دارد. viewer قدیمی آن کلید را هرگز نمی خواند و فایل را بر اساس نمای جدولی render می کند. viewer مدرن آن را دنبال می کند و کل سند را می بیند. Word و Excel سال هاست دقیقاً همین layout را تولید می کنند و به همین خاطر فایل های hybrid یک گوشه عجیب نیستند، بلکه سهم بزرگی از ورودی pipeline های تجاری را تشکیل می دهند
دم انتهایی یک فایل hybrid چه شکلی است
فهم این layout از روی بایت ها آسان تر است. اینجا دم یک فایل hybrid کوچک را می بینید که offset ها کوتاه شده اند. در خروجی واقعی Office مقدار /XRefStm معمولاً offset بزرگی نزدیک انتهای فایل است. ترتیب خواندن همان حرکت از انتها به ابتدا است که در نمای کلی ساختار فایل PDF توضیح داده ایم: %%EOF را پیدا کن، startxref را بخوان، و به جدول بپر
uses
System.SysUtils, pdfium_lib;
procedure LoadAndCheckHybridPDF(const FileName: string);
var
Doc: FPDF_DOCUMENT;
LastError: ULONG;
begin
FPDF_InitLibrary();
try
Doc := FPDF_LoadDocument(PAnsiChar(AnsiString(FileName)), nil);
if Doc = nil then
begin
LastError := FPDF_GetLastError();
case LastError of
FPDF_ERR_FILE: Writeln('File not found or could not be opened.');
FPDF_ERR_FORMAT: Writeln('File not in PDF format or corrupted.');
FPDF_ERR_PASSWORD: Writeln('Password required or incorrect password.');
FPDF_ERR_SECURITY: Writeln('Unsupported security scheme.');
FPDF_ERR_XFDF: Writeln('Invalid XRef or Hybrid Reference structure.');
else
Writeln('Unknown error occurred loading PDF.');
end;
Exit;
end;
Writeln('PDF loaded successfully despite hybrid or structural anomalies.');
// Proceed with processing...
FPDF_CloseDocument(Doc);
finally
FPDF_DestroyLibrary();
end;
end;
بازسازی و ترمیم PDF
اگر workflow شما باید PDF را به سامانه های downstream سخت گیرتر برساند، مثل یک RIP سخت افزاری قدیمی، لازم است ساختار hybrid را هموار کنید. مطمئن ترین راه برای repair کردن یک PDF hybrid خراب در Delphi این است که آن را در یک engine tolerant بارگذاری کنید و بعد یک full save یا همان Save As انجام دهید. این کار parser را مجبور می کند از روی object tree موجود در حافظه یک XRef table تمیز و یکتا دوباره بسازد
// Conceptual example using a high-level wrapper
procedure RebuildPdfStructure(const InputFile, OutputFile: string);
var
Doc: TlxPDFDocument;
begin
Doc := TlxPDFDocument.Create;
try
// Tolerant engine ignores the broken hybrid XRef and walks the objects
Doc.LoadFromFile(InputFile);
// Saving rewrites the file with a clean PDF 1.7 XRef stream
Doc.SaveToFile(OutputFile);
Writeln('PDF structure successfully rebuilt.');
finally
Doc.Free;
end;
end;
دو نکته در همین dump کل سازوکار را حمل می کند. اول اینکه startxref عمداً به بخش کلاسیک اشاره می کند، چون reader قدیمی باید همانجا فرود بیاید. cross-reference stream فقط از طریق کلید /XRefStm داخل trailer dictionary قابل رسیدن است، بنابراین parserی که هرگز دنبال آن کلید نگردد اصلاً نمی فهمد چنین streamی وجود دارد. دوم اینکه object های 2 و 3 از آن دروغ های بی خطر هستند. جدول کلاسیک آن ها را آزاد اعلام می کند، اما در واقع object های واقعی داخل یک ظرف فشرده هستند. همین علامت آزاد است که reader نسخه 1.4 را از برخورد با entry هایی که نمی تواند استفاده کند دور نگه می دارد. مصرف کننده ای که فقط به نمای کلاسیک اعتماد کند نتیجه می گیرد که بخش بزرگی از این سند اصلاً وجود ندارد
این دو نما چطور از هم دور می شوند
یک فایل hybrid که تازه از Word بیرون آمده از نظر داخلی سازگار است: هر دو نما همان سند را توصیف می کنند، هر کدام در محدوده اعلام شده خود. دردسر وقتی شروع می شود که فایل با ابزاری ویرایش شود که فقط یکی از این دو نما را می فهمد. یک stamping utility را در نظر بگیرید که یک incremental update به سبک کلاسیک اضافه می کند: object های جدید، یک بخش xref جدید، یک زنجیره /Prev به بخش قبلی، و یک trailer تازه. اگر آن trailer کلید /XRefStm را حذف کند، نمای stream یتیم می شود؛ اگر مقدار قبلی را به جلو کپی کند، نمای stream هنوز سند را همان طور توصیف می کند که قبل از ویرایش بود. در هر دو حالت، این دو index دیگر درباره محتوای فایل با هم موافق نیستند
فایل حاصل یک امضای خرابی مشخص دارد: object هایی که در یک نما دیده می شوند، در نمای دیگر یا نیستند یا کهنه اند. readerی که از نمای stream resolve می کند نسخه پیش از ویرایش object به روز شده را پیدا می کند یا برای object تازه اضافه شده اصلاً entry نمی بیند. readerی که روی نمای table حرکت می کند ویرایش را می بیند اما object های فشرده ای را که فقط stream جای آن ها را می داند گم می کند. در عمل این وضعیت به شکل field های فرم ظاهر می شود که در یک viewer باقی می مانند و در دیگری ناپدید می شوند، annotation هایی که انگار stamping آن ها را حذف کرده، یا lookup هایی که روی object کاملاً اشتباه فرود می آیند
چیزی که debug این فایل ها را گران می کند این است که Adobe Acrobat معمولاً آن ها را بدون شکایت باز می کند. وقتی index با بایت ها ناسازگار باشد، Acrobat آرام cross-reference data را با جست وجوی object header ها بازسازی می کند، پس کسی که فایل خراب را تولید کرده چیزی اشتباه نمی بیند. خرابی بعداً ظاهر می شود، وقتی فایل به یک strict consumer، یک preflight validator، یک signing service یا یک archival ingest job می رسد که به ساختار اعلام شده اعتماد می کند و object گمشده یا cross-reference mismatch گزارش می دهد. جمله "In Acrobat خوب باز می شود" تقریباً شروع هر ticket مربوط به desynchronization فایل های hybrid است
تشخیص یک فایل hybrid در Delphi ساده
طبقه بندی ورودی ها به PDF library نیاز ندارد. کلید /XRefStm فقط می تواند داخل یک classic trailer dictionary ظاهر شود و trailer فعال هم در یکی دو کیلوبایت انتهایی فایل قرار می گیرد، چون specification می خواهد %%EOF نزدیک انتهای فیزیکی فایل باشد. برای triage همین کافی است که یک tail window محدود را بخوانید و در آن جست وجو کنید:
uses
System.SysUtils, System.Classes, System.StrUtils, System.Math;
function IsHybridReferencePdf(const FileName: string): Boolean;
const
TailWindow = 2048;
var
Stream: TFileStream;
Buf: TBytes;
Tail: string;
Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
Result := False;
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if Stream.Size < 48 then
Exit;
Len := Min(TailWindow, Integer(Stream.Size));
SetLength(Buf, Len);
Stream.Position := Stream.Size - Len;
Stream.ReadBuffer(Buf[0], Len);
finally
Stream.Free;
end;
// Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
Tail := TEncoding.ANSI.GetString(Buf);
// Find the LAST 'trailer' keyword: with incremental updates,
// the newest trailer is the one that governs the file
TrailerPos := 0;
NextPos := Pos('trailer', Tail);
while NextPos > 0 do
begin
TrailerPos := NextPos;
NextPos := PosEx('trailer', Tail, NextPos + 1);
end;
if TrailerPos = 0 then
Exit; // no classic trailer: a pure xref-stream file, not hybrid
// A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
سه نتیجه دقیقاً با سه layout منطبق می شوند. فایل classic-only trailer دارد اما /XRefStm ندارد، پس False. فایلی که کاملاً به cross-reference stream متعهد شده اصلاً کلیدواژه trailer ندارد و trailer key هایش در stream dictionary زندگی می کنند، پس آن هم False است و این درست است چون چنین فایلی فشرده است نه hybrid. فقط layout دو فهرسته مقدار True برمی گرداند
برای استفاده تولیدی، دو سخت سازی ارزش چند خط اضافه را دارند. عدد بعد از /XRefStm را parse کنید، به همان offset seek کنید و تأیید کنید که واقعاً یک stream object با /Type /XRef آنجا نشسته است. فایل truncate شده می تواند کلید را داشته باشد ولی خود stream از بین رفته باشد و این باید در bucket دیگری نسبت به یک hybrid سالم قرار بگیرد. اندازه window را هم پارامتر در نظر بگیرید: 2 KB خروجی معمول Office را پوشش می دهد، اما trailer dictionary غیرعادی بزرگ می تواند کلید را از محدوده بیرون ببرد و بزرگ کردن window بهتر از این است که فایل را اشتباهی classic اعلام کنید
route کردن فایل های hybrid در یک pipeline Delphi
تشخیص به شما یک تصمیم route می دهد. برای فایل هایی که فقط خوانده، render یا validate می شوند از loaderی استفاده کنید که هر دو نما را resolve کند و بعد به جای بایت ها رفتار را verify کنید. PDFium Component هنگام load زنجیره /XRefStm را parse می کند، بنابراین object tableی که کد شما می بیند همان جدول ادغام شده است و بررسی هایی که در مقاله اعتبارسنجی object stream و cross-reference stream آمده بدون تغییر برقرار می ماند. اگر hybrid از همگام خارج شده آن قدر خراب باشد که از load شدن امتناع کند، engine آن را از طریق مجموعه خطاهای خود گزارش می کند، یعنی FPDF_ERR_SUCCESS، FPDF_ERR_UNKNOWN، FPDF_ERR_FILE، FPDF_ERR_FORMAT، FPDF_ERR_PASSWORD، FPDF_ERR_SECURITY و FPDF_ERR_PAGE که در آن FPDF_ERR_FORMAT همان موردی است که خرابی ساختاری تولید می کند. با این حال زیاد به این سیگنال تکیه نکنید: PDFium ذاتاً tolerant است و بیشتر فایل های ناسازگار را بی سر وصدا بازسازی می کند، پس load موفق فقط نشان می دهد فایل recoverable بوده، نه اینکه دو نمای آن با هم موافق بوده اند. بررسی سازگاری معنادار این است که چیزی را که یک full object walk پیدا می کند با مقداری که trailer در /Size اعلام کرده مقایسه کنید
برای فایل هایی که pipeline شما آن ها را modify می کند، امن ترین سیاست این است که اصلاً نگذارید hybrid بمانند. یک load و بعد full save از طریق HotPDF سند را با یک cross-reference یکتا و self-consistent بازنویسی می کند: بدون /XRefStm، بدون نمای دوم که از sync خارج شود، و با هر object که دقیقاً توسط یک index entry مالکیت می شود. همین normalization چیزی است که پیش از archival ingest، پیش از یک downstream RIP یا signing service سخت گیر، و بعد از هر ویرایشی روی ورودی hybrid می خواهید. این روش جواب می دهد چون loader در مسیر ورود دو نما را درست ادغام کرده است، همان سازوکاری که مقاله HotPDF درباره hybrid-reference با جزئیات آن را توضیح می دهد
تنها دسته ای از فایل ها که باید دست نخورده بمانند اسناد digitally signed هستند. full rewrite هر بایت را جابه جا می کند و در نتیجه هر signatureی را که روی بازه های اصلی محاسبه شده باطل می کند. هر تغییری روی یک hybrid امضاشده باید به شکل incremental update درست انجام شود که هر دو نما را حفظ کند، و فایلی که فقط نیاز به خواندن دارد باید بدون دستکاری عبور داده شود. normalization برای فایل هایی است که مالک آن ها هستید؛ فایل های signed فقط باید append شوند
PDF های hybrid-reference خراب نیستند؛ آن ها پل سازگاری خود فرمت هستند و Office applications تا وقتی reader های PDF 1.4 در پایگاه نصب زنده باشند به تولیدشان ادامه می دهند. pipelineی که می تواند کلید /XRefStm را ببیند، سند ادغام شده را با PDFium Component validate کند و خروجی تمیز تک فهرسته را با HotPDF Component دوباره بسازد، با این فایل ها همان طور رفتار می کند که واقعاً هستند: ورودی های عادی با یک signpost اضافه در trailer