Bài viết kỹ thuật

Render font PDF không nhúng bằng font hệ thống trong Delphi

Khi một PDF không nhúng font, component HotPDF render đoạn văn bản đó bằng một font Windows đã cài được chọn qua HPDFMapBaseFontToSystem: nó giải mã tên /BaseFont, cắt phần style, thử vài cách đánh vần cho tới khi GDI xác nhận họ font đã được cài, đo các bề rộng standard 14 còn thiếu từ các font tương thích metric, và chuyển các mã một byte sang Unicode trước khi vẽ. Mỗi bước tồn tại đều vì phiên bản ngây thơ đã thất bại trên các tệp thật. Bộ render trang RenderLoadedPageToBitmap xử lý tốt các font program được nhúng; đây là câu chuyện của những font chẳng hề nằm trong tệp

Vì sao GDI lặng lẽ vẽ nhầm typeface cho một font không nhúng?

GDI không bao giờ báo font thiếu: đưa CreateFontIndirect một tên face mà nó không biết, nó lặng lẽ chọn một chữ thay thế, thường là một serif khác không có độ đậm bold. Bộ render thời sơ khai truyền tên PDF gần như nguyên văn, nên TimesNewRoman,Bold, TimesNewRomanPS-BoldMT và SegoeUI-Semibold đều không khớp gì cả và ra đời dưới dạng bất cứ thứ gì GDI chọn. Tên còn có thể tệ hơn thế nữa. ISO 32000-1 §7.3.5 cho phép một tên ghi bất kỳ byte nào dưới dạng #xx, và các producer CJK ghi tên font dưới dạng UTF-8 escape hay byte code-page cổ điển một cách rất thường xuyên; trước v2.766.69, các escape tự nó trở thành tên face. HPDFMapBaseFontToSystem giờ giải mã các escape trước, trả về một chuỗi byte UTF-8 hợp lệ làm ký tự của nó, và đọc các byte cao còn lại trong code page hệ thống

Cắt phần style là chỗ heuristic cắn người. Một dấu phẩy luôn chấm dứt tên họ (Arial,Bold cho ra Arial), nhưng dấu gạch nối chỉ vậy khi từ đứng sau nó là một style: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra hay Condensed. Luật đó giữ MS-Mincho nguyên vẹn trong khi biến Calibri-Light thành Calibri. HotPDF sau đó thử các cách đánh vần của tên họ, rồi tới các cách đánh vần của tên họ sau khi bỏ hậu tố PSMT, MT hay PS. Kể từ v2.768.18, các cách đánh vần ấy phủ mọi lựa chọn đặt dấu cách tại những chỗ một từ có thể bắt đầu: trước một chữ hoa đứng sau một chữ thường (MyriadPro thành Myriad Pro), tại chữ hoa cuối của một run mà một chữ thường đứng sau (UIGothic), và sau một MS mở đầu (MSPGothic), từ dạng tách dấu cách đầy đủ trở xuống tới tên như được viết; quá bốn chỗ như vậy thì chỉ còn thử dạng tách dấu cách đầy đủ và tên dính liền. Không thể áp dấu cách một cách mù quáng, vì Windows giữ một số từ dính liền: SimSun được cài dưới đúng chính tả đó, trong khi MicrosoftYaHei, MicrosoftJhengHei và MSPGothic thuộc về Microsoft YaHei, Microsoft JhengHei và MS PGothic. Trước v2.768.18, mapper đặt dấu cách trước mọi chữ hoa bên trong, nên MicrosoftYaHei bị tra như Microsoft Ya Hei và không bao giờ thấy. Một ứng viên được tính là đã cài khi CreateFontIndirect rồi GetTextFace trả về đúng cái tên được yêu cầu, hoặc, kể từ v2.768.18, khi bảng name của font được chọn liệt kê nó như một họ, tên họ đầy đủ hay tên họ typographic ở bất kỳ ngôn ngữ nào; câu trả lời được cache theo từng tên, nên các tài liệu chứa nhiều font chưa cài không còn dò Windows từng tên trên từng trang nữa

Pipeline HotPDF render font PDF không nhúng bằng font hệ thống trong Delphi: HPDFMapBaseFontToSystem giải mã các byte escape #xx trong tên /BaseFont, cắt các hậu tố style Bold, Italic và Light trong khi giữ MS-Mincho nguyên vẹn, dựng các ứng viên đánh vần như Myriad Pro và Microsoft YaHei, và chỉ chấp nhận khi GetTextFace hay bảng tên font xác nhận tên đã cài
GDI không bao giờ báo font thiếu, nó lặng lẽ thay thế — phép ánh xạ thử các ứng viên theo thứ tự và chỉ tin một cái tên mà GDI trả lại hay bảng tên của font được chọn liệt kê

Vì hàm ánh xạ này là public trong unit HPDFRenderFontMetrics, một báo cáo preflight có thể cho biết mỗi font chưa nhúng sẽ render bằng họ font đã cài nào, dùng bộ liệt kê font mà THotPDF đã sẵn sàng lộ ra cho tài liệu đã nạp:

uses
  HPDFDoc, HPDFRenderFontMetrics;

procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
  Page, I: Integer;
  Info: THPDFLoadedFontInfo;
begin
  for Page := 0 to Pdf.LoadedPageCount - 1 do
    for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
      if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
        Log.Add(Format('page %d  /%s  %s -> %s',
          [Page + 1, string(Info.ResourceName), string(Info.FontName),
           HPDFMapBaseFontToSystem(Info.FontName)]));
end;

HotPDF đo các font standard 14 mà không có /Widths ra sao?

HotPDF đo các advance còn thiếu trên font đã cài cùng metrics, vì ISO 32000-1 §9.6.2.2 cho phép các font standard 14 bỏ qua /Widths còn thư viện thì không kèm bảng AFM nào. Arial mang metrics của Helvetica, Times New Roman mang của Times và Courier New mang của Courier, nên HPDFMeasureBaseFontWidths dựng face tương ứng tại lfHeight = -1000 và gọi GetCharWidth32W; ở chiều cao đó kết quả đã sẵn sàng nằm trong đơn vị 1/1000 em mà các bề rộng PDF dùng. Bộ render trước hết chuyển từng mã sang Unicode qua /Encoding, /BaseEncoding và /Differences, mặc định là StandardEncoding. Export SVG và trích văn bản dính thêm một cái bẫy: một font Type 1 chuẩn mà chẳng có /Encoding nào đã sinh ra một bộ giải mã không có thông tin encoding, export SVG chẳng bao giờ đăng ký nó, và mọi bề rộng đã đo đều bỏ không. Việc cung cấp StandardEncoding ẩn định đã xử lý chuyện đó, với điều kiện nó được đánh dấu là một encoding định nghĩa sẵn; mà đẩy nó xuống đường CMap-name thì mọi mã đều giải mã thành 0 và mọi bề rộng đi theo

Bold, italic và một lỗi lệch một trong các cờ font descriptor

Entry /Flags của một font descriptor đánh số các bit của mình từ 1 chứ không phải từ 0, nên ForceBold là bit 19 ($40000) và Italic là bit 7 ($40), theo ISO 32000-1 Bảng 123. Code cũ kiểm tra $20000, tức bit 18, SmallCap. Sai sót sống sót từ v2.345.0 tới v2.766.53 vì /FontDescriptor gần như luôn là một tham chiếu indirect còn bộ dựng font chỉ đọc object trực tiếp, nên cả nhánh cờ chưa bao giờ chạy, và chính sự mù đó còn bỏ qua /Widths 12 0 R và sắp văn bản với một advance dự phòng 500 đơn vị. Một khi v2.766.53 bắt đầu phân giải tham chiếu indirect qua renderer, bit ấy phải được sửa trong cùng thay đổi đó, nếu không mọi face small-caps sẽ bỗng nhiên render đậm:

const
  // ISO 32000-1 Bảng 123 đếm vị trí bit từ 1
  FD_ITALIC     = $00040;  // bit 7
  FD_SMALLCAP   = $20000;  // bit 18, chẳng phải độ đậm
  FD_FORCEBOLD  = $40000;  // bit 19

procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
  if (Flags and FD_FORCEBOLD) <> 0 then
    LF.lfWeight := FW_BOLD;
  if (Flags and FD_ITALIC) <> 0 then
    LF.lfItalic := 1;
end;
Cách đánh số bit của entry /Flags trong font descriptor PDF: ISO 32000-1 Bảng 123 đếm từ bit 1, khiến Italic là $40 tại bit 7, SmallCap là $20000 tại bit 18 và ForceBold là $40000 tại bit 19, nên phép kiểm descriptor $20000 của HotPDF nhắm vào SmallCap và chỉ vô hại trong khi các tham chiếu /FontDescriptor indirect chưa từng được phân giải
Nhánh cờ là dead code suốt bốn mươi phiên bản vì descriptor là indirect — một khi các tham chiếu được phân giải, bit lệch một ló ra thành chữ small-caps in đậm

Vì sao các font CJK với CMap UCS2 nhận bề width sai?

Văn bản CJK với một CMap UCS2 định nghĩa sẵn vẽ đúng glyph nhưng sai khoảng cách khi renderer coi mã là CID, vì /W được đánh chỉ số theo CID chứ không phải theo mã ký tự. Với STSong-Light và UniGB-UCS2-H, mã trùng khớp tình cờ với giá trị Unicode, nên GDI vẽ đúng ký tự và bug nấp trong các advance: các chữ thường về thành mã 97 trở lên, rơi ngoài một entry /W kiểu [1 95 500], và toàn bộ nhận bề rộng mặc định /DW bằng 1000. Kể từ v2.766.56, renderer HotPDF đọc các mã qua các dải codespace của CMap (ISO 32000-1 §9.7.6.2) và ánh xạ chúng thành CID trước khi tra bề rộng. Chỉ các bảng UCS2 và UTF16 dựng sẵn cùng các stream CMap nhúng được dùng; một phép xấp xỉ identity cho thứ như GBK-EUC-H sẽ chỉ tỏ ra hỗ trợ trong khi cho ra output sai, nên renderer không giả vờ

Vì sao văn bản CJK với CMap UCS2 vẽ đúng glyph nhưng sai advance: /W được đánh chỉ số theo CID trong khi các mã là giá trị Unicode, nên với STSong-Light và UniGB-UCS2-H, các mã chữ thường từ 97 trở lên trượt khỏi entry /W [1 95 500] và nhận mặc định /DW, được HotPDF sửa bằng cách ánh xạ mã thành CID qua các dải codespace của CMap
Bug nấp được vì mã trùng Unicode ở đây — glyph trông đúng trong khi mọi advance lặng lẽ rơi về mặc định, nên hãy đánh giá render CJK qua khoảng cách, đừng qua hình dáng

Vì sao ký tự có dấu biến thành dấu hỏi trên Windows Trung Quốc?

Các mã một byte không bao giờ được để tới các hàm GDI ANSI ("A"), vì GetGlyphOutlineA và GetGlyphIndicesA hiểu các byte theo code page hệ thống trong khi TextOutA dùng bộ ký tự font được chọn. Trên hệ thống Trung Quốc, byte $A9 của Arial (dấu copyright trong Windows-1252) thành một byte dẫn GBK và render thành "?", một cái bẫy mà đường outline không hint thêm vào ở v2.766.83 dẫm thẳng. v2.767.3 hỏi font đã hiện thực về bộ ký tự của nó bằng GetTextCharset, chuyển nó thành một code page qua TranslateCharsetInfo, đẩy byte qua MultiByteToWideChar và gọi các hàm W; font symbol dùng U+F000 cộng với mã thay thế. Các encoding bất đồng với Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — được ánh xạ sang Unicode trước khi bất kỳ font hệ thống nào nhìn thấy

Vẽ bằng font hệ thống có những giới hạn nào?

Render bằng font hệ thống là một phép xấp xỉ, và component HotPDF thẳng thắn về chỗ nó dừng. Trước v2.768.18, phép kiểm cài đặt chỉ so tên mà GetTextFace trả về, và trên Windows bản địa hóa hàm đó báo tên họ bằng ngôn ngữ hệ thống, nên Microsoft YaHei trên Windows Trung Quốc hay Yu Mincho trên Windows Nhật bị kết luận là thiếu và được vẽ bằng một chữ thay thế của GDI; kể từ v2.768.18, một face quay về dưới một tên khác cũng được tra trong bảng name của font, và các font như vậy được tìm thấy. Tương thích metric chỉ được bảo đảm cho các họ Helvetica, Times và Courier; Symbol ánh xạ sang Symbol và ZapfDingbats sang Wingdings, là một giải pháp tạm chứ không phải một phép khớp. Preflight ở trên cũng chỉ thấy các font trong dictionary /Resources của từng trang, chứ không thấy những font được tham chiếu từ bên trong form XObject. Khi một mã vẫn không vẽ được, bộ theo dõi glyph chưa phân giải lúc vẽ sẽ báo, một tín hiệu tốt hơn nhiều so với soi thumbnail

Bản sửa bền vững nằm ở phía soạn thảo. HotPDF tự nó ghi file với FontEmbedding mặc định True, thay bằng một Arial được nhúng ngay cả khi code gọi SetFont với Helvetica, còn văn bản nhúng đi qua bộ render glyph font nhúng thay vì bất kỳ đoán mò nào ở trên. Một biện pháp phòng hộ rẻ tiền cho các tệp đến tay là cảnh báo trước khi render khi một họ được ánh xạ không nằm trong danh sách font màn hình:

// VCL: Screen.Fonts liệt kê các tên họ font đã cài (unit Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
  Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;

Về component đầy đủ, gồm render trang, trích văn bản và font subsetting phía ghi, xem HotPDF Delphi PDF component trên trang sản phẩm