Tạo một báo cáo, nhúng font TrueType và kết quả đầu ra hiển thị chính xác trong mọi trình xem mà bạn thử nghiệm. Các ký tự (glyphs) hiển thị đúng, văn bản có thể chọn được, tệp tin hoàn toàn hợp lệ. Điều bất thường duy nhất là dung lượng tệp. Một tài liệu chỉ sử dụng vài chục ký tự Latinh lại mang theo toàn bộ font chữ nặng 350 KB. Một tài liệu in một đoạn văn chữ Trung Quốc mang theo toàn bộ font CJK nặng 14 MB thay vì chỉ cần một phân đoạn nửa megabyte cần thiết. Không có ngoại lệ nào được nâng lên, không có cảnh báo nào được ghi nhận và tệp đã vượt qua quá trình xác thực. Đây là biểu hiện của một bước hoàn tất bị sai thứ tự khi nhìn từ bên ngoài: không có lỗi xảy ra và bằng chứng duy nhất là kích thước tệp quá lớn.
Lỗi gây ra hành vi này đã tồn tại trong HotPDF ở một dòng phát hành và đã được khắc phục. Sự cố này đáng được ghi chép lại không phải như một thông báo lỗi mà như một bài học kinh nghiệm, bởi vì tính chất của sai lầm này mang tính tổng quát. Bất kỳ công cụ tài liệu nào cũng có giai đoạn hoàn tất để thay đổi các đối tượng ngay trước khi ghi chúng, và tính chính xác của giai đoạn đó phụ thuộc hoàn toàn vào thứ tự của các bước so với quá trình tuần tự hóa (serialization). Chỉ cần đặt một bước sai phía của quá trình ghi, nó sẽ không hoạt động mà không có bất kỳ cảnh báo nào.
Tính năng font subsetting được kỳ vọng hoạt động như thế nào
Một subset font (tập con font chữ) là phần của tệp TrueType mà tài liệu thực sự sử dụng. ISO 32000-1 §9.9 mô tả cách một chương trình font chữ nhúng chạy trong một luồng (stream) được tham chiếu bởi bộ mô tả font (font descriptor), và đối với chương trình TrueType, luồng đó là /FontFile2 với /Length1 cung cấp số lượng byte chưa nén. Subsetting ghi lại các bảng glyf và loca để chúng chỉ chứa các ký tự mà tài liệu tham chiếu, đánh số lại các định danh ký tự và thêm tiền tố vào tên /BaseFont bằng một thẻ gồm sáu chữ cái như ABCDEF+ để đánh dấu font chữ đó là một tập con, chính xác như đặc tả yêu cầu. Một font chữ Latinh được tạo tập con chỉ còn mười hoặc mười lăm kilobyte là sự khác biệt giữa một tệp PDF tinh gọn và một tệp chứa toàn bộ font chữ chỉ vì một tiêu đề.
Thời điểm diễn ra quá trình này rất quan trọng. Subsetting không phải là một phép biến đổi mà bạn áp dụng cho các byte đã có trên đĩa. Nó chỉnh sửa cấu trúc đối tượng (object graph) trong bộ nhớ: thu nhỏ nội dung luồng /FontFile2, sửa lại /Length1 và viết lại chuỗi /BaseFont. Tất cả những điều đó phải sẵn sàng khi bộ tuần tự hóa duyệt qua cấu trúc đối tượng và xuất ra các byte. Nếu việc chỉnh sửa diễn ra sau khi các byte đã được ghi, chúng chỉ cập nhật các đối tượng mà không ai đọc nữa.
Triệu chứng và lý do tại sao hệ thống không báo lỗi
Hành vi được báo cáo là font chữ đầy đủ xuất hiện ở đầu ra mà không có bất kỳ thông tin chẩn đoán nào. Người dùng đã đăng ký một font TrueType Unicode và tạo ra một tài liệu bình thường nhận thấy rằng đối tượng font nhúng có độ dài bằng với tệp nguồn .ttf, và tên /BaseFont không mang tiền tố tập con sáu chữ cái. Đầu ra không bao giờ thu nhỏ giữa các lần chạy sử dụng mười ký tự và các lần chạy sử dụng mười nghìn ký tự.
Việc không xuất hiện bất kỳ lỗi nào là điều khiến loại lỗi này trở nên nghiêm trọng và tốn kém thời gian khắc phục. Một hàm subsetting chạy sai thời điểm vẫn hoạt động bình thường. Nó duyệt qua danh sách các điểm mã (codepoints) tích lũy đã được sử dụng, tạo ra một tập con hoàn toàn chính xác và áp dụng nó vào cấu trúc đối tượng trong bộ nhớ. Về mặt nội bộ, công việc đã hoàn thành và hàm trả về kết quả thành công. Điều duy nhất sai là cấu trúc đối tượng mà nó chỉnh sửa không còn là thứ được ghi ra nữa, vì bộ ghi đã hoàn thành công việc trước đó. Từ góc nhìn của người gọi, tài liệu đã được tạo và lưu trữ mà không gặp sự cố nào, đây chính là những gì một lỗi âm thầm mang lại.
Nguyên nhân gốc rễ là thứ tự hoàn tất
Trong HotPDF, các công việc đóng tài liệu diễn ra bên trong EndDoc. Bước tạo tập con là một hàm nội bộ có tên là BuildAndApplyUnicodeFontSubset. Nó đọc tập hợp các điểm mã đã sử dụng cho mỗi tài liệu, được lưu trữ trong một bitmap mà luồng xuất văn bản điền vào khi các ký tự được hiển thị, ánh xạ từng điểm mã đã sử dụng thông qua bảng điểm mã sang ký tự (codepoint-to-glyph) được lưu trong bộ nhớ cache thành một định danh ký tự thực tế, và viết lại chương trình font chữ xung quanh cấu trúc đóng đó. Khi một font TrueType Unicode được đăng ký, luồng xuất sẽ đặt một bit trong tập hợp điểm mã đã sử dụng cho mỗi ký tự mà nó vẽ, do đó vào thời điểm tài liệu đóng lại, công cụ sẽ biết chính xác ký tự nào tập con cần giữ lại.
Lỗi xảy ra do BuildAndApplyUnicodeFontSubset được gọi sau khi SaveToStream hoặc SaveToFile đã tuần tự hóa tài liệu. Các chỉnh sửa của bộ subsetter đối với /FontFile2, giá trị /Length1 đã sửa đổi và tiền tố /BaseFont sáu chữ cái đều được tính toán trên một cấu trúc đối tượng vốn đã được chuyển thành các byte. Giải pháp là thay đổi thứ tự trong một dòng mã: di chuyển lệnh gọi subset trước quá trình tuần tự hóa, để bộ ghi xuất ra font chữ đã tạo tập con thay vì font chữ ban đầu. Trình tự đã sửa đổi sẽ chạy bộ subsetter trước và tuần tự hóa sau.
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
Pdf.EndDoc; // subsetting runs here, before the write
Pdf.SaveToFile('Report.pdf');
finally
Pdf.Free;
end;
end;
Với thứ tự được sửa đổi, không có gì thay đổi trong mã nguồn gọi hàm. Subsetting được bật theo mặc định sau khi font TrueType Unicode được đăng ký. Bạn đăng ký font chữ, bắt đầu tài liệu, vẽ và kết thúc nó, tập con sẽ được tạo từ các ký tự bạn đã sử dụng trước khi các byte được giải phóng khỏi bộ nhớ.
Tại sao một bước đặt sai chỗ lại là cả một nhóm lỗi phân loại
Lý do sự cố này đáng để rút ra bài học thay vì chỉ là một ghi chú nhỏ là vì EndDoc thực hiện một danh sách các bước đóng tài liệu, và từng bước trong số đó đều rất nhạy cảm với vị trí của chúng so với lệnh ghi dữ liệu. Font subsetting là một ví dụ. Đầu ra PDF/A yêu cầu một luồng /CIDSet liệt kê chính xác các định danh ký tự xuất hiện trong tập con, một ràng buộc mà ISO 19005 áp đặt để trình xác thực có thể xác nhận chương trình nhúng khớp với những gì bộ mô tả font khai báo; luồng đó được phát ra trong cùng một cửa sổ hoàn tất và phụ thuộc vào việc tập con đã được xây dựng trước. PDF/UA-1 yêu cầu, theo ISO 14289-1 §7.18.3, rằng mọi trang chứa chú thích phải khai báo /Tabs với giá trị /S, và một hàm nội bộ có tên là EnsurePDFUATabsOnAnnotatedPages sẽ đóng dấu khóa đó trong cùng giai đoạn này. Các kiểm tra mục đích đầu ra (output-intent) cũng chạy tại đây.
Cùng một lỗi thứ tự làm vô hiệu hóa subsetting cũng làm mất khóa thứ tự tab PDF/UA trên các trang có chú thích, vì bước đó nằm cùng một phía sai của quá trình ghi. veraPDF và PAC báo cáo thiếu /Tabs /S như một sự vi phạm điểm kiểm tra giao thức Matterhorn 21-001. Do đó, một lệnh gọi bị đặt sai vị trí không chỉ làm tăng kích thước tệp mà còn âm thầm phá vỡ yêu cầu tuân thủ khả năng truy cập (accessibility conformance requirement) trong cùng thời điểm, với cùng hiện tượng không có lỗi nào được báo cáo. Đó là mối nguy hiểm của giai đoạn hoàn tất: các bước của nó chia sẻ một điều kiện tiên quyết, và một lỗi thứ tự duy nhất có thể vô hiệu hóa nhiều bước cùng lúc trong khi mọi lệnh gọi vẫn trả về thành công.
Cách phát hiện lỗi xuất dữ liệu âm thầm một cách thực tế
Một lỗi không tạo ra ngoại lệ sẽ không thể bị phát hiện bằng cách chạy chương trình. Nó chỉ được phát hiện bằng cách kiểm tra kết quả đầu ra và so sánh với những gì đầu vào đáng nhẽ phải tạo ra. Đối với font subsetting, các kiểm tra rất cụ thể. So sánh kích thước tệp đầu ra với mong đợi ước tính: một tài liệu chỉ sử dụng một vài ký tự không được có kích thước của một bộ font đầy đủ. Mở đối tượng font nhúng và đọc độ dài byte của nó; một luồng /FontFile2 đã được tạo subset cho font Latinh chỉ chiếm một phần nhỏ của tệp nguồn. Đọc tên /BaseFont và xác nhận xem tiền tố sáu chữ cái có hiện diện hay không, vì sự vắng mặt của nó là tín hiệu trực tiếp cho thấy không có tập con nào được áp dụng.
var
Pdf: THotPDF;
Output: TMemoryStream;
begin
Output := TMemoryStream.Create;
try
Pdf := THotPDF.Create(nil);
try
Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
Pdf.EndDoc;
Pdf.SaveToStream(Output);
finally
Pdf.Free;
end;
// A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
if Output.Size > 100 * 1024 then
raise Exception.Create('Font subset did not shrink the output');
finally
Output.Free;
end;
end;
Đối với đầu ra PDF/A, việc kiểm tra thậm chí còn rõ ràng hơn vì trình xác thực (validator) sẽ thực hiện công việc đó cho bạn. Đặt mức độ tuân thủ và chạy kết quả qua veraPDF: việc thiếu /CIDSet, hoặc một tập con không khớp với bộ mô tả, sẽ được báo cáo là một điều khoản thất bại thay vì để bạn tự nhận thấy bằng mắt thường. Các công tắc tuân thủ điều khiển công việc hoàn tất này là các thuộc tính trên tài liệu. PDFACompliance nhận một chuỗi như '2B' cho PDF/A-2 Mức B, và PDFUACompliance là một giá trị boolean bật các yêu cầu PDF được gắn thẻ (tagged-PDF) và thứ tự tab.
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B'; // PDF/A-2 Level B, drives /CIDSet emission
Pdf.PDFUACompliance := True; // stamps /Tabs /S on annotated pages
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
Pdf.EndDoc;
Pdf.SaveToFile('Report_PDFA.pdf');
finally
Pdf.Free;
end;
Bài học kinh nghiệm kỹ thuật
Hai quy tắc được rút ra từ điều này. Thứ nhất là bất kỳ bước hoàn tất nào làm thay đổi đối tượng phải chạy trước khi các đối tượng đó được tuần tự hóa, và giai đoạn đóng của một công cụ tài liệu nên được hiểu là một đường ống có thứ tự (ordered pipeline) trong đó tuần tự hóa là hành động cuối cùng, chứ không phải là một hành động trong số nhiều hành động khác nhau. Quy tắc thứ hai là quy tắc tiêu tốn nhiều thời gian nhất ở đây: đối với một bước xuất dữ liệu, việc không có lỗi không phải là bằng chứng của sự thành công. Một hàm xây dựng tập con chính xác và áp dụng nó vào cấu trúc đối tượng sai, đã được ghi trước đó sẽ không báo cáo bất kỳ lỗi nào, vì theo góc nhìn của chính nó thì không có gì sai. Quá trình xác thực phải kiểm tra sản phẩm thực tế, chứ không phải mã trả về. Kiểm tra kích thước đầu ra, đọc độ dài byte của font chữ nhúng và tiền tố /BaseFont của nó, và để veraPDF đánh giá đầu ra PDF/A, nơi việc thiếu /CIDSet biến một thiếu sót âm thầm thành một lỗi được chỉ tên rõ ràng.
Quy trình xử lý font chữ phía tạo lập, cách đăng ký và nhúng các kiểu font để xuất báo cáo, được đề cập trong bài viết về font chữ và hình ảnh khi xuất báo cáo. Quy trình xác thực, nơi các bước hoàn tất này được kiểm tra đối chiếu với các tiêu chuẩn, được trình bày trong hướng dẫn về xác thực PDF/A và PDF/UA. Cả hai đều liên kết chặt chẽ với công việc tạo tập con và tuân thủ tiêu chuẩn được mô tả ở đây, vốn được đi kèm như một phần của HotPDF Component dành cho Delphi và C++Builder bên cạnh các API tải, chỉnh sửa, mã hóa và ký số được đề cập ở các bài viết khác trên blog này.