Uniscribe làm nhiều việc hơn phần lớn caller nhận ra. ScriptItemize thực hiện phân tích hai chiều và phân đoạn script trong một lượt, và ScriptLayout tạo ra thứ tự thị giác của các run kết quả. HarfBuzz, chất thay thế di động mà mọi người với tới, chẳng làm cả hai: nó shaping một run đơn mà hướng và script đã được ai khác quyết định sẵn. Vì vậy phần khó của việc đưa một pipeline văn bản PDF Windows lên Linux hay macOS không phải là gắn một engine shaping. Đó là cung cấp thuật toán hai chiều mà Uniscribe lặng lẽ cung cấp, và trong thành phần PDFium thì đó là việc của FPdfBidi
Unit hiện thực UAX #9 trực tiếp: các quy tắc P2 và P3 cho hướng đoạn, X1 đến X10 cho nhúng và cô lập tường minh, W1 đến W7 cho các kiểu yếu, N0 đến N2 cho trung tính và dấu ngoặc, I1 và I2 cho các mức ngầm định, và L1 cùng L2 cho việc sắp xếp lại cuối cùng. Hai hàm mang nó: PdfResolveBidiLevels trả về một mức nhúng cho mỗi đơn vị mã UTF-16, và PdfBidiVisualOrder biến những mức đó thành phép hoán vị đặt các đơn vị mã từ trái sang phải
Thuật toán cho bạn điều gì, và điều gì thì không
Nó cho bạn những con số. Các mức chẵn là trái-sang-phải, các mức lẻ là phải-sang-trái, và mức của mỗi ký tự mã hóa sự lồng nhau của các run hướng mà ký tự đó nằm trong. Từ những con số đó, L2 suy ra một phép hoán vị. Điều thuật toán cố tình không làm là quyết định dùng phông nào, tạo ligature, hay sắp xếp lại glyph trong một cluster; đó là việc của shaping và thuộc về tầng sau tầng này
uses
FPdfBidi;
var
Levels: TPdfBidiLevels;
Order: TPdfBidiOrder;
ParagraphLevel: Byte;
Text, Visual: WideString;
I: Integer;
begin
Text := SourceLine;
// pbdAuto áp P2-P3: ký tự mạnh đầu tiên quyết định
if PdfResolveBidiLevels(Text, pbdAuto, Levels, ParagraphLevel) then
begin
Order := PdfBidiVisualOrder(Text, Levels);
SetLength(Visual, Length(Order));
for I := 0 to High(Order) do
Visual[I + 1] := Text[Order[I] + 1];
// Visual giờ đọc từ trái sang phải; Levels[] vẫn cho biết
// run nào là RTL để shaper được trao các hướng đúng
end;
end;
Bảng lớp ký tự được sinh ra, chứ không được viết tay
Mọi code point đều có thuộc tính Bidi_Class, và thuật toán hỏi nó liên tục, nên bảng là nền móng mà mọi thứ khác đứng trên. Nó được sinh từ Unicode Character Database chứ không được duy trì bằng tay: trường thứ năm của UnicodeData.txt cho các lớp đã gán, và các khai báo @missing trong DerivedBidiClass.txt cho các mặc định cho những code point mà cơ sở dữ liệu không gán, đó là cách các khối chưa cấp phát mặc định đúng thành R, AL, ET hay BN thay vì L
Mẹo nén là chỉ phát ra các khoảng mà lớp của chúng không phải L. Bất cứ gì rơi ngoài mọi khoảng là L, thứ vừa là mặc định Unicode vừa là lớp của áp đảo phần lớn code point. Điều đó đưa một bảng mà nếu không sẽ chạy tới hàng nghìn mục xuống còn 745 khoảng và khoảng 6,7 KB. Hệ quả vận hành đáng nói: khi bạn chuyển sang một phiên bản Unicode mới, hãy chạy lại bộ sinh. Sửa tay tệp include sẽ chạy được, và nó cũng sẽ âm thầm lệch khỏi cơ sở dữ liệu ở lần nâng cấp kế tiếp
L2 phải sắp xếp lại code point, không phải đơn vị mã UTF-16
Đây là lỗi tạo ra đầu ra hỏng thật sự, và hiện thực đầu tiên đã mắc phải. L2 bảo đảo ngược các run liền kề tại mỗi mức từ mức cao nhất xuống mức lẻ thấp nhất. Viết trên một chuỗi UTF-16, "đảo ngược một run" tự nhiên nghĩa là đảo ngược các đơn vị mã trong nó. Với các ký tự trong Basic Multilingual Plane thì ổn. Với một ký tự RTL trên một mặt phẳng astral, chẳng hạn những khối Cypriot hay Old South Arabian gần U+10800, thì không: ký tự là một cặp surrogate, đảo ngược run đặt surrogate thấp trước surrogate cao, và chuỗi giờ chứa hai surrogate không ghép cặp thay vì một ký tự. Chẳng gì ở hạ nguồn phục hồi nổi nó
Bản sửa là làm L2 trên các đơn vị code point. Hiện thực gộp các đơn vị mã thành các đơn vị code point, thực hiện các phép đảo ngược trên những đơn vị đó, và mở rộng kết quả trở lại thành các chỉ số đơn vị mã ở cuối. Đó là lý do PdfBidiVisualOrder nhận văn bản chứ không chỉ mảng mức: nó không thể biết các biên surrogate nằm đâu chỉ từ mức. Kỷ luật cặp surrogate đó xuyên suốt các API văn bản nói chung, như được mô tả trong bài viết về emoji, CJK và cặp surrogate
Lượt đi xuống qua các mức phải gồm cả những mức không xuất hiện
Lỗi thứ hai tinh vi hơn và không tạo ra crash, chỉ văn bản không được sắp xếp lại. L2 bảo bắt đầu từ mức cao nhất hiện diện và đi xuống mức lẻ thấp nhất. Một tối ưu hóa tự nhiên là thu tập các mức thực sự xuất hiện và lặp qua tập đó. Nó sai
Hãy nghĩ về một dòng văn bản Latin bên trong một nhúng phải-sang-trái. Mức đoạn là 0, phép nhúng đẩy các ký tự Latin lên mức 2, và không ký tự nào nằm ở mức 1. Lặp qua các mức xuất hiện chỉ thấy 0 và 2, và chẳng có mức lẻ nào cả, nên vòng lặp không thực hiện phép đảo ngược nào. Câu trả lời đó đúng, nhưng vì một lý do mà tối ưu hóa không biết: một phép đảo ngược ở mức 2 đi kèm một phép đảo ngược ở mức 1 sẽ triệt tiêu nhau trọn vẹn, nên không thực hiện cả hai là kết quả đúng. Đổi đầu vào một chút, sao cho cả ký tự mức 1 lẫn mức 3 đều tồn tại nhưng mức 2 thì không, và vòng lặp dựa-tập bỏ sót phép đảo ngược mức 2 mà thuật toán đòi hỏi
// Đúng: đi qua mọi mức từ mức lớn nhất xuống mức lẻ thấp nhất,
// gồm cả những mức mà chẳng ký tự nào thực sự có
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
ReverseRunsAtOrAbove(Level); // no-op khi không run nào đủ điều kiện
Dec(Level);
end;
Viết thành một vòng lặp giảm dần giản dị, hành vi tự đến mà chẳng tốn gì, và các vòng lặp no-op chẳng tốn gì đo đếm được. Đây là trường hợp mà tối ưu hóa hiển nhiên không phải sai một chút, nó sai theo cách phụ thuộc đầu vào mà một kho kiểm thử nhỏ sẽ chưa từng lộ ra
Dấu ngoặc: BD16 với một bảng thực dụng
Quy tắc N0 và thuật toán cặp ngoặc BD16 tồn tại để một dấu ngoặc đơn trong văn bản hỗn hợp hướng phân giải về hướng của cái mà nó bao quanh chứ không phải thứ tình cờ nằm kề. Điều đó cần một bảng các cặp ngoặc. Hiện thực mang các cặp thông dụng hơn là toàn bộ nội dung tệp ngoặc Unicode: ASCII, CJK, fullwidth, toán học và trang trí
Một dấu ngoặc không liệt kê không phải là lỗi. Nó phân giải như một trung tính thường qua N1 và N2, chính xác hành vi mà mọi hiện thực từng có trước khi Unicode 6.3 giới thiệu N0. Vì vậy ranh giới là "ít tinh tế hơn với các ngoặc hiếm", chứ không phải "sai". Một chi tiết cần xử lý tường minh: sự tương đương chuẩn tắc giữa các dấu ngoặc nhọn tại U+2329 và U+232A với những cái tại U+3008 và U+3009 phải được gập chữ khi khớp cặp, nếu không một ngoặc mở viết kiểu này sẽ không ghép được với một ngoặc đóng viết kiểu kia
Thử ba mươi quy tắc tương tác với nhau ra sao
Không phải bằng một kho văn liệu lớn, ít nhất không phải đầu tiên. Cách tiếp cận hiệu quả là mười sáu trường hợp được xác minh bằng tay, mỗi trường hợp được chọn để tác động một quy tắc cụ thể và mỗi cái được đối chiếu với các mức mà UAX #9 nói rằng nó nên tạo ra: phát hiện hướng đoạn dưới P2 và P3, các quy tắc kiểu yếu W2, W3 và W7, các quy tắc mức ngầm định I1 và I2, nhúng tường minh qua X2 và X7, cô lập qua X5a và X6a, phép reset L1 của khoảng trắng đuôi và các dấu phân cách, một trường hợp ngoặc N0, và một trường hợp có ký tự astral để khóa chặt cách xử lý surrogate
Mười sáu trường hợp với các mức kỳ vọng đã-biết-là-đúng bắt được nhiều hơn mười sáu trăm trường hợp với đầu ra trông hợp lý, vì dạng thất bại của một hiện thực hai chiều là văn bản đọc gần đúng. Một khi những cái đó qua, một kho văn liệu hữu ích cho việc tìm các khoảng trống bảng và các vấn đề hiệu năng, những lớp khuyết tật khác nhau
Bên trong thành phần PDFium, các mức nuôi hai người tiêu dùng. Phía ghi, chúng báo cho backend shaping hướng của mỗi run, thứ là đầu vào mà HarfBuzz đòi hỏi. Phía đọc, chúng chi phối hình học vùng chọn và thứ tự đọc, vì một cú nhấp trong văn bản RTL phải ánh xạ tới một vị trí logic chứ không phải vị trí thị giác; phép ánh xạ đó được đề cập trong bài viết về vùng chọn dòng thị giác và mô hình thứ tự đọc trong các khối văn bản có cấu trúc và thứ tự đọc. Chi tiết hỗ trợ nền tảng của thành phần nằm trên trang sản phẩm PDFium Delphi component