คำสั่งที่ใช้วางข้อความบนหน้า PDF นั้นตรงไปตรงมา คุณส่งสตริง ฟอนต์ ขนาด และตำแหน่งให้กับ AddText แล้วตัวอักขระก็จะปรากฏขึ้น แต่สิ่งที่มันไม่ได้ทำคือการบอกคุณว่าสตริงนั้นจะมีความกว้างเท่าใดเมื่อวาดเสร็จแล้ว และมันไม่ได้ตัดสตริงยาวๆ ออกเป็นหลายบรรทัด การเรียกหนึ่งครั้งจะวาดข้อความหนึ่งชุดที่ตำแหน่งหนึ่ง หากข้อความนั้นกว้างกว่าคอลัมน์ที่คุณต้องการให้พอดี มันก็จะเลยขอบออกไป และจะไม่มีสิ่งใดในคำสั่งวาดที่เตือนคุณ เมื่อคุณต้องการสร้างย่อหน้าแทนที่จะเป็นเพียงป้ายกำกับเดียว สิ่งที่ขาดหายไปคือความกว้างของสตริงในฟอนต์และขนาดที่เลือก ซึ่งจะต้องวัดก่อนที่คุณจะเขียนลงบนหน้ากระดาษ
นี่คือปัญหาคลาสสิกของการจัดวาง ในการตัดคำของย่อหน้าลงในคอลัมน์ คุณต้องรู้แบบคำต่อคำว่าแต่ละบรรทัดที่เป็นไปได้จะใช้พื้นที่แนวนอนเท่าใด และคุณต้องรู้ล่วงหน้าก่อนที่จะวาดสิ่งใดลงไป การตัดคำคือลูปของการวัดขนาดที่ครอบคลุมคำสั่งวาด และ binding ที่วาดได้อย่างเดียวจะให้เพียงแค่ส่วนที่สองเท่านั้น การรองรับการวัดขนาดข้อความในคอมโพเนนต์ PDFium ช่วยปิดช่องโหว่นั้นด้วยสองฟังก์ชัน ได้แก่ MeasureText และ MeasureTextWidth ซึ่งจะรายงานขนาดพื้นที่การเรนเดอร์ของสตริงโดยไม่ต้องสร้างร่องรอยใดๆ บนหน้ากระดาษ
ทำไมการวัดขนาดถึงเป็น class helper ไม่ใช่เมธอดใหม่ของ TPdf
การรองรับการวัดขนาดมาในรูปแบบของ Delphi class helper สำหรับ TPdf ซึ่งอยู่ในยูนิตของตัวเอง แทนที่จะเป็นเมธอดใหม่ที่ถูกยึดติดเข้าไปในคลาส TPdf Class helper เป็นฟีเจอร์ของภาษาที่ช่วยให้คุณสามารถแนบเมธอดเข้ากับชนิดข้อมูลที่มีอยู่จากภายนอกการประกาศ เมื่อยูนิตนั้นอยู่ในขอบเขต (scope) เมธอดใหม่จะถูกเรียกใช้งานเหมือนกับว่ามันเป็นส่วนหนึ่งของคลาส ดังนั้น helper เมธอดจึงถูกอ่านในรูปแบบ Pdf.MeasureTextWidth(...) โดยไม่ต้องสร้างหรือส่งผ่านอ็อบเจ็กต์แยกต่างหาก
เหตุผลในการจัดเลเยอร์แบบนี้คือการแยกส่วน ประเภทหลักของ TPdf ยังคงเหมือนเดิม โดยไม่มีการเพิ่มฟิลด์หรือแก้ไขโครงสร้าง (signature) ใดๆ ที่มีอยู่ ดังนั้นโปรเจกต์ที่ไม่เคยต้องการการจัดวางหน้าจะไม่ต้องนำเอาโค้ดส่วนการวัดขนาดไปใช้งาน โปรเจกต์ที่ต้องการใช้งานเพียงแค่เพิ่มหนึ่งยูนิตลงในส่วน uses และเมธอดต่างๆ ก็จะพร้อมใช้งาน ความสามารถนี้กลายเป็นตัวเลือกแบบ opt-in ในระดับของแต่ละยูนิต ซึ่งเป็นวิธีที่สะอาดที่สุดในการขยายชนิดข้อมูลที่คุณไม่ได้เป็นเจ้าของหรือไม่ต้องการรบกวน
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // ยูนิต helper ที่นำ MeasureText เข้ามาอยู่ในขอบเขตของ TPdf
// เมื่อยูนิตอยู่ในขอบเขต เมธอดต่างๆ จะถูกอ่านเหมือนเป็นสมาชิกของ TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// ตอนนี้ W และ H คือความกว้างและความสูงที่เรนเดอร์แล้วในหน่วย user unit ของ PDF
end;
การวัดขนาดโดยไม่ต้องแตะหน้ากระดาษ
การวัดขนาดต้องไม่มีผลข้างเคียงใดๆ มันต้องรายงานความกว้างโดยไม่ทิ้งสิ่งใดไว้เบื้องหลัง เพราะคุณต้องเรียกใช้งานหลายครั้งในขณะที่ตัดสินใจเลือกรูปแบบการจัดวาง และหน้ากระดาษต้องดูเหมือนเดิมทุกประการราวกับว่าคุณไม่เคยวัดขนาดเลย เทคนิคที่ทำให้สิ่งนี้เป็นไปได้คือการสร้างอ็อบเจ็กต์ข้อความขึ้นมา สอบถามขนาดของมัน แล้วโยนมันทิ้งไปก่อนที่มันจะถูกนำไปวางบนหน้ากระดาษ
ลำดับขั้นตอนประกอบด้วยการเรียกใช้ PDFium 4 ครั้ง FPDFPageObj_NewTextObj สร้างอ็อบเจ็กต์ข้อความสำหรับเอกสาร โดยระบุชื่อฟอนต์และขนาด FPDFText_SetText ตั้งค่าสตริงที่อ็อบเจ็กต์นั้นเก็บไว้ FPDFPageObj_GetBounds อ่านค่า bounding box (กรอบล้อมรอบ) ของอ็อบเจ็กต์กลับมา FPDFPageObj_Destroy ทำการคืนหน่วยความจำของอ็อบเจ็กต์ สิ่งสำคัญคือไม่มีสิ่งใดในลำดับขั้นตอนนี้ที่เรียกใช้ API สำหรับการแทรกหน้ากระดาษ อ็อบเจ็กต์ถูกสร้างขึ้น ถูกตรวจสอบ และถูกทำลายอย่างโดดเดี่ยว ดังนั้นเอกสารจะไม่เปลี่ยนแปลงเมื่อฟังก์ชันส่งค่ากลับ มันเป็นเครื่องมือตรวจสอบแบบใช้แล้วทิ้ง ซึ่งมีผลลัพธ์เพียงอย่างเดียวคือตัวเลขสี่ตัวของ bounding box
นี่เป็นวิธีที่แข็งแกร่งในการทำงาน เนื่องจาก PDFium ไม่ได้เปิดเผยความกว้างของแต่ละตัวอักขระให้คุณนำไปรวมกันเองได้อย่างสะดวก ตัววัดระยะของอักขระจะขึ้นอยู่กับโปรแกรมฟอนต์ การเข้ารหัส และวิธีที่ PDFium โหลดรูปแบบตัวอักษร และไม่มีการเรียกใช้แบบสาธารณะใดๆ ที่จะให้ข้อมูลความกว้างของตัวอักษรแต่ละตัวในสตริง ในทางกลับกัน bounding box ของอ็อบเจ็กต์ข้อความจริงจะถูกคำนวณโดยกลไกเดียวกับที่จะใช้จัดวางอักขระเพื่อการวาด ดังนั้นมันจึงสะท้อนขนาดพื้นที่การเรนเดอร์ที่แท้จริงแทนที่จะเป็นการประมาณการ การสร้างอ็อบเจ็กต์แบบใช้ครั้งเดียวทิ้งและอ่านค่าขอบเขตของมันเป็นการวัดขนาดที่น่าเชื่อถือที่สุดที่ไลบรารีจะให้ได้
// รูปแบบของ MeasureText แสดงเทียบกับการเรียกใช้ PDFium ที่ตรวจสอบแล้ว
// อ็อบเจ็กต์ข้อความจะถูกสร้าง วัดขนาด แล้วทำลาย โดยไม่มีหน้ากระดาษเข้ามาเกี่ยวข้อง
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
FontSize: Single; out Width, Height: Double);
var
TextObject: FPDF_PAGEOBJECT;
L, B, R, T: Single;
begin
Width := 0;
Height := 0;
if Self.Document = nil then
Exit;
TextObject := FPDFPageObj_NewTextObj(Self.Document,
FPDF_BYTESTRING(AnsiString(Font)), FontSize);
if TextObject = nil then
Exit;
try
if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
Exit;
if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
begin
Width := R - L;
Height := T - B;
end;
finally
FPDFPageObj_Destroy(TextObject); // ทิ้งตัว probe แล้ว หน้ากระดาษไม่ถูกแตะต้อง
end;
end;
พิกัดและหน่วยของผลลัพธ์
bounding box จะถูกส่งกลับมาเป็นขอบทั้งสี่ด้าน ได้แก่ ซ้าย, ล่าง, ขวา และ บน และขนาดทั้งสองมิติจะถูกคำนวณโดยการลบ ความกว้างคือขวาลบซ้าย และความสูงคือบนลบล่าง ทั้งสองค่าจะแสดงในหน่วยผู้ใช้ PDF (PDF user units) ซึ่งหนึ่งหน่วยเท่ากับเศษหนึ่งส่วนเจ็ดสิบสองนิ้ว ซึ่งเป็นพื้นที่พิกัดเดียวกับที่คุณใช้กำหนดตำแหน่งข้อความบนหน้ากระดาษ ไม่มีหน่วยอุปกรณ์ที่ซ่อนอยู่และไม่มีพิกเซลเข้ามาเกี่ยวข้องในขั้นตอนนี้ ความกว้าง 36 หมายถึงครึ่งนิ้วของหน้ากระดาษ ไม่ว่าความละเอียดในการเรนเดอร์ขั้นสุดท้ายจะเป็นเท่าใดก็ตาม
แกนตั้งทำงานในแบบที่ PDF กำหนด โดยที่ค่า Y จะเพิ่มขึ้นเมื่อเลื่อนขึ้นด้านบน นี่คือเหตุผลที่ว่าทำไมความสูงจึงเป็นบนลบล่างแทนที่จะกลับกัน รายละเอียดนั้นมีความสำคัญเมื่อคุณเลื่อนเคอร์เซอร์ลงมาตามคอลัมน์ คุณทำการวัดความสูงของบรรทัด จากนั้นนำไปลบออกจากเส้นฐาน (baseline) ปัจจุบันเพื่อหาตำแหน่งของบรรทัดถัดไป เนื่องจากความหมายของการเลื่อนลงหน้ากระดาษคือการเลื่อนเข้าหาค่า Y ที่น้อยลง หากปลายทางของคุณคือหน้าจอแทนที่จะเป็นกระดาษ คุณสามารถแปลงหน่วยผู้ใช้เป็นพิกเซลของอุปกรณ์ได้ด้วยความละเอียดของจอแสดงผล: ค่าในหน่วยผู้ใช้คูณด้วย DPI และหารด้วย 72 จะให้ผลลัพธ์เป็นพิกเซล ดังนั้นความกว้างของคอลัมน์ที่คุณกำหนดเป็นจุดจะสามารถนำมาเทียบกับข้อความที่วัดได้ ก่อนที่คุณจะตัดสินใจว่าจะทำการตัดคำตรงไหน
เกิดอะไรขึ้นเมื่อข้อมูลอินพุตไม่สมบูรณ์
ฟังก์ชันต่างๆ ถูกเขียนขึ้นให้ล้มเหลวแบบเงียบๆ หากไม่มีเอกสารที่เปิดอยู่ หรือหากไม่สามารถสร้างอ็อบเจ็กต์ข้อความได้ ผลลัพธ์จะเป็นศูนย์แทนที่จะแสดงข้อผิดพลาด (exception) ความกว้างและความสูงจะถูกกำหนดค่าเริ่มต้นให้เป็นศูนย์ที่ด้านบน และจะถูกเขียนทับก็ต่อเมื่อสามารถอ่าน bounding box กลับมาได้สำเร็จเท่านั้น สตริงว่าง เอกสารที่หายไป หรือฟอนต์ที่ไลบรารีไม่สามารถแปลงเป็นอ็อบเจ็กต์ได้ สิ่งเหล่านี้แต่ละอย่างจะส่งคืนค่าศูนย์แทนที่จะแสดงข้อผิดพลาด
ตัวเลือกนี้ช่วยให้ลูปของการวัดขนาดเรียบง่าย เพราะลูปที่รันข้อมูลคำนับพันๆ คำไม่ใช่สถานที่สำหรับการจัดการข้อผิดพลาดในทุกๆ รอบการทำงาน สิ่งที่ต้องแลกมาก็คือผู้เรียกใช้งานจะต้องเป็นผู้รับหน้าที่ตรวจสอบเอง ความกว้างที่เป็นศูนย์คือค่าที่ใช้เพื่อการส่งสัญญาณเตือน (sentinel) ไม่ใช่ข้อเท็จจริงเกี่ยวกับข้อความ ดังนั้นโค้ดที่ทำการหารด้วยความกว้างที่วัดได้ หรืออนุมานว่าเป็นค่าบวก จะต้องป้องกันไม่ให้เป็นศูนย์ก่อนที่จะเชื่อถือค่าเหล่านั้น ถือว่าค่าศูนย์หมายถึง "ไม่สามารถวัดได้" และข้อตกลงก็จะชัดเจน หากละเลยมันไป อินพุตที่ไม่สมบูรณ์ก็จะกลายเป็นรูปแบบที่มีการวางตัวอักขระซ้อนทับกันไปอย่างเงียบๆ
การสร้างการตัดคำแบบตะกละ (greedy word wrap) ที่อิงตามการวัดขนาด
เมื่อมีฟังก์ชันความกว้างอยู่ในมือ การตัดคำก็จะเป็นลูปแบบตะกละสั้นๆ คุณแยกย่อหน้าออกเป็นคำๆ เก็บข้อมูลบรรทัดปัจจุบันไว้ และสำหรับแต่ละคำ คุณจะต้องวัดขนาดว่าบรรทัดนั้นจะเป็นอย่างไรหากคุณนำคำนั้นไปต่อท้าย ตราบใดที่บรรทัดทดลองยังคงอยู่ในกรอบความกว้างของคอลัมน์ คุณก็ยังคงเพิ่มต่อไปเรื่อยๆ เมื่อมันล้น คุณก็นำบรรทัดปัจจุบันไปแสดงด้วย AddText และเริ่มบรรทัดใหม่ด้วยคำที่ไม่สามารถใส่ลงไปได้ การสะสมค่าทั้งหมดนี้ทำได้โดยใช้ MeasureTextWidth และสิ่งเดียวที่จะไปถึงหน้ากระดาษคือบรรทัดที่คุณยืนยันแล้วว่าสามารถใส่ได้พอดี
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<string>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := string(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// วัดขนาดบรรทัดที่กำลังทดลองก่อนที่จะวาดสิ่งใดลงไป
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(Line, Font, FontSize, X, Y); // เขียนบรรทัดที่พอดีออกไป
Y := Y - LineHeight; // Y ลดลงเรื่อยๆ เมื่อเลื่อนลง
Line := Words[I]; // คำที่ล้นเริ่มบรรทัดถัดไป
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(Line, Font, FontSize, X, Y); // เขียนบรรทัดสุดท้ายออกไป
end;
ลูปจะทำการวัดขนาดของบรรทัดทดลองแทนที่จะวัดแต่ละคำแล้วนำมารวมกัน เพราะความกว้างของบรรทัดไม่ใช่ผลรวมความกว้างของคำในบรรทัดนั้น ช่องว่างระหว่างคำก็มีส่วนด้วย และการวัดขนาดทั้งหมดในรวดเดียวจะเก็บค่าสิ่งนี้ได้โดยตรง กฎของความตะกละ คือการใส่คำให้ได้มากที่สุดเท่าที่คอลัมน์จะอนุญาตและตัดคำที่คำสุดท้ายที่สามารถใส่ได้ กฎเดียวกันนี้ช่วยเติมเต็มช่องว่างระหว่างการเรียกใช้ AddText แบบดิบๆ กับย่อหน้าจริง การเรียกคำสั่งวาดนั้นไม่ใช่เรื่องยาก แต่การวัดขนาดที่ต้องทำก่อนหน้านั้นต่างหากที่ยาก และนั่นก็คือสิ่งที่ตัวช่วยนี้มีให้
จะนำไปใช้ในส่วนไหน
การวัดขนาดคือเลเยอร์ที่อยู่ระหว่างการสร้างเนื้อหากับการเรนเดอร์มัน จึงเข้ากันได้อย่างเป็นธรรมชาติกับส่วนที่เหลือของขั้นตอนการสร้างเอกสารตั้งแต่ต้น หากคุณกำลังประกอบหน้ากระดาษและวางข้อความตั้งแต่แรก พื้นฐานอยู่ในการสร้างเอกสาร PDF ตั้งแต่ต้นด้วยคอมโพเนนต์ PDFium ใน Delphi ซึ่งครอบคลุม AddText และการตั้งค่าหน้ากระดาษอย่างครบถ้วน เมื่อฟอนต์ที่คุณกำลังวัดขนาดมีความสำคัญพอๆ กับตัวสตริงเอง เพราะตัววัดระยะขึ้นอยู่กับรูปแบบตัวอักษร การวิเคราะห์คุณสมบัติฟอนต์ PDF ด้วยคอมโพเนนต์ PDFium ใน Delphi แสดงให้เห็นว่าไลบรารีรายงานข้อมูลฟอนต์ที่ขับเคลื่อน bounding box เหล่านั้นอย่างไร ทั้งสองสร้างขึ้นบน binding เดียวกัน คือ PDFium Component สำหรับ Delphi และ Lazarus ซึ่งตัวช่วยการวัดขนาดมาพร้อมกับ API สำหรับเอกสาร หน้ากระดาษ และข้อความที่อธิบายไว้ทั่วบล็อกนี้