Một nhà thiết kế chọn font có chữ a một tầng cho tiêu đề, hoặc số không có gạch chéo cho bảng, hoặc một bộ hoa văn viết tay cho trang bìa. Những glyph đó đã có sẵn trong font. Chúng đơn giản là không phải mặc định. Chữ a mặc định ánh xạ từ ký tự qua bảng cmap đến một glyph, và hình thay thế nằm cách vài glyph id, chỉ có thể tiếp cận qua một quy tắc thay thế. Tạo ra hình thay thế đó trong PDF có nghĩa là đọc quy tắc và phát glyph thay thế vào content stream. Bài viết này nói về việc đọc những quy tắc đó, loại thay thế một glyph, trong Object Pascal mà không có thư viện shaping gốc bên dưới
Phạm vi này được thu hẹp có chủ ý. Bộ kiểu chữ và thay thế là các thay thế một glyph vào, một glyph ra. Đây là phần của OpenType layout mà bạn có thể xử lý với một bước đi bảng nhỏ, xác định, làm cho nó phù hợp với một engine Pascal muốn tránh phụ thuộc C
Tại sao Delphi thuần túy thay vì HarfBuzz
HarfBuzz là câu trả lời rõ ràng cho "shape văn bản này", và đối với shaping hai chiều đầy đủ, Indic, hoặc Arabic thì đó là câu trả lời đúng. Nó cũng là một thư viện C. Ràng buộc nó vào sản phẩm Delphi hoặc C++Builder có nghĩa là phải ship một đối tượng gốc cho mỗi nền tảng mục tiêu và kiến trúc, khớp quy ước gọi của nó, theo dõi chu kỳ phát hành của nó, và đọc điều khoản giấy phép của nó so với của bạn. Không có gì trong số đó là khó khi cô lập. Tất cả đều là ma sát không bao giờ biến mất, và nó không mua được gì khi yêu cầu thực tế là "cho tôi dạng ss01 của chữ cái này"
Thay thế đơn không cần một engine shaping. Nó cần một parser cho một vài định dạng GSUB subtable và một hoặc hai tìm kiếm nhị phân. Viết điều đó trong Pascal giữ toàn bộ toolchain trong một compiler. Giới hạn trung thực là cách tiếp cận này xử lý các lookup thay thế glyph và không có gì khác. Nó không phải là xử lý bidi, nó không phải là sắp xếp lại Indic, và nó không phải là shaping ngữ cảnh tự động. Khi những thứ đó cần thiết, chúng cần thiết, và một lookup thay thế đơn mỗi glyph sẽ không thay thế cho chúng
Phân 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 gián tiếp, và một truy vấn thay thế đi theo chuỗi từ trên xuống. Ở trên cùng là ScriptList. Một thẻ script như latn chọn một mục, và thẻ đặc biệt DFLT là script mặc định áp dụng khi không có script cụ thể hơn nào khớp. Mục script chỉ đến một LangSys, hệ thống ngôn ngữ, với một LangSys mặc định cho trường hợp thông thường và các LangSys có tên tùy chọn cho các ngôn ngữ cần hành vi khác. Tiếng Thổ Nhĩ Kỳ là ví dụ thường gặp, nơi i có chấm và không có chấm đòi hỏi cách xử lý riêng
LangSys đặt tên một tập hợp các chỉ số feature. Mỗi chỉ số trỏ vào FeatureList, nơi một bản ghi feature mang thẻ bốn byte, ss01 trong số đó, và một danh sách các chỉ số lookup. Những chỉ số đó cuối cùng trỏ vào LookupList, nơi các subtable thay thế thực sự nằm. Vì vậy, giải quyết ss01 có nghĩa là: tìm script, tìm LangSys của nó, tìm feature có thẻ là ss01, thu thập các lookup nó đặt tên, và áp dụng chúng. HotPDF mặc định là script DFLT và LangSys mặc định, đây là thứ mà phần lớn thiết kế văn bản Latin đi kèm, và nó cung cấp cách ghi đè thẻ script khi font kết nối các feature của nó dưới một script cụ thể
Bảng Coverage quyết định ai tham gia
Mỗi subtable thay thế bắt đầu với cùng một câu hỏi: 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ỉ số riêng của quy tắc. Câu hỏi đó được trả lời bởi bảng Coverage, và câu trả lời là chỉ số coverage, một số thứ tự nhỏ mà phần còn lại của subtable sử dụng để tìm kiếm glyph trở thành gì
Coverage có hai định dạng. Định dạng 1 là danh sách các glyph id được sắp xếp theo thứ tự tăng dần. Bạn tìm một glyph với tìm kiếm nhị phân, và vị trí của nó trong danh sách là chỉ số coverage của nó. Định dạng 2 là danh sách các bản ghi phạm vi, mỗi bản ghi có glyph bắt đầu, glyph kết thúc, và chỉ số coverage mà glyph bắt đầu ánh xạ tới. Một glyph trong phạm vi nhận chỉ số coverage của nó bằng cách offset từ điểm bắt đầu của phạm vi. Định dạng 1 nhỏ gọn khi các glyph tham gia bị phân tán, Định dạng 2 khi chúng rơi vào các dãy liên tiếp. Cả hai đều được sắp xếp, vì vậy cả hai đều được tìm kiếm theo thời gian logarithmic, và cả hai đều trả về chỉ số coverage hoặc một "không được covered" rõ ràng để engine có thể để nguyên glyph
Single Substitution, hai định dạng
Single Substitution là LookupType 1, và nó ánh xạ một glyph thành chính xác một thay thế. Nó cũng có hai định dạng, và sự phân chia là tối ưu hóa không gian. Định dạng 1 lưu trữ một delta có dấu duy nhất. Glyph id đầu ra là glyph id đầu vào cộng với delta đó, modulo 65536. Đây là cách font mã hóa thay thế khi mỗi glyph tham gia nằm ở cùng một offset cố định từ hình thay thế của nó, ví dụ như một khối chữ số lining được đặt ở khoảng cách không đổi từ các chữ số oldstyle tương ứng. Bảng Coverage cho biết glyph nào đủ điều kiện, và một delta phục vụ tất cả chúng
Định dạng 2 lưu trữ một mảng glyph id thay thế rõ ràng. Chỉ số coverage từ bảng Coverage là chỉ số vào mảng đó, vì vậy glyph ở chỉ số coverage 0 trở thành mục đầu tiên của mảng, chỉ số coverage 1 là thứ hai, v.v. Định dạng 2 được sử dụng khi các hình thay thế không ở một offset đồng đều, đây là trường hợp phổ biến cho các bộ kiểu chữ được xây dựng thủ công. Truy vấn 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 qua Coverage, và nếu được covered, áp dụng delta hoặc đọc slot 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, []);
// Ký tự mặc định cho chữ 'a' thông qua bảng cmap của phông chữ.
BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));
// Tập hợp phong cách 1 (Stylistic Set 1): giải quyết ký tự thay thế qua LookupType 1 của GSUB.
AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');
// AltGID = BaseGID có nghĩa là đặc tính này không tác động đến ký tự này.
if AltGID <> BaseGID then
{ phát ra AltGID trong luồng nội dung (content stream) };
finally
Pdf.Free;
end;
end;
Hợp đồng đáng chú ý là pass-through. GetSingleSubstituteGlyph trả về glyph id đầu vào không thay đổi trên mọi lần bỏ lỡ: không có font, không có bảng GSUB, không có feature khớp, không có coverage hit. Điều đó có nghĩa là lệnh gọi an toàn để thực hiện vô điều kiện. Bạn hỏi về hình thay thế, và nếu không có, bạn nhận lại chính xác những gì bạn đưa vào, vì vậy code gọi không bao giờ cần xử lý đặc biệt cho font thiếu feature
Ý nghĩa của các thẻ feature kiểu chữ
Thẻ feature là toàn bộ từ vựng về hình thay thế nào bạn đang yêu cầu, và các thẻ liên quan đến công việc kiểu chữ là một danh sách ngắn. Cặp tiêu đề là salt, thay thế kiểu chữ, quyền truy cập tổng hợp vào các dạng thay thế của glyph, và ss01 đến ss20, hai mươi bộ kiểu chữ được đánh số mà font có thể định nghĩa, 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. Font có thể đặt chữ a một tầng và chữ R chân thẳng dưới ss03, ví dụ, để bật một bộ đó tạo kiểu lại cả hai
Xung quanh những thẻ đó có thêm một số thẻ thay thế đơn. aalt là access-all-alternates, sự kết hợp của mọi hình thay thế mà một glyph có, thường được trình bày như một feature bảng glyph. titl chọn các chữ hoa tiêu đề được cắt cho kích thước lớn. subs và sups đổi thành chữ số chỉ số dưới và chỉ số trên thực thay vì mặc định thu nhỏ. ordn tạo ra các dạng thứ tự, các chữ cái được nâng cao trong 1st và 2nd. frac xây dựng phân số, mặc dù phân số đường chéo đầy đủ cũng dựa vào logic ligature và ngữ cảnh vượt quá thay thế đơn thuần. Đối với các trường hợp một glyph, cơ chế giống hệt với 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');
// Vẫn là BaseGID nếu không có đặc tính nào trong cả hai bao phủ (covers) được ký tự này.
end;
cmap định dạng 12 và các mặt phẳng bổ sung
Trước khi bất kỳ thay thế nào có thể chạy, một ký tự phải trở thành một glyph, và đó là công việc của bảng cmap. Truy vấn thay thế bắt đầu từ glyph id, vì vậy đường dẫn luôn là ký tự thành glyph qua cmap, sau đó glyph thành hình thay thế qua GSUB. Phần thú vị của cmap là phạm vi của nó. Một subtable định dạng 4 bao gồm Mặt phẳng Đa ngôn ngữ Cơ bản, 65536 code point đầu tiên, và đủ cho hầu hết văn bản Latin. Nó không đủ cho các code point từ U+10000 trở lên, các mặt phẳng bổ sung, là nơi các chữ số học toán, nhiều ký hiệu, và một số chữ viết đang sống nay sống
Định dạng 12 là subtable bao gồm toàn bộ phạm vi U+0000 đến U+10FFFF. Nó là danh sách được sắp xếp các nhóm, mỗi nhóm là một code point bắt đầu, code point kết thúc, và glyph id bắt đầu, vì vậy một dãy liên tiếp các code point ánh xạ tới một dãy liên tiếp các glyph. HotPDF giải quyết code point với chiến lược hỗn hợp khớp với cách dữ liệu được định hình. Các code point trong BMP được phục vụ từ một mảng trực tiếp được đánh chỉ mục theo code point, một lookup duy nhất không cần tìm kiếm. Các code point 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 code point và được tìm kiếm bằng tìm kiếm nhị phân. Kết quả là GetUnicodeGlyphForCodepoint nhận một Cardinal đầy đủ và trả lời đúng trên toàn bộ phạm vi, trả về glyph id 0, glyph .notdef, cho bất kỳ code point nào font không ánh xạ
var
Pdf: THotPDF;
Cp: Cardinal;
GID, StyledGID: Word;
begin
// Mã điểm thuộc mặt phẳng bổ sung (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; // phông chữ không có glyph cho mã điểm (code point) này
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 hình dạng câu hỏi, và đáng nói 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 không xử lý LookupType 2 thay thế nhiều, nơi một glyph trở thành nhiều, cũng không phải LookupType 4 thay thế ligature, nơi nhiều glyph trở thành một. Nó không xử lý các loại ngữ cảnh và chaining-contextual, LookupTypes 5 và 6, chỉ kích hoạt khi một glyph xuất hiện trong một lân cận cụ thể, cũng không phải loại extension và reverse-chaining. Một phân số đường chéo, một liên kết Devanagari, hoặc một chuỗi ban đầu-giữa-cuối Arabic là vấn đề chuỗi, và một lookup thay thế đơn mỗi glyph không thể diễn đạt nó
Nó cũng không thực hiện shaping tự động. Không có gì ở đây kiểm tra một đoạn văn bản, quyết định feature nào để bật, và áp dụng chúng theo thứ tự script yêu cầu. Người gọi chọn thẻ feature và áp dụng nó từng glyph. Đó chính xác là công cụ phù hợp cho các bộ kiểu chữ và thay thế, vốn là opt-in và local, và chính xác là công cụ sai cho một script cần sắp xếp lại. Giữ ranh giới rõ ràng là thứ giúp đường dẫn thay thế giữ nhỏ và có thể dự đoán
Đối với các trường hợp thực sự cần công việc cấp chuỗi, câu chuyện script phức tạp được đề cập trong bài viết của chúng tôi về shaping văn bản script phức tạp trong 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 cũng đặt hình ảnh và phông chữ khác trên trang, hướng dẫn đầu ra báo cáo với phông chữ và hình ảnh bao gồm cách các phần đó khớp với nhau. Tất cả chúng chạy trên cùng một engine, HotPDF Component cho Delphi và C++Builder, mang các truy vấn thay thế GSUB cùng với các API nhúng font, tập hợp con, và văn bản được đề cập ở nơi khác trên blog này