การปั๊มลายน้ำหรือโลโก้ลงบนทุกหน้าของเอกสารดูเหมือนเป็นงานที่ใช้เวลาเพียงห้านาที จนกระทั่งคุณเปิดผลลัพธ์ดูในเครื่องตรวจสอบขนาดไฟล์ วิธีการทั่วไปคือการไล่ดูทีละหน้า และสร้างข้อความหรือวัตถุรูปภาพเดิมซ้ำบนแต่ละหน้า วิธีการนั้นทำงานได้ดีในทางสายตา แต่มันเป็นการสิ้นเปลืองทรัพยากรที่สะสมตัว ลายน้ำแนวทแยงมุมระบุ "DRAFT" ที่วาดลงบนรายงานจำนวนหนึ่งร้อยหน้าโดยตรง จะเป็นการทำซ้ำข้อมูลเส้นทางและข้อความเดียวกันจำนวนหนึ่งร้อยชุดไปอยู่ในกระแสเนื้อหา และไฟล์ที่บันทึกจะเก็บข้อมูลทั้งหมดเหล่านั้นไว้
Form XObject คือโครงสร้างที่ PDF เตรียมไว้เพื่อหลีกเลี่ยงเหตุการณ์นี้โดยเฉพาะ มันจะห่อหุ้มเนื้อหาที่ใช้งานซ้ำได้ เช่น หน้าเอกสารทั้งหมดหรือเทมเพลตขนาดเล็ก ให้เป็นวัตถุที่มีชื่อตัวเดียวซึ่งสามารถนำมาระบายสีได้หลายครั้งในหลายๆ ตำแหน่ง เนื้อหาจะบันทึกอยู่ในไฟล์เพียงครั้งเดียว แต่ละหน้าที่จะนำตราปั๊มมาใช้จะเก็บคำสั่งสั้นๆ ที่ระบุว่า "ระบายสี XObject N ที่นี่ ด้วยการแปลงนี้" ลายน้ำสำหรับหน้าเอกสารหนึ่งร้อยหน้าจะเพิ่มวัตถุเนื้อหาเพียงตัวเดียวไปยังไฟล์แทนที่จะเป็นหนึ่งร้อยตัว และนั่นคือความแตกต่างระหว่างเอกสารที่ขนาดเพิ่มขึ้นเป็นเส้นตรงตามจำนวนหน้ากับเอกสารที่ไม่ได้เป็นเช่นนั้น ลายน้ำ ตราปั๊มโลโก้ เทมเพลตหมายเลขหน้า และตราประทับ ล้วนเป็นรูปแบบปัญหาเดียวกัน และ Form XObject คือเครื่องมือที่ถูกต้องสำหรับปัญหาเหล่านั้นทั้งหมด
ทำไมการบันทึกวัตถุตัวเดียวจึงดีกว่าการวาดใหม่เป็นร้อยครั้ง
การประหยัดนี้เป็นเรื่องของโครงสร้าง ไม่ใช่แค่ลักษณะภายนอก หน้า PDF จะเรนเดอร์ภาพโดยการรันกระแสเนื้อหาซึ่งเป็นลำดับของตัวดำเนินการวาดภาพ เมื่อคุณวาดตราประทับใหม่ในแต่ละหน้า คุณกำลังต่อเติมลำดับตัวดำเนินการทั้งหมดสำหรับตราประทับนั้นเข้ากับกระแสข้อมูลของทุกหน้า และไบต์ข้อมูลจะถูกทำซ้ำตามจำนวนหน้าที่มีอยู่ ส่วน Form XObject จะย้ายตัวดำเนินการเหล่านั้นไปอยู่ภายในกระแสข้อมูลเดี่ยวที่จัดเก็บไว้เพียงครั้งเดียวในเอกสาร การอ้างอิงที่แต่ละหน้าเก็บไว้จะมีขนาดเล็กมาก: มันจะใส่เมทริกซ์การแปลง เรียกใช้งาน XObject และคืนค่าสถานะเดิม จำนวนหน้าจะไม่เป็นตัวคูณต้นทุนการจัดเก็บผลงานศิลปะอีกต่อไป
สิ่งนี้มีความสำคัญมากที่สุดเมื่อตราประทับมีรายละเอียดหนาแน่น ตราประทับเวกเตอร์ที่มีเซกเมนต์เส้นทางเป็นร้อยส่วน หรือโลโก้แบบบิตแมป มีต้นทุนการจัดเก็บสูง เมื่อจัดเก็บเพียงครั้งเดียวและใช้อ้างอิงแทน ส่วนที่มีรายละเอียดมากจะจ่ายต้นทุนการจัดเก็บเพียงครั้งเดียว และภาระงานส่วนเกินบนแต่ละหน้าจะเป็นเพียงคำสั่งเรียกใช้งานไม่กี่ไบต์ ผลลัพธ์ทางสายตาบนหน้ากระดาษจะเหมือนกับการวาดใหม่โดยตรงทุกประการ ซึ่งนั่นคือประเด็นสำคัญ ผู้อ่านจะไม่เห็นความแตกต่าง แต่ขนาดไฟล์จะเห็นได้อย่างชัดเจน
การแคปเจอร์หน้ากระดาษเข้าสู่ XObject
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...PDFium สร้างวัตถุที่ใช้งานซ้ำได้จากหน้ากระดาษที่มีอยู่เดิม แหล่งข้อมูลคือหน้ากระดาษในเอกสารที่คุณกำลังเปิดใช้งาน ไฟล์ PDF ขนาดเล็กหน้าเดียวที่ไม่มีอะไรนอกจากลายน้ำงานศิลปะของคุณ หรือหน้าเฉพาะของไฟล์ขนาดใหญ่ ฟังก์ชัน CreateXObjectFromPage จะแคปเจอร์เนื้อหาของหน้าต้นทางนั้นเข้าสู่ตัวจัดการ (handle) ที่ใช้งานซ้ำได้ซึ่งเป็นของเอกสารปลายทาง ที่คุณต้องการนำตราประทับไปใช้
การจัดวางตราประทับลงบนหน้าเอกสาร
XObject ที่แคปเจอร์มาได้จะไม่มีการทำงานใดๆ ด้วยตนเอง เพื่อให้มันแสดงผลขึ้นมา คุณจะต้องแทรกสำเนาของมันลงบนหน้าปัจจุบันของเอกสารด้วย InsertFormObjectFromXObject การเรียกใช้นั้นจะส่งกลับวัตถุหน้ากระดาษภายใน FPDF_PAGEOBJECT และตัวจัดการที่ส่งคืนคือวิธีที่คุณใช้กำหนดตำแหน่งจัดวาง หากไม่มีการกำหนดการแปลง ตราประทับจะวางลงบนจุดเริ่มต้นในระบบพิกัดของหน้าต้นทางเอง ซึ่งไม่ค่อยเป็นจุดที่คุณต้องการใช้งานจริง
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;เนื่องจาก InsertFormObjectFromXObject จะแทรกสำเนาหนึ่งชุดต่อการเรียกใช้แต่ละครั้งและส่งคืนวัตถุหน้าใหม่ให้ในทุกครั้ง คุณจึงสามารถวาด XObject ตัวเดียวกันหลายครั้งบนหน้าเดี่ยวด้วยการแปลงที่แตกต่างกันได้ และเนื้อหาที่จัดเก็บจะยังคงถูกนับเพียงครั้งเดียวในไฟล์ โลโก้ตรงมุมกระดาษและลายน้ำจางๆ เต็มหน้ากระดาษสามารถสร้างมาจากวัตถุที่แคปเจอร์มาตัวเดียวกันได้
กฎอายุการใช้งานตัวจัดการที่มักทำให้เกิดปัญหา
ข้อจำกัดสองประการที่ควบคุมตัวจัดการ XObject และการละเลยข้อใดข้อหนึ่งจะทำให้เกิดความล้มเหลวที่ดูเหมือนจะไม่เกี่ยวข้องกับสาเหตุ ประการแรก เอกสารต้นทางจะต้องอยู่ในสถานะใช้งานอยู่ในขณะที่คุณเรียกใช้ CreateXObjectFromPage การแคปเจอร์จะอ่านเนื้อหาหน้าต้นทางจากเอกสารต้นทางที่ใช้งานจริง ดังนั้นเอกสารและหน้านั้นจะต้องเปิดอยู่และถูกต้องเมื่อตัวจัดการถูกสร้างขึ้น ประการที่สอง และนี่คือสิ่งที่มักสร้างความประหลาดใจ ตัวจัดการจะต้องได้รับการคืนหน่วยความจำก่อนที่หน้าต้นทางจะถูกปิด และในทางปฏิบัติคือต้องทำก่อนที่คุณจะปิดหรือคืนหน่วยความจำเอกสารต้นทางที่เป็นแหล่งที่มาของมัน
เหตุผลคือ XObject เป็นโครงสร้างอ้างอิงที่เอกสารต้นทางยังคงเป็นเจ้าของอยู่ มันไม่ใช่สำเนาที่เป็นอิสระในตัวเองที่คุณสามารถพกพาไปใช้งานต่อหลังจากแหล่งข้อมูลต้นทางถูกลบไปแล้ว ปิดต้นทางก่อนและตัวจัดการจะถูกทิ้งให้ชี้ไปยังเนื้อหาที่ถูกทำลายไปแล้ว ดังนั้นการคืนหน่วยความจำในภายหลัง หรือการใช้งานอื่นๆ จะเป็นการดำเนินการบนหน่วยความจำที่ใช้งานไม่ได้อีกต่อไป อาการคืออาการคลาสสิกของตัวจัดการที่ค้างอยู่ (dangling handle): ข้อผิดพลาดการเข้าถึงหน่วยความจำ (access violation) ในตอนปิดโปรแกรม หรือความเสียหายแบบสุ่มที่เปลี่ยนจุดไปมาขึ้นอยู่กับลำดับการจัดสรรหน่วยความจำ โดยมีคอลสแต็กชี้ไปยังโค้ดทำความสะอาดแทนที่จะชี้ไปยังบรรทัดที่เป็นสาเหตุที่แท้จริง วิธีแก้ไขคือการจัดลำดับการทำงาน ไม่ใช่การเขียนโค้ดป้องกัน สร้าง XObject แทรกมันลงบนทุกหน้าการทำงาน คืนหน่วยความจำ XObject แล้วจึงค่อยปิดเอกสารต้นหลังสุด ตัวทำลายของ TPdfXObject จะปล่อยตัวจัดการ PDFium ภายในให้คุณ ดังนั้นการคืนหน่วยความจำตัวห่อหุ้มในเวลาที่ถูกต้องจึงเป็นหน้าที่ทั้งหมดของคุณ
เมทริกซ์และความหมายของตัวเลขทั้งหกตัว
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)การจัดวางคือการแปลงแบบ 2D affine แบบเดียวกับที่ PDF ใช้ในทุกที่เพื่อกำหนดตำแหน่งเนื้อหา (ISO 32000-1 หัวข้อ 8.3.4) ประกอบด้วยตัวเลขหกตัว เขียนแทนด้วย a, b, c, d, e, f และ PDFium จะเปิดเผยข้อมูลเหล่านี้ในรูปแบบของเรกคอร์ด FS_MATRIX พวกมันจะแมปจุดจากพื้นที่ของตัววัตถุเองไปยังพื้นที่หน้ากระดาษ:
การปั๊มตราทุกหน้าในลำดับขั้นตอนที่ถูกต้อง
รูปแบบการทำงานเต็มรูปแบบจะนำชิ้นส่วนต่างๆ มารวมกันด้วยลำดับการทำงานที่กฎอายุการใช้งานต้องการ เปิดเอกสารทั้งสอง แคปเจอร์ตราประทับหนึ่งครั้ง ไล่ตามหน้าปลายทางเพื่อเลือกแต่ละหน้าตามลำดับและแทรกพร้อมกำหนดตำแหน่งของสำเนา จากนั้นคืนหน่วยความจำ XObject แล้วสั่งบันทึกไฟล์ และปล่อยให้เอกสารต้นทางปิดตัวลงในขั้นตอนสุดท้าย
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;โครงสร้างของบล็อก try ทำหน้าที่จัดการงานจริง บล็อก finally ด้านในจะคืนหน่วยความจำ XObject ก่อนที่โปรแกรมจะทำงานไปถึงบล็อก finally ด้านนอกที่ทำหน้าที่คืนหน่วยความจำ Stamp ดังนั้นตัวจัดการจะถูกปล่อยเสมอในขณะที่แหล่งข้อมูลต้นทางยังมีชีวิตอยู่ แม้ว่าจะเกิดข้อผิดพลาดขึ้นในระหว่างลูปก็ตาม จัดระเบียบการซ้อนคำสั่งนี้ให้ถูกต้องและกฎอายุการใช้งานจะได้รับการจัดการด้วยตัวเอง (ใช้ตัวเลือกหน้าปัจจุบันใดก็ตามที่ระบบของคุณแสดง โครงสร้างลูปจะเหมือนกันในทุกกรณี)
การปั๊มเป็นเพียงส่วนหนึ่งของชุดเครื่องมือขนาดใหญ่สำหรับสร้างและแก้ไขเนื้อหาหน้ากระดาษ หากตราประทับของคุณเป็นรูปภาพแทนที่จะเป็นหน้าที่แคปเจอร์มา การแปลงรูปภาพเป็นเอกสาร PDF ด้วย PDFium จะช่วยอธิบายวิธีการนำบิตแมปนั้นเข้าสู่เอกสารก่อน และเมื่อสิ่งที่คุณต้องการใส่ควบคู่ไปกับตราประทับที่มองเห็นได้คือไฟล์ข้อมูล ไม่ใช่หมึกบนหน้ากระดาษ การทำงานกับไฟล์แนบ PDF ใน Delphi จะช่วยแสดงฝั่งการจัดการไฟล์แนบ ทั้งหมดนี้จัดส่งมาในตัว PDFium Component สำหรับ Delphi และ C++Builder พร้อมด้วย API การแสดงผล การแก้ไข และเอกสารที่มีอธิบายในส่วนอื่นของบล็อกนี้