บทความเทคนิค

การปรับขนาดหน้า PDF เป็น 70% ด้วยไลบรารี losLab PDF

ขนาดของหน้า PDF จะถูกกำหนดตายตัวในตอนที่สร้างหน้ากระดาษขึ้นมา ดังนั้นคุณจึงไม่สามารถปรับขนาดเนื้อหาให้พอดีหน้าต่าง ๆ ในรูปแบบเดียวกับการปรับขนาดภาพได้ โมเดลของไลบรารีที่ทำให้การย่อขนาดสามารถใช้งานได้จริงคือ การจับภาพแล้ววาดใหม่ (capture-and-redraw): ดึงเนื้อหาของแต่ละหน้าออกมาจากเอกสารเก็บไว้ในแฮนเดิล สร้างหน้ากระดาษเปล่าแผ่นใหม่ด้วยขนาดกระดาษดั้งเดิม จากนั้นวาดเนื้อหาที่จับมาใส่กลับเข้าไปในขอบเขตที่ถูกย่อขนาดลง พื้นที่ว่างที่เหลือโดยรอบจะกลายเป็นขอบกระดาษ (margin) ยกตัวอย่างเช่น ที่อัตราส่วน 70% บนหน้ากระดาษ A4 จะมีพื้นที่เหลือด้านละ 15% ของความกว้างในแต่ละด้าน และใช้สัดส่วนเดียวกันสำหรับด้านบนและด้านล่าง ซึ่งนี่คือสิ่งที่การคำนวณขอบกระดาษด้านล่างนี้จะสร้างออกมา

CapturePage ทำงานอย่างไร

CapturePage จะรับหมายเลขหน้า ยกเนื้อหาของหน้านั้นไปเป็นออบเจกต์ภาพจับ (capture object) ในหน่วยความจำ และลบหน้านั้นออกจากต้นไม้กระดาษ (page tree) ของเอกสาร การลบทิ้งเป็นความตั้งใจและเป็นเหตุผลที่การวนลูป (loop) จะต้องเลือกหน้าที่ 1 เสมอ ไม่ว่าดัชนีของการวนซ้ำจะเป็นอะไร: ทันทีที่หน้า 1 ถูกจับภาพและลบทิ้ง สิ่งที่เคยเป็นหน้า 2 จะกลายเป็นหน้า 1 ใหม่แทน และจะเป็นเช่นนี้ต่อไปเรื่อย ๆ หากคุณเพิ่มตัวเลือกหน้ากระดาษไปพร้อมกับตัวนับรอบการวนลูป คุณจะข้ามหน้าแบบหน้าเว้นหน้าและได้ผลลัพธ์แค่ครึ่งเดียวของที่คาดไว้

แฮนเดิลการจับภาพที่ส่งคืนจาก CapturePage ไม่ใช่ตัวอ้างอิงหน้ากระดาษ; แต่มันเหมือนภาพรวม (snapshot) ของเนื้อหาเสียมากกว่า มันจะยังคงใช้งานได้จนกว่าคุณจะเรียก DrawCapturedPage หรือจนกว่าคุณจะปล่อย (release) มันทิ้งอย่างชัดเจน DrawCapturedPage ใช้แฮนเดิลนั้นควบคู่ไปกับรูปสี่เหลี่ยมปลายทางซึ่งให้มาเป็นค่าออฟเซ็ตจากด้านซ้าย ออฟเซ็ตจากด้านล่าง ความกว้าง และความสูง โดยมีหน่วยเป็นพอยต์ (points) ไลบรารีจะทำการปรับขนาดเนื้อหาที่จับได้ให้พอดีกับรูปสี่เหลี่ยมนั้นอย่างแม่นยำ โดยจะรักษาสัดส่วนของภาพ (aspect ratio) ไว้ก็ต่อเมื่อรูปสี่เหลี่ยมที่คุณให้มาบังเอิญตรงกับสัดส่วนดั้งเดิมเท่านั้น สำหรับการปรับขนาดแบบสมมาตร คุณจะต้องให้รูปสี่เหลี่ยมมีขนาดเท่ากับขนาดเดิมคูณด้วยอัตราส่วนการย่อขยาย แล้วจัดให้อยู่ตรงกลางหน้ากระดาษ

คณิตศาสตร์การจัดกึ่งกลาง

ด้วยอัตราการย่อขนาด 70% ส่วนที่เหลืออีก 30% ของแต่ละมิติกระดาษจะถูกแบ่งเท่า ๆ กันระหว่างสองด้าน ดังนั้นระยะห่างจากขอบแนวนอนคือ pageWidth * (1.0 - 0.70) / 2 ซึ่งเท่ากับ 15% ของความกว้าง และระยะห่างจากขอบแนวตั้งก็ใช้สูตรเดียวกันโดยใช้ความสูงของหน้ากระดาษ รูปสี่เหลี่ยมปลายทางสำหรับ DrawCapturedPage จะเริ่มต้นที่ (horizBorder, vertBorder) และมีความยาวเท่ากับ pageWidth - 2 * horizBorder คูณด้วย pageHeight - 2 * vertBorder การคำนวณนี้ไม่ได้จำเพาะเจาะจงกับไลบรารีใดเป็นพิเศษ มันเป็นเพียงเรขาคณิตของการนำสี่เหลี่ยมรูปเล็กกว่าไปวางไว้ตรงกลางสี่เหลี่ยมรูปใหญ่กว่าแบบสมมาตรเท่านั้น

สิ่งหนึ่งที่ควรทราบ: SetOrigin(1) เป็นการกำหนดจุดกำเนิดของพิกัดไว้ที่มุมซ้ายบนแทนที่จะเป็นมุมซ้ายล่าง ค่าของขอบกระดาษที่คุณส่งไปยัง DrawCapturedPage จะถูกวัดจากจุดกำเนิดไหนก็ตามที่คุณได้ตั้งค่าไว้ ดังนั้นหากคุณสลับโหมดจุดกำเนิดในระหว่างการโหลดและการวาด การจัดกึ่งกลางก็จะเพี้ยนไป

ตัวอย่างการใช้งานใน C#

โค้ดต่อไปนี้จะประมวลผลทุกหน้าของไฟล์ Pages.pdf ผ่านวงจรจับภาพแล้ววาดใหม่ และเขียนผลลัพธ์ลงใน newpages.pdf PDFL คือออบเจกต์ครอบ (wrapper object) ชนิด ActiveX/COM ที่เพิ่มเข้ามาในโปรเจ็กต์จากไฟล์ PDFlibDLL64.dll

private void ScalePages_Click(object sender, EventArgs e)
{
    File.Delete("newpages.pdf");

    double pageWidth, pageHeight, horizBorder, vertBorder;
    double scaleFactor = 0.70;
    int capturedPageId, ret;

    PDFL.LoadFromFile("Pages.pdf", "");
    PDFL.SetOrigin(1);

    int numPages = PDFL.PageCount();

    for (int i = 1; i <= numPages; i++)
    {
        // Always select page 1: CapturePage removes the page, so page 2
        // becomes page 1 on the next iteration.
        PDFL.SelectPage(1);

        pageWidth  = PDFL.PageWidth();
        pageHeight = PDFL.PageHeight();

        horizBorder = pageWidth  * (1.0 - scaleFactor) / 2;
        vertBorder  = pageHeight * (1.0 - scaleFactor) / 2;

        capturedPageId = PDFL.CapturePage(1);

        PDFL.NewPage();
        PDFL.SetPageDimensions(pageWidth, pageHeight);

        ret = PDFL.DrawCapturedPage(
            capturedPageId,
            horizBorder, vertBorder,
            pageWidth  - 2 * horizBorder,
            pageHeight - 2 * vertBorder);
    }

    PDFL.SaveToFile("newpages.pdf");
}

ตัวอย่างการใช้งานใน Delphi

เวอร์ชันสำหรับ Delphi จะใช้งาน TPDFlib โดยตรงแทนที่จะผ่านชั้นการทำงานของ COM แต่ลำดับการเรียกใช้งานก็เหมือนกันทุกประการ ความแตกต่างที่เห็นได้ชัดในทางปฏิบัติคือเรื่องของการป้องกันไฟล์ผลลัพธ์: จะใช้ FileExists ตามด้วย DeleteFile แทนที่จะเป็น File.Delete เนื่องจาก SaveToFile จะทำงานล้มเหลวหากไฟล์ปลายทางถูกล็อกโดยโปรแกรมอ่านเอกสารที่ยังถูกเปิดทิ้งไว้จากการรันในครั้งก่อนหน้า

procedure TForm1.ScalePagesClick(Sender: TObject);
var
  PDFLib: TPDFlib;
  pageWidth, pageHeight, horizBorder, vertBorder: Double;
  scaleFactor: Double;
  capturedPageId, ret, numPages, i: Integer;
begin
  if FileExists('newpages.pdf') then
    DeleteFile('newpages.pdf');

  scaleFactor := 0.70;

  PDFLib := TPDFlib.Create;
  try
    PDFLib.LoadFromFile('Pages.pdf', '');
    PDFLib.SetOrigin(1);

    numPages := PDFLib.PageCount();

    for i := 1 to numPages do
    begin
      PDFLib.SelectPage(1);

      pageWidth  := PDFLib.PageWidth();
      pageHeight := PDFLib.PageHeight();

      horizBorder := pageWidth  * (1.0 - scaleFactor) / 2;
      vertBorder  := pageHeight * (1.0 - scaleFactor) / 2;

      capturedPageId := PDFLib.CapturePage(1);

      PDFLib.NewPage();
      PDFLib.SetPageDimensions(pageWidth, pageHeight);

      ret := PDFLib.DrawCapturedPage(
        capturedPageId,
        horizBorder, vertBorder,
        pageWidth  - 2 * horizBorder,
        pageHeight - 2 * vertBorder);
    end;

    PDFLib.SaveToFile('newpages.pdf');
  finally
    PDFLib.Free;
  end;
end;

อัตราการย่อขยายนี้ควบคุมอะไรบ้าง

ค่า 0.70 ในที่นี้หมายความว่าเนื้อหาที่ถูกวาดลงไปจะกินพื้นที่ 70% ของมิติหน้ากระดาษ ไม่ได้หมายความว่าไฟล์จะมีขนาดลดลงเป็น 70% ของจำนวนไบต์ตั้งต้น ขนาดของไฟล์ที่ได้หลังจากการทำงานนี้จะขึ้นอยู่กับความซับซ้อนของเนื้อหาดั้งเดิม; หน้าที่มีรูปภาพขนาดใหญ่จะไม่หดขนาดลงตามสัดส่วน เนื่องจากการวาดพิกเซลจะทำด้วยความละเอียดเท่าเดิมแต่แค่อยู่ในพื้นที่ที่เล็กลง หากต้องการที่จะบีบอัดระดับไบต์ วิธีการที่ถูกต้องคือใช้ LinearizeFile หรือการบันทึกใหม่ด้วยโหมด stream compression ไม่ใช่ใช้วิธีการปรับสัดส่วนทางเรขาคณิต

ค่า 70% ก็ไม่ได้เป็นข้อจำกัดตายตัว สามารถใช้ค่าใด ๆ ระหว่าง 0.0 ถึง 1.0 ได้ และหากใช้ค่าเกิน 1.0 ก็จะเป็นการขยายเนื้อหาให้ล้นเกินกรอบหน้ากระดาษเดิม ซึ่งจะถูกตัดออกตรงขอบเส้นกระดาษ (media box) เว้นเสียแต่ว่าคุณจะเพิ่มขนาดมิติกระดาษตามไปด้วย เอกสารที่มีขนาดหน้ากระดาษผสมกันจะสามารถถูกจัดการได้อย่างเป็นธรรมชาติเนื่องจาก PageWidth และ PageHeight ถูกสอบถามแบบรายหน้าก่อนการคำนวณขอบหน้ากระดาษ ดังนั้นสำหรับเอกสารที่หน้าคี่เป็นขนาด A4 ส่วนหน้าคู่เป็นแบบ A3 จะมีผลลัพธ์เป็นชิ้นงานที่อยู่ถูกกึ่งกลางในหน้ากระดาษแต่ละขนาดโดยไม่ต้องเขียนโค้ดดักกรณีพิเศษแต่อย่างใด

ข้อควรระวังหรือสิ่งที่อาจผิดพลาด

มีรูปแบบของความล้มเหลวที่เกิดขึ้นบ่อย ๆ สองอย่าง อย่างแรกคือไฟล์ผลลัพธ์ถูกเปิดค้างไว้ในโปรแกรมดู PDF จากรอบการรันครั้งก่อนหน้า: SaveToFile จะแจ้งข้อผิดพลาดหรือทำงานบันทึกเป็นไบต์จำนวน 0 ออกมา (ขึ้นอยู่กับระบบปฏิบัติการ) และข้อมูลใหม่จะไม่ถูกนำมาลงไว้ โค้ดสำหรับดักทำการลบไฟล์ตั้งแต่เริ่มฟังก์ชันนั้นจัดการปัญหานี้ให้ระหว่างการพัฒนา แต่สำหรับระบบท่อส่งข้อมูลในหน้างานจริง (production pipeline) การเขียนไฟล์ลงในพาธชั่วคราวก่อน แล้วทำการเปลี่ยนชื่อก็ต่อเมื่อทำงานเสร็จสมบูรณ์ ถือเป็นวิธีที่ปลอดภัยกว่า

ปัญหาอย่างที่สองคือจำนวนหน้านับได้ไม่ตรง เพราะ CapturePage จะลบหน้ากระดาษออกจากเอกสารหลังจากประมวลผล จำนวนหน้ากระดาษที่คุณอ่านได้จาก PageCount() ในก่อนหน้าเข้าสู่ลูปจึงเป็นขอบเขต (bound) ที่ถูกต้องสำหรับตัวนับรอบการทำงาน การเรียก PageCount() ที่อยู่ด้านในวงลูปจะทำให้เกิดการส่งคืนตัวเลขที่ถอยหลังลงในแต่ละรอบและเกิดการสิ้นสุดก่อนเวลาอันควร ทำให้เหลือหน้าท้าย ๆ ที่ยังไม่ได้ประมวลผล ตัวแปรลูปในตัวอย่างถูกใช้สำหรับการนับจำนวนรอบที่เหลืออยู่เท่านั้น; มันไม่ได้ถูกนำมาใช้สำหรับการเลือกหน้ากระดาษเลย เพราะหน้าสำหรับเลือกจะต้องเป็น 1 ตลอดอย่างที่เคยอธิบายไปก่อนหน้านี้แล้ว

คำสั่งจัดการหน้ากระดาษที่ได้แสดงไว้ รวมถึง CapturePage, DrawCapturedPage, และ SetPageDimensions ล้วนเป็นส่วนหนึ่งของ losLab PDF Library สำหรับภาษา Delphi, C#, VB.NET และ C++