หากคุณส่งประโยคภาษาอาหรับ يوضح ملف PDF هذا ไปให้คำสั่ง TextOut ธรรมดา หน้ากระดาษที่ได้กลับมาจะผิดเพี้ยนไปสองอย่างพร้อมกัน คำต่าง ๆ จะวิ่งจากซ้ายไปขวาแทนที่จะเป็นขวาไปซ้าย และตัวอักษรแต่ละตัวจะแยกห่างกันในรูปแบบโดด ๆ แทนที่จะเชื่อมต่อกันเป็นคำ ไม่มีอะไรแจ้งข้อผิดพลาด โค้ด Delphi คอมไพล์ผ่าน ไฟล์เปิดได้ แต่ผู้ตรวจทานที่อ่านภาษาอาหรับออกจะบอกคุณว่าผลลัพธ์นี้ใช้งานไม่ได้ วิธีแก้ปัญหาคือการเรียกใช้คำสั่งเดียว ไม่ใช่การเปลี่ยนไลบรารี: HotPDF จะส่งข้อความที่อ่านจากขวาไปซ้ายผ่านเมธอดแยกต่างหาก นั่นคือ RtLTextOut ซึ่งจัดการการจัดเรียงใหม่ที่ TextOut ธรรมดาทำไม่ได้ มีสี่ประการเกี่ยวกับเมธอดนี้ที่จะเป็นตัวกำหนดว่าผลลัพธ์นั้นจะใช้งานได้หรือไม่: สิ่งที่มันทำกับสตริง, อาร์กิวเมนต์ charset ใช้เลือกตัวเขียนอย่างไร, การเปลี่ยนแปลงระดับเอกสารที่เกิดขึ้นเป็นผลพลอยได้ และการจัดการฟอนต์ที่ต้องทำก่อน
ทำไมการเขียนขวาไปซ้ายจึงต้องมีคำสั่งของตัวเอง
content stream ของ PDF ไม่ได้จัดเก็บข้อความที่แก้ไขได้ แต่มันจัดเก็บกลีฟ (glyph) ไว้ในตำแหน่งที่ตายตัว ซึ่งหมายความว่าไม่ว่าอะไรจะเป็นตัวปล่อย stream ออกมา สิ่งนั้นจะต้องรับหน้าที่ในการตัดสินใจว่ากลีฟเหล่านั้นจะเรียงลำดับอย่างไร บนหน้าจอ ระบบปฏิบัติการทำให้คุณ: วางภาษาอาหรับลงใน TEdit และระบบจัดการข้อความของ OS จะจัดเรียงและเชื่อมต่อมันก่อนที่คุณจะเห็นพิกเซลด้วยซ้ำ นั่นคือเหตุผลชัดเจนว่าทำไมสตริงถึงดูสมบูรณ์แบบในฟอร์มของคุณแต่กลับพังใน PDF เดสก์ท็อปทำงานอย่างเงียบ ๆ และวินาทีที่คุณเขียน content stream ของคุณเอง งานนี้ก็กลับมาอยู่ในมือคุณแล้ว
TextOut ทำตามที่คุณสั่งอย่างตรงไปตรงมา มันวาดโค้ดพอยต์ (codepoint) ตามลำดับที่คุณส่งเข้าไป จากซ้ายไปขวา ซึ่งถูกต้องสำหรับอักษรละติน ซีริลลิก และ CJK แต่ผิดสำหรับภาษาอาหรับและฮีบรู RtLTextOut คือคำสั่งที่จัดเรียงบรรทัดใหม่ให้เป็นลำดับภาพจากขวาไปซ้ายก่อน จากนั้นจึงวาด HotPDF แยกสองเมธอดนี้ออกจากกันอย่างจงใจแทนที่จะเดาทิศทางจากตัวอักษร ดังนั้นการเลือกคำสั่งใดคำสั่งหนึ่งก็คือการเลือกพฤติกรรมตัวเขียนที่คุณจะได้รับ กลไกเชิงลึกของการจัดเรียงแบบสองทิศทาง (bidirectional) และการเชื่อมต่อตามบริบทของภาษาอาหรับเป็นหัวข้อของมันเอง ซึ่งครอบคลุมอยู่ในบทความเกี่ยวกับภาษาอาหรับและการขึ้นรูปข้อความ RTL ด้วย HotPDF ในที่นี้ประเด็นในทางปฏิบัติจะแคบกว่า: ใช้ RtLTextOut สำหรับชุดข้อความจากขวาไปซ้าย ใช้ TextOut สำหรับสิ่งอื่น ๆ ทั้งหมด และอย่าใช้คำสั่งหนึ่งแทนอีกคำสั่งหนึ่งอย่างเด็ดขาด

อาร์กิวเมนต์ charset เป็นตัวกำหนดตัวเขียน
สิ่งที่บอก RtLTextOut ว่ามันกำลังจัดวางภาษาอาหรับหรือฮีบรูนั้นไม่ใช่ตัวเมธอด แต่เป็นฟอนต์ SetFont รับชุดอักขระ (charset) ของ Windows เป็นอาร์กิวเมนต์ตัวที่สี่ และค่านั้นจะนำพากฎของตัวเขียนเข้าสู่การเรียกแบบขวาไปซ้าย: 178 เลือกภาษาอาหรับ, 177 เลือกฮีบรู ตั้งค่าชุดอักขระ แล้ววาด จากนั้นสองบรรทัดด้านล่างจะออกมาในลำดับการอ่านที่ถูกต้องโดยไม่ต้องกำหนดค่าเพิ่มเติมใด ๆ อีก
// Arabic: charset 178 tells RtLTextOut to apply Arabic rules
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');
// Hebrew: charset 177 switches the rules to Hebrew
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');
รายละเอียดสองจุดเกี่ยวกับพิกัดเหล่านั้นอาจถูกมองข้ามได้ง่าย ตำแหน่งที่คุณส่งให้ยังคงเป็นจุดเริ่มต้นของชุดข้อความในระบบพิกัดของหน้ากระดาษเอง ซึ่งวัดจากมุมซ้ายล่างโดยที่ Y เพิ่มขึ้นไปด้านบน ซึ่งเป็นจุดกำเนิดเดียวกันกับที่ทุก TextOut ใช้ RtLTextOut เปลี่ยนแค่ลำดับของกลีฟ ไม่ใช่จุดที่หน้ากระดาษใช้วัด และเช่นเดียวกับการเรียกวาดใด ๆ คำสั่ง SetFont จะต้องมาก่อนและต้องทำซ้ำหลังจากการเรียก AddPage ทุกครั้ง เนื่องจากฟอนต์ปัจจุบันจะไม่คงอยู่ข้ามการขึ้นหน้าใหม่ หากลืมทำซ้ำ หน้าที่สองจะถอยกลับไปใช้ฟอนต์ใดก็ตามที่เปิดใช้งานอยู่ ซึ่งสำหรับภาษาอาหรับมักจะหมายถึงกล่องสี่เหลี่ยมว่างเปล่า
มันจะไม่กลับด้านข้อความที่คุณกลับด้านเองมาแล้ว
ข้อผิดพลาดเดียวที่กลืนกินเวลาดีบักมากที่สุดในเรื่องนี้ คือการป้อนสตริงที่คุณกลับด้านด้วยมือมาแล้วให้กับ RtLTextOut ผู้คนมักจะมาจบที่เมธอดนี้หลังจากความพยายามครั้งแรกด้วย TextOut ธรรมดาออกมากลับหัวกลับหาง และวิธีแก้ขัดที่พบบ่อยคือการกลับตัวอักษรในโค้ดก่อนทำการวาด RtLTextOut จะทำการกลับด้านเป็นการภายในด้วยตัวเอง ดังนั้นสตริงที่ถูกกลับด้านมาก่อนแล้วจะถูกกลับด้านเป็นครั้งที่สองและกลับไปอยู่จุดเริ่มต้นที่คุณพิมพ์มา ให้ส่งข้อความในลำดับตรรกะ ลำดับที่คุณพิมพ์และอ่านออกเสียง แล้วปล่อยให้เมธอดทำการจัดเรียงใหม่
กับดักนี้น่ากลัวกว่าการกลับด้านธรรมดา เพราะสตริงที่กลับด้านซ้ำซ้อนอาจดูถูกต้องสำหรับวลีทดสอบที่เป็นภาษาอาหรับล้วน และพังทลายลงในทันทีที่บรรทัดนั้นมีคำภาษาละตินหรือตัวเลข ภายในบรรทัดแบบขวาไปซ้าย ชุดข้อความที่ฝังอยู่เหล่านั้นควรจะอ่านจากซ้ายไปขวา และการกลับด้านด้วยมือจะทำลายการซ้อนทับนั้น ในขณะที่กรณีภาษาอาหรับล้วนบังเอิญรอดมาได้ ดังนั้นบั๊กนี้จึงผ่านการทดสอบรอบแรกของคุณฉลุย และไปปรากฏขึ้นในภายหลังบนใบแจ้งหนี้จริงที่มีหมายเลขบัญชีอยู่ข้างใน เอาการกลับด้านด้วยมือทุกจุดออกในวินาทีที่คุณเปลี่ยนไปใช้ RtLTextOut
ผลพลอยได้ของ Direction ที่ควรรู้
การเรียกใช้ RtLTextOut เปลี่ยนแปลงมากกว่าแค่บรรทัดที่คุณกำลังวาด มันยังสลับความชอบในการกำหนดทิศทางการอ่านของเอกสารไปเป็นแบบขวาไปซ้าย ซึ่งเป็นสิ่งเดียวกันกับที่คุณคงตั้งค่าเองผ่านคุณสมบัติ Direction ตัวกำหนดค่านั้นจะเพิ่ม vpDirection ลงใน ViewerPreferences ของเอกสาร ซึ่งจะบอกโปรแกรมอ่านว่าจะจัดรูปแบบสองหน้ากระดาษอย่างไรและเลย์เอาต์หน้าคู่เริ่มจากฝั่งไหน เมื่อทั้งเอกสารเป็นภาษาอาหรับหรือฮีบรู นี่คือสิ่งที่คุณต้องการเป๊ะ ๆ และคุณได้มันมาฟรี ๆ
สิ่งนี้คุ้มค่าที่จะรู้ไว้ เพราะมันมองไม่เห็นบนหน้ากระดาษเดี่ยว หากเอกสารส่วนใหญ่เป็นแบบซ้ายไปขวาแต่มีบล็อกขวาไปซ้ายอยู่เพียงบล็อกเดียว การเรียก RtLTextOut ครั้งแรกก็จะยังคงเอนเอียงความชอบของทั้งไฟล์ และจะไม่มีอะไรในหน้าตัวอย่างของคุณที่แสดงสิ่งนั้น อาการจะปรากฏขึ้นในอีกหลายสัปดาห์ต่อมาเมื่อมีคนพิมพ์หนังสือคู่แบบพิมพ์สองหน้าและหน้าคู่เหล่านั้นออกมาสลับกันคล้ายภาพสะท้อน หากนั่นไม่ใช่สิ่งที่คุณต้องการ ให้ตั้งค่า Direction กลับอย่างชัดเจนหลังจากชุดข้อความแบบขวาไปซ้าย:
// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;
สำหรับเอกสารที่อ่านจากขวาไปซ้ายอย่างแท้จริง ให้ปล่อยทิ้งไว้ ประเด็นคือคุณต้องรู้ว่าคำสั่งนี้มีผลต่อระดับทั้งเอกสาร เพื่อที่เรื่องเซอร์ไพรส์กับหนังสือคู่จะไม่มีวันเกิดขึ้น
ลงทะเบียนฟอนต์ที่คุณจัดส่ง ไม่ใช่ฟอนต์ที่คุณหวังว่าจะถูกติดตั้ง
การจัดเรียงใหม่จะไม่มีความหมายเลยหากฟอนต์ไม่มีกลีฟที่จะวาด ความล้มเหลวแบบคลาสสิกคือรายงานที่เรนเดอร์ได้อย่างไร้ที่ติบนเครื่องของนักพัฒนา ซึ่งบังเอิญมี Arial Unicode MS อยู่ แต่กลับออกมาเป็นแถวของกล่องสี่เหลี่ยมว่างเปล่าบนเซิร์ฟเวอร์ของลูกค้า ซึ่ง Windows ได้แอบสลับไปใช้ฟอนต์ที่ไม่ครอบคลุมภาษาอาหรับเลยอย่างเงียบ ๆ วิธีแก้คือเลิกไว้ใจฟอนต์ระบบที่ถูกติดตั้ง และลงทะเบียนฟอนต์ที่คุณจัดส่งไปพร้อมกับแอปพลิเคชัน
// Ship a known Arabic font and register it before drawing
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');
มีขอบเขตสองประการที่มาพร้อมกับการลงทะเบียน ฟอนต์ที่ถูกนำเข้ามาผ่าน RegisterUnicodeTTF จะถูกฝัง และการจัดการ Unicode แบบฝังของ HotPDF ต้องใช้เอกสารระดับ PDF 1.5 ขึ้นไป; สิ่งนี้จะกลายเป็นปัญหาเฉพาะเมื่อระบบปลายทางยืนกรานจะใช้ PDF 1.4 แต่เมื่อเป็นเช่นนั้นความล้มเหลวก็จะไม่แสดงตัวออกมา อีกประการหนึ่งคือเรื่องของกฎหมายไม่ใช่เทคนิค: ไฟล์ TrueType มีบิตระบุสิทธิ์ในการฝัง และแบบอักษรที่ดูสวยงามบนหน้าจออาจได้รับสิทธิ์อนุญาตในลักษณะที่ห้ามมิให้นำไปจัดส่งภายในเอกสารของลูกค้า ยืนยันสิทธิ์อนุญาตก่อนที่คุณจะฝัง ไม่ใช่ทำหลังจากมีข้อร้องเรียน
ตัวอย่างคอนโซลแบบสมบูรณ์
เมื่อนำชิ้นส่วนต่าง ๆ มารวมกัน นี่คือโปรแกรมแบบสแตนด์อะโลนที่สร้างหน้าหนึ่งหน้าที่มีบรรทัดภาษาอาหรับ บรรทัดฮีบรู และบรรทัดผสมที่มีชื่อผลิตภัณฑ์เป็นอักษรละติน แต่ละบล็อกตั้งค่าชุดอักขระของตัวเอง แล้ววาดตามลำดับตรรกะ
program RtLTextOutDemo;
{$APPTYPE CONSOLE}
uses
HPDFDoc; // HotPDF main unit
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'RtLTextOut.pdf';
Pdf.BeginDoc;
// A Latin heading goes through the ordinary TextOut path
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');
// Arabic: charset 178, logical order, RtLTextOut does the reordering
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 720, 0,
'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');
// Hebrew: charset 177
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 680, 0,
'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');
// Mixed line: the embedded Latin word still reads left to right
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 640, 0,
'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');
Pdf.EndDoc;
Writeln('Wrote RtLTextOut.pdf');
finally
Pdf.Free;
end;
end.
ลองรันแล้วเปิดไฟล์ผลลัพธ์ดู บรรทัดภาษาอาหรับและฮีบรูจะอ่านจากขวาไปซ้าย ตัวอักษรจะเชื่อมต่อกันเมื่อตัวเขียนกำหนดให้ต้องเชื่อม และในบรรทัดสุดท้าย โทเค็น HotPDF จะวางเรียงจากซ้ายไปขวาภายในชุดข้อความภาษาอาหรับ ซึ่งเป็นผลลัพธ์ที่ถูกต้องตามสเปก แม้ว่าจะสร้างความประหลาดใจให้กับผู้ที่เพิ่งเคยเห็นการจัดรูปแบบแบบสองทิศทางเป็นครั้งแรกก็ตาม ประเด็นสุดท้ายนี้คุ้มค่าที่จะระบุลงในเกณฑ์การยอมรับ (acceptance criteria) ของคุณก่อนที่ผู้ที่ใช้ภาษานี้เป็นภาษาแม่จะมาตรวจทานผลลัพธ์ เพราะชุดข้อความที่ฝังอยู่ซึ่งอ่านไปในทางที่ "ผิด" เมื่อเทียบกับตัวเขียนรอบข้าง เป็นเพียงสิ่งเดียวที่มักจะถูกยื่นเรื่องมาว่าเป็นบั๊กทั้งที่จริง ๆ แล้วมันไม่ใช่
ข้อผิดพลาดที่พบบ่อยและวิธีแก้
หน้าที่ดูเหมือนจะถูกต้อง ไม่ได้แปลว่าหน้าที่ทำมานั้นถูกต้องเสมอไป ดังนั้นให้ตรวจสอบแบบเดียวกับที่ระบบปลายทางจะทำ คัดลอกข้อความกลับออกมาจากโปรแกรมอ่านแล้วเทียบโค้ดพอยต์กับสตริงต้นฉบับของคุณ; ลำดับการมองเห็นที่ถูกต้องพร้อมกับลำดับตรรกะที่สลับมั่วคือความล้มเหลวแบบหนึ่งจริง ๆ ลองใช้ระบบค้นหาภายในเอกสารของโปรแกรมอ่านเพื่อค้นหาคำที่คุณสามารถเห็นได้บนหน้ากระดาษ จากนั้นเปิดไฟล์นั้นบนเครื่องที่ไม่มีฟอนต์ในการพัฒนาของคุณ เครื่องนั่นแหละมีแนวโน้มมากที่สุดที่จะเปิดเผยให้เห็นการแทนที่ฟอนต์ที่เกิดขึ้นแบบเงียบ ๆ ท้ายที่สุดแล้วไม่มีอะไรแทนที่ผู้ที่พูดภาษานั้นเป็นภาษาแม่ซึ่งมาอ่านเอกสารฉบับจริงได้เลย พวกเขาจะจับปัญหาที่ไม่มีสตริงการทดสอบสังเคราะห์ใดทำได้ ดังนั้นให้กำหนดการตรวจทานนั้นลงในปฏิทินก่อนที่ฟอร์แมตเอกสารนี้จะออกสู่ระบบจริง
RtLTextOut จัดการการจัดเรียงใหม่แบบสองทิศทางและการเชื่อมต่อบริบทของภาษาอาหรับ ซึ่งครอบคลุมงานรายงานและเอกสารแบบขวาไปซ้ายส่วนใหญ่ เมื่อถึงจุดที่มันไม่รองรับ เช่นตัวเขียนที่ต้องการอะไรที่มากกว่าการจัดเรียงใหม่และการเชื่อมต่อ เช่น ตระกูล Indic และฟีเจอร์ OpenType เสริมที่ทำงานผ่านการแทนที่กลีฟตัวเดี่ยว มีอธิบายไว้ควบคู่ไปกับรายละเอียดครอบคลุมของกลีฟและการขึ้นรูปข้อความในบทความคู่ขนานเรื่องภาษาอาหรับและการขึ้นรูปข้อความ RTL ด้วย HotPDF
คำสั่ง RtLTextOut, SetFont และ RegisterUnicodeTTF ที่แสดงในที่นี้เป็นส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder