Full justification คือ layout ที่ทำให้คอลัมน์ข้อความชิดซ้ายและขวาพร้อมกัน ซึ่งเป็นรูปลักษณ์ที่คุณคาดหวังจากหนังสือพิมพ์หรือรายงานทางการ มันอธิบายได้ง่ายและพลาดพลั้งได้ง่ายอย่างน่าแปลกใจ เพราะคำตอบสำหรับคำถาม "ช่องว่างส่วนเกินไปอยู่ที่ไหน" ไม่เหมือนกันระหว่างภาษาอังกฤษและภาษาญี่ปุ่น และเพราะวิธีวัดแต่ละบรรทัดแบบง่าย ๆ ทำให้หน้าที่เร็วกลายเป็นหน้าที่ช้า HotPDF ให้ justification ที่รับรู้ script ผ่าน single box-layout call และใต้ call นั้นมีการแก้ไขประสิทธิภาพแบบ textbook ที่คุ้มค่าที่จะเข้าใจด้วยตัวมันเอง
บทความนี้พาเดินผ่านทั้งสอง ก่อนคือกฎการพิมพ์ที่ตัดสินว่า slack จะกระจายอย่างไรสำหรับ scripts ที่มีช่องว่างระหว่างคำเทียบกับ scripts ที่ไม่มี ถัดมาคือการเปลี่ยนแปลงการวัดที่ลดต้นทุนต่อหน้าของ justification ลงประมาณแปดสิบเท่าโดยไม่มีความแตกต่างที่มองเห็นในผลลัพธ์ ทั้งสองมีความสำคัญหากคุณสร้างเอกสารเป็นจำนวนมากและต้องการให้อ่านเหมือนการเรียงพิมพ์จริง ๆ มากกว่าผลลัพธ์ monospaced ที่ยืดเต็ม
สิ่งที่ full justification ต้องการจริง ๆ
บรรทัดข้อความที่วาดด้วยความกว้างธรรมชาติของมันแทบจะไม่ถึงขอบขวาของคอลัมน์ มีเสมอ remainder หรือ slack ระหว่างที่ glyph สุดท้ายสิ้นสุดและที่ขอบเขตคอลัมน์อยู่ Left alignment ทิ้ง slack ไว้ทางขวา Right alignment ย้ายมันไปทางซ้าย Centering แบ่งมัน Full justification ลบมันออกโดยขยายบรรทัดเองจนขอบทั้งสองพบกับกล่อง และวิธีซื่อสัตย์เพียงอย่างเดียวในการทำเช่นนั้นคือการดัน glyphs ออกจากกันภายใน
กฎที่แยก justification ที่ดีออกจากที่แย่คือตำแหน่งที่คุณวาง slack script ที่เขียนคำด้วย spaces ระหว่างกันเช่น English และส่วนที่เหลือของ Latin family มีรอยต่อธรรมชาติที่ inter-word space ทุก space การขยาย spaces เหล่านั้นมองไม่เห็นสำหรับดวงตาเพราะผู้อ่านยอมรับแล้วว่าช่องว่างระหว่างคำแตกต่างกัน script ที่เขียนโดยไม่มีช่องว่างระหว่างคำ เช่น Chinese Han characters, Japanese kana หรือ Korean Hangul ไม่มีรอยต่อดังกล่าว ที่นั่น slack ต้องกระจายอย่างเท่าเทียมระหว่าง glyphs ที่อยู่ติดกัน ซึ่งเป็นหลักการที่ช่างพิมพ์ญี่ปุ่นเรียกว่า kintou-waritsuke การเว้นระยะอย่างเท่าเทียม การวาง Latin-style word-gap stretching บนบรรทัด CJK หรือยัด slack ทั้งหมดลงในจุดเดียวที่บรรทัด CJK มี space นั้นทำให้เกิด rivers และ gaps ที่บ่งชี้ผลลัพธ์ของมือใหม่
วิธีที่ HotPDF ตัดสินว่าช่องว่างไปที่ไหน
HotPDF ตัดสินนั้นต่อ gap ไม่ใช่ต่อบรรทัด เมื่อ justify บรรทัดมันเดินผ่านคู่ glyphs ที่อยู่ติดกันทุกคู่และถามว่ามีขอบเขตที่ยืดได้อยู่ระหว่างพวกมันหรือไม่ ขอบเขตสามารถยืดได้เมื่อทั้งสองด้านเป็น space หรือ tab กรณี Latin หรือเมื่อทั้งสองด้านเป็นตัวอักษรที่แยกได้ตาม CJK กรณีการเว้นระยะอย่างเท่าเทียม มันนับขอบเขตเหล่านั้น หาร slack ของบรรทัดอย่างเท่าเทียมระหว่างพวกมัน และเพิ่มส่วนแบ่งนั้นให้แต่ละ gap ที่มีคุณสมบัติ
ผลที่ตามมาเกิดขึ้นตามธรรมชาติ บรรทัดภาษาอังกฤษมีขอบเขตที่ยืดได้เฉพาะที่ word spaces ของมัน ดังนั้น slack ทั้งหมดตกที่นั่นและคำแยกออกจากกันในขณะที่ตัวอักษรภายในแต่ละคำรักษาระยะห่างธรรมชาติ บรรทัด Han หรือ kana มีขอบเขตที่ยืดได้ระหว่างเกือบทุกคู่ glyphs ดังนั้น slack กระจายอย่างเท่าเทียมตลอดทั้งบรรทัด ซึ่งเป็นการเว้นระยะ inter-glyph อย่างเท่าเทียมที่ scripts เหล่านั้นต้องการ บรรทัดที่เป็นคำ Latin ยาวเดียวไม่มี space ภายในไม่มีขอบเขตที่ยืดได้เลย ดังนั้น HotPDF ปล่อยมันไว้ที่ความกว้างธรรมชาติแทนที่จะฉีก word ออกทีละตัวอักษร logic เดียวกันจัดการ runs ผสม Latin และ CJK ในบรรทัดเดียวโดยไม่ต้องมีกรณีพิเศษ เพราะการตัดสินเป็นแบบ local ต่อแต่ละขอบเขต
ขอบเขตหนึ่งถูกยกเว้นโดยตั้งใจทุกที่ ตำแหน่งหลัง glyph สุดท้ายของบรรทัดไม่ถูกปฏิบัติเป็น gap เพราะการยืดที่นั่นจะเพิ่ม right-hand remainder กลับมา ซึ่งตรงข้ามกับ justification
เหตุใดบรรทัดสุดท้ายจึงถูกปล่อยไว้
บรรทัดสุดท้ายของย่อหน้าเป็นพิเศษและการทำผิดคือ justification bug ที่พบบ่อยที่สุด บรรทัดสุดท้ายของย่อหน้ามักสั้น บ่อยครั้งเป็นเพียงไม่กี่คำ และการยืดมันให้เต็มความกว้างคอลัมน์จะดึงคำเหล่านั้นข้ามหน้าออกไปเป็น row ที่กระจายเบาบาง การพิมพ์ที่ถูกต้องปล่อยบรรทัดสุดท้ายไว้ที่ความกว้างธรรมชาติ ชิดซ้าย
HotPDF ตรวจจับบรรทัดท้ายตามตำแหน่ง ขณะ wrap ข้อความเป็นบรรทัด มันรู้ว่าเมื่อบรรทัดที่เพิ่งแยกออกมาถึงจุดสิ้นสุดของ string ที่ส่งมา บรรทัดสุดท้ายนั้นถูก emit ด้วย plain left alignment และรักษาความกว้างธรรมชาติ ทุกบรรทัดก่อนหน้ามันถูก justify ให้ชิดทั้งสองขอบ Hard line breaks ที่คุณเขียนลงในข้อความถูกปฏิบัติตามที่เขียน ดังนั้นบรรทัดสั้นที่ตั้งใจไม่ถูกยืดเช่นกัน ผู้อ่านเห็นบล็อกข้อความสี่เหลี่ยมสะอาดที่บรรทัดสุดท้ายสิ้นสุดตามธรรมชาติ ซึ่งเป็นสิ่งที่สายตาคาดหวัง
ต้นทุนการวัดที่ทำให้ justification ช้า
ในการ justify บรรทัดคุณต้องรู้ความกว้างที่แน่นอนของมัน และคุณต้องรู้ advance ของแต่ละ glyph เพื่อให้วาง extra space ได้อย่างแม่นยำ การ implementation แรกได้ตัวเลขเหล่านั้นด้วยวิธีชัดเจน มันวัดทั้งบรรทัดด้วย full Unicode width query แล้ววัด prefix หลังจาก prefix เพื่อกู้คืน advance ของแต่ละ glyph โดยการ differencing สำหรับบรรทัดของ N glyphs นั่นคือ N+1 calls เข้า measurement engine และแต่ละ call คือ full GDI round-trip ขอให้ operating system ทำ shape และวัดข้อความแล้วส่งคำตอบกลับมา
ต่อบรรทัดนั่นฟังดูไม่แพง ทั่วทั้งหน้ามันไม่ใช่ ลองดูหน้า A4 หนาแน่นของข้อความ body ประมาณสี่สิบห้าบรรทัดของประมาณแปดสิบตัวอักษร ที่ N+1 round-trips ต่อบรรทัดนั่นคือประมาณ 81 round-trips สำหรับทุกบรรทัดและประมาณ 3,645 สำหรับหน้า เกือบทั้งหมดใช้ไปกับการวัดข้อความซ้ำที่ engine เพิ่งดูไปเมื่อกี้ ในงาน batch ที่ผลิตหน้าหลายพันหน้า overhead นั้นครอบงำเวลา layout และทุก round-trip ข้ามขอบเขตระหว่าง process ของคุณและ graphics subsystem
หนึ่ง call แทน N บวกหนึ่ง
การแก้ไขคือการเปลี่ยนแปลงแบบที่ดูเล็กและให้ผลตอบแทนสูง GDI สามารถรายงานความกว้างรวมของ string และตำแหน่งของ glyph ทุกตัวใน single query HotPDF เปิดเผยสิ่งนั้นผ่าน GetWideCharAdvances ซึ่งเติม array ด้วย advance ธรรมชาติของแต่ละ glyph รวม kerning และคืนความกว้างรวมใน one call แทน N+1 routine การ justification ซึ่งภายในชื่อ _HPDFEmitJustifiedWideLine ขอ advances ทั้งหมดครั้งเดียว คำนวณ slack กระจายมันข้าม stretchable boundaries และ emit บรรทัด
สำหรับหน้า A4 เดิม การวัดต่อบรรทัดลดจากประมาณ 81 round-trips เป็นหนึ่ง ดังนั้นหน้าลดจากประมาณ 3,645 round-trips เป็นประมาณ 45 ใกล้เคียงกับการลดแปดสิบเท่า ผลลัพธ์เหมือนกัน byte-for-byte เพราะไม่มีอะไรเกี่ยวกับการวัดที่เปลี่ยนแปลงนอกจากจำนวนครั้งที่ขอ GDI engine เดิม font metrics เดิม kerning เดิมให้ตัวเลขเดิม เฉพาะ round-trip count ที่ลดลง เมื่อการวัดถูกต้องแล้ว การ optimisation ที่ถูกต้องคือการหยุดขอมันซ้ำ ๆ ไม่ใช่การประมาณมัน
วิธีที่บรรทัดถึงหน้ากระดาษ
เมื่อ slack ถูกจัดสรรแล้ว HotPDF emit บรรทัดด้วย ExtTextOut และ per-glyph advance array ซึ่งก็คือ Dx array แต่ละ entry คือระยะทางจาก origin ของ glyph หนึ่งไปยัง origin ถัดไป ซึ่งคือ advance ธรรมชาติของ glyph นั้นบวกส่วนแบ่ง slack เมื่อขอบเขตที่ยืดได้ตามมา สิ่งนี้จับคู่กับ PDF imaging model โดยตรง ข้อความที่มีตำแหน่งเขียนด้วย TJ operator ซึ่งเป็น array ที่สลับ glyph runs กับ horizontal adjustments อย่างชัดเจน และค่า Dx กลายเป็น adjustments เหล่านั้นพอดี นั่นคือเหตุผลที่ extra space ตกระหว่าง glyphs ที่ตำแหน่ง sub-point ที่แม่นยำแทนที่จะถูกปลอมแปลงด้วย padding characters และเหตุผลที่บรรทัด HotPDF ที่ justified วัดได้ถูกต้องหาก downstream tool อ่านกลับ
คุณไม่ต้องเรียก ExtTextOut ด้วยตัวเองสำหรับ justified paragraphs จุดเข้าคือ WideTextOutBox ซึ่ง wrap Unicode string เข้าใน box และใช้ alignment ที่คุณขอ มัน split ข้อความเป็นบรรทัดที่พอดีกับความกว้าง box วางแต่ละบรรทัดลง box height และคืนจำนวนตัวอักษรที่จัดการใส่ได้ก่อนจะหมดพื้นที่แนวตั้ง alignment ถูกเลือกด้วย justification enum
type
THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);
สามตัวแรกคือ left, centered และ right alignment ที่อธิบายตัวเองได้ ตัวที่สี่คือ jtJustify ซึ่งเป็น full both-edge justification ที่อธิบายไว้ที่นี่ และมันคือค่าที่ WideTextOutBox อ่านเพื่อเปิด script-aware spacing
การ justify paragraph ในทางปฏิบัติ
ตัวอย่างที่สมบูรณ์สร้างเอกสาร ตั้งค่าฟอนต์ และเทย่อหน้าลงใน box ด้วย full justification โค้ดเดียวกัน justify ข้อความ Latin และ CJK โดยไม่ต้องเปลี่ยน flag เพราะ script-awareness อยู่ใต้ API
uses
HPDFDoc;
procedure JustifyParagraph;
var
Pdf: THotPDF;
Body: WideString;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'Justified.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', 11);
Body :=
'Full justification spreads the slack on each filled line so both ' +
'edges meet the column, while the last line keeps its natural width. ' +
'For scripts with word gaps the space lands between words; for ' +
'scripts without them it spreads evenly between glyphs.';
// X, Y, LineSpacing, BoxWidth, BoxHeight, Text, Align
Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
ในการวาด block เดียวกันแบบ left-aligned, centered หรือ right-aligned ให้เปลี่ยนเฉพาะ argument สุดท้ายเป็น jtLeft, jtCenter หรือ jtRight การ wrapping, line placement และ return value ยังคงเหมือนเดิม ความกว้างที่วัดได้ที่ขับเคลื่อนทุกสี่ path มาจาก GetWideTextWidth ซึ่งเป็น Unicode-aware width query ที่วัด WideString ได้ถูกต้องในขณะที่การวัดแบบ byte-wise รุ่นเก่าจะวัด size ผิดสำหรับทุกอย่างที่เกิน Latin-1 ซึ่งคือสิ่งที่ทำให้ box wrap ข้อความ CJK และ surrogate-pair ในที่ที่ถูกต้องตั้งแต่แรก
Justification คือหนึ่งชั้นของ text-shaping stack ที่ใหญ่กว่า เมื่อบรรทัดมี scripts ที่ reorder หรือเชื่อม glyphs ของมัน การตัดสินเรื่องระยะห่างที่นี่อยู่บน work ที่อธิบายไว้ใน บทความของเราเกี่ยวกับ complex-script text shaping และเมื่อฟอนต์มี typographic variants ที่คุณต้องการเลือก ดู วิธีขับเคลื่อน OpenType GSUB stylistic alternates ทั้งหมดนี้ ship มาพร้อมกับ HotPDF Component สำหรับ Delphi และ C++Builder ร่วมกับ text, layout และ document APIs ที่กว้างกว่าที่ครอบคลุมในบล็อกนี้