Bài viết kỹ thuật

PDFlibPas: Dự phòng phông tự động cho CJK và Emoji

PDFlibPas giải quyết các ký tự mà phông đã chọn không thể vẽ được bằng cách tìm trong một chuỗi dự phòng gồm các mặt chữ đã cài đặt, theo từng cụm một, đồng thời vẫn giữ nguyên thứ tự tạo hình và thứ tự chạy hai chiều. Bạn bật tính năng này bằng SetAutomaticFontFallback, mở rộng chuỗi dự phòng bằng AddFontFallback, và chỉ những phông dự phòng thực sự được dùng để xuất ra mới được nhúng vào tệp

Vấn đề mà tính năng này giải quyết là điều mà mọi công cụ tạo tài liệu đều gặp phải ngay lần đầu tiên tên khách hàng xuất hiện bằng một hệ chữ viết mà phông mẫu chưa từng tính đến. Lỗi này diễn ra âm thầm, và chính điều đó khiến nó tốn kém

Vì sao văn bản không được hỗ trợ lại biến mất thay vì báo lỗi?

Vì PDF không có khái niệm về một phông không thể vẽ được một ký tự. Phông đơn giản ánh xạ mã byte sang tên glyph thông qua một bảng mã; phông tổng hợp ánh xạ mã qua CMap sang chỉ số glyph. Yêu cầu một glyph mà mặt chữ không có sẽ nhận được chỉ số glyph bằng 0, .notdef, mà hầu hết các mặt chữ vẽ thành khoảng trống hoặc một ô rỗng. Tệp vẫn hợp lệ về mặt cấu trúc, toán tử văn bản vẫn đúng định dạng, và trang vẫn hiển thị bình thường. Chỉ là chỗ lẽ ra hiện tên lại để trống

Không có điều khoản nào trong ISO 32000-1 buộc trình tạo phải nhận biết điều này. Một công cụ ghi văn bản mà không kiểm tra độ phủ sẽ tạo ra một PDF hợp chuẩn về mặt kỹ thuật nhưng đã âm thầm mất nội dung, và sự mất mát đó chỉ lộ ra trên màn hình khách hàng nhiều tuần sau. Đây là lý do tính năng dự phòng và báo cáo glyph bị thiếu luôn đi cùng nhau: giải quyết những gì có thể giải quyết chỉ là một nửa công việc, còn báo cáo những gì không thể giải quyết là nửa còn lại

Dự phòng diễn ra theo từng cụm, không theo từng điểm mã

Độ chi tiết là yếu tố phân biệt một triển khai thực sự hoạt động với một triển khai chỉ trông có vẻ hợp lý. Văn bản không phải là một chuỗi các ký tự độc lập. Một âm tiết Devanagari, một emoji có bộ chỉnh tông màu da, một chữ cái cơ bản kèm dấu kết hợp: mỗi thứ đó là một cụm phải được hiển thị bởi một phông duy nhất, vì các quyết định tạo hình bên trong nó phụ thuộc vào các bảng dữ liệu của chính mặt chữ đó

PDFlibPas xử lý dự phòng theo cụm, nên một cụm được một mặt chữ dự phòng phủ sẽ được vẽ hoàn toàn bởi mặt chữ đó. Việc tách giữa cụm rồi vẽ một nửa từ phông chính, một nửa từ phông dự phòng sẽ tạo ra kết quả tuy có hiện diện về mặt kỹ thuật nhưng nhìn rõ là hỏng, và có thể coi là còn tệ hơn cả khoảng trống ban đầu. Thứ tự chạy cũng được giữ nguyên, nên một lượt dự phòng bên trong một đoạn chạy phải-sang-trái không làm đảo thứ tự văn bản xung quanh; cùng cơ chế này cũng là nền tảng cho bố cục dọc được mô tả trong bố cục viết dọc cho tiếng Nhật và tiếng Trung

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.SetAutomaticFontFallback(1);

    // Thứ tự tìm kiếm: kết quả khớp đầu tiên thắng, nên đặt mặt chữ bao quát nhất ở cuối
    Lib.AddFontFallback('Microsoft YaHei');   // Tiếng Trung giản thể
    Lib.AddFontFallback('Meiryo');            // Tiếng Nhật
    Lib.AddFontFallback('Segoe UI Symbol');
    Lib.AddFontFallback('Segoe UI Emoji');

    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);

    Lib.AddTrueTypeFont('Arial', 1);          // 1 = nhúng mặt chữ
    Lib.SetTextSize(11);
    Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
    Lib.DrawText(72, 700, 'Delivery status: on time');

    Lib.SaveToFile('invoice.pdf');
  finally
    Lib.Free;
  end;
end;

Hãy sắp xếp chuỗi dự phòng một cách có chủ đích. Việc xử lý sẽ chọn mặt chữ đầu tiên phủ được cụm đó, nên nếu đặt một phông pan-Unicode bao quát lên đầu, nó sẽ thắng gần như mọi trường hợp và các mặt chữ theo hệ chữ viết cụ thể mà bạn đã chọn kỹ càng sẽ không bao giờ được dùng đến. Hãy đặt các mặt chữ cụ thể lên trước và phông bao quát chung ở cuối

Báo cáo hay hủy bỏ: bạn muốn kiểu lỗi nào?

SetMissingGlyphPolicy nhận PDF_MISSING_GLYPH_REPORT, giá trị mặc định tương thích ngược, hoặc PDF_MISSING_GLYPH_ABORT. Theo chính sách báo cáo, thao tác văn bản vẫn tiếp tục, các điểm mã không giải quyết được bị bỏ qua như trước, và mỗi điểm mã đó được ghi lại. Theo chính sách hủy bỏ, thao tác văn bản bị từ chối trước khi bất kỳ nội dung nào được ghi ra, và LastErrorCode được đặt thành 521

Hãy chọn dựa trên mục đích của tài liệu. Một lô báo cáo nội bộ nên tiếp tục hiển thị và ghi log lại các chỗ thiếu, vì một báo cáo hơi thiếu sót hôm nay vẫn còn tốt hơn là không có báo cáo nào. Một hợp đồng có giá trị pháp lý, một hóa đơn, hay bất cứ thứ gì có ghi tên đều nên dùng chính sách hủy bỏ, vì một ký tự bị âm thầm bỏ sót trong tên một bên là lỗi mà bạn muốn phát hiện ngay trong quy trình nội bộ của mình chứ không phải trong một vụ tranh chấp. Chính sách hủy bỏ thất bại trước khi ghi ra, nên không để lại luồng nội dung dang dở nào

var
  Lib: TPDFlib;
  Report: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
    // ... xây dựng tài liệu ...

    if Lib.DrawText(72, 660, CustomerName) <> 1 then
      if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
      begin
        Report := Lib.GetMissingGlyphReportJSON;
        // {"valid":false,"policy":1,"eventCount":1,"events":[
        //   {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
        //    "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
        //    "fontType":"TrueType","operation":"DrawText"}]}
        EscalateToOperator(Report);
      end;
  finally
    Lib.Free;
  end;
end;

Báo cáo được thiết kế có chủ đích để máy đọc được và có giới hạn rõ ràng. Mỗi sự kiện mang theo số trang, chỉ số UTF-16 bên trong chuỗi, điểm mã ở cả dạng số lẫn dạng U+XXXX, phông đã được chọn, loại phông đó và thao tác gặp sự cố, nhờ đó một ticket hỗ trợ có thể nêu đích danh ký tự gặp lỗi thay vì chỉ mô tả triệu chứng. Bộ theo dõi giữ lại 256 sự kiện gần nhất, đủ để chẩn đoán một tài liệu và đủ nhỏ để một lượt chạy bất thường không thể biến việc chẩn đoán thành vấn đề bộ nhớ

Đo lường và vẽ phải thống nhất với nhau

Việc đo chiều rộng sử dụng cùng các quyết định dự phòng theo cụm như khi vẽ. Điều này nghe có vẻ hiển nhiên, nhưng lại chính là điểm mà hầu hết các lớp dự phòng tự viết mắc sai lầm: họ chỉ vá đường vẽ, còn phần đo lường vẫn dựa vào phông chính, khiến mọi hộp văn bản, căn lề phải và cột bảng cuối cùng đều được tính từ các chiều rộng không khớp với những gì thực sự được vẽ ra

Vì cả hai đường xử lý đều dùng chung cách giải quyết dự phòng, một chuỗi được đo trước khi vẽ sẽ chiếm đúng chiều rộng đã đo, kể cả các đoạn chạy dự phòng. Đó là điều khiến việc bật dự phòng trên toàn cục trở nên an toàn, thay vì chỉ bật ở những chỗ bạn đã kiểm tra thủ công

Chỉ những gì bạn thực sự dùng mới được nhúng

Các phông dự phòng được nhúng theo kiểu lazy: một mặt chữ trong chuỗi mà chưa từng giải quyết cụm nào sẽ không đóng góp gì vào đầu ra. Một tài liệu chứa một ký tự Trung Quốc và 5.000 ký tự Latin sẽ không mang theo cả một mặt chữ CJK đầy đủ; nó chỉ mang những gì bước tạo tập con tạo ra cho riêng glyph đó, đúng như hành vi được mô tả trong tối ưu dung lượng tệp và tạo tập con phông

Chính cơ chế lazy đó khiến việc cấu hình một chuỗi dự phòng rộng trở nên ít tốn kém. Hãy đăng ký các mặt chữ mà tập tài liệu của bạn có thể cần cho mọi locale bạn phục vụ, và mỗi PDF riêng lẻ chỉ phải trả giá cho những gì nó thực sự dùng. Đối với các tài liệu bạn không tự tạo ra, nơi các mặt chữ còn thiếu đã nằm sẵn trong một tệp có sẵn, hướng khắc phục sẽ khác và được trình bày trong nhúng các phông còn thiếu vào một PDF có sẵn

Có một lưu ý triển khai đáng nói rõ ràng: dự phòng chỉ hoạt động dựa trên các mặt chữ đã cài đặt trên máy chạy mã của bạn. Một máy chủ không cài phông CJK sẽ không có gì để dự phòng, và báo cáo sẽ cho bạn biết điều đó ngay từ tài liệu đầu tiên chứ không phải sau khiếu nại đầu tiên. Hãy đóng gói kèm các phông bạn phụ thuộc, và xác nhận giấy phép cho việc nhúng chúng

PDFlibPas là thư viện PDF cho Delphi, C++Builder và Lazarus, đi kèm giao diện DLL và ActiveX tương ứng, nên các API dự phòng và báo cáo glyph bị thiếu cũng khả dụng cho các bên gọi không dùng Pascal. Tài liệu đầy đủ có tại trang thư viện PDF PDFlibPas cho Delphi