Bài viết kỹ thuật

RtLTextOut trong HotPDF: văn bản PDF RTL trong Delphi

Đưa câu tiếng Ả Rập يوضح ملف PDF هذا cho TextOut thuần và trang trả về sai theo hai cách cùng lúc. Các từ chạy từ trái sang phải thay vì từ phải sang trái, và các chữ cái đứng rời nhau ở dạng cô lập thay vì nối lại thành từ liền mạch. Chẳng có lỗi nào cả. Delphi biên dịch được, tệp mở được, rồi một người soát xét biết đọc tiếng Ả Rập nói với bạn rằng đầu ra không dùng được. Cách sửa là một lệnh gọi, không phải đổi thư viện: HotPDF đưa văn bản phải sang trái qua một phương thức riêng, RtLTextOut, lo phần sắp xếp lại mà TextOut thuần không làm. Trang này là tài liệu tham chiếu thực dụng cho phương thức đó: chữ ký cùng các tham số, tham số charset chọn hệ chữ viết, tác dụng phụ ở cấp tài liệu, phần thiết lập font phải làm trước, và những kiểu hỏng thực sự đến với bộ phận hỗ trợ, mỗi kiểu kèm cách sửa

Chữ ký và tham số

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

XY neo đoạn chữ trong hệ tọa độ của chính trang, đo từ góc dưới-trái với Y tăng dần lên trên, cùng gốc tọa độ mà mọi lệnh gọi TextOut dùng; RtLTextOut thay đổi thứ tự glyph, không thay đổi nơi trang lấy làm mốc đo. angle xoay đường cơ sở y hệt như trong TextOut, nên 0 vẽ một dòng ngang. Text là chuỗi theo thứ tự logic, thứ tự bạn sẽ gõ ra, còn phiên bản nạp chồng thứ hai nhận đúng dữ liệu UTF-16 ấy dưới dạng một bộ đệm PWORD thô kèm số đơn vị mã tường minh, đây là dạng nên dùng khi văn bản đến từ một API chứ không phải từ một chuỗi Delphi. Trên các phiên bản Delphi cũ chưa có phân giải nạp chồng cho những kiểu này, dạng chuỗi được phơi bày dưới tên RtLTextOutStr với danh sách tham số y hệt

Sự phân công giữa hai lệnh gọi xuất chữ là nghiêm ngặt. TextOut vẽ các điểm mã theo đúng thứ tự bạn truyền vào, điều này đúng với Latin, Cyrillic và CJK còn sai với tiếng Ả Rập và Do Thái. RtLTextOut sắp xếp lại từng dòng thành thứ tự hiển thị phải sang trái trước rồi mới vẽ, giữ cho các từ Latin và chữ số nhúng bên trong vẫn đọc từ trái sang phải trong dòng. HotPDF cố ý giữ hai phương thức tách biệt thay vì đoán chiều từ các ký tự, nên việc chọn gọi hàm nào chính là việc chọn hành vi hệ chữ viết bạn nhận được; dùng RtLTextOut cho các đoạn phải sang trái, TextOut cho mọi thứ khác, và đừng bao giờ đẩy cái này qua cái kia. Vì sao việc sắp xếp lại tồn tại, Thuật toán hai chiều của Unicode cùng phép nối chữ theo ngữ cảnh trong tiếng Ả Rập thực sự làm gì, và phần tạo hình của HotPDF dừng ở đâu là chủ đề của bài đồng hành về tạo hình văn bản Ả Rập và RTL với HotPDF; mọi thứ bên dưới là phần thiết lập thực dụng

Sơ đồ cách RtLTextOut sắp xếp lại một dòng lẫn tiếng Ả Rập và Latin thành thứ tự hiển thị phải sang trái trước khi vẽ nó vào PDF
RtLTextOut sắp xếp lại từng dòng thành thứ tự hiển thị trước khi vẽ: các đoạn phải sang trái giữ nguyên trình tự còn từ Latin và chữ số nhúng bên trong vẫn đọc từ trái sang phải trong dòng

Tham số charset quyết định hệ chữ viết

Thứ cho RtLTextOut biết nó đang dàn trang tiếng Ả Rập hay tiếng Do Thái không phải là phương thức, mà là font. SetFont nhận một charset của Windows làm tham số thứ tư, và giá trị đó mang các quy tắc hệ chữ viết vào lệnh gọi phải sang trái: 178 chọn tiếng Ả Rập, 177 chọn tiếng Do Thái. Đặt charset, rồi vẽ, và hai dòng bên dưới hiện ra đúng thứ tự đọc mà không cần cấu hình thêm gì

// Tiếng Ả Rập: charset 178 bảo RtLTextOut áp dụng quy tắc tiếng Ả Rập
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Tiếng Do Thái: charset 177 chuyển quy tắc sang tiếng Do Thái
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Có một chi tiết về trình tự rất dễ bỏ sót: SetFont phải đi trước và phải được lặp lại sau mỗi lần AddPage, bởi font hiện hành, kể cả charset, không sống sót qua một lần sang trang. Quên lặp lại thì trang thứ hai sẽ lùi về bất kỳ font nào đang hoạt động, với tiếng Ả Rập thường nghĩa là những ô trống rỗng

Nó không đảo ngược đoạn văn bản bạn đã tự đảo

Lỗi duy nhất ngốn nhiều thời gian gỡ rối nhất ở đây là đưa cho RtLTextOut một chuỗi bạn đã tự lật bằng tay. Người ta tìm tới phương thức này sau khi lần thử đầu với TextOut thuần cho ra chữ ngược, và một cách chữa cháy phổ biến là đảo các ký tự trong mã trước khi vẽ. RtLTextOut tự đảo bên trong, nên một chuỗi đã đảo trước sẽ bị đảo lần thứ hai và quay về đúng chỗ ban đầu. Hãy truyền văn bản theo thứ tự logic, thứ tự bạn sẽ gõ ra và đọc thành tiếng, rồi để lệnh gọi lo phần sắp xếp lại

Cái bẫy này khó chịu hơn một cú lật đơn thuần, bởi một chuỗi bị đảo hai lần có thể trông đúng với một câu thử toàn tiếng Ả Rập rồi vỡ ngay khoảnh khắc một dòng mang theo một từ Latin hay một con số. Bên trong một dòng phải sang trái, các đoạn nhúng ấy phải đọc từ trái sang phải, và việc đảo tay phá hỏng cách lồng đó trong khi trường hợp thuần Ả Rập lại tình cờ sống sót. Vậy nên con lỗi lướt qua bài kiểm nhanh đầu tiên của bạn rồi nổi lên về sau trên một hóa đơn thật có số tài khoản trong đó. Hãy gỡ bỏ mọi phép đảo tay ngay lúc bạn chuyển sang RtLTextOut

Tác dụng phụ với Direction mà bạn nên biết

Gọi RtLTextOut thay đổi nhiều hơn cái dòng bạn đang vẽ. Nó còn lật thiết lập chiều đọc ưa dùng của tài liệu sang phải-sang-trái, đúng thứ mà lẽ ra bạn tự đặt qua thuộc tính Direction. Hàm setter đó thêm vpDirection vào ViewerPreferences của tài liệu, thứ cho trình xem biết cách bố trí các trang đôi và cạnh nào là nơi bố cục trang đối diện bắt đầu. Khi cả tài liệu là tiếng Ả Rập hay Do Thái thì đây đúng là điều bạn muốn, và bạn được nó miễn phí

Điều này đáng biết chính vì nó vô hình trên một trang đơn lẻ. Nếu tài liệu chủ yếu là trái sang phải với một khối phải sang trái, lệnh gọi RtLTextOut đầu tiên vẫn sẽ lật thiết lập ưa dùng của cả tệp, và chẳng có gì trong bản in thử một trang của bạn cho thấy điều đó. Triệu chứng xuất hiện vài tuần sau khi ai đó in một cuốn sổ hai mặt và các trang đôi ra ngược gương. Nếu đó không phải điều bạn muốn, hãy đặt Direction trở lại một cách tường minh sau đoạn phải sang trái:

// RtLTextOut đã đặt chiều tài liệu thành RightToLeft;
// hãy khôi phục trái sang phải nếu tài liệu chủ yếu là LTR
Pdf.Direction := LeftToRight;

Với một tài liệu thực sự đọc từ phải sang trái, cứ để nguyên. Điều quan trọng là biết lệnh gọi này có tác dụng trên toàn tài liệu, để bất ngờ với cuốn sổ kia không bao giờ xảy ra

Hãy đăng ký font bạn phát hành kèm, không phải font bạn hy vọng đã được cài

Mọi việc sắp xếp lại đều vô nghĩa nếu font không có glyph nào để vẽ. Kiểu hỏng kinh điển là một báo cáo kết xuất hoàn hảo trên máy lập trình viên, nơi tình cờ có sẵn Arial Unicode MS, rồi hiện ra hàng loạt ô trống trên máy chủ của khách hàng, nơi Windows lặng lẽ thay bằng một font hoàn toàn không phủ tiếng Ả Rập. Cách chữa là thôi tin vào font hệ thống đã cài và đăng ký một font bạn phát hành kèm ứng dụng

// Phát hành kèm một font tiếng Ả Rập đã biết và đăng ký nó trước khi vẽ
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Có hai ranh giới đi kèm việc đăng ký. Một font đưa vào qua RegisterUnicodeTTF sẽ được nhúng, và cách xử lý Unicode nhúng của HotPDF cần tài liệu ở PDF 1.5 trở lên; điều đó chỉ cắn khi có thứ gì đó phía sau nhất định đòi PDF 1.4, mà khi ấy thì lỗi lại lặng lẽ. Ranh giới kia là pháp lý chứ không phải kỹ thuật: tệp TrueType mang các bit quyền nhúng, và một kiểu chữ trông ổn trên màn hình vẫn có thể được cấp phép theo cách cấm phát hành nó bên trong tài liệu của khách hàng. Hãy xác nhận giấy phép trước khi nhúng, đừng đợi tới lúc có khiếu nại

Một ví dụ console hoàn chỉnh

Ghép các mảnh lại, đây là một chương trình khép kín ghi ra một trang có một dòng tiếng Ả Rập, một dòng tiếng Do Thái, và một dòng trộn mang tên sản phẩm bằng Latin. Mỗi khối đặt charset của nó rồi vẽ theo thứ tự logic

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // unit chính của HotPDF

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

    // Một tiêu đề Latin đi theo đường TextOut thông thường
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Tiếng Ả Rập: charset 178, thứ tự logic, RtLTextOut lo phần sắp xếp lại
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // Tiếng Do Thái: charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // Dòng trộn: từ Latin nhúng bên trong vẫn đọc từ trái sang phải
    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.

Chạy nó rồi mở kết quả. Các dòng tiếng Ả Rập và Do Thái đọc từ phải sang trái, chữ cái nối lại ở nơi hệ chữ viết nối chúng, và ở dòng cuối cùng mã hiệu HotPDF nằm theo chiều trái sang phải bên trong đoạn tiếng Ả Rập. Cách lồng đó là kết quả hai chiều đúng đắn, không phải một con lỗi, dù người soát xét lần đầu vẫn thường báo nó như một con lỗi; bài viết về tạo hình dẫn ở trên giải thích vì sao các quy tắc Unicode đòi hỏi như vậy và cách viết tiêu chí nghiệm thu sao cho báo cáo ấy không bao giờ bị gửi đi

Các lỗi thường gặp và cách sửa

Mọi kiểu hỏng bên dưới đều từng xuất hiện trong một cuộc trao đổi hỗ trợ có thật, và mỗi kiểu đều truy về một trong các mục ở trên

  • Đầu ra đọc ngược hoặc rối tung ở các dòng trộn — chuỗi đã bị đảo bằng tay trước lệnh gọi, thường là tàn dư của một cách chữa cháy từ lần thử với TextOut. Hãy xóa mọi phép đảo tay và truyền theo thứ tự logic; RtLTextOut tự đảo bên trong
  • Chữ cái in ra rời rạc ở dạng cô lập — văn bản đã đi qua TextOut thuần, hoặc SetFont được gọi mà không có charset phải sang trái. Hãy vẽ bằng RtLTextOut và truyền 178 cho tiếng Ả Rập hoặc 177 cho tiếng Do Thái làm tham số thứ tư của SetFont
  • Ô trống trên máy khách hàng — Windows đã thay bằng một font không phủ tiếng Ả Rập hay Do Thái. Hãy thôi gọi tên font đã cài; đăng ký một kiểu chữ bạn phát hành kèm qua RegisterUnicodeTTF rồi SetFont theo đúng tên đó
  • Trang thứ hai kết xuất sai font — font hiện hành không sống sót qua AddPage. Hãy lặp lại lệnh gọi SetFont, kể cả charset, sau mỗi lần sang trang
  • Trang đôi in ngược gương trên một tài liệu chủ yếu LTR — lệnh gọi RtLTextOut đầu tiên đã lật Direction của tài liệu như một tác dụng phụ. Hãy đặt Pdf.Direction := LeftToRight sau đoạn phải sang trái
  • Văn bản Unicode nhúng âm thầm xuống cấp ở phía sau — thứ gì đó trong pipeline ép PDF 1.4, còn cách xử lý Unicode nhúng của HotPDF cần 1.5 trở lên. Hãy nâng phiên bản tài liệu hoặc gỡ bỏ ràng buộc phía sau

Trước khi định dạng được phát hành, hãy kiểm chứng vượt quá việc liếc mắt: sao chép văn bản ngược ra khỏi trình xem, chạy tìm kiếm trong tài liệu, mở tệp trên một máy không có font phát triển của bạn, và đặt một tài liệu thật trước mặt một người đọc bản ngữ. Danh sách kiểm chứng đầy đủ, bản đồ độ phủ theo từng hệ chữ viết, và kho chuỗi thử đáng dựng đều nằm trong bài đồng hành về tạo hình văn bản Ả Rập và RTL với HotPDF

Các lệnh gọi RtLTextOut, SetFontRegisterUnicodeTTF trình bày ở đây là một phần của HotPDF Delphi Component cho Delphi và C++Builder