การเย็บหน้า (Page stitching) เป็นการรวม เนื้อหา (content) ของหน้าเดิมที่มีอยู่เข้าด้วยกันบนผืนผ้าใบ (canvas) ร่วมกัน ซึ่งเป็นการทำงานที่แตกต่างจากการรวมไฟล์ (merging files) losLab PDF Library ได้นำการเย็บหน้าแบบ Form XObject และเทมเพลตลายน้ำที่จัดวางประกอบได้มาใช้สำหรับ Delphi: StitchPageOverlay, StitchPagesSideBySide และ StitchPagesVertically จะวางหน้ากระดาษที่จับภาพมาได้ที่ระดับการปรับสเกลใดๆ ด้วยความละเอียดแม่นยำของเวกเตอร์อย่างสมบูรณ์ และ TPDFlibCompositeWatermark จะซ้อนลายน้ำข้อความและรูปภาพตามช่วงหน้ากระดาษที่กำหนดได้ในการเรียกเพียงครั้งเดียว หากสิ่งที่คุณต้องการจริงๆ คือการต่อหน้าทั้งหมดจากเอกสารหลายฉบับเข้าด้วยกันเป็นไฟล์เดียว นั่นคือตระกูลการควบรวม (merge family) ซึ่งครอบคลุมไว้ในบทความเกี่ยวกับการรวม PDF ระดับไบต์อย่างรวดเร็ว ส่วนบทความนี้จะเป็นเรื่องเกี่ยวกับการรวมเนื้อหาหน้าเว็บภายในพื้นที่พิกัดเดียว
ความต้องการที่นำมาสู่เรื่องนี้เป็นเรื่องปกติที่พบได้บ่อยๆ ตัวอย่างเช่น โรงเรียนแห่งหนึ่งต้องการรวมหน้าข้อสอบสองหน้าเข้าด้วยกันบนกระดาษแนวนอนแผ่นเดียวเพื่อลดการใช้กระดาษลงครึ่งหนึ่ง ผู้ตรวจสอบการแปลต้องการให้หน้าภาษาอังกฤษและคำแปลแสดงอยู่เคียงข้างกันในหน้าเดียวเพื่อเลื่อนดูไปพร้อมกัน ทีมกฎหมายต้องการให้เอกสารร่างทุกฉบับประทับตราสามชั้นพร้อมกัน ได้แก่ โลโก้ที่มุมกระดาษ, คำว่า CONFIDENTIAL ในแนวทะแยงมุมตรงกลาง และบรรทัดท้ายกระดาษขนาดเล็ก โดยทำเฉพาะหน้า 1 ถึง 3 เท่านั้น ทั้งสามประการนี้เป็นปัญหาพื้นฐานเดียวกัน นั่นคือการจัดวางเนื้อหาของหน้าที่ผ่านการจัดรูปแบบเรียบร้อยแล้วไปไว้ที่อื่นในขนาดอื่นโดยไม่ลดทอนคุณภาพไปเป็นบิตแมป
ทำไมการเชื่อมต่อสตรีมเนื้อหา PDF (Content Streams) จึงล้มเหลว?
การเย็บหน้าแบบธรรมดาๆ โดยการคัดลอกสตรีม /Contents ของหน้าต้นทางไปยังหน้าปลายทาง จะล้มเหลวด้วยเหตุผลทางโครงสร้าง 3 ประการ ประการแรก เกิดการชนกันของพิกัด: สตรีมเนื้อหาทั้งสองสตรีมกำหนดจุดเริ่มต้นของตนเองไว้ที่มุมล่างซ้ายของ MediaBox ของตนเอง ดังนั้นเนื้อหาที่วางจึงไปทับเนื้อหาเดิมในตำแหน่งที่ผิด ประการที่สอง เกิดการชนกันของชื่อทรัพยากร: แต่ละหน้าจะดึงข้อมูลตัวดำเนินการเช่น /F1 Tf และ /Im0 Do เทียบกับพจนานุกรม /Resources ของตนเอง และหน้าสองหน้ามักจะผูกชื่อเดียวกันเข้ากับฟอนต์หรือรูปภาพที่ต่างกัน ส่งผลให้อีกหน้าหนึ่งแสดงผลด้วยทรัพยากรที่ผิดพลาดไปโดยไม่แจ้งเตือน ประการที่สาม การคัดลอกสตรีมดิบทำให้คุณไม่สามารถปรับสเกลขนาดได้เลย สิ่งที่ดีที่สุดที่คุณทำได้คือการย้ายพิกัด (translate) เท่านั้น
Form XObjects สามารถแก้ปัญหาทั้งสามประการนี้ได้พร้อมกัน และนั่นคือสาเหตุที่ losLab PDF Library สร้างการเย็บหน้าขึ้นบนพื้นฐาน of Form XObject ข้อกำหนด ISO 32000-1 §8.10 นิยาม Form XObject ว่าเป็นสตรีมเนื้อหาแบบเบ็ดเสร็จในตัวที่มีพจนานุกรม /Resources และพื้นที่พิกัดของตนเอง ซึ่งสามารถนำไปวางในหน้าเอกสารได้โดยการเรียกใช้ชื่อของมันภายใต้เมทริกซ์การแปลงข้อมูล (transformation matrix) ปัจจุบันที่กำลังใช้งานอยู่ ฟังก์ชัน CapturePage จะรวมหน้าเอกสารเดิมทั้งหมด ทั้งเนื้อหาและทรัพยากร เข้าไว้ใน XObject ประเภท /Subtype /Form จากนั้นฟังก์ชันเย็บหน้าจะจัดตำแหน่งด้วยเมทริกซ์ cm เพื่อย่อขยาย, ย้ายพิกัด หรือหมุนตามต้องการ เนื่องจากต้นทางยังคงเป็นเวกเตอร์ ข้อความจึงยังคงเป็นข้อความและเส้นทาง (paths) ยังคงเป็นเส้นทางในทุกระดับการซูม คุณสมบัติเดียวกันนี้ทำให้กราฟิกแบบเวกเตอร์และรูปแบบการไล่โทนสี (shading patterns) ไม่ขึ้นกับความละเอียด ไม่มีสิ่งใดถูกแปลงเป็นพิกเซล และพจนานุกรมทรัพยากรที่แยกเป็นเอกเทศหมายความว่าหน้าที่จับภาพได้จะไม่ขัดแย้งกับหน้าปลายทางในเรื่องการใช้ชื่อ /F1
จะรวมหน้า PDF สองหน้าเข้าไว้ในหน้าเดียวกันได้อย่างไร?
StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) คือคำตอบโดยตรง: ฟังก์ชันนี้จะจับภาพหน้าต้นทางทั้งสองหน้า สร้างหน้าใหม่ที่กว้างขึ้น และวางภาพที่จับได้ทั้งสองฝั่งซ้ายและขวาโดยมีระยะห่าง (gutter) ระหว่างกลาง โดยแต่ละหน้าจะถูกปรับขนาดให้พอดีกับ TargetHeight ฟังก์ชันนี้จะส่งกลับหมายเลขหน้าใหม่ (เริ่มจาก 1) หรือส่งกลับ 0 เมื่อล้มเหลว และหน้าต้นทางทั้งสองหน้าจะถูกจับภาพและซ่อนไว้เป็นส่วนหนึ่งของการทำงาน กรณีการสอบแบบสองหน้าต่อแผ่นกระดาษจึงใช้โค้ดเพียงสามบรรทัดเท่านั้น
var
Lib: TPDFlib;
NewPage: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('exam.pdf', '');
// Pages 1 and 2 on one new wider page with an 18pt gutter,
// both scaled to a 595pt output height (A4 landscape)
NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
if NewPage > 0 then
Lib.SaveToFile('exam-2up.pdf');
finally
Lib.Free;
end;
end;
สำหรับการประสานหน้าในแนวตั้ง ฟังก์ชัน StitchPagesVertically(PageRanges, Gap, TargetWidth) จะซ้อนช่วงหน้าทั้งหมดลงในหน้ากระดาษยาวแผ่นเดียวสไตล์โปสเตอร์ PageRanges จะใช้ไวยากรณ์ช่วงมาตรฐาน เช่น '1-3,5' หน้าต้นทางแต่ละหน้าจะถูกปรับขนาดเพื่อให้ความกว้างสอดคล้องกับ TargetWidth ดังนั้นความสูงของแต่ละหน้าที่จะปรากฏคือความสูงของตัวมันเองคูณด้วยอัตราการปรับสเกลของมัน และความสูงของหน้ากระดาษผลลัพธ์คือผลรวมของความสูงเหล่านั้นบวกกับค่า Gap ระหว่างหน้าแต่ละคู่ นี่คือรูปแบบที่คุณต้องการสำหรับเอกสารการตรวจสอบแบบเลื่อนต่อเนื่อง หรือใบเสร็จยาวๆ ที่ประกอบขึ้นจากแหล่งข้อมูลขนาด Letter
จะซ้อนทับหน้าหนึ่งลงบนอีกหน้าหนึ่งได้อย่างไร?
StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) จะวาดหน้าเอกสารที่มีอยู่หน้าหนึ่งลงบนอีกหน้าหนึ่ง แทนการสร้างหน้าใหม่ หน้าต้นทางจะถูกจับภาพเป็น Form XObject และวาดลงบนหน้าปลายทาง ณ สี่เหลี่ยมผืนผ้าที่คุณระบุ พร้อมกำหนดค่าความทึบแสง Opacity ตั้งแต่ 0 ถึง 1 (1 คือทึบแสง) ความกว้างและความสูงเป็นพารามิเตอร์อิสระ ดังนั้นภาพที่วางลงไปอาจยืดหดได้หากสัดส่วนของคุณแตกต่างจากอัตราส่วนภาพดั้งเดิมของหน้าต้นทาง ให้ส่งคู่พิกัดที่เป็นสัดส่วนกันหากคุณต้องการรูปภาพตัวอย่าง (thumbnail) ของต้นทางโดยไม่มีการบิดเบี้ยว การใช้งานทั่วไปคือเอกสารที่แผงข้อมูลการอนุมัติถูกแยกไว้เป็นหน้าของตนเองและต้องการปั๊มตราลงบนหน้าปก
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('report.pdf', '');
// Page 5 holds the approval block; place it on page 1
// in a 260 x 180 box at 45% opacity. Page 5 is consumed:
// it is captured and hidden, so pages 6..n become 5..n-1
if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
Lib.SaveToFile('report-approved.pdf');
finally
Lib.Free;
end;
end;
เกิดอะไรขึ้นกับหมายเลขหน้าหลังการจับภาพ?
CapturePage จะใช้หน้าต้นทางไปเลย: เนื้อหาของหน้ากระดาษจะถูกย้ายเข้าไปใน XObject และหน้าที่ว่างเปล่าในตอนนี้จะถูกซ่อนไว้ ดังนั้นทุกหน้าที่อยู่ถัดจากหน้าต้นทางจะเลื่อนขึ้นมาแทนที่ (หมายเลขลดลงหนึ่งหน้า) ทันที นี่คือความหมายที่สำคัญที่สุดใน stitching API ทั้งหมด losLab PDF Library จะจัดการการคำนวณภายในแต่ละการเรียกเย็บหน้า ฟังก์ชัน StitchPageOverlay จะปรับเปลี่ยนดัชนีปลายทางเมื่อปลายทางอยู่สูงกว่าต้นทาง และ StitchPagesSideBySide จะจับภาพหน้าที่ตัวเลขสูงกว่าก่อน เพื่อให้ดัชนีของการจับภาพที่สองยังคงถูกต้อง แต่โค้ดแวดล้อมของคุณต้องคำนึงถึงการเลื่อนหมายเลขหน้าระหว่างการเรียกด้วย กฎสองข้อที่จะรักษาความถูกต้องของโค้ดการเย็บหลายหน้าคือ: ให้บันทึกขนาดหน้าหรือหมายเลขหน้าใดๆ ที่คุณจำเป็นต้องใช้ ก่อน การจับภาพครั้งแรก เนื่องจากสิ่งเหล่านั้นจะเปลี่ยนไปในภายหลัง และเมื่อคุณจับหลายหน้าด้วยตัวเองในลูป ให้ทำงานแบบย้อนกลับ (เรียงจากเลขหน้ามากไปน้อย) เพื่อให้การซ่อนหน้าที่มีเลขมากกว่าจะไม่ส่งผลกระทบต่อดัชนีของหน้าที่น้อยกว่าซึ่งคุณยังไม่ได้จับภาพ
จะใช้ลายน้ำหลายชั้นกับช่วงของหน้าเอกสารได้อย่างไร?
เลเยอร์เทมเพลตลายน้ำใน PDFlibWatermark.pas จะแปลงการทำงานประเภท "นิยามครั้งเดียว นำไปใช้ได้กับหลายหน้า" ให้กลายเป็นอ็อบเจกต์ TPDFlibWatermarkBase จะมีข้อมูลที่ลายน้ำทุกอันต้องใช้ร่วมกัน ได้แก่ ตำแหน่ง 9 จุด Position (wpTopLeft ไปจนถึง wpBottomRight บวกกับ wpCustom ซึ่งพิกัด X/Y ดิบ), มุม Angle เป็นองศา, ความทึบแสง Opacity ตั้งแต่ 0 ถึง 1 และสตริง PageRange ที่ค่าว่างจะหมายถึงทุกหน้า TPDFlibTextWatermark จะระบุข้อความ, ID ฟอนต์ (ค่าเริ่มต้นคือ Helvetica), ขนาด และสี RGB; TPDFlibImageWatermark จะระบุ ID ของรูปภาพที่เพิ่มเข้าไปก่อนหน้านี้ รวมถึงความกว้างและความสูงในการวาง TPDFlibCompositeWatermark จะเก็บรายการของลายน้ำย่อยและนำไปใช้ในลักษณะซ้ำซ้อน (recursively) และ TPDFlib.ApplyWatermark เป็นจุดเข้าใช้งานในการเรียกเพียงครั้งเดียวที่สอดคล้องกับช่วงหน้าของเทมเพลตและส่งคืนจำนวนหน้าที่ถูกประทับตรา ตัวอย่างการประทับตราทางกฎหมายสามชั้นมีลักษณะดังนี้
var
Lib: TPDFlib;
Logo: TPDFlibImageWatermark;
Diagonal, Footer: TPDFlibTextWatermark;
Stack: TPDFlibCompositeWatermark;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('draft.pdf', '');
Stack := TPDFlibCompositeWatermark.Create;
try
Logo := TPDFlibImageWatermark.Create;
Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
Logo.Position := wpTopRight;
Logo.Width := 90;
Logo.Height := 32;
Stack.Add(Logo);
Diagonal := TPDFlibTextWatermark.Create;
Diagonal.Text := 'CONFIDENTIAL';
Diagonal.Position := wpCenter;
Diagonal.Angle := 45;
Diagonal.FontSize := 60;
Diagonal.Opacity := 0.15;
Stack.Add(Diagonal);
Footer := TPDFlibTextWatermark.Create;
Footer.Text := 'Internal review copy';
Footer.Position := wpBottomCenter;
Footer.FontSize := 9;
Footer.Opacity := 0.6;
Stack.Add(Footer);
Stack.PageRange := '1-3'; // children inherit this range
Lib.ApplyWatermark(Stack);
Lib.SaveToFile('draft-marked.pdf');
finally
Stack.Free; // composite frees its children
end;
finally
Lib.Free;
end;
end;
รายละเอียดการประกอบสองประการที่ควรทำความเข้าใจเพิ่มเติม ประการแรก การสืบทอดช่วงเป็นแบบทิศทางเดียวและไม่มีการลบข้อมูลทำลาย: วัตถุลูกที่มีค่า PageRange ว่างเปล่าจะรับสืบทอดช่วงของวัตถุแม่ที่เป็นคอมโพสิตชั่วคราวระหว่างการใช้งาน Apply ในขณะที่วัตถุลูกที่ตั้งค่าช่วงของตัวเองไว้จะรักษาค่าเดิมไว้ ดังนั้นคุณจึงสามารถตั้งค่าส่วนท้ายไว้ที่ทุกหน้าได้ ในขณะที่ประทับตราแนวทะแยงมุมเฉพาะบนส่วนของเอกสารร่างเท่านั้น ประการที่สอง ความเป็นเจ้าของจะเป็นไปตามคอมโพสิต: TPDFlibCompositeWatermark.Destroy จะล้างหน่วยความจำของอ็อบเจกต์ลูกทั้งหมด ดังนั้นคุณเพียงแค่ล้างหน่วยความจำของคอมโพสิตเพียงครั้งเดียวและไม่ต้องกังวลสิ่งอื่นใด ดังเช่นในตัวอย่างข้างต้น
ขอบเขตที่ควรทราบ
มีข้อจำกัดที่จำเป็นต้องชี้แจงสองประการ ซึ่งทั้งสองประการเป็นผลมาจากหลักการทำงานของกลไกนี้ ประการแรก การจับภาพจะครอบคลุมเฉพาะสตรีมเนื้อหาและทรัพยากรของหน้าเท่านั้น คำอธิบายประกอบ, ลิงก์, ฟิลด์ฟอร์ม และข้อคิดเห็นจะถูกเก็บเป็นวัตถุแยกต่างหากในพจนานุกรมของหน้า ไม่ได้อยู่ในสตรีมเนื้อหา ดังนั้นพวกมันจึงไม่ได้ย้ายเข้าไปใน XObject ด้วย เลย์เอาต์ที่เย็บหน้าแล้วจะนำเนื้อหาของหน้าที่มองเห็นได้ไปด้วย แต่ไม่มีเลเยอร์แบบโต้ตอบ (interactive layer) ของต้นฉบับ ประการที่สอง การระบุตำแหน่งข้อความ 9 จุดเป็นการประมาณค่า: หากไม่มีเลย์เอาต์เอนจินแบบเต็มรูปแบบ ไลบรารีจะไม่สามารถทราบความกว้างที่แท้จริงของการเรนเดอร์สตริงได้ ดังนั้นจึงประมาณการกล่องข้อความไว้ที่ 40% ของความกว้างหน้าเมื่อต้องแปลตำแหน่งอย่างเช่น wpCenter วิธีนี้มีความถูกต้องเพียงพอสำหรับการจัดกึ่งกลางป้ายข้อความสั้นๆ แต่สำหรับการวางพิกัดที่แม่นยำในระดับพิกเซลของสตริงที่ยาว ให้สลับไปใช้ wpCustom และคำนวณพิกัดด้วยตัวคุณเอง
มาตราส่วนไม่ใช่ข้อจำกัดในเชิงปฏิบัติของที่นี่ เนื่องจากการจับภาพและการเย็บหน้าจะเป็นการดำเนินการกับจุดอ้างอิงอ็อบเจกต์ (object references) แทนที่จะเป็นการเข้ารหัสเนื้อหาของหน้าใหม่ หากแหล่งข้อมูลของคุณมีขนาดหลายร้อยเมกะไบต์ ให้ใช้ระเบียบปฏิบัติเดียวกันกับที่อธิบายไว้ในคู่มือการรวมและการแยกไฟล์ขนาดใหญ่ด้วยการเข้าถึงโดยตรง ก่อนที่คุณจะเย็บหน้า ฟังก์ชันเย็บหน้าและคลาสเทมเพลตลายน้ำที่แสดงไว้ที่นี่มีมาพร้อมกับ losLab PDF Library ปัจจุบันสำหรับ Delphi, C# และ VB.NET ควบคู่ไปกับคำสั่งระดับล่างเช่น CapturePage, DrawCapturedPage และ DrawCapturedPageMatrix เมื่อคุณต้องการเลย์เอาต์ที่การเรียกเย็บหน้าสำเร็จรูปทั้งสามวิธีไม่ครอบคลุม