هفت صفحه را از یک handbook دویستصفحهای بیندازید و هر بوکمارک جای اشتباهی فرود میآید. درستکن بازسازی outline از یک لیست تخت عنوانها نیست. PDFiumPas TPdfOutlineEditor را در اختیارتان میگذارد، که درخت outline واقعی را لود میکند، اجازه میدهد آیتمها را جابهجا و بازهدفگذاری کنید، و بعد ApplyPageMap را اجرا میکند تا هر مقصد صریح را از طریق طرح صفحه شما جابهجا کند
چرا حذف صفحات همه بوکمارکها را میشکند؟
چون یک آیتم outline شماره صفحه ذخیره نمیکند. یک ارجاع به شیء صفحه ذخیره میکند، و وقتی اشیاء صفحه عوض میشوند آن ارجاع یا به صفحهای اشاره میکند که جابهجا شده یا به هیچ چیز. ISO 32000-1 §12.3.2.2 یک مقصد صریح را آرایهای تعریف میکند که عنصر اولش ارجاع غیرمستقیم به یک دیکشنری صفحه است و بعدش یک نام fit مثل /Fit یا /XYZ میآید. صفحه را حذف کنید و با یک ارجاع آویزان تنها میمانید؛ صفحات را جابهجا کنید و ارجاع هنوز معتبر است اما حالا فصلی متفاوت را توصیف میکند. PDFiumPas آن آرایه را هنگام لود به یک شماره صفحه حل میکند، پس TPdfOutlineItem.PageNumber یک ایندکس صفحه یکمبنا به شما میدهد که با API عمومی TPdf جور است نه با یک شماره شیء. کل نقطه این انتزاع همین است: منطق بازنگاشت شما در همان دستگاه مختصاتی کار میکند که طرح صفحهای که هنگام split یا جابهجایی یا imposition سند ساختید. اگر دارید آن طرح را میسازید، همین قرارداد یکمبنا در تقسیم اسناد PDF به چند فایل و در imposition n-up و جابهجایی صفحات هم جریان دارد
outline یک درخت پیوند دوطرفه است، نه یک لیست
دلیل اینکه نمیتوانید صرفاً یک آرایه تخت از عنوانها را سریالایز کنید این است که ISO 32000-1 §12.3.3 هر آیتم outline را با پنج پیوند جدا به هم میبافد: /Parent و /Prev و /Next و /First و /Last. جابهجا کردن یک زیردرخت تنها پس بازنویسی والد قدیمی، والد جدید، هر دو همسطح مجاور در هر دو طرف نقطه برش و نقطه درج، و اشارهگر والد خود گره جابهجاشده است. یکی از اینها را غلط بگیرید و خوانندههای منطبق یک درخت بریده یا یک حلقه نشان میدهند. PDFiumPas وضعیت ویرایش را بهشکل یک آرایه depth-first از رکوردهای TPdfOutlineItem با یک Id صحیح پایدار نگه میدارد، پس یک زیردرخت یک برش پیوسته است و زنجیره همسطح مشتق میشود، هرگز دستی نگهداری نمیشود. TPdfOutlineEditor.Move آن برش را بلند میکند، زیر والد جدید در ایندکس همسطح درخواستی دوباره درج میکند و فقط ریشه بلوک را دوباره تخصیص میدهد. همچنین از دو جابهجایی که گراف را خراب میکنند امتناع میکند: جابهجا کردن یک آیتم به درون زیردرخت خودش، و نام بردن از والدی که وجود ندارد
چرا /Count علامتدار است؟
چون علامت وضعیت باز بودن را حمل میکند، نه اندازه را. /Count مثبت یعنی آیتم باز است و عدد تعداد نوادگیهایی است که در حال حاضر مرئیاند؛ /Count منفی یعنی آیتم بسته است. PDFiumPas برای هر آیتمی که بچه دارد تعداد نوادگی را مینویسد و وقتی IsOpen برابر False است آن را منفی میکند، و هنگام لود وضعیت را بهشکل IsOpen := HasCount and (CountValue > 0) برمیخواند. این رایجترین باگ دستساز در نویسندههای outline است: نوشتن یک count بیعلامت و باز کردن بیسروصدای کل درخت
var
Source, Dest: TMemoryStream;
Editor: TPdfOutlineEditor;
Options: TPdfOutlineEditOptions;
Report: TPdfOutlineValidationReport;
RootId, ChapterId: Integer;
begin
Source := TMemoryStream.Create;
Dest := TMemoryStream.Create;
Editor := nil;
try
Source.LoadFromFile('handbook.pdf');
Options := TPdfOutlineEditOptions.Default; // MaxItems 100000، MaxDepth 64
if not TPdfOutlineEditor.TryLoad(Source, Options, Editor, Report) then
raise Exception.Create(Report.ErrorMessage);
RootId := Editor[0].Id;
ChapterId := Editor[2].Id;
Editor.Move(ChapterId, RootId, 1); // بچه دوم ریشه میشود
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // یک /Count منفی مینویسد
Editor.Retarget(ChapterId, 12, '/XYZ 10 20 1');
if not Editor.SaveIncremental(Source, Dest, Report) then
raise Exception.Create(Report.ErrorMessage);
Dest.SaveToFile('handbook-edited.pdf');
finally
Editor.Free;
Dest.Free;
Source.Free;
end;
end;
Retarget هر دو شکلی که مشخصات اجازه میدهد را هندل میکند. DestinationInAction را False پاس بدهید و PDFiumPas یک آرایه /Dest مستقیم مینویسد؛ True پاس بدهید و یک اکشن Go-To مینویسد، /A << /S /GoTo /D [ page ref suffix ] >>، طبق ISO 32000-1 §12.6.4.2. به هر حال اول هر /Dest و /A موجود را از آیتم میچیند تا این دو نتوانند همزمان وجود داشته باشند و ناسازگار شوند. پسوند به /Fit دیفالت است و باید با یک نام PDF شروع شود، به همین دلیل پسوند خالی یا بدشکل بلافاصله raise میکند بهجای اینکه آرایه مقصدی تولید کند که هیچ خوانندهای نمیتواند parse کند
ApplyPageMap چطور یک طرح صفحه را مصرف میکند؟
ApplyPageMap دقیقاً همان آرایهای را میگیرد که طرح صفحه شما از قبل اعتبارسنجی کرده: NewPageNumbers، ایندکسگذاریشده با صفحه قدیمی منهای یک، شامل شماره صفحه جدید یکمبنا یا صفر وقتی آن صفحه زنده نمانده. آرایه آیتمها را به عقب پیمایش میکند تا حذف یک زیردرخت هرگز ایندکسی را که هنوز ملاقات نکرده نامعتبر نکند، و کاری که کرد را از طریق RemappedDestinationCount و RemovedDanglingItemCount گزارش میدهد
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// یک مدخل به ازای هر صفحه از سند اصلی
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 یعنی این صفحه انداخته شده
NewPageNumbers[0] := 1; // صفحه قدیمی 1 به صفحه جدید 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // صفحه قدیمی 10 به صفحه جدید 3
// True: حذف کل زیردرخت آویزان. False: نگه داشتن آیتم، چیدن هدفش
if not Editor.ApplyPageMap(NewPageNumbers, True, Report) then
raise Exception.Create(Report.ErrorMessage);
WriteLn(Format('%d remapped, %d dangling items removed',
[Report.RemappedDestinationCount, Report.RemovedDanglingItemCount]));
end;
فلگ DeleteDangling سیاست را برای مقصدی که به صفر نگاشت شده تعیین میکند، و هر دو شاخه عمدیاند. با True، PDFiumPas آیتم و کل زیردرختش را حذف میکند، چون گره outlineای که هدفش غیبش زده معمولاً سری را میسازد که همراهش غیبش زده. با False، آیتم با عنوان و سلسلهمراتب دستنخورده زنده میماند اما /Dest و /Aاش چیده شده، که همان چیزی است که میخواهید وقتی یک انسان قرار است در بازبینی بازهدفگذاریاش کند. ورودی واقعاً بدشکل هنوز بلند شکست میخورد بهجای وصله شدن: یک مدخل منفی یا مقصدی که به بعد از انتهای map دادهشده اشاره میکند False برمیگرداند با IssueKind ستشده به poviInvalidPageMap
مدخلهای مبهم و مصالحه صادقانه
هر آیتم outlineای شماره صفحهای ندارد که PDFiumPas بتواند دربارهاش استدلال کند. سه نوع دستنخورده حمل میشوند: مقصدهای نامدار، اکشنهایی که /S /GoTo نیستند، و کلیدهای دیکشنری ناشناختهای که هر چیزی که فایل را تولید کرده اضافه کرده. اینها با PageNumber برابر صفر لود میشوند، بایتهای اصلیشان را در آیتم نگه میدارند و عیناً برگردانده میشوند مگر اینکه صریحاً Retarget را رویشان صدا بزنید
- یک مقصد نامدار یک کلید به درون درخت نام سند است، پس بازنگاشت درست آن یعنی حل کردن درخت و بازنویسی مدخل هدف، نه حدس زدن در سطح outline
- یک اکشن
/URIیا/Launchیا JavaScript اصلاً معناشناسی صفحه ندارد و نباید بیسروصدا به یک Go-To تبدیل شود - کلیدهای مخصوص فروشنده و مقصدهای ساختاری حفظ میشوند چون انداختن چیزی که نمیفهمید همان روشی است که رفتوبرگشتها داده گم میکنند
هزینه واقعی است و ارزش گفتن صریح دارد: ApplyPageMap آن آیتمها را کاملاً رد میکند، پس سندی که همه بوکمارکهایش از مقصد نامدار استفاده میکنند از یک حذف صفحه با outlineای ساختاراً معتبر و معناییاً منقضی بیرون میآید. آن انتخاب عمدی است — یک لینک منقضی که بازبینیکننده میتواند بگیرد از یک لینک مطمئنِ غلط که کسی نمیبیند بهتر است. اگر فایلهای ورودی را قبل از ویرایش triage میکنید، یک گذر inventory در workbench بازبینی ورودی PDF به شما میگوید کدام اسناد در آن سطل میافتند
ذخیره: بازنگری افزایشی، بعد یک reload مستقل
TPdfOutlineEditor.SaveIncremental یک بازنگری افزایشی پراکنده را به فایل اضافه میکند بهجای بازنویسی فایل. آیتمهایی که لود شده بودند ارجاع غیرمستقیم اصلیشان را شامل نسل دقیق نگه میدارند، پس ارجاعهای متقابل موجود معتبر میمانند؛ فقط آیتمهایی که شما اضافه کردید یک شماره تازه میکشند، تخصیصیافته از یکی بعد از بیشینه شماره شیء بازنگری. کاتالوگ در همان بازنگری بهروز میشود، و وقتی مبدأ اصلاً outline نداشت یک مدخل /Outlines غایب به آن اضافه میشود
آنچه بعد از نوشتن اتفاق میافتد بخشی است که ارزش کپی کردن دارد. PDFiumPas استریم مقصد را با یک ویرایشگر کاملاً مستقل دوباره باز میکند و درخت reloadشده را با درخت درون حافظه مقایسه میکند — تعداد آیتمها، عنوانها، شماره صفحات، پسوندهای مقصد، شکل اکشن در مقابل مقصد مستقیم، استایلها، وضعیت باز بودن و روابط والد. هر ناسازگاری، یا هر شکست لود، استریم مقصد را خالی میکند و poviVerificationFailure برمیگرداند بهجای اینکه فایلی موجهنما به شما بدهد. مبدأهای رمزشده از همان اول با poviEncryptedInput رد میشوند، چون عنوانها و مقصدهای جدید محتوای رشتهای میسازند که با کپی کردن /Encrypt trailer به جلو قابل تولید نیست
if not Editor.SaveIncremental(Source, Dest, Report) then
case Report.IssueKind of
poviEncryptedInput:
Log('Source is encrypted; outline editing needs an unprotected copy');
poviInvalidDestination:
Log(Format('Item %d %d targets a missing page',
[Report.ObjectNumber, Report.Generation]));
poviVerificationFailure:
Log('Reload check rejected the written revision: ' + Report.ErrorMessage);
else
Log(Report.ErrorMessage);
end;
با outline همانطور که هست رفتار کنید — یک گراف شیء پیوندی با ناورداهای خودش — و حذف صفحه از یک فاجعه بوکمارک به یک page map تبدیل میشود که به یک فراخوانی متد میدهید. TPdfOutlineEditor و ApplyPageMap و نویسنده افزایشی راستیآزماییشده از v3.98.0 در PDFiumPas برای Delphi و C++Builder و Lazarus عرضه میشوند؛ میتوانید API کامل را مرور و یک نسخه آزمایشی روی صفحه محصول PDFium Delphi Component دانلود کنید