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

RtLTextOut ใน HotPDF: ข้อความ PDF ขวาไปซ้ายใน Delphi

ส่งประโยคภาษาอาหรับ يوضح ملف PDF هذا ให้ TextOut ธรรมดา แล้วหน้าที่ได้กลับมาจะผิดสองทางพร้อมกัน คำวิ่งจากซ้ายไปขวาแทนที่จะเป็นขวาไปซ้าย และตัวอักษรแยกกันอยู่ในรูปเดี่ยวแทนที่จะเชื่อมกันเป็นคำ ไม่มีข้อผิดพลาดใด ๆ Delphi คอมไพล์ผ่าน ไฟล์เปิดได้ และผู้ตรวจที่อ่านภาษาอาหรับออกจะบอกคุณว่าผลลัพธ์ใช้ไม่ได้ ทางแก้คือการเรียกเมธอดหนึ่งตัว ไม่ใช่การเปลี่ยนไลบรารี: HotPDF ส่งข้อความจากขวาไปซ้ายผ่านเมธอดแยกต่างหากชื่อ RtLTextOut ที่จัดการการเรียงลำดับใหม่ซึ่ง TextOut ธรรมดาไม่ทำให้ หน้านี้คือคู่มืออ้างอิงที่ใช้งานได้จริงของเมธอดนั้น: ลายเซ็นฟังก์ชันและพารามิเตอร์ อาร์กิวเมนต์ charset ที่เลือกระบบเขียน ผลข้างเคียงระดับเอกสาร การตั้งค่าฟอนต์ที่ต้องมาก่อน และความล้มเหลวที่ส่งถึงฝ่ายสนับสนุนจริง ๆ พร้อมทางแก้ของแต่ละข้อ

ลายเซ็นฟังก์ชันและพารามิเตอร์

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

X และ Y ยึดตำแหน่งของช่วงข้อความในระบบพิกัดของหน้าเอง วัดจากมุมซ้ายล่างโดยค่า Y เพิ่มขึ้นเมื่อขึ้นบน เป็นจุดกำเนิดเดียวกับที่ทุกการเรียก TextOut ใช้ RtLTextOut เปลี่ยนลำดับของกลีฟ ไม่ได้เปลี่ยนจุดที่หน้าเริ่มวัด angle หมุนเส้นฐานเหมือนกับที่ทำใน TextOut ทุกประการ ค่า 0 จึงวาดเป็นบรรทัดแนวนอน Text คือสตริงในลำดับตรรกะ ลำดับเดียวกับที่คุณจะพิมพ์มันออกมา ส่วนโอเวอร์โหลดตัวที่สองรับข้อมูล UTF-16 ชุดเดียวกันในรูปบัฟเฟอร์ PWORD ดิบพร้อมจำนวนหน่วยรหัสที่ระบุชัด ซึ่งเป็นรูปแบบที่ควรใช้เมื่อข้อความมาจาก API แทนที่จะมาจากสตริงของ Delphi บน Delphi รุ่นเก่าที่มาก่อนการเลือกโอเวอร์โหลดสำหรับชนิดเหล่านี้ รูปแบบที่รับสตริงถูกเปิดออกมาภายใต้ชื่อ RtLTextOutStr พร้อมรายการพารามิเตอร์ชุดเดียวกัน

การแบ่งหน้าที่ระหว่างการเรียกวาดข้อความสองตัวนี้เข้มงวด TextOut วาดโค้ดพอยต์ตามลำดับที่คุณส่งเข้าไป ซึ่งถูกต้องสำหรับอักษรละติน ซีริลลิก และ CJK แต่ผิดสำหรับภาษาอาหรับและฮีบรู ส่วน RtLTextOut เรียงลำดับแต่ละบรรทัดใหม่ให้เป็นลำดับเชิงสายตาแบบขวาไปซ้ายก่อน แล้วจึงวาด โดยรักษาคำละตินและตัวเลขที่แทรกอยู่ให้อ่านจากซ้ายไปขวาภายในบรรทัด HotPDF จงใจแยกสองเมธอดนี้ออกจากกันแทนที่จะเดาทิศทางจากตัวอักขระ การเลือกว่าจะเรียกตัวไหนจึงเป็นการเลือกว่าจะได้พฤติกรรมของระบบเขียนแบบใด ใช้ RtLTextOut สำหรับช่วงข้อความขวาไปซ้าย ใช้ TextOut สำหรับอย่างอื่นทั้งหมด และอย่าส่งอย่างหนึ่งผ่านอีกอย่างเด็ดขาด เหตุผลว่าทำไมการเรียงลำดับใหม่จึงมีอยู่ อัลกอริทึมทวิทิศทางของ Unicode และการเชื่อมตัวอักษรตามบริบทของภาษาอาหรับทำอะไรจริง ๆ และการจัดรูปอักษรของ HotPDF หยุดตรงไหน คือหัวข้อของบทความคู่กันว่าด้วยการจัดรูปข้อความอาหรับและ RTL ด้วย HotPDF ทุกอย่างด้านล่างคือการตั้งค่าเชิงปฏิบัติ

แผนภาพแสดงวิธีที่ RtLTextOut เรียงบรรทัดผสมภาษาอาหรับกับละตินใหม่ให้เป็นลำดับเชิงสายตาแบบขวาไปซ้ายก่อนวาดลงใน PDF
RtLTextOut เรียงแต่ละบรรทัดใหม่ให้เป็นลำดับเชิงสายตาก่อนวาด ช่วงข้อความขวาไปซ้ายรักษาลำดับของมันไว้ ขณะที่คำละตินและตัวเลขที่แทรกอยู่อ่านจากซ้ายไปขวาภายในบรรทัด

อาร์กิวเมนต์ charset เป็นตัวตัดสินระบบเขียน

สิ่งที่บอก RtLTextOut ว่ากำลังจัดวางภาษาอาหรับหรือฮีบรูไม่ใช่ตัวเมธอด แต่คือฟอนต์ SetFont รับค่า charset ของ Windows เป็นอาร์กิวเมนต์ตัวที่สี่ และค่านั้นพากฎของระบบเขียนเข้าไปในการเรียกแบบขวาไปซ้าย: 178 เลือกภาษาอาหรับ 177 เลือกภาษาฮีบรู ตั้ง charset แล้ววาด สองบรรทัดด้านล่างก็ออกมาในลำดับการอ่านที่ถูกต้องโดยไม่ต้องตั้งค่าอะไรเพิ่ม

// อาหรับ: charset 178 บอก RtLTextOut ให้ใช้กฎของภาษาอาหรับ
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// ฮีบรู: charset 177 สลับกฎไปเป็นภาษาฮีบรู
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

มีรายละเอียดเรื่องลำดับหนึ่งข้อที่พลาดได้ง่าย: SetFont ต้องมาก่อน และต้องเรียกซ้ำหลัง AddPage ทุกครั้ง เพราะฟอนต์ปัจจุบันซึ่งรวม charset ด้วยนั้นไม่รอดข้ามการขึ้นหน้าใหม่ ลืมเรียกซ้ำแล้วหน้าที่สองจะถอยไปใช้ฟอนต์อะไรก็ตามที่ยังทำงานอยู่ ซึ่งสำหรับภาษาอาหรับมักหมายถึงกล่องเปล่า

มันไม่ได้กลับด้านข้อความที่คุณกลับด้านไปแล้ว

ความผิดพลาดข้อเดียวที่กลืนเวลาไล่บั๊กไปมากที่สุดตรงนี้คือการป้อนสตริงที่คุณกลับด้านด้วยมือไปแล้วให้ RtLTextOut ผู้คนมาถึงเมธอดนี้หลังจากลองครั้งแรกด้วย TextOut ธรรมดาแล้วได้ผลกลับหลัง และทางแก้ขัดตาทัพที่พบบ่อยคือกลับลำดับตัวอักษรในโค้ดก่อนวาด RtLTextOut กลับด้านภายในตัวมันเองอยู่แล้ว สตริงที่กลับด้านมาก่อนจึงถูกกลับด้านอีกรอบแล้วลงเอยกลับที่เดิม ให้ส่งข้อความในลำดับตรรกะ ลำดับที่คุณจะพิมพ์และอ่านออกเสียง แล้วปล่อยให้การเรียกนั้นจัดลำดับใหม่เอง

กับดักนี้ร้ายกว่าการกลับด้านธรรมดา เพราะสตริงที่ถูกกลับด้านสองรอบอาจดูถูกต้องกับวลีทดสอบที่เป็นภาษาอาหรับล้วนหนึ่งวลี แล้วพังทันทีที่บรรทัดหนึ่งมีคำละตินหรือตัวเลขติดมา ภายในบรรทัดขวาไปซ้าย ช่วงข้อความที่แทรกอยู่เหล่านั้นควรอ่านจากซ้ายไปขวา และการกลับด้านด้วยมือทำลายการซ้อนชั้นนั้น ขณะที่กรณีอาหรับล้วนบังเอิญรอดมาได้ บั๊กจึงแล่นผ่านการทดสอบเบื้องต้นครั้งแรกของคุณ แล้วโผล่ทีหลังบนใบแจ้งหนี้จริงที่มีเลขบัญชีอยู่ในนั้น ให้ถอนการกลับด้านด้วยมือทุกจุดออกทันทีที่คุณเปลี่ยนมาใช้ RtLTextOut

ผลข้างเคียงเรื่อง Direction ที่ควรรู้ไว้

การเรียก RtLTextOut เปลี่ยนมากกว่าบรรทัดที่คุณกำลังวาด มันยังพลิกค่าความชอบเรื่องทิศทางการอ่านของเอกสารไปเป็นขวาไปซ้ายด้วย ซึ่งเป็นสิ่งเดียวกับที่คุณจะตั้งเองผ่านคุณสมบัติ Direction ตัวตั้งค่านั้นเพิ่ม vpDirection เข้าไปใน ViewerPreferences ของเอกสาร ซึ่งบอกโปรแกรมอ่านว่าจะจัดหน้าคู่อย่างไรและเลย์เอาต์แบบหน้าประกบเริ่มจากด้านไหน เมื่อทั้งเอกสารเป็นภาษาอาหรับหรือฮีบรู นี่คือสิ่งที่คุณต้องการพอดี และคุณได้มันมาฟรี

เรื่องนี้ควรรู้ไว้เพราะมันมองไม่เห็นบนหน้าเดียวโดยเฉพาะ ถ้าเอกสารเป็นซ้ายไปขวาเสียส่วนใหญ่แล้วมีบล็อกขวาไปซ้ายอยู่บล็อกเดียว การเรียก RtLTextOut ครั้งแรกก็ยังพลิกค่าความชอบของทั้งไฟล์อยู่ดี และไม่มีอะไรในหน้าตัวอย่างหน้าเดียวของคุณจะแสดงให้เห็น อาการจะโผล่หลายสัปดาห์ต่อมาเมื่อมีคนพิมพ์หนังสือเล่มเล็กแบบสองหน้า แล้วหน้าคู่ออกมากลับด้าน ถ้านั่นไม่ใช่สิ่งที่คุณต้องการ ให้ตั้ง Direction กลับอย่างชัดเจนหลังช่วงข้อความขวาไปซ้าย:

// RtLTextOut ตั้งทิศทางของเอกสารเป็น RightToLeft ไปแล้ว
// คืนค่าเป็นซ้ายไปขวาถ้าเอกสารเป็น LTR เป็นหลัก
Pdf.Direction := LeftToRight;

สำหรับเอกสารที่อ่านจากขวาไปซ้ายจริง ๆ ก็ปล่อยไว้อย่างนั้น ประเด็นคือให้รู้ว่าการเรียกนี้มีผลทั่วทั้งเอกสาร เพื่อไม่ให้เซอร์ไพรส์เรื่องหนังสือเล่มเล็กเกิดขึ้นเลย

ลงทะเบียนฟอนต์ที่คุณส่งไปด้วย ไม่ใช่ฟอนต์ที่คุณหวังว่าจะมีติดตั้งอยู่

การเรียงลำดับใหม่ทั้งหมดไม่มีความหมายเลยถ้าฟอนต์ไม่มีกลีฟให้วาด ความล้มเหลวคลาสสิกคือรายงานที่เรนเดอร์ออกมาสมบูรณ์แบบบนเครื่องนักพัฒนา ซึ่งบังเอิญมี Arial Unicode MS อยู่ แต่ออกมาเป็นแถวของกล่องเปล่าบนเซิร์ฟเวอร์ของลูกค้าที่ Windows แอบแทนที่ด้วยฟอนต์ซึ่งไม่รองรับภาษาอาหรับเลย ยาแก้คือเลิกเชื่อฟอนต์ของระบบที่ติดตั้งไว้ แล้วลงทะเบียนฟอนต์ที่คุณส่งไปพร้อมแอปพลิเคชัน

// ส่งฟอนต์อาหรับที่รู้จักไปด้วย แล้วลงทะเบียนก่อนวาด
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 มีบิตสิทธิ์การฝังติดมาด้วย แบบอักษรที่ดูดีบนจอก็อาจมีสัญญาอนุญาตที่ห้ามส่งมันไปในเอกสารของลูกค้า ตรวจสัญญาอนุญาตก่อนจะฝัง ไม่ใช่หลังจากมีคนร้องเรียน

ตัวอย่างโปรแกรมคอนโซลแบบครบชุด

เมื่อประกอบชิ้นส่วนทั้งหมดเข้าด้วยกัน นี่คือโปรแกรมที่เบ็ดเสร็จในตัวซึ่งเขียนหนึ่งหน้าที่มีบรรทัดภาษาอาหรับ บรรทัดภาษาฮีบรู และบรรทัดผสมที่มีชื่อผลิตภัณฑ์เป็นอักษรละติน แต่ละบล็อกตั้ง charset ของตัวเอง แล้วจึงวาดในลำดับตรรกะ

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // ยูนิตหลักของ HotPDF

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'RtLTextOut.pdf';
    Pdf.BeginDoc;

    // หัวข้อภาษาละตินไปตามเส้นทาง TextOut ธรรมดา
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // อาหรับ: charset 178 ลำดับตรรกะ RtLTextOut จัดลำดับใหม่ให้
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // ฮีบรู: charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // บรรทัดผสม: คำละตินที่แทรกอยู่ยังอ่านจากซ้ายไปขวา
    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 วางตัวจากซ้ายไปขวาอยู่ภายในช่วงข้อความภาษาอาหรับ การซ้อนชั้นนั้นคือผลลัพธ์ทวิทิศทางที่ถูกต้อง ไม่ใช่บั๊ก แม้ผู้ตรวจครั้งแรกจะแจ้งว่าเป็นบั๊กอยู่เป็นประจำ บทความเรื่องการจัดรูปอักษรที่ลิงก์ไว้ข้างต้นอธิบายว่าทำไมกฎของ Unicode จึงเรียกร้องเช่นนั้น และจะเขียนเกณฑ์การยอมรับอย่างไรให้ไม่มีใครแจ้งเรื่องนี้อีก

ข้อผิดพลาดที่พบบ่อยและทางแก้

ความล้มเหลวทุกข้อด้านล่างเคยปรากฏในเธรดสนับสนุนจริง และแต่ละข้อสาวกลับไปหาหัวข้อใดหัวข้อหนึ่งข้างต้นได้

  • ผลลัพธ์อ่านกลับหลังหรือสับสนบนบรรทัดผสม — สตริงถูกกลับด้านด้วยมือก่อนการเรียก มักเป็นทางแก้ที่ตกค้างจากการลองใช้ TextOut ให้ลบการกลับด้านด้วยมือทุกจุดแล้วส่งลำดับตรรกะเข้าไป RtLTextOut กลับด้านภายในตัวมันเอง
  • ตัวอักษรพิมพ์ออกมาแยกกันในรูปเดี่ยว — ข้อความไปผ่าน TextOut ธรรมดา หรือ SetFont ถูกเรียกโดยไม่มี charset ของขวาไปซ้าย ให้วาดด้วย RtLTextOut แล้วส่ง 178 สำหรับภาษาอาหรับหรือ 177 สำหรับภาษาฮีบรูเป็นอาร์กิวเมนต์ตัวที่สี่ของ SetFont
  • กล่องเปล่าบนเครื่องของลูกค้า — Windows แทนที่ด้วยฟอนต์ที่ไม่รองรับภาษาอาหรับหรือฮีบรู ให้เลิกเรียกชื่อฟอนต์ที่ติดตั้งไว้ในระบบ แล้วลงทะเบียนแบบอักษรที่คุณส่งไปเองผ่าน RegisterUnicodeTTF และเรียก SetFont ด้วยชื่อนั้น
  • หน้าที่สองเรนเดอร์ด้วยฟอนต์ผิด — ฟอนต์ปัจจุบันไม่รอดข้าม AddPage ให้เรียก SetFont ซ้ำโดยรวม charset ด้วย หลังการขึ้นหน้าใหม่ทุกครั้ง
  • หน้าคู่แบบสองหน้าพิมพ์ออกมากลับด้านบนเอกสารที่เป็น LTR เป็นหลัก — การเรียก RtLTextOut ครั้งแรกพลิกค่า Direction ของเอกสารเป็นผลข้างเคียง ให้ตั้ง Pdf.Direction := LeftToRight หลังช่วงข้อความขวาไปซ้าย
  • ข้อความ Unicode ที่ฝังไว้เสื่อมคุณภาพอย่างเงียบ ๆ ที่ปลายน้ำ — มีบางอย่างในไปป์ไลน์บังคับให้เป็น PDF 1.4 ขณะที่การจัดการ Unicode แบบฝังของ HotPDF ต้องการ 1.5 ขึ้นไป ให้ยกระดับเวอร์ชันเอกสารหรือถอดข้อจำกัดที่ปลายน้ำออก

ก่อนส่งมอบรูปแบบไฟล์ ให้ตรวจสอบให้พ้นการมองด้วยตา: คัดลอกข้อความออกจากโปรแกรมอ่านกลับมา ลองค้นหาภายในเอกสาร เปิดไฟล์บนเครื่องที่ไม่มีฟอนต์สำหรับการพัฒนาของคุณ และวางเอกสารจริงหนึ่งฉบับต่อหน้าเจ้าของภาษา รายการตรวจสอบฉบับเต็ม แผนที่ความครอบคลุมรายระบบเขียน และคลังสตริงทดสอบที่ควรสร้างไว้ ล้วนอยู่ในบทความคู่กันว่าด้วยการจัดรูปข้อความอาหรับและ RTL ด้วย HotPDF

การเรียก RtLTextOut, SetFont และ RegisterUnicodeTTF ที่แสดงไว้ที่นี่เป็นส่วนหนึ่งของ HotPDF Delphi Component สำหรับ Delphi และ C++Builder