การผสาน (merge) และการแยก (split) ไฟล์คือฟังก์ชันจัดการหน้าสองแบบแรกที่ทุกคนมักจะเรียกใช้งาน และมันสามารถนำมาประยุกต์ใช้งานได้หลายอย่าง แต่มันก็ยังไม่สามารถตอบโจทย์ได้ครอบคลุมทุกเรื่อง ยังมีการทำงานอีกกลุ่มหนึ่งที่เน้นการจัดเรียงหน้าใหม่ภายในมากกว่าการย้ายไฟล์ทั้งไฟล์ เช่น การจัดวางสไลด์สี่หน้าไว้ในกระดาษแผ่นเดียวสำหรับทำเอกสารแจก การลากหน้ากระดาษจากท้ายเอกสารมาไว้ด้านหน้า หรือการคัดแยกหน้า 3, 7 และ 12 ออกมาเป็นไฟล์ย่อยโดยไม่แก้ไขข้อมูลส่วนอื่น ๆ PDFium มีสามเมธอดที่ออกแบบมาเพื่องานด้านนี้โดยเฉพาะ และแต่ละตัวจะมีพฤติกรรมการทำงานที่แตกต่างจากฟังก์ชันการผสานและการแยกข้อมูลทั่วไปที่คุณรู้จัก บทความนี้จะพาทัวร์ข้อมูลการทำงานของมัน ตำแหน่งของจุดส่งออกไฟล์ผลลัพธ์ และรายละเอียดเรื่องสิทธิ์ความเป็นเจ้าของจุดหนึ่งที่เคยสร้างปัญหาทำให้ระบบหยุดทำงานมาแล้ว
ทั้งสามเมธอดนี้ได้แก่ ImportNPagesToOne สำหรับการจัดหน้าแบบ N-up, MovePages สำหรับจัดเรียงหน้าใหม่ในเครื่อง และ ImportPagesByIndex สำหรับคัดแยกบางหน้าออกมา ฟังก์ชันผสานข้อมูลจะนำเอกสารมาวางต่อท้ายกันและส่งผลให้จำนวนหน้ารวมเท่ากับผลรวมของไฟล์อินพุต ส่วนฟังก์ชันแยกไฟล์จะเขียนไฟล์ผลลัพธ์ย่อยออกมาหลายไฟล์จากอินพุตเดียว ส่วนการประมวลผลทั้งสามแบบในที่นี้จะเป็นรูปแบบที่อยู่ตรงกลาง: แบบแรกจะเปลี่ยนจำนวนหน้าต้นฉบับที่จะแบ่งปันพื้นที่ร่วมกันในหน้ากระดาษแผ่นเดียว แบบที่สองจะสลับลำดับหน้าภายในเอกสารเดียว และแบบที่สามจะคัดลอกเฉพาะบางหน้าที่เลือกไปยังเอกสารอื่น การทำความเข้าใจการทำงานของแต่ละตัวจะช่วยให้คุณประหยัดเวลาไม่ต้องรันลำดับคำสั่งผสานและสั่งลบโดยไม่จำเป็น ทั้งที่การเรียกใช้เพียงคำสั่งเดียวก็เพียงพอแล้ว
การจัดหน้าแบบ N-up ทำหน้าที่อะไรจริง ๆ
Imposition คือคำศัพท์ด้านการพิมพ์ที่ใช้เรียกการจัดเรียงหน้ากระดาษต้นฉบับหลายหน้าลงในกระดาษแผ่นใหญ่แผ่นเดียว เพื่อให้เมื่อพิมพ์และพับออกแล้วจะได้เอกสารที่มีลำดับหน้าถูกต้อง รูปแบบการทำที่พบเห็นได้บ่อยในชีวิตประจำวันคือเอกสารแจกแบบ 2-up สมุดเล่มเล็กแบบ 4-up หรือแผ่นภาพตัวอย่าง (contact sheet) ที่รวมภาพตัวอย่างย่อขนาดจำนวนมากไว้ในหน้าเดียว PDFium จัดการงานด้านมิติพิกัดเหล่านี้ผ่านคำสั่งเรียกเดียว:
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
พารามิเตอร์ NumX และ NumY จะอธิบายทิศทางตาราง ค่าของ 2, 1 จะวางหน้าต้นฉบับสองหน้าเรียงข้างกัน 2, 2 จะจัดหน้าสี่หน้าให้อยู่ในสี่มุมกระดาษ 4, 3 จะสร้างแผ่นภาพตัวอย่างแบบสิบสองภาพในหนึ่งหน้า PDFium จะอ่านหน้าต้นฉบับตามลำดับ ย่อขนาดแต่ละหน้าให้พอดีกับช่องตาราง และเติมข้อมูลลงในตารางจากซ้ายไปขวา บนลงล่าง พร้อมเริ่มต้นสร้างหน้ากระดาษผลลัพธ์แผ่นใหม่ทันทีที่ตารางปัจจุบันเต็ม หน้าต้นฉบับจะไม่ได้รับการแก้ไขใด ๆ สิ่งที่คุณจะได้รับกลับคืนมาคือเอกสารฉบับใหม่ที่หน้าที่ประกอบร่างขึ้นมาใหม่
ขนาดผลลัพธ์ระบุในหน่วยพอยต์ไม่ใช่พิกเซล
พารามิเตอร์ OutputWidth และ OutputHeight คือหน่วยใช้งานของ PDF โดยหนึ่งหน่วยใช้งาน PDF จะมีค่าเท่ากับหนึ่งพอยต์ (point) ซึ่งมีขนาดเป็นหนึ่งในเจ็ดสิบสองของนิ้ว หน่วยนี้ใช้ระบุขนาดจริงทางกายภาพของหน้ากระดาษผลลัพธ์ และไม่มีส่วนเกี่ยวข้องใด ๆ กับจำนวนพิกเซลหน้าจอหรือค่า render DPI นี่คือจุดที่เกิดข้อผิดพลาดในการจัดหน้าได้บ่อยที่สุด เนื่องจากโปรแกรมเมอร์ที่คุ้นชินกับการประมวลผลบิตแมปมักจะกำหนดค่าเป็นพิกเซลและส่งผลให้ได้หน้ากระดาษผลลัพธ์ที่มีขนาดเท่ากับแสตมป์ส่งจดหมายหรือแผ่นป้ายบิลบอร์ดโฆษณาขนาดใหญ่แทน
ตัวเลขที่คุณควรจำคือขนาดหน้ากระดาษสองแบบที่คุณจะได้ใช้งานบ่อยที่สุด ขนาด US Letter คือ 612 คูณ 792 พอยต์ เนื่องจากขนาด 8.5 นิ้วคูณ 72 เท่ากับ 612 และ 11 นิ้วคูณ 72 เท่ากับ 792 ส่วนขนาด A4 จะมีขนาดประมาณ 595 คูณ 842 พอยต์ ซึ่งมาจากขนาดกว้างยาว 210 คูณ 297 มิลลิเมตร ในส่วนหัวของตัวเชื่อมโยงจะระบุความสัมพันธ์นี้ไว้อย่างชัดเจนว่าหนึ่งหน่วยคือเศษหนึ่งส่วนเจ็ดสิบสองของนิ้ว และตัวยูนิตจะมีค่าคงที่ PointsPerInch เท่ากับ 72 พร้อมใช้งานหากคุณต้องการให้คำนวณขนาดจากนิ้วในโค้ดแทนการเขียนตัวเลขตรง ๆ
const
LetterW = 612.0; // 8.5 in * 72
LetterH = 792.0; // 11 in * 72
var
Source, Composite: TPdf;
begin
Source := TPdf.Create(nil);
Composite := nil;
try
Source.FileName := 'slides.pdf';
Source.Active := True;
// Four source pages per Letter sheet, 2 by 2 grid.
Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
if Composite = nil then
raise Exception.Create('PDFium rejected the imposition arguments');
Composite.SaveAs('slides-4up.pdf');
finally
Composite.Free; // see the next section: this is mandatory
Source.Free;
end;
end;
ตัวจัดการที่ส่งกลับคืนมาเป็นหน้าที่ของคุณที่จะต้องสั่งเคลียร์หน่วยความจำ
ลองดูประกาศของคำสั่งนี้อีกครั้ง ImportNPagesToOne จะคืนค่ากลับมาเป็นอ็อบเจกต์ TPdf ไม่ใช่บูลีน ค่าที่ส่งกลับนี้คือตัวจัดการเอกสาร (document handle) ฉบับใหม่ที่ถูกจองแยกต่างหากจากแหล่งข้อมูลต้นทาง และตัวผู้เรียกใช้จะมีสิทธิ์เป็นเจ้าของมัน ตัวแปรต้นฉบับ TPdf ที่คุณเรียกใช้เมธอดจะยังคงเหมือนเดิมและเป็นเจ้าของตัวจัดการของมันเอง ส่วนผลลัพธ์ที่จัดหน้าแล้วจะเป็นอ็อบเจกต์ที่สองที่เป็นอิสระแยกออกไป หากคุณปล่อยให้ TPdf ที่ส่งกลับหลุดขอบเขตไปโดยที่ไม่มีการเรียกปลดปล่อยหน่วยความจำ คุณจะสร้างความเสียหายทำให้เกิดข้อมูลเอกสาร PDFium รั่วไหลทั้งเอกสาร
ข้อผิดพลาดที่อันตรายกว่านั้นจะทำงานในทางตรงกันข้าม โดยเบื้องหลังเมธอดนี้จะร้องขอ FPDF_DOCUMENT ชุดใหม่จาก PDFium ผ่านฟังก์ชัน FPDF_ImportNPagesToOne จากนั้นนำตัวจัดการดิบดังกล่าวมาครอบไว้ใน TPdf เพื่อให้ช่วงชีวิตการทำงานของตัวครอบควบคุมการทำงานของตัวจัดการ ตั้งแต่นั้นเป็นต้นไป จะมีเจ้าของตัวจัดการเพียงหนึ่งเดียว และจุดที่ควรสั่งปิดตัวจัดการจะมีเพียงจุดเดียวเท่านั้น: นั่นคือตอนที่คุณเรียกสั่ง Free อ็อบเจกต์ที่ได้รับคืนมา หากพาธกรณีเกิดข้อผิดพลาดรันคำสั่งโดยไม่ระมัดระวัง สั่งปลดปล่อยตัวครอบและเรียกใช้ FPDF_CloseDocument กับตัวจัดการดิบไปพร้อมกัน จะเท่ากับสั่งปิดเอกสาร PDFium เดียวกันสองครั้ง ซึ่งนำไปสู่ความเสียหายแบบ double-free และนี่คือบั๊กที่เคยสร้างปัญหาให้กับโค้ดใช้งานจุดหนึ่ง กฎป้องกันปัญหานี้สั้นมาก: ให้สั่งปิดเอกสารในเส้นทางเดียวเท่านั้นโดยการปลดปล่อย TPdf ที่เมธอดส่งคืนให้คุณ และห้ามเข้าถึงข้ามตัวครอบเพื่อสั่งปิดตัวจัดการที่ถูกรับช่วงดูแลไปแล้วเป็นอันขาด
มีสองข้อปฏิบัติหลักที่ได้จากสิ่งนี้ ประการแรก เมธอดจะคืนค่า nil เสมอเมื่อ PDFium ปฏิเสธพารามิเตอร์ที่ระบุเข้ามา เช่น มีการระบุเลขศูนย์ในแกนตารางหรือเกิดปัญหาการจองพื้นที่ล้มเหลว ดังนั้นควรมีขั้นตอนการตรวจสอบค่าเป็น nil ก่อนที่จะเริ่มจัดการกับผลลัพธ์ ประการที่สอง ให้กำหนดค่าตัวแปรผลลัพธ์เริ่มต้นเป็น nil ก่อนเข้าบล็อก try และสั่งปลดปล่อยมันในบล็อก finally ดังตัวอย่างข้างต้น เพื่อไม่ให้ความล้มเหลวระหว่างขั้นตอนส่งผลเสียทำให้เกิดการพยายามปลดปล่อยตัวแปรที่ไม่ได้ประกาศค่าหรือข้ามขั้นตอนการปลดปล่อยหน่วยความจำไปทั้งหมด
การจัดเรียงหน้าใหม่โดยไม่ต้องเขียนเนื้อหาใหม่
การจัดหน้าจะสร้างเอกสารชุดใหม่ขึ้นมา ส่วนการจัดเรียงหน้าใหม่จะปรับเปลี่ยนลำดับในเอกสารเดิมในเครื่อง เมธอด MovePages จะหยิบกลุ่มหน้ากระดาษที่ต้องการออกจากตำแหน่งปัจจุบันและวางมันลงในตำแหน่งปลายทาง พร้อมเลื่อนหน้ากระดาษส่วนที่เหลือรอบ ๆ บล็อกย้ายเพื่อให้จำนวนหน้ารวมยังคงเท่าเดิม:
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
ดัชนีของหน้ากระดาษจะเริ่มต้นด้วยเลขศูนย์ PageIndices จะเก็บรายชื่อหน้าที่ต้องการย้าย ตามลำดับที่ต้องการจัดเรียง และ DestPageIndex คือดัชนีที่หน้าแรกของกลุ่มที่ย้ายจะไปวางตำแหน่งหลังจากกระบวนการย้ายเสร็จสิ้น เนื่องจาก PDFium จะทำการปรับตำแหน่งย้ายหน้ากระดาษแทนการคัดลอกและบีบอัดข้อมูลเนื้อหาใหม่ การดำเนินการนี้จึงมีราคาประหยัดและไม่มีการสูญเสียข้อมูลเลย: อ็อบเจกต์หน้ากระดาษจะยังคงเก็บสตรีม ทรัพยากร และความถูกต้องครบถ้วนดั้งเดิมเอาไว้ นี่คือคำสั่งเรียกใช้งานที่อยู่เบื้องหลังแผงควบคุมการลากเพื่อจัดเรียงลำดับหน้ากระดาษ ซึ่งผู้ใช้จะดึงภาพตัวอย่างย่อขนาดไปยังช่องใหม่และคุณจะบันทึกการจัดเรียงลำดับใหม่ด้วยการย้ายเพียงครั้งเดียว คำสั่งจะส่งคืนค่า False เมื่อระบุดัชนีเกินขอบเขตจริง ดังนั้นควรอ่านค่าวิเคราะห์ผลลัพธ์เสมอแทนที่จะคิดเอาเองว่าจัดลำดับสำเร็จแล้ว
var
Doc: TPdf;
begin
Doc := TPdf.Create(nil);
try
Doc.FileName := 'report.pdf';
Doc.Active := True;
// Move the last page (index 4 in a 5-page file) to the very front.
if not Doc.MovePages([4], 0) then
raise Exception.Create('MovePages rejected the index');
Doc.SaveAs('report-reordered.pdf');
finally
Doc.Free;
end;
end;
การดึงข้อมูลบางส่วนด้วยระบบดัชนี
การดำเนินการแบบที่สามจะคัดลอกกลุ่มหน้าที่เลือกอย่างเฉพาะเจาะจงจากเอกสารหนึ่งไปยังอีกเอกสารหนึ่ง เมธอด ImportPagesByIndex จะรับเอกสารแหล่งข้อมูลต้นทางและอาร์เรย์ดัชนีแบบเริ่มต้นด้วยศูนย์ และนำหน้าที่เลือกดังกล่าวไปแทรกลงในเอกสารปลายทางที่ตำแหน่งที่ต้องการ:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
คุณจะเรียกใช้เมธอดนี้บนเอกสารปลายทางและส่งเอกสารต้นฉบับเข้าในอาร์กิวเมนต์ตัวแรก พารามิเตอร์ PageIndices จะระบุหน้าที่คุณต้องการดึงมาจากต้นฉบับ ตามลำดับการวางที่คุณต้องการ InsertAt คือตำแหน่งแบบเริ่มต้นด้วยศูนย์ในปลายทางที่หน้าแรกที่นำเข้าจะไปวางตำแหน่งตำแหน่ง ดังนั้น 0 จะเป็นการวางตำแหน่งก่อนหน้ากระดาษแรกที่มีอยู่เดิม และจำนวนหน้าปัจจุบันของปลายทางจะถูกขยายออกไป หากส่งค่าอาร์เรย์ว่างเข้ามาจะเป็นการนำเข้าหน้ากระดาษทุกหน้า ซึ่งมีประโยชน์เมื่อต้องการคัดลอกไฟล์ทั้งหมด เมธอดจะคืนค่า False หากมีดัชนีระบุเกินขอบเขตจริงในแหล่งข้อมูลต้นทาง
นี่คือจุดที่ความแตกต่างจากคำสั่งแยกไฟล์มีความสำคัญ ฟังก์ชันแยกไฟล์จะเขียนไฟล์แยกต่างหากออกมาหลายไฟล์ การทำงานครั้งเดียวจะสร้างเอาต์พุตหลายชุดบนดิสก์ แต่ ImportPagesByIndex จะทำรูปงานในมุมตรงกันข้าม: มันจะรวบรวมกลุ่มหน้าที่เลือกไว้เข้ามาในเอกสารปลายทางเพียงเอกสารเดียวในหน่วยความจำ ซึ่งหลังจากนั้นคุณสามารถบันทึกเพียงครั้งเดียว เมื่อโจทย์ของงานคือ "คัดแยกหน้า 3, 7 และ 12 มารวมในไฟล์ PDF สั้น ๆ ไฟล์เดียว" นี่คือช่องทางการทำงานที่สั้นและตรงจุดที่สุด โดยในส่วนเบื้องหลังจะเรียกใช้งานฟังก์ชัน FPDF_ImportPagesByIndex
var
Source, Excerpt: TPdf;
begin
Source := TPdf.Create(nil);
Excerpt := TPdf.Create(nil);
try
Source.FileName := 'manual.pdf';
Source.Active := True;
Excerpt.CreateDocument; // start an empty target
// Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
raise Exception.Create('A requested page index is out of range');
Excerpt.SaveAs('manual-excerpt.pdf');
finally
Excerpt.Free;
Source.Free;
end;
end;
การรวบรวมการทำงานอย่างหมดจด
รูปการทำงานแบบต้นจนจบจะมีความคล้ายกันในเมธอดทั้งสามแบบ: ให้เปิดไฟล์ต้นฉบับโดยกำหนดค่า FileName และปรับสถานะ Active เป็น True สั่งดำเนินการประมวลผล บันทึกไฟล์ด้วยคำสั่ง SaveAs และปลดปล่อยหน่วยความจำของอ็อบเจกต์ที่คุณครอบครอง สิทธิ์การเป็นเจ้าของที่ต้องการความระมัดระวังเป็นพิเศษคือการระบุว่าคำสั่งใดที่มีการจองพื้นที่เอกสารฉบับใหม่ เมธอด MovePages จะปรับเปลี่ยนเอกสารเดิมที่คุณถือครองอยู่แล้ว จึงมีอ็อบเจกต์เดียวที่ต้องสั่งคืนหน่วยความจำ ImportPagesByIndex จะเขียนเนื้อหาไปยังเอกสารปลายทางที่คุณสร้างขึ้นเอง คุณจึงต้องปลดปล่อยทั้งต้นทางและปลายทางที่คุณเป็นผู้เปิด ส่วน ImportNPagesToOne จะมีความต่างไปจากตัวอื่น เนื่องจากเอกสารชุดใหม่คือค่าส่งคืนกลับมาจากเมธอด ไม่ใช่สิ่งที่คุณสร้างขึ้นมาเอง และการลืมว่าสิทธิ์ตัวจัดการนี้เป็นตัวแยกอิสระที่เป็นของฝั่งผู้เรียกคือสาเหตุหลักที่สร้างปัญหาข้อมูลรั่วไหลและข้อบกพร่อง double-free ให้กำหนดค่าเริ่มต้นตัวแปรเป็น nil เสนอ ตรวจสอบค่าผลลัพธ์หลังเรียกใช้งาน และสั่งคืนหน่วยความจำในพาธประมวลผลเส้นทางเดียว
หากงานที่คุณต้องทำจริง ๆ คือการรวมทั้งไฟล์เข้าด้วยกันแทนที่จะเป็นการจัดเรียงลำดับหน้ากระดาษ สามารถดูเพิ่มเติมได้ที่ การรวมไฟล์ PDF หลายไฟล์เข้าเป็นเอกสารเดียว หรือหากเป็นในทางกลับกัน คือการแบ่งไฟล์เอกสารเดียวออกเป็นหลายไฟล์ย่อย สามารถศึกษาได้จาก การแบ่งเอกสาร PDF ออกเป็นหลายไฟล์ เมธอดการจัดหน้าและการย้ายหน้ากระดาษที่อธิบายไว้ที่นี่พร้อมใช้งานในฐานะส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ C++Builder ร่วมกับ API สำหรับการโหลด การแสดงผล และการแก้ไขที่ครอบคลุมในส่วนอื่น ๆ ของบล็อกนี้