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

ข้อความแนวตั้ง CJK ใน PDF ด้วย Delphi และ HotPDF

เมื่อเปิดนวนิยายญี่ปุ่น สิ่งแรกที่คุณสังเกตเห็นคือข้อความจะวิ่งลงมาตามคอลัมน์ ไม่ใช่ตามแนวนอน และคอลัมน์จะเลื่อนจากขอบขวาของหน้ากระดาษไปทางซ้าย ผู้อ่านที่เติบโตมากับรูปแบบนี้อาจรู้สึกว่าข้อความแนวนอนดูแข็งกระด้าง ปัญหาทางวิศวกรรมคือ PDF รวมถึงระบบข้อความดิจิทัลเกือบทั้งหมด ถูกสร้างขึ้นบนพื้นฐานของเส้นฐานแนวนอนที่ขยายจากซ้ายไปขวา และ content stream ไม่มีแนวคิดของคำสั่ง "เขียนย่อหน้านี้ลงด้านล่างแทน" ดังนั้นเมื่อแอปพลิเคชัน Delphi ต้องสร้างใบรับรอง บทกวี ป้าย หรือเอกสารทางกฎหมายในรูปแบบดั้งเดิมสำหรับผู้อ่านชาวไต้หวัน ญี่ปุ่น หรือเกาหลี การจัดรูปแบบจะต้องถูกประกอบขึ้นด้วยมือ: ตัวอักษรหนึ่งตัวอยู่ใต้ตัวก่อนหน้า และคอลัมน์หนึ่งอยู่ทางซ้ายของคอลัมน์ก่อนหน้า

HotPDF มีสวิตช์ที่ช่วยจัดการการจัดเรียงระดับตัวอักษรให้คุณ ฟอนต์ที่คุณตั้งค่าจะมีแฟล็ก IsVertical และเมื่อเปิดใช้งาน การเรียก TextOut เพียงครั้งเดียวจะซ้อนสตริงทั้งหมดเป็นคอลัมน์แนวตั้งแทนที่จะวิ่งตามเส้นฐาน การจัดวางคอลัมน์ ลำดับจากขวาไปซ้าย และการแทนที่กลีฟที่สำคัญอย่างเงียบ ๆ คือสิ่งที่จะอธิบายในส่วนที่เหลือของหน้านี้

หน้า PDF ขนาด A4 ที่สร้างโดย HotPDF แสดงข้อความภาษาจีน ญี่ปุ่น และเกาหลีที่จัดเรียงเป็นคอลัมน์แนวตั้งซึ่งอ่านจากขวาไปซ้าย
หน้า A4 เดี่ยวที่มีข้อความภาษาจีน ญี่ปุ่น และเกาหลีในคอลัมน์แนวตั้ง สร้างขึ้นจากกระบวนการ Delphi เดียว

สวิตช์อยู่ที่ SetFont

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

// SetFont(FontName, FontStyle, Size, FontCharset, IsVertical)
// The 5th argument flips the current font into vertical mode.
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, False); // horizontal
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, True);  // vertical

เนื่องจากแฟล็กนี้ผูกอยู่กับฟอนต์ คุณจึงสามารถสลับระหว่างการเขียนแนวนอนและแนวตั้งได้ง่าย ๆ โดยเรียก SetFont อีกครั้งพร้อมอาร์กิวเมนต์ตัวสุดท้ายที่ต่างไป หน้าหนึ่งอาจมีหัวเรื่องแนวนอนพาดผ่านด้านบนและมีข้อความเนื้อหาแนวตั้งอยู่ด้านล่าง และ HotPDF จะแยกทั้งสองโหมดนี้ออกจากกันตามออบเจกต์ฟอนต์ไม่ใช่ตามหน้ากระดาษ นี่คือสิ่งที่ทำให้การจัดรูปแบบผสมเป็นไปได้โดยที่คุณไม่ต้องจัดการโหมดพิเศษใด ๆ: ทุก TextOut ที่ตามหลัง SetFont แนวตั้งจะซ้อนลงล่าง ทุก TextOut ที่ตามหลังแบบแนวนอนจะวิ่งตามเส้นฐาน และ SetFont ล่าสุดจะมีผลใช้งาน

อาร์กิวเมนต์ที่สี่คือชุดอักขระของ Windows ซึ่งเป็นตัวเดียวกับที่การเรียกแบบแนวนอนใช้ การส่ง DEFAULT_CHARSET ช่วยให้ระบบวิเคราะห์กลีฟตามสตริง ซึ่งมีความสำคัญในที่นี้เนื่องจากเอกสารแนวตั้งมักมีการผสมตัวเขียน ระบบอื่น ๆ ของฟอนต์ยังคงมีผล: ฟอนต์ต้องถูกติดตั้งในเครื่องที่บิลด์ และคุณมักจะต้องการให้ FontEmbedding := True เสมอ เพื่อให้ไฟล์เรนเดอร์กลีฟ CJK เดียวกันบนโปรแกรมอ่านที่ไม่เคยมี Arial Unicode MS

การเรียก TextOut หนึ่งครั้งคือหนึ่งคอลัมน์

เมื่อใช้ฟอนต์แนวตั้ง การเรียก TextOut จะไม่กางสตริงออกไปทางด้านข้างจากจุดที่กำหนดอีกต่อไป มันจะวางตัวอักษรแรกไว้ที่ด้านบนแล้วค่อย ๆ เรียงตัวที่เหลือตรงลงมา โดยขยับตามความสูงของบรรทัดฟอนต์สำหรับแต่ละกลีฟ ค่า X ที่คุณส่งเข้ามากำหนดคอลัมน์ ส่วนค่า Y กำหนดจุดเริ่มต้นด้านบนของคอลัมน์ ดังนั้นเพื่อจัดรูปแบบข้อความจริง คุณต้องเรียก TextOut หนึ่งครั้งต่อคอลัมน์และขยับ X ไปทางซ้ายระหว่างการเรียก เนื่องจากคอลัมน์ CJK จะอ่านจากขวาไปซ้าย

var
  Pdf: THotPDF;
const
  ColTop = 760;   // y of the first glyph in every column (points)
  ColGap = 28;    // horizontal distance between columns
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'VerticalText.pdf';
    Pdf.FontEmbedding := True;       // embed the CJK face for portable rendering
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := psA4;

    // A horizontal heading first, in the ordinary writing mode.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 16, DEFAULT_CHARSET, False);
    Pdf.CurrentPage.TextOut(60, 800, 0, 'Tang poem, vertical layout');

    // Switch the font into vertical mode; every TextOut below now stacks.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 18, DEFAULT_CHARSET, True);

    // Columns advance right to left, so X decreases with each call.
    Pdf.CurrentPage.TextOut(520, ColTop, 0, '床前明月光');
    Pdf.CurrentPage.TextOut(520 - ColGap,     ColTop, 0, '疑是地上霜');
    Pdf.CurrentPage.TextOut(520 - ColGap * 2, ColTop, 0, '舉頭望明月');
    Pdf.CurrentPage.TextOut(520 - ColGap * 3, ColTop, 0, '低頭思故鄉');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

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

พิกัด: เลื่อนลงมาตามหน้ากระดาษ จากขวาไปซ้าย

มีสองแกนที่ทำงานอยู่และดึงไปในทิศทางตรงกันข้าม ดังนั้นการทำความเข้าใจให้แม่นยำจึงสำคัญ HotPDF วัดจากมุมล่างซ้ายของหน้า โดยค่า Y เพิ่มขึ้นไปด้านบน มีหน่วยเป็นพอยต์ (1/72 นิ้ว) คอลัมน์แนวตั้งจึงเริ่มต้นที่ค่า Y สูง (ใกล้ด้านบนของกระดาษ) และตัวอักษรจะลดหลั่นลงมาจากตรงนั้นเมื่อ HotPDF ลบความสูงของบรรทัดสำหรับแต่ละตัวอักษร คุณกำหนดค่า Y เริ่มต้นนั้นครั้งเดียวต่อคอลัมน์และคอมโพเนนต์จะจัดการการเรียงลงล่างให้เอง

แกนนอนคือสิ่งที่คุณควบคุมด้วยตนเอง แต่ละคอลัมน์อยู่ที่ตำแหน่ง X ของตัวเอง และคอลัมน์ถัดไปจะขยับไปยังค่า X ที่เล็กลงเนื่องจากลำดับการอ่านคือจากขวาไปซ้าย จังหวะที่เหมาะสมคือการเลือก X ของคอลัมน์ขวาสุด แล้วลบด้วยช่องว่างระหว่างคอลัมน์คงที่สำหรับการเรียกครั้งต่อ ๆ ไป ดังที่ตัวอย่างทำกับ ColGap คุณสามารถเลือกช่องว่างได้เอง หากแคบเกินไปคอลัมน์ที่อยู่ติดกันจะชนกัน หากกว้างเกินไปบล็อกจะดูห่าง สำหรับข้อความเนื้อหาขนาด 12 ถึง 18 พอยต์ ช่องว่างที่ใหญ่กว่าขนาดฟอนต์เล็กน้อยจะทำให้อ่านได้สบายตา

กลีฟที่ต้องเปลี่ยนรูปร่าง

มีอักขระสองสามตัวที่ไม่ใช่แค่การหมุนแบบแนวนอนของตัวเอง พวกมันต้องถูกแทนที่เมื่อข้อความเปลี่ยนเป็นแนวตั้ง สิ่งหนึ่งที่ HotPDF จัดการให้คุณคือเครื่องหมายเสียงยาวภาษาญี่ปุ่น (U+30FC) ซึ่งเป็นขีดสระยาวที่ปรากฏในคำคาตาคานะอย่าง コーヒー เมื่อวาดแนวนอนจะเป็นขีดสั้นบนเส้นฐาน หากคุณนำกลีฟเดียวกันนั้นไปซ้อนในคอลัมน์ มันจะนอนราบขวางคอลัมน์ ซึ่งไม่ถูกต้อง: ในภาษาญี่ปุ่นแบบแนวตั้ง เครื่องหมายนี้จะกลายเป็นขีดแนวตั้งที่เชื่อมตัวอักษรสองตัวที่ขนาบมันอยู่ HotPDF จะตรวจจับ U+30FC ในเส้นทางแนวตั้งและเรนเดอร์เป็นขีดแนวตั้ง (U+007C) ดังนั้นเครื่องหมายสระยาวจะชี้ในทิศทางที่ถูกต้องโดยที่คุณไม่ต้องทำอะไรเลย

การแทนที่เพียงอย่างเดียวนั้นครอบคลุมกรณีที่มักทำให้การนำไปใช้แบบเรียบง่ายพัง แต่ก็คุ้มค่าที่จะรู้ว่าปัญหาโดยทั่วไปนั้นลึกกว่านั้น การจัดรูปแบบอักษรแนวตั้งแบบเต็มยังหมุนตัวอักษรละตินและเครื่องหมายวรรคตอนตะวันตกไปเก้าสิบองศา เลื่อนคานะตัวเล็ก และจัดตำแหน่งวงเล็บและจุลภาคใหม่ให้อยู่ในรูปแบบแนวตั้ง และการนำไปใช้ที่สมบูรณ์นั้นจะอยู่ในฟีเจอร์ OpenType แนวตั้งของฟอนต์มากกว่าในกฎที่ตายตัว HotPDF สามารถใช้ประโยชน์จากฟีเจอร์เหล่านั้นได้เมื่อฟอนต์รองรับ: ส่วนสลับแนวตั้ง (ฟีเจอร์ GSUB vert และ vrt2) และการจัดช่องไฟแนวตั้ง (การค้นหา GPOS vkrn และ vpal) เป็นตัวเลือกที่ผู้ใช้เลือกได้ (opt-in) โดยจะนำไปใช้ตามเส้นทางแนวตั้งเมื่อฟอนต์ที่ใช้งานอยู่กำหนดไว้จริง ๆ เท่านั้น สำหรับข้อความ CJK แบบผสมในฟอนต์เดียวที่ครอบคลุมกว้างอย่าง Arial Unicode MS การจัดการ U+30FC ในตัวรวมถึงการเลื่อนความสูงบรรทัดที่สม่ำเสมอนั้นเพียงพอที่จะสร้างคอลัมน์ที่ถูกต้องและอ่านได้ ฟีเจอร์ OpenType มีความสำคัญเมื่อคุณเปลี่ยนไปใช้ฟอนต์ที่ออกแบบมาเพื่อการจัดแนวตั้งอย่างละเอียดและต้องการตำแหน่งคานะและระยะห่างระหว่างกลีฟแบบเนทีฟ

การผสมตัวเขียนและการจัดวางในหน้าเดียว

เอกสารจริงแทบจะไม่ได้บริสุทธิ์ 100% หน้ากระดาษญี่ปุ่นแบบแนวตั้งอาจมีคำบรรยายภาพภาษาอังกฤษแนวนอน หมายเลขหน้าที่ด้านล่าง หรือบล็อกข้อความภาษาเกาหลีแบบแนวตั้งอยู่ข้าง ๆ ภาษาญี่ปุ่น เนื่องจากแฟล็กแนวตั้งเป็นสวิตช์ระดับฟอนต์ คุณจึงสามารถประกอบสิ่งเหล่านี้ได้โดยสลับการเรียก SetFont แทนที่จะต้องจัดการสถานะของทั้งหน้ากระดาษ ตั้งค่าฟอนต์แนวนอน เขียนส่วนหัวและเลขหน้า ตั้งค่าฟอนต์แนวตั้ง วางคอลัมน์ และตั้งค่าฟอนต์แนวนอนอีกครั้งสำหรับส่วนท้ายกระดาษ แต่ละส่วนจะหยิบโหมดของการเรียก SetFont ครั้งล่าสุดไปใช้ ดังนั้นสิ่งที่คุณต้องทำคือเรียกใช้งานเมื่อต้องการเปลี่ยนทิศทาง

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

จากตัวอย่างไปสู่ระบบจริง

ชิ้นส่วนเหล่านี้เล็กและประกอบเข้าด้วยกันอย่างคาดเดาได้ เปิดใช้งานแฟล็กแนวตั้งใน SetFont ใช้ TextOut หนึ่งครั้งต่อคอลัมน์จากตำแหน่ง Y ด้านบนคงที่ และลดค่า X สำหรับแต่ละการเรียกเพื่อให้คอลัมน์อ่านจากขวาไปซ้าย ฝังฟอนต์เพื่อที่กลีฟ CJK จะแสดงผลได้ถูกต้องบนเครื่องของผู้อ่าน และปล่อยให้คอมโพเนนต์จัดการเครื่องหมายสระยาว U+30FC รวมถึงฟีเจอร์แนวตั้งของ OpenType เมื่อฟอนต์รองรับ จากจุดนี้ การนำไปใช้ในระบบจริงมักจะเป็นเรื่องของคณิตศาสตร์: การหาตำแหน่ง X ของคอลัมน์จากความกว้างบล็อกที่วัดไว้ การแบ่งข้อความยาวเป็นคอลัมน์ที่พอดีกับความสูงของหน้า และการกันพื้นที่สำหรับส่วนหัวแนวนอนและเลขหน้า

สำหรับภาพรวมที่กว้างขึ้นของข้อความและฟอนต์ที่เรื่องนี้ต่อยอดมา รวมถึงธรรมเนียมปฏิบัติของ TextOut แนวนอน และการลงทะเบียนฟอนต์ Unicode โปรดดู ตัวอย่าง Hello World หลายภาษา และ ตัวอย่าง TextOut สวิตช์แนวตั้งของ SetFont และการเรียก TextOut ที่แสดงในที่นี้เป็นส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder