دو PDF را با دست ادغام کنید و یک شیء صفحه را تنها به سند مقصد منتقل کنید، کپی مستقیم به یک access violation میخورد. PDFlibPas این را در CopyForeignObject حل کرده است: یک شیء غیرمستقیم بهعلاوه کل بستار ارجاعهایش را بهصورت عمیق کپی میکند و ارجاعهای برگشتی چرخهای مثل /Parent را بهجای بازگشت بیشتر، به null حل میکند
چرا کپی کردن فقط یک صفحه بین اسناد کرش میکند؟
زیرا درخت صفحه PDF فقط وقتی درخت است که آن را از بالا به پایین بخوانید. اگر مثل یک کپیکن بازگشتی روی آن حرکت کنید و هر مقدار داخل هر دیکشنری را دنبال کنید، دیکشنری صفحه /Parent را به شما میدهد که به همان گره /Pagesی اشاره میکند که از آن آمدهاید، و آن گره /Kids را میدهد که به خود صفحه برمیگردد. ISO 32000-1 §7.7.3 وجود /Parent را روی هر گره درخت صفحه غیر از ریشه الزامی میکند، پس این فایل بدشکلی نیست که بشود ردش کرد — این شکل عادی هر سندی است که تا به حال تحویل گرفتهاید
نیمه دوم مشکل شمارهگذاری است. اشیای غیرمستقیم با شماره شیئی شناسایی میشوند که مختص یک فایل است (ISO 32000-1 §7.3.10)، پس شیئی که از سند A به سند B کشیده میشود باید شمارهاش عوض شود و هر ارجاع به آن داخل بستار کپیشده هم به همان شکل دوباره شمارهگذاری شود، وگرنه دو ارجاعی که قبلاً به یک فونت مشترک اشاره میکردند به دو چیز بیربط اشاره خواهند کرد. این شمارهگذاری مجدد همان کاری است که یک ادغام سریع در سطح بایت انجام میدهد و کنار هم گذاشتن این دو ارزش خواندن دارد: جابهجایی ارجاع در سطح بایت برای ادغام سریع PDF آن را با ترجمه کل فایل حل میکند، در حالی که کپی در سطح شیء باید آن را یک یال در هر بار حل کند
CopyForeignObject در PDFlibPas واقعاً چه چیزی را کپی میکند
TPDFlib.CopyForeignObject(SourceDocumentID, ObjectNumber) یک شیء غیرمستقیم و هر چیزی را که از آن قابل دستیابی است — دیکشنریهای تو در تو، آرایهها، رشتهها، نامها، اعداد و استریمها همراه با دیکشنریهایشان — در سند انتخابشده فعلی کلون میکند و یک هندل غیرصفر به ارجاع غیرمستقیم جدید برمیگرداند. شمارههای شیء مبدأ از طریق یک نگاشت زنده که به مدت فراخوان نگه داشته میشود دوباره نگاشت میشوند، پس شیئی که دو بار در بستار دیده شود یک بار کلون و دو بار به اشتراک گذاشته میشود. وقتی شناسه سند مبدأ ناشناخته باشد، وقتی مبدأ همان سند انتخابشده باشد یا وقتی ObjectNumber کمتر از 1 باشد، بدون صدور استثنا صفر برمیگرداند
var
Lib: TPDFlib;
SourceDoc, TargetDoc, Handle: Integer;
begin
Lib := TPDFlib.Create;
try
TargetDoc := Lib.NewDocument;
if Lib.LoadFromFile('source.pdf', '') <> 1 then
Exit; // LoadFromFile در صورت موفقیت 1 برمیگرداند
SourceDoc := Lib.SelectedDocument; // عمل لود، همان چیزی که لود کرد را انتخاب میکند
Lib.SelectDocument(TargetDoc); // کپی در سند انتخابشده نوشته میشود
Handle := Lib.CopyForeignObject(SourceDoc, 12);
if Handle = 0 then
raise Exception.Create('cross-document copy rejected');
finally
Lib.Free;
end;
end;
دو جزئیات در اولین اجرا آدمها را گاز میگیرد. LoadFromFile جوابش 1 یا 0 است نه یک شناسه سند، پس هندلی که لازم دارید بلافاصله بعد از لود از SelectedDocument میآید؛ و کپی همیشه در هر چیزی که SelectDocument آخرین بار فعلی کرده مینویسد، هرگز در سندی که از آن لود کردهاید نه. در داخل، بازگشت یک سقف عمق سخت 64 هم حمل میکند که پشتیبانِ تو در توسازی مرضی است، نه سازوکار مدیریت چرخه — مدیریت چرخه جدا و آگاهانه است
چرا رزرو نگاشت Nil چرخه را نمیشکند؟
زیرا Nil در جدول نگاشت همزمان دو معنای متفاوت دارد و کد نمیتواند آنها را از هم تشخیص دهد. دفاع واضح در برابر چرخه این است که رکورد نگاشت را قبل از بازگشت به شیء اضافه کنید تا هر چیزی که به عقب برمیگردد رکورد را پیدا کند و متوقف شود. اما رکورد هنوز نمیتواند مقصد واقعی را نگه دارد — مقصد تا وقتی بستار پایینتر نوشته نشده وجود ندارد — پس Nil نگه میدارد، و جستوجویی که قرار بود یال برگشتی را بگیرد Nil میخواند و نتیجه میگیرد که شیء هرگز نگاشت نشده است
// خراب: مقصد رزروشده Nil از حالت «هنوز نگاشت نشده» قابل تفکیک نیست
NewRef := FindMapped(SrcRef.ObjNum);
if not Assigned(NewRef) then
begin
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // رزرو شده، هنوز Nil
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // فقط در مسیر بازگشت پر میشود
end;
این را در حلقه صفحه دنبال کنید. کلون صفحه به /Parent میرسد، به گره /Pages بازگشت میکند، که به /Kids میرسد، که به خود صفحه بازگشت میکند — رکورد رزروشده آن هنوز Nil است، پس بار دوم کلون میشود، بار سوم هم، و هر سطح یک فریم تازه و یک شیء نیمهساخته تازه روی پشته میگذارد. آنچه مشاهده میکنید حتی سرریز پشته تمیزی هم نیست: فریمهای بیرونی روی ارجاعهایی نشستهاند که مقصدشان هرگز مقدار نگرفته، پس اولین نوشتن از طریق یکی از آن اسلاتها یک access violation در جایی است که هیچ شباهتی به کپی صفحهای که باعثش شد ندارد
راهحل: یک حالت صریح in-progress
تعمیر این است که بار معنایی اضافه را از دوش Nil بردارید و پرسش را مستقیم بپرسید. رکورد نگاشتی که مقصدش هنوز مقدار نگرفته یعنی این شیء در حال کلون شدن است، و گزاره InProgress دقیقاً همین را پیش از اجرای جستوجوی عادی میآزماید. وقتی true باشد، یال یک چرخه بازگشت به جد کپی فعلی است و PDFlibPas به جای دنبال کردنش یک شیء null برای آن صادر میکند
// رکورد نگاشت با مقصد Nil نشانه یک کلون در جریان است
function InProgress(Num: Integer): Boolean;
var
I: Integer;
begin
Result := False;
for I := 0 to High(Map) do
if (Map[I].SourceObjNum = Num) and (not Assigned(Map[I].Target)) then
Exit(True);
end;
// ... داخل CloneObject، برای یک ارجاع غیرمستقیم:
if InProgress(SrcRef.ObjNum) then
Exit(FStructure.NewNull); // یال برگشتی چرخهای، بازگشت نکن
NewRef := FindMapped(SrcRef.ObjNum);
if not Assigned(NewRef) then
begin
SrcInd := SourceDoc.FindObj(SrcRef.ObjNum, SrcRef.GenNum);
if (not Assigned(SrcInd)) or (not Assigned(SrcInd.Obj)) then
Exit(FStructure.NewNull); // ارجاع مبدأ بیریشه
SetLength(Map, Length(Map) + 1);
Map[High(Map)].SourceObjNum := SrcRef.ObjNum;
Map[High(Map)].Target := nil; // رزرو کن، بعد بازگشت بزن
NewRef := NewObjRef(CloneObject(SrcInd.Obj, Depth + 1));
Map[High(Map)].Target := NewRef; // پر کردن نهایی
end;
Exit(NewRef);
این تعمیم فقط به یک واقعیت ساختاری درباره PDF امن است: چرخهها در گراف شیء روی پیوندهای برگشتی ظاهر میشوند، نه روی یالهای محتوا. /Parent در درخت صفحه و /Prev در زنجیره فهرست مطالب به بالا یا عقب، به چیزی که از قبل دیده شده اشاره میکنند؛ بستار یک فونت، یک XObject تصویر یا یک XObject فرم رو به پایین میرود و پایان مییابد. پس کپی یک توصیفگر فونت، یک فضای رنگ یا یک دیکشنری سایهزنی از جایگزینی null بیتأثیر میماند — هیچ چیز در این بستارها به InProgress نمیخورد. هزینه، که باید صریح گفت، این است که یال چرخهای از کپی جان سالم نمیبرد. دیکشنری صفحهای که اینطور کلون شود با /Parent بهشکل شیء null میرسد، که ISO 32000-1 §7.3.9 آن را معادل نبودِ کلید میداند، پس صفحه کپیشده یک شیء معتبر است که به هیچ درخت صفحهای تعلق ندارد تا آن را خودتان به گره /Pages مقصد پیوند بزنید و /Count را اصلاح کنید. یک آیتم فهرست کپیشده هم /Prevاش را به همین شکل از دست میدهد و زنجیره خواهری باید بازسازی شود. این همان معامله صادقانه است: CopyForeignObject یک بستار درست به شما میدهد و باز-والدی ساختاری را به فراخوان واگذار میکند، همان مرزی که جایگزینی صفحات با حفظ شماره اشیاء در آن کار میکند
چرا رکورد نگاشت باید قبل از NewObjRef رزرو شود
یک جایگزین بدیهی کل این رقص in-progress را دور میزد: اول یک شیء پوسته خالی تخصیص بده، شماره واقعیاش را در نگاشت ثبت کن، بعد وقتی فرزندان کلون شدند پوسته را پر کن. اینجا کار نمیکند، چون TPDFIndObj.Obj فقطخواندنی است و محتوایش بعد از ساخت قابل جایگزینی نیست — پوستهای برای پر کردن وجود ندارد. شماره و محتوا با هم توسط NewObjRef تعیین میشوند، یعنی رکورد نگاشت باید قبل از فراخوان بازگشتی ساخته و بعد از آن کامل شود، و فاصله میان این دو لحظه دقیقاً همان چیزی است که InProgress باید پوشش دهد. یک پیامد که بهتر است قبل از diff گرفتن از خروجی بدانید: چون NewObjRef بعد از نوشته شدن بستار فرزند اجرا میشود، شمارهگذاری در مقصد از پایین به بالا در میآید و شماره اشیاء ترتیب مبدأ را منعکس نخواهند کرد. فرمت فایل به هیچ کدام اهمیت نمیدهد، اما مقایسه بایتی با یک انتظار دستساز اهمیت میدهد. اگر یک اجرا اشیایی به جا بگذارد که تصمیم گرفتهاید به هیچ چیز پیوند نزنید، آنها ارجاعنشدهاند نه خراب، و جمعآوری mark-and-sweep اشیاء PDF دستنیافتنی ابزاری است که پیش از ذخیره آنها را پاک میکند
رگرسیونی که این را پوشش میدهد به جزئیاتی نیاز دارد که نویسندگان تستهای TPDFlib را غافلگیر میکند: سازنده از قبل یک سند پیشفرض نگه میدارد، پس DocumentCount از 1 شروع میشود و یک fixture دوسندیه باید >= 2 را اثبات کند نه = 2. در کنار کپی موفق، تست سه پسزدن را سنجاق میکند — شناسه مبدأ ناشناخته، سند انتخابشده بهعنوان مبدأ خودش، و شماره شیء صفر — که همه 0 برمیگردانند نه استثنا صادر میکنند، چون حلقه ادغام جای بدی است که بفهمید یک گارد استثنا میاندازد
جای این قابلیت در خط لوله ادغام کجاست
کپی در سطح شیء آن پیشفرضی است که وقتی ادغام کلفایل خیلی درشتدانه است به سراغش میروید: بیرون کشیدن یک برنامه فونت از یک قالب، آوردن یک XObject فرم تنها به یک سند مهر و موم، یا جابهجایی یک حاشیهنویسی همراه با استریمهای ظاهرش بین فایلها بدون کشیدن بقیه صفحه پشت سرش. PDFlibPas آن را به شکل یک فراخوان تنها روی اسناد لودشده در دسترس میگذارد و میتوانید در مرجع PDFlibPas Delphi PDF Library ببینید در کنار بقیه API شیء سطح پایین چطور مینشیند