مقاله فنی

جایگزینی صفحات PDF در Delphi بدون شکستن نشانک‌ها

جایگزین‌کردن صفحه ۳ یک قرارداد امضاشده نباید فهرست مطالب را جابه‌جا کند. صفحه قدیمی را حذف کنید، جدیدی را درج کنید، و هر نشانکی که قبلاً به آنجا اشاره می‌کرد اکنون جایی دیگر فرود می‌آید. کتابخانه PDFlibPas Delphi PDF از آن با نگه‌داشتن خود آبجکت صفحه هدف و انتقال فقط مدخل‌هایی که محتوای بصری حمل می‌کنند اجتناب می‌کند

چرا نشانک‌ها پس از جایگزینی یک صفحه PDF می‌شکنند؟

نشانک‌ها می‌شکنند چون یک مقصد PDF یک صفحه را با ارجاع غیرمستقیم آبجکت نام می‌برد، نه با شماره صفحه. ISO 32000-1 §12.3.2.2 یک مقصد صریح را به‌عنوان آرایه‌ای تعریف می‌کند که اولین عنصرش یک ارجاع غیرمستقیم به آبجکت صفحه است. آن آبجکت را حذف کنید و جایگزینی را ضمیمه کنید، و ارجاع آویزان می‌شود: بیشتر نمایشگرها با فرودآوردن خواننده روی صفحه ۱ پاسخ می‌دهند، که دقیقاً همان علامتی است که مردم پس از یک جایگزینی حذف-سپس-درج گزارش می‌کنند. درخت صفحه کاملاً درست به‌نظر می‌رسد، شمارش صفحه درست است، رندرینگ درست است، و کل لایه ناوبری در سکوت اشتباه است

مقصدهای نام‌دار هم شما را نجات نمی‌دهند. §12.3.2.3 یک نام را از راه درخت نام /Dests در فهرست سند مسیریابی می‌کند، اما برگی که آن نام به آن حل می‌شود همچنان یک آرایه مقصد صریح است که همان ارجاع صفحه را نگه می‌دارد. نام‌گذاری یک لایه واسط بالای ارجاع صفحه اضافه می‌کند، نه پیرامونش. همین استدلال بقیه لایه تعاملی توصیف‌شده در §12.5 را می‌پوشاند: یک حاشیه‌نویسی لینک یک /Dest یا یک عمل GoTo /A حمل می‌کند که /Dاش همان آرایه است، هر حاشیه‌نویسی ممکن است یک مدخل /P حمل کند که یک ارجاع غیرمستقیم به صفحه‌اش است، و یک ویجت فیلد فرم دقیقاً روی همان بنیان یک حاشیه‌نویسی است. یک تعویض ساده‌لوح صفحه چهار زیرسیستم را یکجا جدا می‌کند، و اگر می‌خواهید آن‌ها را روی یک فایل واقعی شمارش‌شده ببینید، همان گراف آبجکت چیزی است که درون‌نگری outline، حاشیه‌نویسی و عمل می‌پیماید

کدام مدخل‌های صفحه هویت حمل می‌کنند و کدام ظاهر

یک دیکشنری صفحه دو نوع مدخل را مخلوط می‌کند، و یک جایگزینی درجا دقیقاً وقتی موفق می‌شود که آن‌ها را جدا کنید. سمت ظاهر محدود و شمارش‌پذیر است: /Contents، /Resources، پنج جعبه صفحه /MediaBox، /CropBox، /BleedBox، /TrimBox و /ArtBox، به‌علاوه /Rotate، /Group، /UserUnit و /BoxColorInfo. آن یازده مدخل هرچیزی را که یک رستریزکننده برای صفحه تولید می‌کند تعیین می‌کنند، و هیچ‌چیز دیگری در فایل با نام به آن‌ها اشاره نمی‌کند

سمت هویت چیزی است که بقیه سند خودش را به آن مقید کرده: شماره آبجکت و نسل صفحه، پیوند بازگشتی /Parent به درخت صفحه، و /Annots. PDFlibPas هرکدام از آن‌ها را دست‌نخورده نگه می‌دارد. ReplacePageRanges یازده مدخل بصری را از دیکشنری صفحه هدف پاک می‌کند و دوباره از صفحه منبع وارد‌شده اضافه می‌کند، پس آبجکت صفحه هدف درجا تغییر می‌یابد نه جایگزین. ساختار درخت صفحه الزام‌شده توسط §7.7.3 نیز بایت‌به‌بایت هم‌شکل می‌ماند: ترتیب /Kids، /Count، و هر /Parent باقی‌مانده پیش و پس یکسان است، چون هیچ گره هرگز قطع نشده

PDFlibPas چگونه یک صفحه را بدون شماره‌گذاری‌مجدد آبجکت‌ها جایگزین می‌کند؟

این فراخوانی یک سند منبع، یک صفحه شروع هدف ۱-پایه، یک عبارت بازه منبع، و یک پرچم گزینه می‌گیرد. هر دو سند باید در همان نمونه باز باشند، و سند هدف آنی است که انتخاب شده. چون شمارش صفحه هدف هرگز تغییر نمی‌کند، بازه‌ای که درخواست می‌کنید باید درون سند شروع‌شده در TargetStartPage جا بگیرد، و آن پیش از ساختن هرچیز بررسی می‌شود

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    // The document whose bookmarks and links must survive
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // The revised clause page, rendered by whatever produced it
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // Source page 1 overwrites the visuals of target page 3.
    // Page count, page 3 object number, bookmarks and annotations are kept.
    if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
      Lib.SaveToFile('contract-final.pdf');
  finally
    Lib.Free;
  end;
end;

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

مسیر حذفی که آنچه تازه منتقل کردید را نابود می‌کرد

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

رفع مشکل یک حالت حفظ-آبجکت‌های-ارجاع‌شده روی مسیر حذف داخلی است. وقتی ست شده باشد، حذف هم جاروب منابع غیرمشترک و هم پاک‌سازی جریان محتوا را رد می‌کند، و هیچ کاری جز جداکردن صفحات از درخت صفحه و تعمیر دفترداری درخت انجام نمی‌دهد. آبجکت‌های منتقل‌شده با مالک جدید جان سالم به‌در می‌برند، و مالکیت آبجکت پس از عملیات چیزی است که روی یک تخته سفید می‌کشید: یک جریان محتوا، یک صفحه مالک، یک شماره آبجکت که هرگز حرکت نکرده. قواعد چرخه حیات مرتبط برای ساخت، حذف و بازترتیب صفحات در یادداشت‌های عملیات چرخه حیات سند و صفحه جداگانه پوشش داده شده‌اند

ترتیب، تکرارها، و شکست همه-یا-هیچ

پرچم گزینه انتخاب می‌کند بازه منبع چگونه تفسیر شود. 0 شماره صفحات تجزیه‌شده را مرتب می‌کند و تکرارها را حذف می‌کند، که پیش‌فرض عاقلانه است وقتی فراخوان‌کننده چیزی مانند '4-6,2' را می‌گذرد و به‌سادگی یعنی آن چهار صفحه. 1 ترتیبی که نوشته‌اید را حفظ می‌کند و اجازه می‌دهد یک صفحه تکرار شود، پس '2,1,2' واقعاً یعنی سه جایگزینی گرفته‌شده از دو صفحه منبع. اعتبارسنجی ابتدا اجرا می‌شود و کاملاً اجرا می‌شود: نحو بازه، هر شماره صفحه علیه شمارش صفحه منبع، خود مقدار گزینه، و ظرفیت هدف همه پیش از ساخته‌شدن یک آبجکت منفرد بررسی می‌شوند. یک فراخوانی ردشده LastErrorCode را به ۴۱۲ ست می‌کند، صفحه پیش‌تر انتخاب‌شده را بازمی‌گرداند، و سند را دقیقاً همان‌طور که بود رها می‌کند

var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: source order is preserved and repeats are allowed, so
  // target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // On success the selection is the first replaced page
  Assert(Lib.SelectedPage = 5);
end;

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

// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);

جایگزینی درجا هنوز چه چیزی برایتان انجام نمی‌دهد؟

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

دو مرز دیگر ارزش بررسی روی فایل‌های خودتان را دارند. اول، /Annots حفظ می‌شود اما هندسه صفحه نه، پس جایگزینی یک صفحه ۲۲۰میلی‌متری با یک صفحه ۳۲۰میلی‌متری مستطیل‌های حاشیه‌نویسی را در مختصات قدیمی‌شان درون یک /MediaBox با اندازه متفاوت نگه می‌دارد؛ اگر هندسه تغییر می‌کند، حاشیه‌نویسی‌هایی که نگه داشتید را جای‌گذاری‌مجدد کنید. دوم، مدخل‌های بیرون از یازده کلید بصری با طراحی با صفحه هدف می‌مانند، که برای /Trans یا /AA درست است و برای /Thumb کهنه، پس پس از یک جایگزینی تصاویر کوچک را بازتولید کنید. اسناد برچسب‌گذاری‌شده یک فکر اضافی نیاز دارند: عناصر ساختار همچنان از راه /Pg به آبجکت صفحه درست اشاره می‌کنند، اما شناسه‌های محتوای علامت‌گذاری‌شده‌شان محتوایی را توصیف می‌کنند که دیگر آنجا نیست، پس یک تعویض صفحه درون یک گردش‌کار PDF/UA یک ویرایش درخت ساختار است هم به‌اندازه یک ویرایش محتوا. اگر کار شما واقعاً ترکیب است نه تعویض، لایه‌گذاری اثر هنری روی صفحاتی که نگه می‌دارید، رویکرد دوخت صفحه و الگو ابزار ارزان‌تری است

هرچیز شرح‌داده‌شده در اینجا، شامل نحو عبارت بازه، مقادیر گزینه و API دستکاری صفحه پیرامون، در کتابخانه PDFlibPas Delphi PDF استاندارد برای Delphi و C++Builder ارائه می‌شود، که مستندات مرجعش مدخل کامل برای فراخوانی جایگزینی صفحه و کدهای خطایش را در بر دارد