HotPDF dựng hình các font PDF nhúng trong Delphi mà không cần cài đặt bất kỳ thứ gì trên máy tính: pipeline dựng hình glyph nhúng trong HPDFGlyphRender.pas phân tích các chương trình font được lưu trữ bên trong chính file PDF — bao gồm outline TrueType glyf từ FontFile2, charstring CFF Type 2 từ FontFile3, và các content stream glyph Type 3 — và vẽ lại chúng dưới dạng các đường dẫn vector GDI được tô đầy. Bài viết này là phần tìm hiểu sâu về độ chính xác của font đằng sau việc dựng hình các trang PDF thành TBitmap bằng HotPDF: phần đó nói về trình dựng hình nói chung, còn phần này nói về cách văn bản trên các trang đó có được hình dạng chính xác
Tại sao một file PDF lại hiển thị các ô vuông thay vì văn bản?
Các ô vuông, khoảng trống hoặc các ký tự sai lệch một cách tinh vi trong kết quả dựng hình PDF hầu như luôn có nghĩa là trình dựng hình đã yêu cầu hệ điều hành cung cấp một font chữ thay vì sử dụng font chữ được nhúng trong file. Lỗi này luôn xuất hiện theo cùng một cách: tài liệu trông hoàn hảo trên máy tính tạo ra nó, nhưng khi khách hàng mở nó trên một server sạch hoặc một máy tính desktop bị khóa quyền, hóa đơn tiếng Nhật sẽ hiển thị các ô vuông (tofu squares), hoặc một font chữ thay thế có hình dạng tương tự làm thay đổi mọi vị trí ngắt dòng. Các font chữ đó chưa từng có trên máy tính đó — chúng chỉ nằm bên trong file PDF — và một trình dựng hình dừng lại ở việc thay thế font hệ thống sẽ không thể nhìn thấy chúng. Các font chữ dạng subset (tập hợp con) còn làm cho vấn đề tồi tệ hơn: một subset có thể mang bốn mươi glyph dưới các mã ký tự được gán riêng cho duy nhất file đó, một cách gán mà không font cài đặt nào chia sẻ
Tiêu chuẩn ISO 32000-1 §9.9 định nghĩa ba loại container cho một chương trình font nhúng trong font descriptor: FontFile chứa chương trình Type 1 ban đầu, FontFile2 chứa chương trình TrueType, và FontFile3 chứa một stream CFF trần (Type1C hoặc CIDFontType0C) hoặc một wrapper OpenType. Dạng thứ tư, font Type 3 của tiêu chuẩn ISO 32000-1 §9.6.5, không nhúng bất kỳ thứ gì dưới dạng nhị phân cả — mỗi glyph là một stream nội dung PDF nhỏ được thực thi tại chỗ. Ba loại container khác nhau về mặt toán học outline (quadratic B-splines so với cubic charstrings so với các toán tử trang tùy ý), do đó một trình dựng hình chuẩn xác cần một trình thông dịch riêng cho mỗi loại, cộng với một lớp mã hóa (encoding layer) để chuyển đổi mã ký tự thành chỉ mục glyph chính xác trước khi xử lý outline
Làm thế nào HotPDF chuyển đổi outline TrueType glyf thành đường dẫn GDI?
THPDFEmbeddedTTF trong HPDFGlyphRender.pas đọc bảng loca để định vị từng bản ghi glyph, duyệt qua các contour glyf theo từng điểm, và phát ra một đường dẫn GDI. Có hai quy ước TrueType cần xử lý rõ ràng. Thứ nhất, các điểm off-curve (nằm ngoài đường cong) liên tiếp ngụ ý một điểm on-curve (nằm trên đường cong) ở trung điểm của chúng, và một contour có tất cả các điểm đều là off-curve sẽ bắt đầu ở trung điểm của điểm cuối cùng và điểm đầu tiên của nó — bỏ qua một trong hai quy tắc này và các glyph tròn sẽ bị phẳng hoặc bị hỏng. Thứ hai, các đường cong TrueType là quadratic Bézier (bậc hai) trong khi hàm PolyBezierTo của GDI nhận cubic Bézier (bậc ba), vì vậy mỗi phân đoạn bậc hai sẽ được nâng bậc chính xác thay vì bị làm phẳng thành các phân đoạn thẳng
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
Việc nâng bậc là không tổn hao: đường cong bậc ba vẽ ra đường cong giống hệt, vì vậy outline dựng hình khớp với những gì một trình xem chuẩn vẽ từ cùng một bảng, ở bất kỳ mức thu phóng nào. Công việc còn lại là định vị. Mỗi glyph được thiết kế theo đơn vị font (thường là một lưới 1000 hoặc 2048 đơn vị trên mỗi em), và trình dựng hình sẽ kết hợp ma trận thu phóng, ma trận văn bản và ma trận biến đổi hiện tại thành một phép biến đổi duy nhất từ glyph sang thiết bị trước khi đường dẫn được tô đầy. Thứ tự thực hiện ở đây quan trọng hơn vẻ bề ngoài của nó: kết hợp ngược ba ma trận đó và mọi glyph sẽ thu nhỏ về gốc tọa độ — một trang trông có vẻ bị lỗi mà nguyên nhân thực chất chỉ là một dòng code đại số ma trận bị sai
Cách trình thông dịch charstring Type 2 xử lý font CFF
THPDFEmbeddedCFF cung cấp cho các chương trình FontFile3 một trình thông dịch charstring Type 2 thực sự: nó phân tích các cấu trúc CFF INDEX, Top DICT và Private DICT, sau đó thực thi từng charstring và phát các phân đoạn đường dẫn trực tiếp tới GDI. Một OpenType wrapper (container OTTO) sẽ được gỡ bỏ trước để tiếp cận bảng CFF trần; các stream CIDFontType0C và Type1C trần được tiêu thụ trực tiếp. Charstring là một ngôn ngữ stack nhỏ gọn, và ba quy ước của nó quyết định xem trình thông dịch có giữ đồng bộ với stream byte hay không. Tiền tố chiều rộng tùy chọn có nghĩa là toán tử xóa stack đầu tiên có thể mang một toán hạng dẫn đầu bổ sung. Toán tử hintmask ngụ ý một vstemhm khi các toán hạng vẫn còn trên stack, và số lượng byte mask cần bỏ qua phụ thuộc vào số lượng stem tích lũy được — nếu tính sai số lượng này một lần, mọi opcode tiếp theo sẽ bị đọc sai. Và các lệnh gọi subroutine sẽ thêm một độ lệch (bias) vào chỉ mục của chúng (107, 1131, hoặc 32768 tùy thuộc vào số lượng subroutine) trước khi tìm kiếm, do đó một lệnh gọi không có độ lệch sẽ trỏ hoàn toàn sai subroutine
CFF dạng CID-keyed thêm một mức gián tiếp dễ làm các triển khai đơn giản bị lỗi: mã ký tự chọn một CID, nhưng chỉ mục charstring lại là một GID, và bảng ký tự (charset) của font sẽ ánh xạ GID sang CID — vì vậy trình dựng hình sẽ xây dựng bản đồ ngược từ CID sang GID trước khi vẽ, và chọn Private DICT cho mỗi glyph thông qua FDSelect đối với các font mang nhiều Private DICT. Các chương trình Type1C dạng name-keyed, container phổ biến cho các font Type 1 đơn giản, thay vào đó sẽ phân giải các mã một byte thông qua bảng encoding tích hợp của chương trình CFF hoặc thông qua cơ chế encoding cấp độ PDF được mô tả tiếp theo. Một lưu ý thực tế: trình thông dịch đọc các toán tử hint để giữ đồng bộ stream nhưng không thực thi việc tạo hint (hinting), một giới hạn được thảo luận ở cuối bài
Font Type 3 là gì và nó được vẽ như thế nào?
Một glyph Type 3 hoàn toàn không phải là một outline — tiêu chuẩn ISO 32000-1 §9.6.5 định nghĩa nó là một content stream, vì vậy HotPDF dựng hình nó bằng cách đẩy trạng thái đồ họa (graphics state), kết hợp ma trận font, kích thước font và ma trận văn bản lên CTM, và thực thi quy trình glyph thông qua cùng một trình thông dịch toán tử vẽ các trang, với phạm vi hiệu lực của /Resources của chính font đó. Có hai chi tiết đặc tả quan trọng đối với tính chính xác. Các thuộc tính /Widths của Type 3 được biểu thị trong không gian glyph thay vì không gian văn bản 1/1000 mà mọi loại font khác sử dụng, vì vậy các bước tiến phải đi qua /FontMatrix — các font mã vạch với ma trận 0.01 nếu không sẽ bước sai một bậc độ lớn. Và một quy trình glyph mở đầu bằng toán tử d1 sẽ đưa ra hai cam kết mà trình dựng hình thực thi: việc vẽ được cắt (clip) theo bounding box đã khai báo, và theo tiêu chuẩn ISO 32000-1 §9.6.5.2, glyph bỏ qua các toán tử màu của chính nó và vẽ bằng màu tô hiện tại của bên gọi, do đó các toán tử rg, g, k và các toán tử nét vẽ cùng nhóm bên trong quy trình sẽ bị triệt tiêu trong thời gian vẽ glyph đó. Bỏ qua quy tắc màu này và một font mã vạch d1 được đóng dấu màu xanh bởi trang sẽ hiển thị màu đen; bỏ qua vùng cắt và một glyph bị lỗi sẽ vẽ ra ngoài ô của nó
Cách mã ký tự chuyển thành ID glyph
Trình thông dịch outline chỉ là một nửa công việc, vì các byte trong một chuỗi văn bản PDF là mã ký tự, không phải chỉ mục glyph, và tiêu chuẩn ISO 32000-1 dành riêng hai điều khoản phụ cho việc ánh xạ này. Đối với các font đơn giản, §9.6.6 quy định một mức ưu tiên nghiêm ngặt: một mảng /Differences sẽ ghi đè lên base encoding (WinAnsiEncoding, MacRomanEncoding, hoặc StandardEncoding), vốn sẽ ghi đè lên bản đồ của chính chương trình font. HotPDF phân giải chuỗi đó thành một bảng chuyển đổi từ mã sang GID gồm 256 mục, dịch tên glyph thành chỉ mục glyph theo ba con đường: khớp bảng ký tự chính xác bên trong chương trình CFF, các tên dạng số gNN/glyphNN được coi là chỉ mục literal, và dịch tên sang Unicode theo Adobe Glyph List rồi thực hiện tìm kiếm cmap cho các chương trình TrueType. Đối với các font composite (hỗn hợp), §9.7 giao quyền cho CIDToGIDMap: trường hợp phổ biến là /Identity, nhưng mục này có thể là một stream gồm các cặp big-endian được chỉ mục bởi CID — và đầu ra Unicode của chính HotPDF sử dụng chính xác dạng stream đó cho các subset nhỏ gọn, vì vậy đường dẫn stream không phải là một góc khuất xa lạ
// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // out of range maps to .notdef
Khi cần tìm kiếm cmap TrueType, HotPDF sẽ đi qua một chuỗi fallback chứ không tin tưởng vào một bảng phụ duy nhất: các bảng phụ Windows Unicode (định dạng 4, sau đó là định dạng 12 cho các mặt phẳng bổ sung) được ưu tiên hàng đầu, sau đó là bảng phụ ký hiệu (3,0) với quy ước vùng sử dụng riêng F000 của nó được ánh xạ xuống byte thấp — lý do một font ký hiệu như Wingdings phản hồi cho các mã ASCII thông thường — sau đó là các định dạng cũ 6 và 0. Các bảng phụ định dạng 2 cố ý không được thông dịch: chúng ánh xạ các trang mã đa byte cũ như Shift-JIS và Big5, không phải Unicode, và các font CJK hiện đại dù sao cũng luôn mang bảng phụ định dạng 4 hoặc định dạng 12. Bất kỳ mã nào không vượt qua được các con đường này sẽ chuyển sang vẽ GDI cho duy nhất glyph đó, vì vậy một ký tự không thể ánh xạ chỉ làm giảm chất lượng của một glyph đó chứ không phải toàn bộ đoạn văn bản
Những gì đường dẫn nhúng không thực hiện
Các giới hạn đáng được nêu rõ ràng. Việc tạo hint (hinting) không được thực hiện — outline được tô đầy theo thiết kế ban đầu, điều này không thể phân biệt được với đầu ra có hint ở độ phân giải từ 150 DPI trở lên nhưng có thể khác biệt một pixel so với một rasterizer có hint ở kích thước rất nhỏ. Các chương trình Type 1 ban đầu trong FontFile (charstring mã hóa eexec) không được thông dịch, và các trục variable-font OpenType không được áp dụng; cả hai trường hợp này, giống như một chương trình font bị hỏng hoặc một bảng glyf không có bất kỳ cmap khả dụng nào, sẽ chuyển sang vẽ bằng font hệ thống thay vì làm hỏng trang. Cách tiếp cận ưu tiên độ chính xác tương tự cũng được áp dụng ở những nơi khác trong trình dựng hình — axial và radial shading pattern cũng được xử lý như các gradient xứng đáng được nhận — và phía tạo file có câu chuyện riêng về sự tinh tế của font trong bài viết về cách EndDoc sắp xếp các subset font
Sử dụng pipeline không yêu cầu bất kỳ code cụ thể nào về font — mọi cơ chế trên đều tự động kích hoạt bên trong lệnh gọi dựng hình trang
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Embedded TrueType, CFF, and Type 3 fonts render from the
// file itself — nothing needs to be installed on this machine
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Kết quả thực tế là điều mà hộp thư hỗ trợ của bạn quan tâm: một file PDF mang theo các font chữ của nó sẽ dựng hình bằng chính các font chữ đó, trên một build server, trong một container Windows, hoặc trên máy tính desktop của khách hàng chưa từng cài đặt kiểu chữ đó. Pipeline dựng hình glyph nhúng đi kèm như một phần của HotPDF Component dành cho Delphi and C++Builder — một thư viện VCL gốc hỗ trợ tạo, chỉnh sửa, trích xuất văn bản và dựng hình trang PDF mà không cần phụ thuộc vào bất kỳ DLL bên ngoài nào