การแทนที่หน้า 3 ของสัญญาที่เซ็นเรียบร้อยแล้วไม่ควรทำให้สารบัญเลื่อนตำแหน่ง ถ้าลบหน้าเก่าแล้วแทรกหน้าใหม่เข้าไป bookmark ทุกอันที่เคยชี้ไปที่นั่นจะไปตกที่อื่นทันที PDFlibPas Delphi PDF library หลีกเลี่ยงปัญหานี้ด้วยการคงอ็อบเจกต์หน้าเป้าหมายไว้เหมือนเดิม แล้วย้ายเฉพาะ entry ที่บรรจุเนื้อหาภาพเท่านั้น
ทำไม bookmark ถึงพังหลังแทนที่หน้า PDF
Bookmark พังเพราะ destination ของ PDF ระบุหน้าด้วย indirect object reference ไม่ใช่ด้วยหมายเลขหน้า ISO 32000-1 §12.3.2.2 นิยาม explicit destination ว่าเป็น array ที่สมาชิกตัวแรกเป็น indirect reference ไปยัง page object ถ้าลบอ็อบเจกต์นั้นแล้วต่อท้ายด้วยตัวแทนใหม่ reference ก็จะค้างเติ่งอยู่กลางอากาศ viewer ส่วนใหญ่จะตอบสนองด้วยการโยนผู้อ่านไปที่หน้า 1 ซึ่งเป็นอาการที่คนรายงานเข้ามาบ่อยที่สุดหลังทำการแทนที่ด้วยวิธีลบแล้วแทรก page tree ดูสมบูรณ์แบบดี จำนวนหน้าก็ถูกต้อง การ render ก็ถูกต้อง แต่ทั้ง navigation layer กลับผิดไปอย่างเงียบ ๆ
Named destination ก็ช่วยไม่ได้เช่นกัน §12.3.2.3 ส่งชื่อผ่าน name tree /Dests ในแคตตาล็อกของเอกสาร แต่ leaf ที่ชื่อนั้น resolve ไปถึงก็ยังคงเป็น explicit destination array ที่เก็บ page reference เดียวกันอยู่ดี การตั้งชื่อเพิ่มชั้นของ indirection ไว้เหนือ page reference ไม่ใช่ล้อมรอบมัน เหตุผลเดียวกันนี้ครอบคลุมส่วนที่เหลือของ interactive layer ที่อธิบายไว้ใน §12.5 ด้วย: link annotation บรรทุก /Dest หรือ GoTo action /A ที่ /D ของมันคือ array นั้น annotation ทุกตัวสามารถมี entry /P ที่เป็น indirect reference ไปยังหน้าของมันได้ และ form field widget ก็เป็น annotation ที่อยู่บนฐานเดียวกันเป๊ะ การสลับหน้าแบบไร้เดียงสาเพียงครั้งเดียวจะตัดขาดระบบย่อยถึงสี่ระบบพร้อมกัน และหากอยากเห็นระบบเหล่านี้ถูกไล่เรียงบนไฟล์จริง object graph เดียวกันนี้ก็คือสิ่งที่ การตรวจสอบ outline และ annotation ไล่ตามอยู่
Entry ของหน้าตัวไหนบรรจุ identity และตัวไหนบรรจุรูปลักษณ์
Page dictionary ผสมกันระหว่าง entry สองประเภท และการแทนที่แบบ in-place จะสำเร็จได้ก็ต่อเมื่อคุณแยกทั้งสองออกจากกัน ฝั่งรูปลักษณ์นั้นมีจำนวนจำกัดและนับได้ทั้งหมด คือ /Contents, /Resources, page box ทั้งห้า /MediaBox, /CropBox, /BleedBox, /TrimBox กับ /ArtBox บวก /Rotate, /Group, /UserUnit และ /BoxColorInfo entry สิบเอ็ดตัวนี้ตัดสินทุกอย่างที่ rasteriser จะสร้างออกมาสำหรับหน้านั้น และไม่มีอะไรอื่นในไฟล์ชี้ไปที่มันด้วยชื่อเลย
ฝั่ง identity คือสิ่งที่ส่วนที่เหลือของเอกสารผูกตัวเองไว้ด้วย นั่นคือหมายเลขและ generation ของ page object, back-link /Parent เข้าไปใน page tree และ /Annots PDFlibPas คงทุกสิ่งเหล่านี้ไว้ไม่แตะต้องเลย ReplacePageRanges ล้าง entry ด้านรูปลักษณ์ทั้งสิบเอ็ดตัวออกจาก page dictionary เป้าหมาย แล้วเติมกลับเข้าไปจากหน้าต้นทางที่นำเข้ามา ดังนั้น page object เป้าหมายจึงถูกแก้ไขในที่เดิมแทนที่จะถูกแทนที่ โครงสร้าง page tree ที่ §7.7.3 กำหนดไว้ก็ยังคงรูปร่างเหมือนเดิมทุก byte ด้วย: ลำดับ /Kids, /Count และ /Parent ของแต่ละตัวที่รอดอยู่เหมือนกันทั้งก่อนและหลัง เพราะไม่มี node ใดถูกตัดการเชื่อมโยงเลย
PDFlibPas แทนที่หน้าโดยไม่เปลี่ยนหมายเลขอ็อบเจกต์ได้อย่างไร
การเรียกนี้รับเอกสารต้นทาง หน้าเริ่มต้นเป้าหมายแบบนับจาก 1 นิพจน์ช่วงต้นทาง และ flag ตัวเลือก เอกสารทั้งสองต้องถูกเปิดอยู่ใน instance เดียวกัน และเอกสารเป้าหมายคือเอกสารที่ถูกเลือกอยู่ เนื่องจากจำนวนหน้าของเป้าหมายไม่เปลี่ยนแปลงเลย ช่วงที่คุณร้องขอจึงต้องพอดีกับเอกสารโดยเริ่มที่ 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;
ภายในระบบ หน้าต้นทางไม่สามารถถูกอ่านข้ามขอบเขตเอกสารได้โดยตรง เพราะ indirect reference ทุกตัวข้างในนั้นเป็นของระบบหมายเลขอ็อบเจกต์ของต้นทาง ดังนั้นช่วงต้นทางจึงถูกนำเข้าตามวิธีปกติก่อน ในฐานะหน้าชั่วคราวที่ต่อท้ายหลังหน้าจริงสุดท้าย ซึ่งจะรัน object-graph remapping แบบเต็มรูปแบบ: content stream, ฟอนต์, XObject, shading และ colour space ทั้งหมดถูกเปลี่ยนหมายเลขใหม่เข้าสู่เอกสารเป้าหมาย หลังจากนั้นเท่านั้นที่ entry ด้านรูปลักษณ์ทั้งสิบเอ็ดตัวจึงถูกคัดลอกจากแต่ละหน้าชั่วคราวไปยังหน้าเป้าหมายของมัน และหลังจากนั้นเท่านั้นที่หน้าชั่วคราวจะถูกตัดการเชื่อมโยงออกจาก page tree งาน remapping เกิดขึ้นในจุดที่ทำได้ถูกและปลอดภัย และการแก้ไขแบบทำลายล้างก็ลดลงเหลือแค่การสลับ dictionary บนหน้าที่มีอยู่แล้วเท่านั้น
Path การลบที่จะทำลายสิ่งที่คุณเพิ่งย้ายมา
การลบหน้าชั่วคราวเหล่านั้นเป็นขั้นตอนที่ดูเหมือนไม่สำคัญแต่จริง ๆ แล้วไม่ใช่เลย path ลบหน้าปกติในไลบรารีทำมากกว่าแค่ตัดการเชื่อมโยง node มันรวม layer ของแต่ละหน้าที่กำลังถูกลบเข้าด้วยกัน ล้าง content stream แรกให้ว่างเปล่า และเก็บกวาด resource ที่ไม่มีหน้าอื่นใช้ร่วมด้วย นั่นคือพฤติกรรมที่ถูกต้องสำหรับการลบจริง ๆ แต่เป็นหายนะในกรณีนี้ เพราะตอนที่หน้าชั่วคราวถูกลบ หน้าเป้าหมายก็อ้างถึง content stream และ resource object เหล่านั้นอยู่แล้วพอดี การล้างให้ว่างจะทำให้หน้าที่คุณเพิ่งแทนที่กลายเป็นเปล่าไปด้วย และการกวาด resource จะเก็บฟอนต์กับภาพที่ตอนนี้มีเจ้าของที่ยังมีชีวิตอยู่ไปทิ้ง
วิธีแก้คือโหมด preserve-referenced-objects บน internal delete path เมื่อตั้งค่านี้ไว้ การลบจะข้ามทั้งการกวาด resource ที่ไม่ได้ใช้ร่วมและการล้าง content stream และไม่ทำอะไรนอกจากแยกหน้าออกจาก page tree และแก้ไขบัญชี tree ให้ถูกต้อง อ็อบเจกต์ที่ถูกย้ายมาก็รอดโดยมีเจ้าของใหม่ และความเป็นเจ้าของอ็อบเจกต์หลังการทำงานนี้ก็เป็นสิ่งที่คุณสามารถวาดบนไวท์บอร์ดได้เลย: content stream หนึ่งตัว หน้าเจ้าของหนึ่งหน้า หมายเลขอ็อบเจกต์หนึ่งตัวที่ไม่เคยขยับเลย กฎ lifecycle ที่เกี่ยวข้องสำหรับการสร้าง ลบ และเรียงลำดับหน้าใหม่นั้นครอบคลุมแยกต่างหากไว้ในบันทึกเรื่อง การดำเนินการ lifecycle ของเอกสารและหน้า
ลำดับ ความซ้ำ และความล้มเหลวแบบทั้งหมดหรือไม่มีเลย
Flag ตัวเลือกกำหนดว่าจะตีความช่วงต้นทางอย่างไร 0 จะเรียงหมายเลขหน้าที่ parse แล้วและลบตัวซ้ำออก ซึ่งเป็นค่าเริ่มต้นที่สมเหตุสมผลเมื่อผู้เรียกส่งอะไรอย่าง '4-6,2' เข้ามาและหมายความง่าย ๆ ว่าคือสี่หน้านั้น 1 คงลำดับตามที่คุณเขียนไว้และอนุญาตให้หน้าซ้ำได้ ดังนั้น '2,1,2' จึงหมายถึงการแทนที่สามครั้งที่นำมาจากหน้าต้นทางสองหน้าจริง ๆ การตรวจสอบจะรันก่อนและรันแบบครบถ้วน: ไวยากรณ์ของช่วง หมายเลขหน้าทุกตัวเทียบกับจำนวนหน้าต้นทาง ค่าตัวเลือกเอง และความจุของเป้าหมาย ทั้งหมดนี้ถูกตรวจสอบก่อนที่จะสร้างอ็อบเจกต์สักตัวเดียว การเรียกที่ถูกปฏิเสธจะตั้งค่า LastErrorCode เป็น 412 คืนหน้าที่ถูกเลือกไว้ก่อนหน้ากลับมา และปล่อยให้เอกสารเป็นเหมือนเดิมทุกประการ
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;
ความเป็น atomic ขยายเลยจากการตรวจสอบไปถึงตัวการโอนย้ายเองด้วย ก่อนที่หน้าต้นทางแรกจะถูกนำเข้า entry ด้านรูปลักษณ์ทั้งสิบเอ็ดตัวของทุกหน้าเป้าหมายในช่วงนั้นจะถูกถ่ายภาพ snapshot ไว้เป็นค่าที่เข้ารหัสแล้ว ถ้าการนำเข้าล้มเหลว หรือจำนวนหน้าที่นำเข้าไม่ตรงกับที่ร้องขอ snapshot จะถูก decode กลับไปยังหน้าเป้าหมาย และหน้าชั่วคราวจะถูกลบออก ดังนั้นความล้มเหลวกลางทางก็ยังคงทิ้งรูปลักษณ์เดิมไว้บนอ็อบเจกต์เดิมของมัน เรื่องนี้สำคัญกว่าที่ฟังดู เพราะช่วงหน้าที่ถูกแทนที่ไปครึ่งหนึ่งในสัญญาฉบับหนึ่งเลวร้ายยิ่งกว่าการเรียกที่ล้มเหลวไปเลย เพราะไม่มีอะไรในไฟล์ที่ทำเครื่องหมายไว้ว่ามันทำไปครึ่งเดียว
// 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);
อะไรที่การแทนที่แบบ in-place ยังไม่ทำให้คุณ
Annotation ต้นทาง form field ต้นทาง และ outline ต้นทางจงใจไม่ถูกนำเข้ามาด้วย การนำ widget ข้ามมาโดยไม่มี field entry /AcroForm ของมัน หรือ annotation ที่บรรจุ marked-content โดยไม่มีความเป็นเจ้าของ structure tree จะสร้างอ็อบเจกต์ interactive ที่นำเข้ามาครึ่ง ๆ กลาง ๆ ซึ่งไม่มี viewer ตัวไหนตีความได้ ดังนั้นการดำเนินการนี้จึงย้ายเฉพาะรูปลักษณ์เท่านั้น ผลในทางปฏิบัติคือ ถ้าหน้าที่แทนที่ควรมี form field ใหม่หรือลิงก์ใหม่ คุณต้องเพิ่มมันเข้าไปยังหน้าเป้าหมายภายหลัง โดยอ้างกับ page object เป้าหมายที่ยังอยู่ตรงนั้นรอมันอยู่
มีขอบเขตอีกสองอย่างที่ควรตรวจสอบกับไฟล์ของคุณเองด้วย อย่างแรก /Annots ถูกคงไว้แต่รูปทรงเรขาคณิตของหน้าไม่ถูกคง ดังนั้นการแทนที่หน้าขนาด 220 มม. ด้วยหน้าขนาด 320 มม. จะทำให้สี่เหลี่ยมของ annotation ยังอยู่ที่พิกัดเดิมภายใน /MediaBox ที่มีขนาดต่างออกไป ถ้ารูปทรงเปลี่ยน ให้จัดตำแหน่ง annotation ที่คงไว้ใหม่ อย่างที่สอง entry ที่อยู่นอกเหนือคีย์ด้านรูปลักษณ์ทั้งสิบเอ็ดตัวจะยังอยู่กับหน้าเป้าหมายตามการออกแบบ ซึ่งถูกต้องสำหรับ /Trans หรือ /AA แต่ล้าสมัยสำหรับ /Thumb ดังนั้นควรสร้าง thumbnail ใหม่หลังการแทนที่ เอกสารที่ tag ไว้ต้องคิดเพิ่มอีกอย่างหนึ่ง: structure element ยังคงชี้ไปยัง page object ที่ถูกต้องผ่าน /Pg แต่ marked-content identifier ของมันอธิบายเนื้อหาที่ไม่มีอยู่ที่นั่นแล้ว ดังนั้นการสลับหน้าใน workflow ของ PDF/UA จึงเป็นการแก้ไข structure tree ควบคู่ไปกับการแก้ไขเนื้อหาด้วย ถ้างานของคุณจริง ๆ แล้วคือการประกอบ (compositing) ไม่ใช่การสลับ คือการวางลวดลายทับหน้าที่คุณเก็บไว้ แนวทาง page stitching และ template เป็นเครื่องมือที่ประหยัดกว่า
ทุกอย่างที่อธิบายไว้ที่นี่ รวมถึงไวยากรณ์นิพจน์ช่วง ค่าตัวเลือก และ API จัดการหน้าโดยรอบ มาพร้อมกับ PDFlibPas Delphi PDF Library มาตรฐานสำหรับ Delphi และ C++Builder ซึ่งเอกสารอ้างอิงมีรายการเต็มของฟังก์ชันแทนที่หน้าและรหัสข้อผิดพลาดของมัน