یک PDF را که Microsoft Word یا Excel تولید کرده باز کنید، چند صفحه را ورق بزنید، و هیچ چیز غیرعادی به نظر نمی رسد. آن را در یک برنامه Delphi بارگذاری کنید، تعداد صفحه ها را بخوانید، و عدد درست است. سپس آن را با فعال کردن encryption دوباره ذخیره کنید و کار با یک EListError متوقف می شود، یا خروجی با هشدار damaged cross-reference باز می شود. فایل هرگز خراب نبوده است. این یک فایل hybrid-reference است، و دقیقاً همان ساختاری که به یک viewer پانزده ساله اجازه باز کردنش را می دهد، همان ساختاری است که یک loader را که خیلی زود خواندن را متوقف می کند شکست می دهد
این یکی از رایج ترین راه هایی است که یک pipeline PDF که از همه تست های داخلی گذشته است، با فایلی روبه رو می شود که نمی تواند آن را round-trip کند. همه ورودی ها داخل سازمان تولید شده بودند، بنابراین هیچ کدام hybrid نبودند. اولین فایل hybrid همان روزی می رسد که یک مشتری فاکتوری export شده از spreadsheet را برای شما forward می کند
Word و Excel واقعاً چه چیزی می نویسند
ISO 32000-1 در §7.5.8.4 چیدمان hybrid-reference را توضیح می دهد. برنامه ای که می خواهد از قابلیت های PDF 1.5 مثل object stream ها استفاده کند و در عین حال اجازه دهد یک readerِ PDF 1.4 هم فایل را باز کند، اطلاعات cross-reference را دوبار می نویسد. یک cross-reference table کلاسیک وجود دارد، یعنی همان سطرهای ASCII با عرض ثابت که تا نسخه 1.4 انتهای هر PDF را تشکیل می دادند، و یک cross-reference stream هم وجود دارد که بقیه چیزها را index می کند. trailerِ بخش کلاسیک یک entry به نام /XRefStm دارد که مقدارش offset بایتی آن stream است
این تقسیم کار عامدانه است. اشیایی که یک reader قدیمی باید به آن ها برسد، از جمله catalog و page tree، از طریق جدول کلاسیک قابل آدرس دهی هستند. اشیایی که در object stream های فشرده جمع شده اند در جدول کلاسیک به عنوان free با یک entry از نوع f علامت گذاری می شوند تا reader نسخه 1.4 بی درنگ از کنارشان بگذرد و اصلاً به ساختاری که نمی تواند parse کند برخورد نکند. مکان واقعی آن ها فقط در cross-reference stream وجود دارد. امضای چنین فایلی در tail آن است: یک بخش کلاسیک کوتاه، اغلب چیزی بیشتر از xref و یک subsection header به شکل 0 0 نیست، که trailer آن به /XRefStm اشاره می کند، جایی که داده واقعی بازیابی نشسته است
چرا درست بودن تعداد صفحه هیچ چیز را ثابت نمی کند
از آنجا که catalog و page tree عمداً از طریق جدول کلاسیک قابل دسترس شده اند، loader ای که فقط همان جدول را می خواند /Root را پیدا می کند، page tree را طی می کند، و تعداد درست صفحه ها را گزارش می دهد. هرچه یک reader قدیمی نیاز دارد وجود دارد، بنابراین فایل سالم به نظر می رسد. اشیایی که ناپدید شده اند همان هایی هستند که داخل object stream ها بسته بندی شده اند: dictionary های فیلد AcroForm، عناصر ساختاری tagged-PDF، و دنباله بلند dictionary های کوچکی که لازم نبود برای یک viewer قدیمی قابل دیدن باشند
شما این فاصله را تا وقتی چیزی آن اشیا را لمس نکند متوجه نمی شوید، و یک ذخیره سازی کامل همه آن ها را لمس می کند. پیمایش سند برای re-encrypt کردن یا بازنویسی کردن دقیقاً همان عملیاتی است که به نوبت هر شماره شیء را درخواست می کند، و به همین دلیل علامت مشکل هنگام save ظاهر می شود نه هنگام load، و از علت اصلی خود دور می افتد
تله این است که detector فقط xref را ببیند و متوقف شود
راه ارزان برای تشخیص نوع index شدن فایل این است که startxref را دنبال کنید و چند بایت اولی را که به آن اشاره می کند بررسی کنید. کلیدواژه xref یعنی جدول کلاسیک؛ یک stream object یعنی cross-reference stream. این تست برای هر فایلی که فقط یکی از این دو الگو را انتخاب کرده باشد درست است. اما برای فایل hybrid اشتباه است، چون startxref آن فقط برای راضی کردن reader های قدیمی به بخش کلاسیک اشاره می کند، در حالی که /XRefStm در trailer همان بخش جایی است که بیشتر سند واقعاً index شده است. detector ای که با دیدن نخستین xref برچسب "classic" می زند، هرگز /XRefStm را نمی خواند و هر شیئی که فقط در stream زندگی می کند نامرئی می شود
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;
وقتی detectorِ خروج زودهنگام سر جای خود باشد، load کاملاً خوب به نظر می رسد و این re-save است که غیبت اشیا را فاش می کند. اصلاح کار این نیست که در ابتدای فایل بایت های بیشتری بخوانید؛ باید trailer hybrid را تشخیص دهید و پیش از اینکه نتیجه بگیرید فایل تمام شده، /XRefStm را دنبال کنید
ترتیب merge قابل مذاکره نیست
وقتی هر دو index خوانده شدند، فقط در یک جهت می توان آن ها را ترکیب کرد. cross-reference stream باید اول merge شود و entry های کلاسیک اطراف آن پر شوند. دلیلش همان فریب کوچکی است که در دل این فرمت خوابیده است. یک فایل hybrid، اشیای فشرده خود را در جدول کلاسیک به عنوان free علامت می زند تا reader های قدیمی آن ها را نادیده بگیرند. loader ای که سیاست first-seen-wins را اجرا کند و اول جدول کلاسیک را بخواند، این شماره های شیء را free ثبت می کند، سپس entry های stream را که واقعاً مکان آن ها را پیدا می کنند دور می اندازد، چون slot ها از قبل پر شده اند. اگر ترتیب را برعکس کنید، entry های type 2 از stream، که هر کدام شامل یک شماره object stream و یک index هستند، slot هایی را می گیرند که متعلق به خودشان است و entry های کلاسیک اطرافشان می نشینند
همین انضباط از زنده شدن دوباره یک شیء حذف شده در revision قدیمی تر هم جلوگیری می کند. incremental update ها از طریق /Prev به عقب زنجیر می شوند، و یک entry آزاد از نوع 0 یک sentinel است که می گوید بخش جدیدتر یک شماره شیء را بازنشسته کرده است. نباید اجازه داد بخشی قدیمی تر که بعداً در زنجیره دیده می شود، آن sentinel را با یک مکان کهنه overwrite کند. اگر free marker را بر اساس first-seen authoritative بدانید، شیء حذف شده حذف شده می ماند؛ اگر بی احتیاط رفتار کنید، تاریخچه خود فایل محتوایی را که آخرین revision حذف کرده بود دوباره زنده می کند
این موضوع در HotPDF چه معنایی دارد
موتور HotPDF فایل های hybrid-reference را برای شما resolve می کند، و این کار را در هر مسیری انجام می دهد که ناچار است داده cross-reference را parse کند. یک سند را با LoadFromFile یا LoadFromStream بارگذاری کنید، تغییرات خود را اعمال کنید، و سپس SaveLoadedDocument را صدا بزنید؛ یا یک عملیات یک مرحله ای مثل EncryptFile را اجرا کنید که ورودی را می خواند و خروجی را می نویسد. در هر دو حالت، بازیابی /XRefStm را می خواند، بخش stream را جلوتر از entry های کلاسیک merge می کند، و اشیایی را که در stream زندگی می کنند پیش از آنکه نوشتن بخواهد آن ها را enumerate کند resolve می کند. مسیر encryption با AES-256 همان جایی بود که این مشکل نخستین بار خودش را نشان داد، چون encrypt کردن سند همه اشیا را بازنویسی می کند و بنابراین لازم دارد که همه اشیا از قبل locate شده باشند
// 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 و فهرست بلندی از pipeline های «Save as PDF» می آیند، به طور معمول hybrid هستند، بنابراین loader ای که فقط با خروجی generator خودتان تمرین داده شده باشد ممکن است هرگز در تست یکی از آن ها را نبیند. fixture های خود را فقط با فایل هایی که کد خودتان ساخته seed نکنید؛ سندهای export شده از Office واقعی را هم داخلشان بگذارید
بررسی فایلی که به آن مشکوک هستید
دو inspection پرسش را خیلی سریع حل می کنند. فایل را در یک نمای hex باز کنید و بایت های بعد از آخرین startxref را بخوانید؛ یک فایل hybrid یک بخش کلاسیک کوتاه نشان می دهد که trailer dictionary آن شامل /XRefStm است. یا تعداد شیئی را که یک parse کامل گزارش می کند با بالاترین شماره شیئی که /Size در trailer اعلام می کند مقایسه کنید. یک فاصله بزرگ یعنی اشیا در stream هایی پنهان شده اند که loader هنوز بازشان نکرده است، و همین کمبود بعداً به شکست هنگام save تبدیل می شود
دم یک خروجی معمولی Excel این بررسی اول را ملموس می کند. هرچه بعد از آخرین کلیدواژه xref می آید ASCII ساده است، بنابراین امضا را می توان مستقیم در نمای hex خواند، البته با offset های نمونه و annotation های اضافه شده
xref
0 0 % empty classic subsection: no rows at all
trailer
<< /Size 216 % one past the highest object number in use
/Root 1 0 R
/Info 15 0 R
/ID [<5C9A...> <5C9A...>]
/XRefStm 87325 % byte offset of the cross-reference stream
>>
startxref
88710 % points at the classic section above
%%EOF
subsectionِ 0 0 همان سرنخ است: یک جدول کلاسیک با صفر entry فقط برای حمل کردن trailer وجود دارد، و trailer هم عمدتاً برای گفتن /XRefStm 87325 وجود دارد. detector ای که در این نقطه با دیدن کلیدواژه xref متوقف شود در حقیقت index هیچ چیز را دیده است. اگر ترجیح می دهید این بررسی را به جای نگاه کردن دستی اسکریپت کنید، این marker همیشه در یکی دو کیلوبایت آخر فایل می نشیند، بنابراین یک backward read محدود کافی است
// Returns the /XRefStm offset from the file's tail, or -1 if the
// marker is absent (the file is not hybrid, or not a PDF at all)
function FindXRefStm(const FileName: string): Int64;
var
FS: TFileStream;
Tail: AnsiString;
Len, P: Integer;
begin
Result := -1;
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Len := 2048; // the trailer lives in the tail
if FS.Size < Len then
Len := Integer(FS.Size);
FS.Position := FS.Size - Len; // bounded backward read: 2 KB max
SetLength(Tail, Len);
FS.ReadBuffer(Tail[1], Len);
finally
FS.Free;
end;
P := Pos(AnsiString('/XRefStm'), Tail);
if P = 0 then
Exit; // no hybrid marker in the tail
Inc(P, Length('/XRefStm'));
while (P <= Len) and (Tail[P] in [' ', #9, #13, #10]) do
Inc(P); // skip whitespace after the key
Result := 0;
while (P <= Len) and (Tail[P] in ['0'..'9']) do
begin
Result := Result * 10 + Ord(Tail[P]) - Ord('0');
Inc(P);
end;
end;
// Usage: a non-negative result names the byte where the stream starts
if FindXRefStm('Invoice_XLS.pdf') >= 0 then
Writeln('hybrid-reference file: resave will need the /XRefStm section');
این probe را triage در نظر بگیرید، نه parser: به شما می گوید کدام فایل ها در یک batch پیش از اجرای کار re-save سزاوار توجه هستند، و نه بیشتر. اینکه یک loader پس از یافتن این offset باید دقیقاً چه بکند، یعنی دنبال کردن زنجیره section ها، merge کردن entry های stream پیش از entry های کلاسیک و احترام گذاشتن به free-entry sentinel ها، قدم به قدم در مقاله همراه ما درباره رسیدگی به PDF های hybrid-reference ساخته شده توسط Office توضیح داده شده است
سمت writer این داستان، یعنی اینکه object stream ها و cross-reference های فشرده اصلاً چگونه تولید می شوند، در مقاله ما درباره object stream ها و incremental update ها پوشش داده شده است. وقتی فایل hybrid مورد بحث همزمان بسیار بزرگ هم باشد، تکنیک های بارگذاری در راهنمای Direct File API برای workflow های PDF بزرگ به شما اجازه می دهند آن را بدون خواندن کامل در حافظه بررسی کنید. هر دوی این نوشته ها به صورت طبیعی با recovery ای که اینجا توصیف شد جفت می شوند، و آن recovery بخشی از HotPDF Component برای Delphi و C++Builder است، در کنار API های بارگذاری، ویرایش، encryption و signing که در جاهای دیگر این وبلاگ پوشش داده شده اند