HotXLS คอมโพเนนต์ Excel สำหรับ Delphi และ C++Builder ลดขนาดฟอนต์ PDF ที่ฝังไว้ผ่านการทำ subset ฟอนต์ TrueType ในเวลา export PDF มันเรียกฟังก์ชัน CreateFontPackage จาก fontsub.dll ซึ่งเป็นไลบรารีระบบของ Windows เพื่อสร้างฟอนต์ TrueType ที่ฝังไว้ใหม่โดยรอบเฉพาะ Unicode code point ที่เวิร์กชีตใช้จริงเท่านั้น แทนที่จะส่งไฟล์ typeface ทั้งไฟล์ไปด้วย รายงานที่มีสองร้อยแถวของชื่อสินค้าภาษาจีนอาจต้องการอักษรฮั่นที่ต่างกันแค่ไม่กี่ร้อยตัว แต่ฟอนต์ CJK ที่ Windows แถมมามักมีขนาด 5 ถึง 20 MB ต่อไฟล์ ฝังทั้งไฟล์เข้าไปแล้ว ฟอนต์เพียงอย่างเดียวก็อาจหนักกว่า object อื่นทั้งหมดใน PDF รวมกัน
fontsub.dll ไม่ใช่ไลบรารีที่นักพัฒนา Delphi ส่วนใหญ่เคยได้ยินชื่อ และมีเหตุผลอยู่ Microsoft แถมมันมาในฐานะ utility DLL เล็กๆ ที่มีเอกสารประกอบน้อยมาก แทนที่จะเป็น Win32 API หลัก HotXLS ปฏิบัติต่อมันเป็นความสามารถแบบเสริม ไม่ใช่ dependency ที่จำเป็น ดังนั้นวิธีที่ตัว exporter โหลดมัน เรียกมัน และ fallback เมื่อมันหายไป จึงบอกอะไรได้มากพอๆ กันทั้งเรื่องการเขียนโปรแกรม Windows แบบป้องกันตัว และเรื่องฟอร์แมตฟอนต์ ทั้งสองด้านของเรื่องนี้ควรค่าแก่การเดินผ่าน
ทำไมข้อความ Unicode ถึงทำให้ PDF export ของ HotXLS บวมขึ้น
ตัว exporter PDF ของ HotXLS จะหันไปใช้ฟอนต์ TrueType ที่ฝังไว้ก็ต่อเมื่อข้อความในเวิร์กชีตอยู่นอกเหนือ WinAnsi เท่านั้น และคงอยู่กับตระกูล Helvetica ในตัวตลอดเวลาที่เหลือ ซึ่งเป็นเส้นทางเริ่มต้นที่คู่มือการ export เวิร์กชีตเป็น PDFครอบคลุมไว้อย่างละเอียด WinAnsi ครอบคลุมข้อความยุโรปตะวันตกได้ดีพอที่ workbook จำนวนมากไม่เคยกระตุ้นการฝังฟอนต์เลย PDF แค่อ้างอิง Helvetica ด้วยชื่อ และตัวอ่านก็จัดหามันเองในเครื่อง ดังนั้นไฟล์จึงยังคงเล็ก ทันทีที่เซลล์หนึ่งมีอะไรที่ WinAnsi แสดงไม่ได้ ชื่อสินค้าภาษาจีน บันทึกภาษาเกาหลี สัญลักษณ์แปลกๆ ในความคิดเห็น ตัว exporter ต้องฝังโปรแกรมฟอนต์จริงเข้าไป เพราะ PDF reader ไม่มีแหล่ง glyph สำรองสำหรับตัวอักษรนอกเหนือฟอนต์มาตรฐาน 14 ตัว
HotXLS ค้นหาฟอนต์นั้นโดยอัตโนมัติ สแกนโฟลเดอร์ Fonts ของ Windows หารายชื่อสั้นๆ ของฟอนต์ที่ติดตั้งไว้ รวมถึง typeface ที่รองรับ CJK ที่ Windows แถมมาสำหรับการ render ภาษาจีนและเกาหลี เว้นแต่ property UnicodeFontFile ของ exporter จะชี้ไปยังไฟล์เฉพาะเจาะจงอยู่แล้ว และไม่ว่ามันจะลงเอยที่ฟอนต์ไหน ฟอนต์นั้นจะถูกฝังทั้งไฟล์ก่อนที่การทำ subset จะรันเลย ข้อกำหนดการฝังนี้เฉพาะเจาะจงกับ PDF เท่านั้น เส้นทางexport แบบ RTF และ HTMLของ HotXLS รักษาข้อความ Unicode ให้คงเดิมด้วยการ escape code point เข้าไปใน byte stream แทนที่จะส่งโปรแกรมฟอนต์ไปด้วย ซึ่งเป็นเหตุผลที่ปัญหาขนาดที่บทความนี้ครอบคลุมไม่มีสิ่งที่เทียบเท่าในสองฟอร์แมตนั้น
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 คืออะไร และทำไมไม่เขียนตัวทำ subset เองตั้งแต่ต้น
fontsub.dll เป็นไลบรารีระบบเล็กๆ ของ Windows แถมมาตั้งแต่ Windows XP ที่เปิดฟังก์ชันเดียวซึ่งเกี่ยวข้องตรงนี้ คือ CreateFontPackage ส่งไบต์ของฟอนต์ TrueType ต้นทางและรายการ Unicode code point ที่จะเก็บไว้เข้าไป แล้วมันจะส่งฟอนต์ที่เล็กที่สุดกลับมาซึ่งยังตอบสนองข้อจำกัดของฟอร์แมตฟอนต์ทุกข้อ glyph index ถูกกำหนดหมายเลขใหม่ glyf และ loca ถูกสร้างใหม่รอบเฉพาะ outline ที่เก็บไว้ hmtx และ cmap ถูกเขียนใหม่ให้ตรงกัน HotXLS ประกาศ function pointer type ตรงตามสัญญานั้นโดยตรง
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;
การเขียนงานของ CreateFontPackage ด้วยมือแทนการเรียกมัน จะหมายถึงการ implement ตัวทำ subset TrueType ที่ถูกต้อง เดินผ่าน composite glyph เพื่อดึงทุก component glyph ที่ glyph ที่เก็บไว้อ้างอิงถึง สร้าง offset ของ loca ใหม่หลังจาก outline ถูกทิ้งไป เคารพบิตสิทธิ์การฝังใน table OS/2 ของฟอนต์ และทำทั้งหมดนี้ให้ถูกต้องข้ามฟอนต์แปลกๆ ใดๆ ก็ตามที่บังเอิญติดตั้งอยู่ในเครื่องของลูกค้า Microsoft แก้ปัญหานั้นไปแล้วและแถมทางแก้มาเป็นส่วนหนึ่งของ Windows เอง ดังนั้นการเรียก system DLL ที่พวกเขาดูแล ทดสอบกับ stack การ render ฟอนต์ของตัวเอง และแจกจ่ายไปยังทุกเครื่องฟรี มีต้นทุนแค่การโหลดแบบ dynamic และ function pointer สำหรับ HotXLS การ implement logic เดียวกันซ้ำจะหมายถึงการเป็นเจ้าของ parser สำหรับฟอร์แมต binary ที่มี edge case มานานหลายทศวรรษ สำหรับฟีเจอร์ที่สำคัญก็ต่อเมื่อฟอนต์บังเอิญมีขนาดใหญ่เท่านั้น
การสร้าง keep-list จาก glyph ที่ render จริง
HotXLS สร้าง keep-list สำหรับการทำ subset จาก map ที่มันดูแลอยู่แล้วด้วยเหตุผลอื่น ดังนั้นการนับจึงไม่มีต้นทุนเพิ่มเติมเลย ทุกครั้งที่โค้ด render หน้าวาดตัวอักษรที่ต้องการฟอนต์ Unicode ที่ฝังไว้ มันจะค้นหา glyph index ของตัวอักษรนั้นและบันทึกคู่นั้นไว้ใน FUnicodeGlyphMap ซึ่งเป็นตาราง glyph-ไปยัง-codepoint ที่ยังขับเคลื่อน CMap ToUnicode ของ PDF ด้วย เพื่อให้การ copy-and-paste จากเอกสารที่เสร็จแล้วคืนข้อความต้นฉบับกลับมา แทนที่จะเป็น glyph ID ดิบ เมื่อถึงเวลาที่ content stream ของหน้าเสร็จสิ้น map นั้นก็ลงรายการชุด Unicode code point ที่เอกสารใช้แล้วพอดีเป๊ะ ไม่มากไม่น้อยไปกว่านั้น
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;
ในเวลา finalize HotXLS เดินผ่าน map เดียวกันนั้นเป็นครั้งที่สอง เพื่อสร้าง keep-list ที่ CreateFontPackage คาดหวัง เป็น array ธรรมดาของ Unicode code point ที่จะเก็บไว้ในรูปแบบ 16 บิตที่อาร์กิวเมนต์ keep-list ของ API ต้องการ เพราะอาร์กิวเมนต์นั้นเป็น array ของคำ 16 บิต มันจึงระบุ Basic Multilingual Plane ได้อย่างสะอาด ซึ่งครอบคลุมข้อความ CJK, ซีริลลิก, กรีก และอาหรับทั่วไปโดยไม่มีความซับซ้อน เวิร์กชีตที่พึ่งพาตัวอักษรใน supplementary plane เช่น อีโมจิบางตัวหรือสคริปต์ประวัติศาสตร์ที่หายาก จะอยู่นอกเหนือสิ่งที่ keep-list entry เดียวจะระบุได้โดยตรง ซึ่งเป็นขอบเขตที่ควรรู้ไว้มากกว่าจะเป็นข้อบกพร่อง เพราะสเปรดชีตธุรกิจที่ใช้ Unicode หนักส่วนใหญ่ไม่เคยเข้าใกล้ plane นั้นเลยตั้งแต่แรก
เกิดอะไรขึ้นเมื่อ fontsub.dll หายไป
HotXLS ไม่เคยสมมติว่า fontsub.dll มีอยู่ และการ export PDF ก็ไม่เคยล้มเหลวเพราะมันไม่มี ไลบรารีถูกโหลดแบบ dynamic ในขณะที่ต้องการทำ subset ด้วย SafeLoadLibrary และ GetProcAddress แทนที่จะเป็น static import โดยเฉพาะเพราะ fontsub.dll ไม่ใช่ public API ที่มีเอกสารและรับประกันว่ามีอยู่แบบเดียวกับ kernel32.dll มันเป็นเครื่องมือฝังฟอนต์ที่มัดมาให้ และไม่มีอะไรในสัญญาของ Microsoft ที่รับประกันว่ามันจะอยู่รอดในทุก SKU ทุก servicing branch หรือทุก compatibility layer ที่พยายามจำลอง 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;
ทุกเส้นทางความล้มเหลวพับกลับไปยังผลลัพธ์เดียวกัน DLL หายไป, export หายไป, return code ไม่เป็นศูนย์ หรือฟอนต์ที่ table OS/2 ของมันห้ามการทำ subset ผ่านบิตสิทธิ์การฝัง HotXLS แค่คงฟอนต์เต็มที่มันฝังไว้แล้วและทำงานต่อไป ไม่มีอะไร throw ไม่มีอะไรยกเลิกการ export และโค้ดที่เรียกใช้ไม่เคยต้องห่อการปรับแต่งฟอนต์ด้วย exception handling ของตัวเอง PDF ที่ export ออกมาถูกต้องไม่ว่ากรณีไหน ตัวแปรเดียวคือมันจะลงเอยเล็กหรือใหญ่กว่าเล็กน้อยเท่านั้น
PDF เล็กลงได้มากแค่ไหนจริงๆ
การทำ subset ฟอนต์ TrueType ของ HotXLS มักลด PDF ที่ export จากเวิร์กชีตที่ใช้ Unicode หนักให้เหลือประมาณหนึ่งในยี่สิบถึงหนึ่งในแปดของขนาดที่ไม่ได้ subset ลดลง 8 ถึง 20 เท่า ซึ่งขนาดของมันแปรผันตามว่าเอกสารหนึ่งๆ แตะฟอนต์เต็มมากแค่ไหนจริงๆ ใบสั่งซื้อที่สร้างรอบตัวอักษรจีนที่ต่างกันไม่กี่ร้อยตัว จะเก็บแค่ไม่กี่ร้อย glyph นั้นจากหลายหมื่นตัวที่ typeface CJK แถมมา ในขณะที่ชีตที่ครอบคลุมตัวอักษรหลากหลายกว่าจะเก็บมากขึ้นตามสัดส่วน HotXLS ซ้อนรอบการบีบอัด Flate เพิ่มเติมทับไบต์ฟอนต์ที่ subset แล้วก่อนเขียนมันเข้าสตรีม /FontFile2 ของ PDF ซึ่งเป็นการบีบอัดแบบเดียวกับที่ content stream อื่นๆ ของเอกสารผ่านอยู่แล้ว และไม่มีสิ่งใดในนี้ที่ขอสิ่งเพิ่มเติมจากโค้ดที่เรียกใช้เลย เวิร์กชีตที่ไม่เคยออกจาก WinAnsi จะไม่แตะเส้นทางนี้เลยและยัง export ผ่าน Helvetica ธรรมดาต่อไป ในขณะที่เวิร์กชีตที่กระตุ้นเส้นทางฟอนต์ Unicode จะได้การทำ subset โดยอัตโนมัติ ไม่มี property ให้ตั้งค่าและไม่มีการเรียกแยกต่างหากให้ทำ และ property เดียวที่เกี่ยวข้องคือ UnicodeFontFile เลือกแค่ว่าฟอนต์ไหนจะถูกฝังและ subset เท่านั้น ไม่ใช่ว่าการทำ subset จะเกิดขึ้นหรือไม่
การทำ subset ฟอนต์เป็นรายละเอียดหนึ่งภายในพื้นผิว PDF export ที่กว้างกว่าของHotXLS Delphi Excel Component ควบคู่ไปกับการแบ่งหน้า, ข้อมูล meta การพิมพ์เวิร์กชีต และเส้นทาง export แบบ CSV, HTML และ RTF ที่มันมาพร้อมด้วย