เมื่อ PDF ไม่ได้ฝังฟอนต์มา component ของ HotPDF จะ render ข้อความนั้นด้วยฟอนต์ Windows ที่ติดตั้งไว้ซึ่งถูกเลือกโดย HPDFMapBaseFontToSystem: มันถอดรหัสชื่อ /BaseFont ตัดส่วนสไตล์ทิ้ง ลองการสะกดหลายแบบจน GDI ยืนยันว่าตระกูลฟอนต์ติดตั้งอยู่ วัดความกว้างที่หายไปของฟอนต์มาตรฐาน 14 ตัวจากฟอนต์ที่ metric เข้ากันได้ และแปลงรหัสไบต์เดียวเป็น Unicode ก่อนวาด ทุกขั้นตอนพวกนี้เกิดขึ้นเพราะรุ่นหลง ๆ พังกับไฟล์จริง renderer หน้า RenderLoadedPageToBitmapจัดการ program ที่ฝังมาได้ดี นี่คือเรื่องของฟอนต์ที่ไม่อยู่ในไฟล์เลย
ทำไม GDI ถึงวาด typeface ผิดแบบเงียบ ๆ ให้ฟอนต์ที่ไม่ฝังมา
GDI ไม่เคยรายงานว่าฟอนต์หาย: เอาชื่อตระกูลที่มันไม่รู้จักไปให้ CreateFontIndirect มันก็เงียบ ๆ เลือกตัวแทนมาให้ มักเป็น serif อีกตระกูลที่ไม่มีน้ำหนัก bold renderer รุ่นแรกส่งชื่อจาก PDF ไปแทบตรงตัว TimesNewRoman,Bold, TimesNewRomanPS-BoldMT กับ SegoeUI-Semibold จึง match ไม่เจอสักตัวและออกมาด้วยอะไรก็ตามที่ GDI เลือกให้ ชื่อยังแย่ไปกว่านั้นได้ ISO 32000-1 §7.3.5 ให้ชื่อเขียนไบต์ใดก็ได้เป็น #xx และผู้ผลิตฝั่ง CJK สะกดชื่อฟอนต์เป็น UTF-8 แบบ escape หรือไบต์ code page รุ่นเก่าเป็นเรื่องปกติ ก่อน v2.766.69 ตัว escape เองต่างหากที่กลายเป็นชื่อตระกูล HPDFMapBaseFontToSystem ตอนนี้ถอดรหัส escape ก่อน คืนลำดับไบต์ UTF-8 ที่ถูกต้องเป็นอักขระ และอ่านไบต์สูงอื่นด้วย code page ของระบบ
การตัดสไตล์ออกคือจุดที่ heuristic โดนงูตัด Arial,Bold ให้ Arial ด้วยเครื่องหมายจุลภาคที่ปิดตระกูลเสมอ แต่ hyphen ปิดเฉพาะเมื่อคำข้างหลังเป็นสไตล์: Bold, Italic, Oblique, Regular, Roman, Medium, Light, Black, Heavy, Semi, Demi, Thin, Extra, Ultra หรือ Condensed กฎนี้คง MS-Mincho ไว้ทั้งตัวแต่เปลี่ยน Calibri-Light เป็น Calibri HotPDF ลองการสะกดของตระกูลต่อ ด้วยการสะกดของตระกูลที่ตัด suffix PSMT, MT หรือ PS ออก ตั้งแต่ v2.768.18 การสะกดพวกนี้ครอบคลุมทุกทางเลือกของการเว้นวรรค ณ จุดที่คำจะเริ่มได้: หน้าตัวพิมพ์ใหญ่ที่ตามหลังตัวพิมพ์เล็ก (MyriadPro กลายเป็น Myriad Pro) ที่ตัวพิมพ์ใหญ่ตัวสุดท้ายของ run ที่ตามด้วยตัวพิมพ์เล็ก (UIGothic) และหลัง MS ขึ้นหน้า (MSPGothic) จากรูปเว้นวรรคเต็มลงไปถึงชื่อตามที่เขียนไว้ ผ่านสี่จุดไปแล้วจะลองแค่รูปเว้นวรรคเต็มกับชื่อติดกัน เว้นวรรคแบบมั่วไม่ได้ เพราะ Windows เก็บบางคำไว้ติดกัน: SimSun ติดตั้งอยู่ใต้การสะกดนั้นเป๊ะ ๆ ขณะที่ MicrosoftYaHei, MicrosoftJhengHei กับ MSPGothic เป็นของ Microsoft YaHei, Microsoft JhengHei และ MS PGothic ก่อน v2.768.18 mapper เว้นวรรคหน้าตัวพิมพ์ใหญ่ข้างในทุกตัว MicrosoftYaHei จึงถูกค้นเป็น Microsoft Ya Hei และไม่มีวันเจอ ผู้สมัครนับเป็นติดตั้งเมื่อ CreateFontIndirect ตามด้วย GetTextFace คืนชื่อที่ขอไว้ หรือตั้งแต่ v2.768.18 เมื่อตาราง name ของฟอนต์ที่ถูกเลือกบันทึกมันไว้เป็นตระกูล ชื่อเต็มหรือชื่อตระกูลแบบ typographic ในภาษาใดก็ได้ คำตอบถูกแคชต่อหนึ่งชื่อ เอกสารที่มีฟอนต์ไม่ติดตั้งจำนวนมากจึงไม่ต้องแหย่ Windows ทีละชื่อทุกหน้าอีกต่อไป
เพราะฟังก์ชัน mapping เป็น public ในยูนิต HPDFRenderFontMetrics รายงาน preflight จึงชี้ได้ว่าฟอนต์ที่ไม่ฝังแต่ละตัวจะ render ด้วยตระกูลติดตั้งไหน โดยใช้การ enumerate ฟอนต์ที่ THotPDF เปิดออกมาให้กับเอกสารที่โหลดไว้อยู่แล้ว:
uses
HPDFDoc, HPDFRenderFontMetrics;
procedure ListSystemFontMappings(Pdf: THotPDF; Log: TStrings);
var
Page, I: Integer;
Info: THPDFLoadedFontInfo;
begin
for Page := 0 to Pdf.LoadedPageCount - 1 do
for I := 0 to Pdf.GetLoadedFontCount(Page) - 1 do
if Pdf.GetLoadedFontInfo(Page, I, Info) and not Info.IsEmbedded then
Log.Add(Format('page %d /%s %s -> %s',
[Page + 1, string(Info.ResourceName), string(Info.FontName),
HPDFMapBaseFontToSystem(Info.FontName)]));
end;
HotPDF วัดฟอนต์มาตรฐาน 14 ตัวที่ไม่มี /Widths อย่างไร
HotPDF วัด advance ที่หายไปบนฟอนต์ติดตั้งที่มี metric เดียวกัน เพราะ ISO 32000-1 §9.6.2.2 อนุญาตให้ฟอนต์มาตรฐาน 14 ตัวตัด /Widths ทิ้งได้ และ library ไม่ได้แถมตาราง AFM มาให้ Arial แบก metric ของ Helvetica, Times New Roman แบก Times และ Courier New แบก Courier HPDFMeasureBaseFontWidths จึงสร้างตระกูลที่จับคู่กันที่ lfHeight = -1000 แล้วเรียก GetCharWidth32W ที่ความสูงนั้นผลลัพธ์อยู่ในหน่วย 1/1000 em ที่ความกว้างของ PDF ใช้อยู่แล้ว renderer แปลงรหัสแต่ละตัวเป็น Unicode ผ่าน /Encoding, /BaseEncoding กับ /Differences ก่อน โดยเริ่มต้นที่ StandardEncoding export ฝั่ง SVG กับการสกัดข้อความโดนกับดักอีกอัน: ฟอนต์ Type 1 มาตรฐานที่ไม่มี /Encoding เลยผลิต decoder ที่ไม่มีข้อมูล encoding ออกมา export ฝั่ง SVG จึงไม่เคยลงทะเบียนมันและความกว้างที่วัดได้ทั้งหมดถูกทิ้งไป การเติม StandardEncoding ที่นัยอยู่แล้วแก้ได้ ในเมื่อมันถูกติดตายว่าเป็น predefined encoding ส่วนถ้ารีดมันลงเส้นทางชื่อ CMap ทุกรหัสจะถอดเป็น 0 และความกว้างทุกตัวก็ตามไปเป็นแบบนั้น
Bold, italic กับ off-by-one ในธงของ font descriptor
entry /Flags ของ font descriptor นับ bit เริ่มจาก 1 ไม่ใช่ 0 ForceBold จึงเป็น bit 19 ($40000) และ Italic เป็น bit 7 ($40) ตาม ISO 32000-1 Table 123 โค้ดเก่าเทส $20000 ซึ่งเป็น bit 18 คือ SmallCap ความพลาดนี้รอดมาได้จาก v2.345.0 ถึง v2.766.53 เพราะ /FontDescriptor แทบเป็น indirect reference เสมอและตัวสร้างฟอนต์อ่านเฉพาะ object ตรง กิ่งธงทั้งกิ่งจึงไม่เคยรัน และความตานั้นเองที่มองข้าม /Widths 12 0 R ไปด้วย แล้วจัด layout ข้อความด้วย advance สำรอง 500 หน่วย พอ v2.766.53 เริ่ม resolve indirect reference ผ่าน renderer ตัว bit ก็ต้องถูกแก้ในการเปลี่ยนเดียวกัน ไม่งั้นฟอนต์ small-caps ทุกตัวจะ render เป็น bold ขึ้นมาทันตา:
const
// ISO 32000-1 Table 123 นับตำแหน่ง bit เริ่มจาก 1
FD_ITALIC = $00040; // bit 7
FD_SMALLCAP = $20000; // bit 18, ไม่ใช่น้ำหนัก
FD_FORCEBOLD = $40000; // bit 19
procedure ApplyDescriptorFlags(Flags: Integer; var LF: TLogFont);
begin
if (Flags and FD_FORCEBOLD) <> 0 then
LF.lfWeight := FW_BOLD;
if (Flags and FD_ITALIC) <> 0 then
LF.lfItalic := 1;
end;
ทำไมฟอนต์ CJK ที่มี CMap แบบ UCS2 ถึงได้ความกว้างผิด
ข้อความ CJK ที่มี predefined CMap แบบ UCS2 วาด glyph ถูกแต่ระยะห่างผิดเมื่อ renderer ใช้รหัสเป็น CID ตรง ๆ เพราะ /W ชี้ด้วย CID ไม่ใช่รหัสอักขระ กับ STSong-Light กับ UniGB-UCS2-H รหัสบังเอิญเท่ากับค่า Unicode GDI จึงวาดตัวอักษรถูกและบั๊กซ่อนอยู่ใน advance: ตัวอักษรพิมพ์เล็กมาเป็นรหัส 97 ขึ้นไป หลุดนอก entry ของ /W อย่าง [1 95 500] แล้วทุกตัวได้ความกว้างเริ่มต้นของ /DW คือ 1000 ตั้งแต่ v2.766.56 renderer ของ HotPDF อ่านรหัสผ่านช่วง codespace ของ CMap (ISO 32000-1 §9.7.6.2) แล้ว map เป็น CID ก่อนไปค้นความกว้าง ใช้เฉพาะตาราง UCS2 กับ UTF16 ที่มีมาในตัวกับ CMap stream ที่ฝังมา การประมาณด้วย identity สำหรับอย่าง GBK-EUC-H จะแค่ดูเหมือนรองรับทั้งที่ให้ output ผิด renderer จึงไม่แกล้งทำ
ทำไมอักษรมีวรรณยุกต์กลายเป็นเครื่องหมายคำถามบน Chinese Windows
รหัสไบต์เดียวต้องไม่มีวันไปถึงฟังก์ชัน GDI สาย ANSI ("A") เพราะ GetGlyphOutlineA กับ GetGlyphIndicesA ตีความไบต์ตาม code page ของระบบ ขณะที่ TextOutA ใช้ชุดอักขระของฟอนต์ที่ถูกเลือก บนระบบจีนไบต์ $A9 ของ Arial (เครื่องหมายลิขสิทธิ์ใน Windows-1252) กลายเป็นไบต์นำของ GBK แล้ว render ออกมาเป็น "?" กับดักที่เส้นทาง outline แบบไม่มี hint เพิ่มใน v2.766.83 เดินตกลงไปตรง ๆ v2.767.3 ถามฟอนต์ที่ realize แล้วว่าชุดอักขระอะไรด้วย GetTextCharset แปลงเป็น code page ผ่าน TranslateCharsetInfo รีดไบต์ผ่าน MultiByteToWideChar แล้วเรียกฟังก์ชันสาย W ฟอนต์สัญลักษณ์ใช้ U+F000 บวกกับรหัสแทน encoding ที่ไม่ลงรอยกับ Windows-1252 — /Differences, StandardEncoding, MacRomanEncoding — ถูก map เป็น Unicode ก่อนฟอนต์ระบบตัวใดจะได้เห็น
ข้อจำกัดของการวาดด้วยฟอนต์ระบบคืออะไร
การ render ด้วยฟอนต์ระบบเป็นการประมาณ และ component ของ HotPDF ตรงไปตรงมาเรื่องจุดที่มันหยุด ก่อน v2.768.18 การเช็คว่าติดตั้งหรือยังเทียบแค่ชื่อที่ GetTextFace คืนมา บน Windows ที่ localized ฟังก์ชันนี้รายงานชื่อตระกูลเป็นภาษาของระบบ Microsoft YaHei บน Chinese Windows หรือ Yu Mincho บน Japanese Windows จึงถูกตัดสินว่าหายแล้ววาดด้วยตัวแทนของ GDI ตั้งแต่ v2.768.18 ตระกูลที่กลับมาในชื่ออื่นถูกค้นต่อในตาราง name ของฟอนต์ด้วย ฟอนต์พวกนี้จึงถูกเจอ ความเข้ากันได้ของ metric รับประกันเฉพาะตระกูล Helvetica, Times กับ Courier Symbol map ไป Symbol และ ZapfDingbats ไป Wingdings ซึ่งเป็นไม้ซีกไม้ซี่กว่าจะจับคู่กันได้ preflight ข้างบนยังเห็นเฉพาะฟอนต์ใน dictionary ของ /Resources ของแต่ละหน้า ไม่รวมที่ถูกอ้างจากข้างใน Form XObjects เมื่อรหัสไหนยังวาดไม่ได้ การติดตาม glyph ที่ resolve ไม่ได้ตอนวาดจะรายงานมัน ซึ่งเป็นสัญญาณที่ดีกว่าจ้อง thumbnail
ทางแก้ที่อยู่ยงอยู่ฝั่งคนเขียนเอกสาร HotPDF เองเขียนโดยตั้ง FontEmbedding เป็น True เป็นค่าเริ่มต้น แทนที่ด้วย Arial ที่ฝังมาแม้โค้ดจะเรียก SetFont ด้วย Helvetica และข้อความที่ฝังมาผ่านrenderer glyph ของฟอนต์ที่ฝังมาแทนการเดาใด ๆ ข้างบน ไม้ประกบราคาถูกสำหรับไฟล์ที่ไหลเข้ามาคือเตือนก่อน render เมื่อตระกูลที่ map แล้วไม่อยู่ในลิสต์ฟอนต์หน้าจอ:
// VCL: Screen.Fonts ลิสต์ชื่อตระกูลที่ติดตั้ง (ยูนิต Forms)
function MissingSystemFamily(const BaseFont: AnsiString): Boolean;
begin
Result := Screen.Fonts.IndexOf(HPDFMapBaseFontToSystem(BaseFont)) < 0;
end;
สำหรับ component เต็ม รวมถึงการ render หน้า การสกัดข้อความและ font subsetting ฝั่งการเขียน ดูที่หน้าผลิตภัณฑ์ HotPDF Delphi PDF component