Bỏ qua để đến nội dung

Phông chữ: phần khó

Evidence: Mixed evidence

Phông chữ là nơi một tệp PDF có thể trông hoàn toàn đúng nhưng vẫn âm thầm bị hỏng. Một trang có thể kết xuất đúng glyph nhưng lại không thể tìm kiếm, không thể sao chép dưới dạng văn bản và không tuân thủ một hồ sơ lưu trữ. Tất cả những điều đó có thể xảy ra cùng lúc mà không có dấu hiệu nào cảnh báo bạn. Trang này nói về ba việc phải làm đúng — nhúng, tạo tập con, mã hóa — và cách NextPDF xử lý từng việc.

“Trông ổn mà” là câu nói nguy hiểm nhất khi làm việc với PDF, và phông chữ là nơi câu đó gây hại rõ nhất. Ba điều độc lập với nhau phải đúng:

  1. Nhúng — chương trình phông chữ đi kèm trong tệp, nên nó kết xuất giống nhau trên máy chưa cài đặt phông chữ đó.
  2. Tạo tập con — chỉ những glyph thực sự được dùng mới được mang theo, nên một phông chữ CJK 20 MB không làm mọi tài liệu phình to.
  3. Mã hóa — có ánh xạ đúng từ các mã ký tự trên trang trở lại Unicode, nên văn bản có thể được tìm kiếm, sao chép, lập chỉ mục và đọc bằng công nghệ hỗ trợ.

Kết xuất hình ảnh chỉ chứng minh được một phần của điều thứ nhất. Một tài liệu có thể hiển thị glyph hoàn hảo nhưng vẫn hoàn toàn không đạt điều thứ ba — văn bản chỉ là hình ảnh của các từ, chứ không phải là các từ. Đó là lỗi có thể vượt qua mọi lần soát xét “trông ổn mà” rồi lại thất bại trong một đợt kiểm tra tuân thủ hoặc một yêu cầu cung cấp chứng cứ.

  • Một phông chữ trong PDF là một dictionary cộng với, thường là, một luồng chương trình phông chữ được nhúng.
  • Tạo tập con viết lại chương trình đó để chỉ chứa những glyph được dùng. Tên của một phông chữ tập con được gắn một thẻ gồm sáu chữ in hoa và một dấu + để trình đọc xem nó là một phông riêng biệt.
  • Mã hóa là vấn đề riêng về ánh xạ các mã ký tự sang Unicode. Một CMap /ToUnicode là thứ làm cho văn bản có thể tìm kiếm và sao chép được — và nó độc lập với việc glyph trông có đúng hay không.
  • Văn bản trông đúng nhưng không có (hoặc có sai) /ToUnicode là lỗi âm thầm kinh điển: hoàn hảo trên màn hình nhưng không tìm kiếm được trong thực tế.
  • NextPDF tạo tập con cho phông chữ TrueType, giữ nguyên định danh glyph để kết xuất đúng, phát ra một CMap /ToUnicode để việc trích xuất hoạt động — và có thể thực thi quy tắc nhúng của PDF 2.0 thay vì chỉ cảnh báo.

Tạo tập con. FontSubsetter (src/Typography/FontSubsetter.php) phân tích cú pháp thư mục bảng TrueType gốc và đọc cmap để ánh xạ các điểm mã Unicode sang glyph ID. Nó xử lý được cả định dạng 4 BMP lẫn định dạng 12 Unicode đầy đủ, là thứ CJK cần. Sau đó, nó làm bước mà các bộ tạo tập con sơ sài thường bỏ sót: giải quyết các phụ thuộc của glyph hợp thành bằng bao đóng bắc cầu. Một glyph có dấu được tạo từ một chữ cái cơ sở cộng với một dấu kết hợp sẽ tham chiếu đến các glyph khác làm thành phần. Nếu các thành phần đó bị loại bỏ, glyph sẽ kết xuất sai. Bộ tạo tập con duyệt qua đồ thị đó cho đến khi không còn thành phần mới nào xuất hiện, kèm theo một cơ chế chống chu trình để một phông chữ dị dạng không thể lặp mãi mãi.

Trong tệp đó có hai lựa chọn kỹ thuật đáng nêu rõ. Thứ nhất, các glyph ID được giữ nguyên, không ánh xạ lại — các vị trí không dùng được lấp số không trong glyf/loca để các chỉ mục glyph gốc của luồng nội dung vẫn hợp lệ dưới CIDToGIDMap /Identity. Ánh xạ lại sẽ cho kích thước nhỏ hơn, nhưng nó đòi hỏi viết lại từng tham chiếu glyph. Giữ nguyên định danh là lựa chọn đúng ngay từ thiết kế. Thứ hai, quá trình duyệt được sắp xếp (gid tăng dần), nên tập con mang tính tất định ở mức byte — cùng một phông chữ và cùng các glyph được dùng sẽ tạo ra cùng các byte tập con, đúng như điều mà các bản dựng tái lập đòi hỏi. Nếu việc tạo tập con tiết kiệm chưa đến ~10% của tệp, bản gốc được trả về nguyên trạng. Chi phí phát sinh không đáng để đổi lấy một lợi ích không đáng kể.

Nhúng. Một chính sách rõ ràng quyết định một chương trình phông chữ có được mang theo hay không — không bao giờ phỏng đoán. Pdf20FontEmbeddingPolicy (src/Writer/Pdf20FontEmbeddingPolicy.php) có hai chế độ. Theo hồ sơ PDF 2.0, Strict từ chối một tham chiếu Type 1 tiêu chuẩn không nhúng (“Base14”) bằng một ngoại lệ có kiểu — đó là hành vi đúng về mặt tuân thủ. AllowBase14 giữ lại lối tư vấn kiểu cũ. Trong một khoảng thời gian chuyển đổi, nó phát ra font descriptor tối thiểu mà tiêu chuẩn vẫn yêu cầu và gửi đi một cảnh báo thay vì ném ngoại lệ. Bên gọi đưa ra lựa chọn rõ ràng trên tài liệu; lựa chọn này không bao giờ được suy ra từ phông chữ.

Mã hóa. Đối với phông chữ hợp thành (Type 0), EmbeddedTtfFontDictBuilder (src/Writer/EmbeddedTtfFontDictBuilder.php) phát ra hậu duệ CIDFontType2, phần tử cha Type0, và một luồng CMap /ToUnicode để các mã ký tự phân giải trở lại Unicode. Luồng /ToUnicode chỉ có thể vắng mặt một cách chính đáng trong một trường hợp: khi một CMap CJK định sẵn có khả năng tự mô tả đã cung cấp sẵn cho trình đọc ánh xạ từ ký tự sang Unicode. Ở đó, CMap chính là phần mã hóa, nên hồ sơ thông thường bỏ qua một luồng /ToUnicode dư thừa để tiết kiệm byte. Ngoài trường hợp đó, luồng /ToUnicode chính là thứ giữ cho văn bản vẫn là văn bản.

Mối quan tâmNó bảo đảm điều gìkhông bảo đảm điều gìLỗi âm thầm nếu sai
NhúngKết xuất giống nhau dù chưa cài đặt phông chữRằng văn bản có thể tìm kiếm đượcPhông chữ bị thay thế; sai số đo trên máy khác
Tạo tập conTệp nhỏ; chỉ các glyph được dùngBất cứ điều gì về mã hóaThiếu thành phần hợp thành → glyph có dấu bị hỏng
Mã hóa (/ToUnicode)Văn bản tìm kiếm được, sao chép được, tiếp cận đượcRằng glyph kết xuất đúngTrang trông hoàn hảo nhưng không tìm kiếm được / lỗi ký tự khi sao chép

Ba mối quan tâm về phông chữ độc lập với nhau. Nhúng và tạo tập con liên quan đến diện mạo và kích thước; mã hóa liên quan đến ý nghĩa. Một trang có thể đạt hai điều đầu tiên và thất bại ở điều thứ ba mà không có gì hiện ra để cho thấy điều đó.

Quy tắc đặt tên tập con vừa mang tính quy phạm vừa rất chính xác. Spec: ISO 32000-2, §9.9.2 yêu cầu rằng tên PostScript của tập con phông chữ — tức BaseFontFontName của descriptor — phải bắt đầu bằng một thẻ gồm đúng sáu chữ in hoa, rồi đến một dấu cộng, rồi đến tên PostScript của phông chữ gốc. Quy tắc này còn yêu cầu rằng các tập con khác nhau của cùng một phông chữ trong một tệp phải dùng các thẻ khác nhau. Chính quy tắc đó cho phép trình đọc phân biệt được hai tập con và hợp nhất các tài liệu một cách đúng đắn. Evidence: Standard-backed

Mã hóa là một điều khoản tách biệt với kết xuất. Spec: ISO 32000-2, §9.10.3 định nghĩa /ToUnicode là một luồng chứa một CMap ánh xạ các mã ký tự sang các giá trị Unicode, và quy trình trích xuất văn bản trong Spec: ISO 32000-2, §9.10.2 dùng CMap đó để chuyển các mã ký tự sang Unicode phục vụ việc tìm kiếm và lập chỉ mục. Không có gì trong cơ chế vẽ glyph chạm đến /ToUnicode — và đó chính là lý do vì sao văn bản có thể trông đúng nhưng trích xuất ra sai.

Về việc nhúng, tiêu chuẩn nêu rằng hầu hết các font dictionary đều mang một font descriptor, trong đó luồng tệp phông chữ được nhúng là tùy chọn nhưng rất nên có. PDF 2.0 siết chặt điều này riêng cho mười bốn phông chữ Type 1 tiêu chuẩn. Chính sách Strict của NextPDF là cách hiểu đúng về mặt tuân thủ đối với phần siết chặt đó. AllowBase14 là lối thoát tương thích ngược rõ ràng, do bạn chủ động chọn — engine không bao giờ âm thầm hạ cấp.

Strict PDF 2.0 font-embedding enforcement — edition availability
Edition Availability
Core

Có sẵn. Tạo tập con, phát ra /ToUnicode, và chính sách nhúng rõ ràng Strict / AllowBase14 đều là hành vi cốt lõi của engine.

Pro

Bổ sung việc thực thi tuân thủ sâu hơn và báo cáo về việc nhúng phông chữ ở cấp độ hồ sơ.

Enterprise

Bổ sung cùng việc thực thi tuân thủ đó trong bề mặt vận hành cấp doanh nghiệp.

Dưới đây là hai nửa của một phông chữ hợp thành được nhúng, tạo tập con và có thể tìm kiếm đúng cách. Thẻ tập con tuân theo quy tắc sáu chữ của tiêu chuẩn; tham chiếu /ToUnicode giữ cho văn bản vẫn trích xuất được.

% The Type 0 (composite) font dictionary
20 0 obj
<< /Type /Font /Subtype /Type0
/BaseFont /ABCDEF+NotoSans % six-letter subset tag + '+'
/Encoding /Identity-H
/DescendantFonts [21 0 R]
/ToUnicode 23 0 R >> % the map that makes text searchable
endobj
% The descendant CIDFontType2 (carries the subsetted program)
21 0 obj
<< /Type /Font /Subtype /CIDFontType2
/BaseFont /ABCDEF+NotoSans
/CIDToGIDMap /Identity % glyph IDs preserved, not remapped
/FontDescriptor 22 0 R >>
endobj

Mục /ToUnicode 23 0 R của object 20 chính là khác biệt giữa một tài liệu tìm kiếm được và một bức hình của tài liệu đó. Bỏ nó đi (ngoài trường hợp CMap định sẵn) thì mọi glyph vẫn được vẽ hoàn hảo, nhưng tìm bất kỳ từ nào trên trang cũng không ra kết quả.

Cái bẫy, nói thẳng ra, là: glyph kết xuất đúng không hề nói lên điều gì về việc văn bản có phải là văn bản hay không. Kết xuất đi theo đường từ mã hóa đến glyph. Tìm kiếm và sao chép đi theo đường từ mã đến Unicode (/ToUnicode). Đó là những cơ chế khác nhau, đọc các phần khác nhau của font dictionary. Vì vậy, một tài liệu có thể có đầu ra hình ảnh hoàn hảo nhưng lại thiếu hoặc có sai /ToUnicode. Kết quả là một trang trông đáng tin nhưng về mặt chức năng lại không tìm kiếm được — lỗi sống sót qua mọi lần soát xét trực quan, vì theo định nghĩa thì chẳng có gì để mắt thấy.

Một cái bẫy liên quan là cho rằng “phông chữ đã được nhúng nên ta ổn về mặt lưu trữ.” Nhúng là cần nhưng chưa đủ. Một hồ sơ như PDF/A còn mong đợi các tập con được đặt tên theo quy tắc sáu chữ và được mã hóa đúng. Đã nhúng nhưng không tìm kiếm được thì vẫn không đạt.

Bộ tạo tập con của NextPDF cụ thể là bộ tạo tập con TrueType. Nó cần đến các bảng TrueType thiết yếu và trả về phông chữ gốc nguyên trạng khi các bảng đó thiếu hoặc khi lợi ích thấp hơn ngưỡng ~10%. Việc tạo tập con và một CMap /ToUnicode làm cho văn bản trích xuất được, nhưng chúng không thể cứu một phông chữ nguồn thiếu thông tin để ánh xạ một glyph trở lại một ký tự có ý nghĩa. Ở những chỗ không thể xác định được giá trị Unicode, dù phát ra bao nhiêu CMap cũng không thể bịa ra một giá trị.

Trang này nói về việc tạo ra cấu trúc phông chữ đúng trong những tài liệu mà NextPDF ghi ra. Nó không phải là công cụ sửa phông chữ cho các tệp PDF đầu vào bất kỳ. Và việc phát ra một tập con cùng mã hóa tuân thủ, tự thân nó, không chứng nhận rằng một tài liệu đạt một hồ sơ lưu trữ đầy đủ — đó là một bước kiểm tra riêng, rộng hơn.

Vì sao lại là thẻ sáu chữ — sao không dùng tên phông chữ? Để trình đọc có thể phân biệt hai tập con khác nhau của cùng một phông chữ và hợp nhất các tài liệu mà không làm xung đột các tập glyph của chúng. Tập con khác nhau thì thẻ khác nhau, theo quy tắc.

Khi nào có thể chấp nhận việc không có /ToUnicode? Khi một CMap CJK định sẵn có khả năng tự mô tả đã cung cấp sẵn ánh xạ từ ký tự sang Unicode. Ở đó CMap chính là phần mã hóa. Một /ToUnicode riêng sẽ là dư thừa. Ngoài trường hợp đó, sự vắng mặt của nó là một khiếm khuyết.

Tạo tập con có bao giờ gây hại không? Chỉ khi làm sai. Loại bỏ các thành phần của glyph hợp thành sẽ làm hỏng các glyph có dấu. Ánh xạ lại glyph ID mà không viết lại các tham chiếu sẽ làm hỏng kết xuất. NextPDF tránh cả hai bằng cách giải quyết bao đóng thành phần và giữ nguyên định danh glyph.

  • Chương trình phông chữ được nhúng — tệp phông chữ thực tế (TrueType/CFF/Type 1) được mang bên trong tệp PDF dưới dạng một luồng, để việc kết xuất không phụ thuộc vào các phông chữ đã cài đặt của trình đọc.
  • Tạo tập con — viết lại một chương trình phông chữ để chỉ chứa những glyph mà tài liệu dùng đến, nhằm giảm kích thước.
  • Thẻ tập con — tiền tố bắt buộc gồm sáu chữ in hoa cộng với + trên tên của một phông chữ tập con (ví dụ, ABCDEF+NotoSans).
  • /ToUnicode — một luồng CMap ánh xạ các mã ký tự sang các giá trị Unicode; thứ giúp văn bản PDF tìm kiếm được, sao chép được và tiếp cận được.
  • Glyph hợp thành — một glyph được tạo bằng cách tham chiếu đến các glyph khác làm thành phần; các thành phần của nó phải được giữ lại khi tạo tập con.
  • CIDToGIDMap /Identity — chế độ trong đó các chỉ mục glyph của luồng nội dung chính là các glyph ID của phông chữ giữ nguyên không đổi; NextPDF giữ nguyên định danh glyph để điều này vẫn hợp lệ.
  • Base14 — mười bốn phông chữ Type 1 tiêu chuẩn; PDF 2.0 mong đợi các phông chữ được nhúng thay vì tham chiếu theo tên.