HotXLS, thành phần Excel cho Delphi và C++Builder, cắt giảm kích thước font PDF được nhúng thông qua kỹ thuật cắt tập con font TrueType: tại thời điểm xuất PDF, nó gọi hàm CreateFontPackage của thư viện hệ thống Windows fontsub.dll để dựng lại một font TrueType được nhúng chỉ quanh những code point Unicode mà một worksheet thực sự đã dùng, thay vì gửi kèm cả file kiểu chữ. Một báo cáo với hai trăm hàng tên sản phẩm tiếng Trung có thể chỉ cần vài trăm ký tự Hán khác biệt, thế nhưng các font CJK mà Windows đi kèm thường xuyên nặng từ 5 đến 20 MB mỗi font. Nhúng nguyên cả font, và riêng font đó có thể nặng hơn mọi đối tượng khác trong PDF cộng lại
fontsub.dll không phải là một thư viện mà hầu hết lập trình viên Delphi từng nghe đến, và có lý do cho điều đó: Microsoft phát hành nó như một DLL tiện ích nhỏ, tài liệu thưa thớt, thay vì một API Win32 nổi bật. HotXLS coi nó như một khả năng tùy chọn, không phải một phụ thuộc bắt buộc, nên cách bộ xuất nạp nó, gọi nó, và rơi về phương án dự phòng khi nó vắng mặt nói lên nhiều về lập trình Windows phòng thủ chẳng kém gì về định dạng font, và cả hai nửa câu chuyện đó đều đáng để đi qua
Vì sao văn bản Unicode làm phình to một bản xuất PDF của HotXLS?
Bộ xuất PDF của HotXLS chỉ tìm đến một font TrueType nhúng khi văn bản worksheet nằm ngoài WinAnsi, và ở lại trên họ Helvetica tích hợp sẵn trong mọi trường hợp còn lại, đường xử lý mặc định được nói đến sâu trong bài hướng dẫn xuất worksheet ra PDF. WinAnsi bao phủ văn bản Tây Âu đủ tốt để rất nhiều workbook không bao giờ kích hoạt việc nhúng font: PDF chỉ tham chiếu Helvetica bằng tên và trình đọc tự cung cấp nó cục bộ, nên file vẫn nhỏ. Ngay khi một ô chứa thứ gì đó WinAnsi không thể biểu diễn, một tên sản phẩm tiếng Trung, một ghi chú tiếng Hàn, một ký hiệu lạc lõng trong một comment, bộ xuất phải nhúng một chương trình font thật sự, vì một trình đọc PDF không có nguồn glyph dự phòng nào cho các ký tự nằm ngoài 14 font chuẩn
HotXLS tự động định vị font đó, quét thư mục Fonts của Windows tìm một danh sách ngắn các ứng viên đã cài đặt, gồm cả các kiểu chữ hỗ trợ CJK mà Windows đi kèm để render tiếng Trung và tiếng Hàn, trừ khi thuộc tính UnicodeFontFile của bộ xuất đã trỏ sẵn vào một file cụ thể, và bất kể font nào nó chọn trúng đều được nhúng nguyên vẹn trước khi việc cắt tập con chạy. Yêu cầu nhúng đó là đặc thù của PDF: các đường xuất RTF và HTML của HotXLS giữ nguyên văn bản Unicode bằng cách escape các code point vào luồng byte thay vì gửi kèm một chương trình font, đó là lý do vấn đề kích thước bài viết này nói đến không có phiên bản tương đương trên hai định dạng đó
uses
lxHandle, lxPDF;
var
Book: TXLSWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSWorkbook.Create;
try
Book.Open('catalog-cn.xlsx');
Exporter := TXLSPDFExport.Create;
try
// Optional: pin a specific CJK-capable font instead of the
// exporter's automatic Windows\Fonts scan.
Exporter.UnicodeFontFile := 'C:\Windows\Fonts\simhei.ttf';
Exporter.SaveAsPDF(Book.ActiveSheet, 'catalog-cn.pdf');
finally
Exporter.Free;
end;
finally
Book.Free;
end;
end;
fontsub.dll là gì, và vì sao không tự viết một bộ cắt tập con từ đầu?
fontsub.dll là một thư viện hệ thống Windows nhỏ, đi kèm từ Windows XP, phơi bày một hàm liên quan ở đây: CreateFontPackage. Đưa cho nó các byte của một font TrueType nguồn và một danh sách các code point Unicode cần giữ lại, nó trả về một font tối giản vẫn thỏa mãn mọi ràng buộc định dạng font: chỉ số glyph được đánh số lại, glyf và loca được dựng lại chỉ quanh các đường nét được giữ lại, hmtx và cmap được viết lại cho khớp. HotXLS khai báo kiểu con trỏ hàm trực tiếp theo đúng hợp đồng đó
const
TTFCFP_FLAGS_SUBSET = 1;
TTFMFP_SUBSET = 0;
TTFCFP_MS_PLATFORMID = 3;
TTFCFP_UNICODE_CHAR_SET = 1;
type
TCreateFontPackage = function(puchSrcBuffer: Pointer; ulSrcBufferSize: Cardinal;
var puchFontPackageBuffer: PAnsiChar; var pulFontPackageBufferSize: Cardinal;
var pulBytesWritten: Cardinal; usFlags, usTTCIndex, usSubsetFormat,
usSubsetLanguage, usSubsetPlatform, usSubsetEncoding: Word;
pusSubsetKeepList: PWordArray; usSubsetKeepListCount: Word;
lpfnAllocate, lpfnReAllocate, lpfnFree, reserved: Pointer): Cardinal; cdecl;
Tự viết tay công việc của CreateFontPackage thay vì gọi nó sẽ có nghĩa là phải triển khai một bộ cắt tập con TrueType đúng đắn: duyệt các glyph composite để kéo vào mọi glyph thành phần mà một glyph được giữ lại tham chiếu tới, dựng lại các offset loca sau khi các đường nét bị loại bỏ, tôn trọng các bit quyền nhúng trong bảng OS/2 của một font, và làm đúng tất cả những điều đó trên bất kỳ font kỳ quặc nào mà máy của khách hàng tình cờ đã cài. Microsoft đã giải quyết sẵn vấn đề đó và gửi kèm giải pháp như một phần của chính Windows, nên việc gọi một DLL hệ thống mà Microsoft duy trì, test đối chiếu với chính stack render font của họ, và phân phối miễn phí đến mọi máy chỉ tốn của HotXLS một lần nạp động và một con trỏ hàm; tự triển khai lại cùng logic đó sẽ có nghĩa là phải sở hữu một bộ phân tích cho một định dạng nhị phân với hàng thập kỷ trường hợp biên, cho một tính năng chỉ quan trọng khi một font tình cờ lớn
Dựng danh sách giữ lại từ các glyph thực sự đã render
HotXLS dựng danh sách giữ lại cho việc cắt tập con từ một bảng ánh xạ nó vốn đã duy trì vì một lý do khác, nên việc kế toán này không tốn thêm gì cả. Mỗi lần code render trang vẽ một ký tự cần font Unicode được nhúng, nó tra cứu chỉ số glyph của ký tự đó và ghi lại cặp đó trong FUnicodeGlyphMap, một bảng glyph-sang-codepoint cũng điều khiển CMap ToUnicode của PDF để việc copy-paste ra khỏi tài liệu hoàn tất trả về văn bản gốc thay vì ID glyph thô. Đến lúc các content stream của trang hoàn tất, bảng đó đã liệt kê đúng tập hợp các code point Unicode mà tài liệu đã dùng, không nhiều hơn không ít hơn
var
keepList: array of Word;
keepCount, i: Integer;
codePoint: LongWord;
begin
SetLength(keepList, FUnicodeGlyphMap.Count);
keepCount := 0;
for i := 0 to FUnicodeGlyphMap.Count - 1 do
begin
codePoint := LongWord(StrToIntDef('$' + FUnicodeGlyphMap.ValueFromIndex[i], 0));
if codePoint > 0 then
begin
keepList[keepCount] := Word(codePoint);
Inc(keepCount);
end;
end;
end;
Tại thời điểm hoàn tất, HotXLS duyệt lại chính bảng đó lần thứ hai để dựng danh sách giữ lại mà CreateFontPackage mong đợi, một mảng thuần túy các code point Unicode cần giữ lại dưới dạng 16-bit mà tham số danh sách giữ lại của API yêu cầu. Vì tham số đó là một mảng các word 16-bit, nó địa chỉ hóa gọn gàng Mặt phẳng Đa ngôn ngữ Cơ bản (BMP), bao phủ văn bản CJK thông thường, Cyrillic, Hy Lạp, và Ả Rập mà không gặp rắc rối; một worksheet dựa vào các ký tự thuộc mặt phẳng bổ sung, một số emoji hoặc chữ viết lịch sử hiếm, nằm ngoài những gì một mục danh sách giữ lại duy nhất có thể đặt tên trực tiếp, đây là một ranh giới đáng biết chứ không phải một khiếm khuyết, vì đại đa số bảng tính doanh nghiệp nặng Unicode không bao giờ chạm đến mặt phẳng đó ngay từ đầu
Điều gì xảy ra khi fontsub.dll bị thiếu?
HotXLS không bao giờ giả định fontsub.dll có mặt, và việc xuất PDF không bao giờ thất bại vì nó vắng mặt. Thư viện được nạp động vào đúng thời điểm cần một tập con, bằng SafeLoadLibrary và GetProcAddress thay vì một import tĩnh, chính xác là vì fontsub.dll không phải một API công khai có tài liệu, được đảm bảo có mặt theo cách kernel32.dll được đảm bảo: nó là công cụ nhúng font đi kèm, và không có gì trong hợp đồng của Microsoft hứa hẹn nó tồn tại trên mọi SKU, mọi nhánh cập nhật dịch vụ, hay mọi lớp tương thích cố mô phỏng Windows
var
hFontSub: HMODULE;
CreateFontPackage: TCreateFontPackage;
begin
hFontSub := SafeLoadLibrary('FontSub.dll');
if hFontSub = 0 then
Exit; // no subsetting available - keep the full embedded font
try
@CreateFontPackage := GetProcAddress(hFontSub, 'CreateFontPackage');
if not Assigned(CreateFontPackage) then
Exit;
// ... call CreateFontPackage, check its return code ...
finally
FreeLibrary(hFontSub);
end;
end;
Mọi đường lỗi đều gấp lại về cùng một kết quả. Một DLL bị thiếu, một export bị thiếu, một mã trả về khác không, hay một font mà bảng OS/2 của nó cấm việc cắt tập con thông qua các bit quyền nhúng, HotXLS chỉ giữ nguyên font đầy đủ nó đã nhúng sẵn và tiếp tục. Không có gì ném lỗi, không có gì hủy việc xuất, và code gọi không bao giờ phải bọc một tối ưu hóa font trong xử lý ngoại lệ của riêng nó; PDF được xuất ra hợp lệ trong cả hai trường hợp, và biến số duy nhất là liệu nó có kết thúc nhỏ gọn hay hơi lớn hơn
PDF thực sự nhỏ đi bao nhiêu?
Việc cắt tập con font TrueType của HotXLS thường thu nhỏ bản PDF xuất ra của một worksheet nặng Unicode xuống còn khoảng một phần hai mươi đến một phần tám kích thước chưa cắt tập con, một mức giảm 8 đến 20 lần mà quy mô của nó theo sát mức độ một tài liệu cho trước thực sự chạm đến bao nhiêu phần của một font đầy đủ: một đơn đặt hàng dựng quanh vài trăm ký tự Trung Quốc khác biệt chỉ giữ lại vài trăm glyph đó trong số hàng chục nghìn mà một kiểu chữ CJK đi kèm, trong khi một sheet trải rộng trên một hỗn hợp ký tự phong phú hơn giữ lại tỉ lệ nhiều hơn tương ứng. HotXLS xếp thêm một lượt nén Flate lên trên các byte font đã cắt tập con trước khi ghi chúng vào stream /FontFile2 của PDF, cùng phép nén mà phần còn lại của content stream tài liệu đã trải qua, và không điều nào trong số đó đòi hỏi thêm gì từ code gọi: một worksheet không bao giờ rời khỏi WinAnsi không bao giờ chạm đến đường xử lý này và tiếp tục xuất qua Helvetica thuần túy, trong khi một worksheet kích hoạt đường font Unicode nhận được việc cắt tập con tự động, không có thuộc tính nào cần đặt và không có lệnh gọi riêng nào cần thực hiện, và thuộc tính duy nhất liên quan, UnicodeFontFile, chỉ chọn font nào được nhúng và cắt tập con, không quyết định việc cắt tập con có xảy ra hay không
Việc cắt tập con font là một chi tiết bên trong bề mặt xuất PDF rộng hơn của HotXLS Delphi Excel Component, cùng với phân trang, metadata in worksheet, và các đường xuất CSV, HTML, và RTF đi kèm với nó