Technical Article

การปั๊มหน้าเอกสารซ้ำผ่าน Form XObject ด้วย PDFium

การปั๊มลายน้ำหรือโลโก้ลงบนทุกหน้าของเอกสารดูเหมือนเป็นงานที่ใช้เวลาเพียงห้านาที จนกระทั่งคุณเปิดผลลัพธ์ดูในเครื่องตรวจสอบขนาดไฟล์ วิธีการทั่วไปคือการไล่ดูทีละหน้า และสร้างข้อความหรือวัตถุรูปภาพเดิมซ้ำบนแต่ละหน้า วิธีการนั้นทำงานได้ดีในทางสายตา แต่มันเป็นการสิ้นเปลืองทรัพยากรที่สะสมตัว ลายน้ำแนวทแยงมุมระบุ "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 การแสดงผล การแก้ไข และเอกสารที่มีอธิบายในส่วนอื่นของบล็อกนี้