Một glyph còn thiếu trong PDF không phải là một lỗi. Trình tạo xin một ký tự mà phông được chọn không ánh xạ được, phông trả về chỉ số glyph không, và tệp tạo ra hợp lệ về cấu trúc, mở được ở mọi nơi, và hiện một ô rỗng ở chỗ lẽ ra là một cái tên hay một khoản tiền. Không ai trong pipeline tạo ra hay biết. Người nhận thì có. HotPDF khép vòng lặp đó bằng TrackUnresolvedGlyphs: bật nó lên và con đường vẽ văn bản ghi lại mọi code point mà việc tra cứu glyph phân giải về chỉ số không, bắn OnUnresolvedGlyph một lần cho mỗi phát hiện duy nhất kèm code point, phông nó thất bại, script nó thuộc về, và một gợi ý các phông có thể che phủ nó
Phát hiện là một nửa câu trả lời. Nửa kia là SetFontFallbackChain, hàm đăng ký một danh sách phông có thứ tự theo từng script, để các trường hợp phổ biến tự giải quyết và chỉ khoảng trống thật sự đến được handler của bạn. Cùng nhau, chúng biến một lớp khuyết tật vốn được khách hàng báo cáo thành một phép kiểm tra lúc build
Vì sao một glyph còn thiếu không báo gì?
Vì ISO 32000 không đặt nghĩa vụ nào lên trình tạo để xác minh độ phủ, và chỉ số glyph không là một glyph hợp pháp. Đó là .notdef, mà nét phác của nó do nhà thiết kế phông chọn: thường là một hình chữ nhật rỗng hoặc rỗng ruột, thỉnh thoảng chẳng có gì cả. Một trình xem vẽ nó là đang hành xử đúng. Trích xuất văn bản thậm chí có thể trả về đúng các ký tự, vì ánh xạ /ToUnicode được viết từ văn bản nguồn chứ không từ các nét phác, nên một phép kiểm tra vòng trọn tự động sẽ vui vẻ qua một tài liệu mà văn bản hiển thị đang bị thủng
Hệ quả thực tiễn là độ phủ phải được kiểm tại thời điểm vẽ, khi thư viện còn biết code point nào đã được xin và phông thực sự cung cấp glyph nào. Sau đó thông tin đã mất
Bộ dò phải dõi trạng thái subset, không phải device context
Đây là nơi hiện thực đầu tiên đi sai, và lý do đáng hiểu vì nó áp dụng cho bất kỳ phép kiểm độ phủ nào được gắn thêm vào một pipeline văn bản. HotPDF có hai con đường văn bản. Một phát qua một phông TrueType Unicode đã đăng ký với một bản đồ ký tự trong bộ nhớ dựng tại thời điểm đăng ký. Kia là một con đường GDI kiểu cũ tạo một device context và handle phông mới cho mỗi lượt chạy ký tự
Xét độ phủ từ con đường GDI là vô vọng. Ánh xạ của nó không phải ánh xạ kết thúc trong luồng nội dung được phát ra, và hai bên không được đồng bộ, nên một bộ dò đọc kết quả GDI báo toàn bộ vùng ASCII in được là chưa phân giải. Câu trả lời có thẩm quyền nằm trong phông đã đăng ký: bản đồ ký tự mà RegisterUnicodeTTF phân tích, được hỏi qua GetUnicodeGlyphForCodepoint. Bộ dò vì vậy được cổng trên trạng thái sẵn sàng subset, không trên điều kiện GDI nào, và nó đơn giản không chạy trên các tài liệu chưa từng đăng ký phông Unicode, điều đúng vì những tài liệu đó vốn bị giới hạn trong các bảng mã chuẩn
Một cái bẫy thứ hai nằm kế bên. Tên họ GDI của một phông và tên PostScript được trích từ nhị phân phông tại thời điểm đăng ký là hai chuỗi khác nhau, và không theo cách bạn có thể chuẩn hóa: một họ tên Arial Unicode MS mang tên PostScript ArialMT. Bất kỳ cổng nào được viết thành "phông đang được chọn có phải cái chúng ta đăng ký không", so theo tên, là mã chết chưa từng bắn. Hãy cổng trên trạng thái, chưa bao giờ trên tên phông
Đừng thử một bộ dò glyph bằng emoji
Trường hợp thử hiển nhiên là một gương mặt cười, và nó sẽ thuyết phục bạn rằng bộ dò bị hỏng. Các code point emoji phổ biến trong các mặt phẳng astral phân giải qua một con đường tổng hợp private-use ánh xạ chúng thẳng tới một chỉ số glyph, nên chúng chưa từng chạm nhánh độ phủ chung. Bộ dò đang hành xử đúng và phép thử đang đo nhầm con đường
Hãy dùng một code point chưa được chỉ định thay thế. U+0378 vĩnh viễn không được cấp phát trong Unicode, nên không phông nào ánh xạ được nó một cách hợp pháp, và nó tác động đúng nhánh bạn muốn xác minh. Sự phân biệt giữa "tính năng bị hỏng" và "phép thử chọn một đầu vào né tính năng" tốn những giờ thật, và code point chưa chỉ định là cách rẻ nhất tránh điều đó
type
TCoverageAudit = class
private
FFindings: TStringList;
public
procedure Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
property Findings: TStringList read FFindings;
end;
procedure TCoverageAudit.Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
begin
// Bắn một lần cho mỗi code point duy nhất, không phải mỗi lần xuất hiện
FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
[Info.CodePoint, String(Info.FontName), Ord(Info.Script),
String(Info.SuggestedFonts)]));
end;
// Nó vào một tác vụ tạo tài liệu
Pdf := THotPDF.Create(nil);
try
Pdf.TrackUnresolvedGlyphs := True;
Pdf.OnUnresolvedGlyph := Audit.Handle;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
Pdf.EndDoc;
if Audit.Findings.Count > 0 then
// Làm hỏng tác vụ thay vì phát hành một trang đầy ô vuông
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
Các chuỗi fallback là theo script, không theo phông
Lý do fallback được phạm vi hóa theo script chứ không theo phông nguồn là các khoảng trống độ phủ tụ lại theo hệ thống chữ viết. Một phông văn bản Latin thiếu Devanagari, Thái, Hán và emoji, tất cả cùng lúc, và chất thay thế cho mỗi cái là một phông khác. Vì vậy tuyên bố một chuỗi cho mỗi script mô tả bản triển khai thật: một phông Latin cho phần thân, một phông CJK, một phông emoji, một phông hứng tất cả
// THPDFFontScript phủ hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji và hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);
Fallback và phát hiện bổ sung cho nhau thay vì thay thế nhau. Các chuỗi xử lý phần độ phủ bạn đã lường trước; bộ dò báo phần độ phủ bạn không lường, thứ trên một hệ thống xử lý dữ liệu khách tùy ý mới là nửa thú vị. Lưu ý rằng thay một phông làm thay đổi số đo, nên một đoạn văn rơi vào fallback có thể chảy lại; nếu bố cục quan trọng, hành vi closure và subsetting của phông thay thế đáng để đọc kỹ trong bài viết về closure subset phông, còn các script cần sắp lại thứ tự hay nối glyph được xử lý bởi tầng shaping được mô tả trong shaping văn bản script phức tạp
Cách ghép thêm hành vi mà không rủi ro cho con đường hiện có
Cùng bản phát hành đó thêm một phương án dự phòng bảng kern kiểu cũ cho khoảng cách cặp, và cách nó được phạm vi hóa là một mẫu hình đáng sao chép. Thay vì thêm một điểm quyết định mới vào logic kerning, phương án dự phòng treo vào nhánh thoát sớm vốn đã tồn tại cho các phông không có bảng GPOS. Một phông hiện đại có GPOS chưa từng chạm tới nó, nên hành vi của nó không đổi do cấu trúc chứ không nhờ kiểm thử. Các con đường không đăng ký phông Unicode tạo ra hai offset không, nên chúng cũng không đổi
Đó là hình dạng chung của một cuộc ghép thêm rủi ro thấp trong một thư viện kết xuất trưởng thành: tìm nhánh hiện đang không tạo ra gì và đặt hành vi mới vào đó. Nó biến "chúng ta tin rằng điều này không làm hồi quy gì" thành "điều này không thể làm hồi quy gì", một câu tốt hơn nhiều để nói về một engine văn bản mà hóa đơn của người khác đi qua
Biến nó thành cổng, không phải log
Các phát hiện độ phủ chỉ hữu ích nếu có thứ gì đó thất bại vì chúng. Trong một dịch vụ tạo tài liệu, cách sắp xếp hiệu quả là giữ tracking bật trong tác vụ regression hằng đêm trên một kho văn liệu tên, địa chỉ và mô tả sản phẩm thật của khách, và làm hỏng tác vụ với bất kỳ phát hiện nào. Vì sự kiện bắn một lần cho mỗi code point duy nhất chứ không phải mỗi lần xuất hiện, đầu ra vẫn nhỏ đủ để đọc ngay cả khi cả một script bị thiếu
Trong sản xuất, handler đó dùng tốt hơn như telemetry: ghi lại code point và phông, tiếp tục phục vụ tài liệu, và để tổng thể kể cho bạn biết script nào nên thêm vào tập phông triển khai tiếp theo. Hành vi kết xuất cho phông nhúng và phông thay thế được đề cập sâu hơn trong kết xuất glyph phông nhúng, và danh sách thuộc tính đầy đủ gồm TrackUnresolvedGlyphs được ghi tài liệu trên trang sản phẩm HotPDF Delphi PDF component