Một nhà thiết kế chọn một phông chữ có chữ a một tầng (single-story) cho các tiêu đề, hoặc chữ số không có gạch chéo cho các bảng, hoặc một tập hợp các chữ hoa kiểu cách (swash capitals) cho trang bìa. Những glyph đó đã có sẵn trong phông chữ. Chúng chỉ đơn giản là không phải là mặc định. Chữ a mặc định ánh xạ từ ký tự thông qua bảng cmap đến một glyph, và ký tự thay thế (alternate) nằm cách đó vài glyph ID, chỉ có thể tiếp cận được thông qua một quy tắc thay thế (substitution rule). Việc tạo ra ký tự thay thế đó trong một tệp PDF có nghĩa là đọc quy tắc và xuất glyph thay thế trong content stream. Bài viết này nói về việc đọc các quy tắc đó, loại thay thế đơn (single-substitution), trong Object Pascal mà không cần thư viện định hình (shaping library) bản địa bên dưới.
Phạm vi được thu hẹp có mục đích. Các bộ ký tự mỹ thuật (stylistic set) và ký tự thay thế là các phép thay thế một-glyph-vào, một-glyph-ra. Chúng là phần của bố cục OpenType mà bạn có thể giải quyết bằng một quy trình duyệt bảng nhỏ, mang tính quyết định, giúp chúng trở thành một lựa chọn phù hợp cho một công cụ Pascal muốn tránh xa các phụ thuộc C.
Tại sao là Delphi thuần túy thay vì HarfBuzz
HarfBuzz is câu trả lời rõ ràng cho việc "định hình văn bản này", và đối với việc định hình hai chiều (bidirectional), tiếng Ấn Độ (Indic), hoặc tiếng Ả Rập đầy đủ, nó là câu trả lời đúng. Nó cũng là một thư viện C. Việc liên kết nó vào một sản phẩm Delphi hoặc C++Builder có nghĩa là phải vận chuyển một đối tượng bản địa cho mọi nền tảng và kiến trúc mục tiêu, khớp với quy ước gọi của nó, theo dõi tiến trình phát hành của nó, và đọc các điều khoản giấy phép của nó đối với các điều khoản của riêng bạn. Không điều nào trong số đó là khó khăn khi đứng riêng lẻ. Tất cả chúng đều là lực cản không bao giờ biến mất, và không mang lại giá trị gì khi yêu cầu thực tế chỉ là "cho tôi dạng ss01 của chữ cái này".
Thay thế đơn không cần một công cụ định hình (shaping engine). Nó cần một trình phân tích cú pháp cho một số định dạng bảng con GSUB và một hoặc hai phép tìm kiếm nhị phân. Viết mã đó bằng Pascal giữ cho toàn bộ chuỗi công cụ nằm trong một trình biên dịch duy nhất. Giới hạn thực tế là cách tiếp cận này chỉ xử lý các tìm kiếm thay thế glyph (glyph substitution lookup) và không có gì khác. Nó không phải là phân giải bidi, không phải là sắp xếp lại Indic, và không phải là định hình theo ngữ cảnh tự động. Ở những nơi cần những thứ đó, chúng là bắt buộc, và một truy vấn thay thế đơn sẽ không thể thay thế cho chúng.
Hệ cấp GSUB, từ trên xuống dưới
Bảng Glyph Substitution được tổ chức như một chuỗi các liên kết gián tiếp, và một truy vấn thay thế sẽ duyệt qua chuỗi đó từ trên xuống. Ở trên cùng là ScriptList. Một thẻ script như latn sẽ chọn một mục nhập, và thẻ đặc biệt DFLT là script mặc định được áp dụng khi không có script cụ thể hơn nào khớp. Mục nhập script trỏ đến một LangSys, hệ thống ngôn ngữ, với một LangSys mặc định cho trường hợp phổ biến và các mục nhập có tên tùy chọn cho các ngôn ngữ cần hành vi khác nhau. Tiếng Thổ Nhĩ Kỳ là ví dụ thông thường, nơi chữ i có dấu chấm và không có dấu chấm yêu cầu cách xử lý riêng của chúng.
LangSys đặt tên cho một tập hợp các chỉ mục tính năng (feature index). Mỗi chỉ mục trỏ vào FeatureList, nơi một bản ghi tính năng mang một thẻ bốn byte, bao gồm cả ss01, và một danh sách các chỉ mục tìm kiếm (lookup index). Các chỉ mục đó cuối cùng trỏ vào LookupList, nơi các bảng con thay thế thực tế tồn tại. Vì vậy, phân giải ss01 có nghĩa là: tìm script, tìm LangSys của nó, tìm tính năng có thẻ là ss01, thu thập các lookup mà nó đặt tên, và áp dụng chúng. HotPDF mặc định sử dụng script DFLT và LangSys mặc định, đây là những gì phần lớn các thiết kế văn bản Latin xuất xưởng, và nó hiển thị một cách để ghi đè thẻ script khi phông chữ kết nối các tính năng của nó dưới một script cụ thể.
Bảng Coverage quyết định ai tham gia
Mỗi bảng con thay thế đều bắt đầu bằng cùng một câu hỏi: liệu glyph đầu vào này có tham gia vào quy tắc này không, và nếu có, nó nằm ở đâu trong chỉ mục của chính quy tắc đó. Câu hỏi đó được trả lời bởi một bảng Coverage, và câu trả lời là một chỉ mục vùng phủ (coverage index), một số thứ tự nhỏ mà phần còn lại của bảng con sử dụng để tra cứu xem glyph sẽ trở thành gì.
Coverage có hai định dạng. Định dạng 1 là một danh sách các ID glyph được sắp xếp theo thứ tự tăng dần. Bạn tìm một glyph bằng tìm kiếm nhị phân, và vị trí của nó trong danh sách chính là chỉ mục vùng phủ của nó. Định dạng 2 là một danh sách các bản ghi phạm vi (range record), mỗi bản ghi gồm một glyph bắt đầu, một glyph kết thúc, và chỉ mục vùng phủ mà glyph bắt đầu ánh xạ tới. Một glyph bên trong một phạm vi nhận được chỉ mục vùng phủ của nó bằng cách bù đắp từ điểm bắt đầu của phạm vi. Định dạng 1 nhỏ gọn khi các glyph tham gia nằm rải rác, Định dạng 2 khi chúng rơi vào các lượt chạy liên tục. Cả hai đều được sắp xếp, vì vậy cả hai đều được tìm kiếm trong thời gian logarit, và cả hai đều trả về chỉ mục vùng phủ hoặc một giá trị "không được phủ" (not covered) rõ ràng để công cụ để glyph yên.
Thay thế đơn (Single Substitution), hai định dạng
Thay thế đơn là LookupType 1, và nó ánh xạ một glyph tới chính xác một thay thế. Nó cũng có hai định dạng, và việc phân chia là một tối ưu hóa không gian. Định dạng 1 lưu trữ một delta có dấu duy nhất. ID glyph đầu ra là ID glyph đầu vào cộng với delta đó, modulo 65536. Đây là cách một phông chữ mã hóa một phép thay thế trong đó mọi glyph tham gia đều nằm ở cùng một khoảng bù cố định so với ký tự thay thế của nó, ví dụ một khối các chữ số bằng đầu (lining figures) được đặt cách một khoảng không đổi so với các chữ số kiểu cổ (oldstyle figures) tương ứng. Bảng Coverage cho biết các glyph nào đủ điều kiện, và một delta duy nhất đó phục vụ cho tất cả chúng.
Định dạng 2 lưu trữ một mảng rõ ràng các ID glyph thay thế. Chỉ mục vùng phủ từ bảng Coverage là chỉ mục vào mảng đó, vì vậy glyph tại chỉ mục vùng phủ 0 trở thành phần tử mảng đầu tiên, chỉ mục vùng phủ 1 trở thành phần tử thứ hai, v.v. Định dạng 2 được sử dụng khi các ký tự thay thế không ở một khoảng bù đồng nhất, đây là trường hợp phổ biến đối với các bộ ký tự mỹ thuật được xây dựng thủ công. Truy vấn là giống nhau từ phía người gọi trong cả hai trường hợp. Lấy glyph đầu vào, chạy nó qua Coverage, và nếu nó được phủ, hãy áp dụng delta hoặc đọc ô mảng.
var
Pdf: THotPDF;
BaseGID, AltGID: Word;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
Pdf.SetFont('My Stylistic Face', 12, []);
// Default glyph for 'a' through the font's cmap.
BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));
// Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');
// AltGID = BaseGID means the feature did not touch this glyph.
if AltGID <> BaseGID then
{ emit AltGID in the content stream };
finally
Pdf.Free;
end;
end;
Hợp đồng đáng lưu ý là việc đi thẳng qua (pass-through). GetSingleSubstituteGlyph trả về ID glyph đầu vào không thay đổi trong mọi trường hợp bỏ lỡ: không có phông chữ, không có bảng GSUB, không có tính năng phù hợp, không có cú va chạm vùng phủ. Điều đó có nghĩa là lệnh gọi này an toàn để thực hiện một cách vô điều kiện. Bạn yêu cầu ký tự thay thế, và nếu không có ký tự nào, bạn nhận lại chính xác những gì bạn đã đưa vào, vì vậy mã gọi không bao giờ cần xử lý trường hợp đặc biệt cho một phông chữ thiếu tính năng đó.
Ý nghĩa của các thẻ tính năng mỹ thuật (stylistic feature tags)
Thẻ tính năng là toàn bộ từ vựng chỉ ra ký tự thay thế nào bạn đang yêu cầu, và các thẻ liên quan đến công việc thiết kế mỹ thuật là một danh sách ngắn. Cặp dòng đầu tiêu đề là salt, các ký tự thay thế mỹ thuật (stylistic alternates), quyền truy cập bao quát vào các dạng thay thế của glyph, và từ ss01 đến ss20, hai mươi bộ ký tự mỹ thuật được đánh số mà một phông chữ có thể xác định, mỗi bộ là một gói thay thế được đặt tên mà nhà thiết kế nhóm lại với nhau. Ví dụ, một phông chữ có thể đặt chữ a một tầng và chữ R chân thẳng dưới thẻ ss03, do đó việc bật bộ ký tự đơn lẻ đó sẽ tạo kiểu lại cho cả hai.
Xung quanh chúng là một số thẻ thay thế đơn khác. aalt là quyền truy cập tất cả ký tự thay thế (access-all-alternates), tập hợp của mọi ký tự thay thế mà một glyph có, thường được trình bày dưới dạng tính năng bảng màu glyph. titl chọn các chữ hoa tiêu đề được cắt cho các kích thước lớn. subs và sups hoán đổi các chữ số chỉ số dưới (subscript) và chỉ số trên (superscript) thực sự thay vì các giá trị mặc định được thu nhỏ. ordn tạo ra các dạng số thứ tự, các chữ cái được nâng lên trong 1st và 2nd. frac xây dựng các phân số, mặc dù các phân số chéo đầy đủ cũng dựa trên liên kết tự động (ligature) và logic ngữ cảnh vượt qua thay thế đơn thông thường. Đối với các trường hợp glyph đơn lẻ, cơ chế này giống hệt như ss01: truyền thẻ vào truy vấn thay thế và đọc lại glyph thay thế.
// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
const PreferredTag: AnsiString): Word;
begin
Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
if Result = BaseGID then
Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
// Still BaseGID if neither feature covers this glyph.
end;
Định dạng cmap 12 và các mặt phẳng bổ sung
Trước khi bất kỳ ký tự nào có thể trở thành một glyph, đó là công việc của bảng cmap. Truy vấn thay thế bắt đầu từ một ID glyph, vì vậy đường dẫn luôn là từ ký tự sang glyph thông qua cmap, sau đó là glyph sang ký tự thay thế thông qua GSUB. Phần thú vị của cmap là tầm tiếp cận của nó. Bảng con định dạng 4 bao gồm Mặt phẳng đa ngôn ngữ cơ bản (Basic Multilingual Plane), 65.536 điểm mã đầu tiên, và thế là đủ cho hầu hết văn bản Latin. Nó không đủ cho các điểm mã từ U+10000 trở lên, tức là các mặt phẳng bổ sung (supplementary planes), vốn là nơi các chữ số và chữ cái toán học, nhiều ký hiệu, và một số chữ viết đang hoạt động hiện sinh sống.
Định dạng 12 là bảng con bao gồm toàn bộ phạm vi từ U+0000 đến U+10FFFF. Nó là một danh sách các nhóm được sắp xếp, mỗi nhóm gồm một điểm mã bắt đầu, một điểm mã kết thúc, và một ID glyph bắt đầu, do đó một lượt chạy liên tục các điểm mã sẽ ánh xạ tới một lượt chạy liên tục các glyph. HotPDF phân giải các điểm mã bằng một chiến lược lai khớp với cách định hình dữ liệu. Các điểm mã trong BMP được phục vụ từ một mảng trực tiếp được lập chỉ mục bởi điểm mã, một tra cứu duy nhất không cần tìm kiếm. Các điểm mã trong các mặt phẳng bổ sung được phục vụ từ một bảng thưa được sắp xếp theo điểm mã và được tìm kiếm bằng tìm kiếm nhị phân. Kết quả là GetUnicodeGlyphForCodepoint nhận một giá trị Cardinal đầy đủ và trả lời chính xác trên toàn bộ phạm vi, trả về ID glyph là 0, tức glyph .notdef, cho bất kỳ điểm mã nào mà phông chữ không ánh xạ.
var
Pdf: THotPDF;
Cp: Cardinal;
GID, StyledGID: Word;
begin
// A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
Cp := $1D49C;
GID := Pdf.GetUnicodeGlyphForCodepoint(Cp); // format 12 lookup
if GID <> 0 then
StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
else
StyledGID := 0; // font has no glyph for this code point
end;
Nơi các truy vấn này dừng lại
Các API thay thế đơn trả lời một dạng câu hỏi, và cần phải làm rõ về những gì chúng không trả lời. LookupType 1 là một trong tám loại thay thế. Truy vấn này không xử lý LookupType 2 thay thế nhiều (multiple substitution), nơi một glyph trở thành nhiều glyph, cũng không xử lý LookupType 4 thay thế liên kết tự động (ligature substitution), nơi nhiều glyph trở thành một. Nó không xử lý các loại theo ngữ cảnh và chuỗi ngữ cảnh, LookupType 5 và 6, vốn chỉ kích hoạt khi một glyph xuất hiện trong một vùng lân cận cụ thể, cũng như các loại mở rộng và chuỗi ngược. Một phân số chéo, một liên kết Devanagari, hoặc một chuỗi ban đầu-giữa-cuối của tiếng Ả Rập là một vấn đề về trình tự, và một tra cứu thay thế đơn trên từng glyph không thể biểu thị nó.
Nó cũng không thực hiện định hình tự động. Không có gì ở đây kiểm tra một đoạn văn bản, quyết định tính năng nào sẽ bật, và áp dụng chúng theo thứ tự mà script yêu cầu. Người gọi chọn thẻ tính năng và áp dụng nó theo từng glyph. Đó chính xác là công cụ phù hợp cho các bộ ký tự mỹ thuật và ký tự thay thế, vốn là tùy chọn (opt-in) và cục bộ, và chính xác là công cụ sai cho một script cần sắp xếp lại thứ tự. Việc giữ cho ranh giới rõ ràng là điều giúp đường dẫn thay thế luôn nhỏ gọn và dự đoán được.
Đối với các trường hợp thực sự cần làm việc ở cấp độ trình tự, câu chuyện về chữ viết phức tạp được trình bày trong our article on complex-script text shaping in Delphi. Nếu các thay thế của bạn là một phần của công việc báo cáo lớn hơn vốn cũng đặt hình ảnh và các phông chữ khác trên trang, the guide to report output with fonts and images covers how those pieces fit together. All of these run on the same engine, the HotPDF Component for Delphi and C++Builder, which carries the GSUB substitution queries alongside the font embedding, subsetting, and text APIs covered elsewhere on this blog.