เมื่อคุณสร้างรายงาน ฝังฟอนต์ TrueType และเปิดไฟล์ผลลัพธ์ ทุกอย่างจะแสดงผลอย่างถูกต้องในทุกโปรแกรมอ่านที่คุณลองใช้งาน รูปร่างตัวอักษร (glyphs) ถูกต้อง ข้อความสามารถเลือกได้ และไฟล์ก็ใช้งานได้ปกติ สิ่งเดียวที่ผิดปกติคือขนาดไฟล์ เอกสารที่ใช้อักษรละตินเพียงไม่กี่สิบตัวกลับพกฟอนต์ขนาด 350 KB ไปทั้งหมด ส่วนเอกสารที่พิมพ์อักษรจีนเพียงหนึ่งย่อหน้าก็พกฟอนต์ CJK ขนาด 14 MB แทนที่จะเป็นเพียงส่วนเสี้ยวครึ่งเมกะไบต์ที่จำเป็นจริง ๆ ไม่มีการแจ้งข้อผิดพลาด (exception) ไม่มีบันทึกคำเตือน และไฟล์ผ่านการตรวจสอบความถูกต้อง นี่คือลักษณะของขั้นตอนการปิดท้ายเอกสาร (finalization) ที่จัดลำดับผิดพลาดเมื่อมองจากภายนอก: ไม่มีอะไรล้มเหลว และหลักฐานเพียงอย่างเดียวคือขนาดไฟล์ที่ใหญ่เกินไป
บั๊กที่ก่อให้เกิดปัญหานี้เคยอยู่ใน HotPDF สำหรับรุ่นหนึ่งและได้รับการแก้ไขแล้ว ปัญหานี้คุ้มค่าที่จะหยิบยกมาอธิบายไม่ใช่ในฐานะรายงานข้อบกพร่อง แต่ในฐานะบทเรียน เนื่องจากรูปแบบของความผิดพลาดนี้เกิดขึ้นได้ทั่วไป เอ็นจิ้นเอกสารทุกตัวมีขั้นตอนการปิดท้ายเพื่อปรับเปลี่ยนอ็อบเจกต์ก่อนที่จะเขียนลงไป และความถูกต้องของขั้นตอนนี้ขึ้นอยู่กับลำดับของขั้นตอนที่สัมพันธ์กับการแปลงเป็นข้อมูลอนุกรม (serialization) ทั้งสิ้น หากมีขั้นตอนใดขั้นตอนหนึ่งวางผิดตำแหน่งไปอยู่หลังการเขียน มันจะไม่ทำงานเลยอย่างเงียบ ๆ
สิ่งที่ Font Subsetting ควรจะทำ
ฟอนต์แบบซับเซต (subset) คือส่วนของไฟล์ TrueType ที่เอกสารใช้งานจริงเท่านั้น มาตรฐาน ISO 32000-1 §9.9 อธิบายว่าโปรแกรมฟอนต์ที่ฝังอยู่นั้นจะอยู่ในสตรีมที่อ้างอิงโดยตัวอธิบายฟอนต์ (font descriptor) และสำหรับโปรแกรม TrueType สตรีมนั้นคือ /FontFile2 ที่มี /Length1 ระบุจำนวนไบต์ที่ไม่ได้บีบอัด การทำ Subsetting จะเขียนตาราง glyf และ loca ใหม่เพื่อให้มีเฉพาะ glyphs ที่เอกสารถ้างถึงเท่านั้น พร้อมเปลี่ยนหมายเลขรหัสของ glyph และขึ้นต้นชื่อ /BaseFont ด้วยแท็กตัวอักษรหกตัว เช่น ABCDEF+ เพื่อทำเครื่องหมายฟอนต์เป็นซับเซตตามที่ข้อกำหนดระบุไว้ ฟอนต์ละตินที่ทำซับเซตเหลือสิบหรือสิบห้ากิโลไบต์คือความแตกต่างระหว่างไฟล์ PDF ที่มีขนาดกะทัดรัดกับไฟล์ที่ต้องขนชุดแบบอักษรทั้งหมดมาเพื่อใช้กับหัวเรื่องเพียงหัวเรื่องเดียว
จุดที่กระบวนการนี้เกิดขึ้นมีความสำคัญมาก การทำ Subsetting ไม่ใช่การแปลงข้อมูลที่คุณใช้กับไบต์ที่อยู่บนดิสก์แล้ว แต่มันคือการแก้ไขกราฟอ็อบเจกต์ในหน่วยความจำ: มันจะลดขนาดเนื้อหาของสตรีม /FontFile2 ปรับปรุง /Length1 และเขียนสตริง /BaseFont ใหม่ ทั้งหมดนี้จะต้องพร้อมใช้งานเมื่อตัวแปลงอนุกรม (serializer) ทำการไล่ตามกราฟและส่งไบต์ออกไป หากการแก้ไขเกิดขึ้นหลังจากเขียนไบต์เสร็จแล้ว ข้อมูลก็จะถูกอัปเดตในอ็อบเจกต์ที่ไม่มีใครอ่านอีกเลย
อาการของปัญหา และเหตุใดจึงไม่มีการแจ้งเตือนใด ๆ
พฤติกรรมที่ได้รับรายงานคือฟอนต์ขนาดเต็มปรากฏในผลลัพธ์โดยไม่มีการวิเคราะห์ข้อผิดพลาด ผู้ใช้ที่ลงทะเบียนฟอนต์ Unicode TrueType และสร้างเอกสารตามปกติพบว่าอ็อบเจกต์ฟอนต์ที่ฝังอยู่มีขนาดเท่ากับไฟล์ .ttf ต้นฉบับ และชื่อ /BaseFont ไม่มีคำนำหน้าซับเซตหกตัวอักษร ผลลัพธ์ที่ได้จึงไม่เคยลดขนาดลงเลย ไม่ว่าจะสลับระหว่างการใช้งานสิบ glyph หรือหนึ่งหมื่น glyph ก็ตาม
การไม่มีข้อผิดพลาดแจ้งเตือนคือส่วนที่ทำให้บั๊กประเภทนี้มีราคาแพง รูทีนการทำ subsetting ที่ทำงานผิดเวลาก็ยังคงทำงานของมัน มันยังคงไล่ดูการใช้งานรหัสอักขระ (codepoint) ที่รวบรวมไว้ สร้างซับเซตที่ถูกต้องสมบูรณ์ และนำไปใช้กับกราฟอ็อบเจกต์ในหน่วยความจำ ในส่วนภายในงานเสร็จสิ้นและคืนค่ากลับมาอย่างราบรื่น สิ่งเดียวที่ผิดพลาดคือกราฟอ็อบเจกต์ที่แก้ไขนั้นไม่ใช่สิ่งที่จะถูกเขียนอีกต่อไปเนื่องจากตัวเขียนทำงานเสร็จสิ้นไปแล้ว ในมุมมองของผู้เรียกใช้งาน เอกสารถูกสร้างและบันทึกโดยไม่มีปัญหาใด ๆ ซึ่งเป็นภาพลวงตาที่ความล้มเหลวแบบเงียบ ๆ สร้างขึ้น
สาเหตุหลักคือลำดับขั้นตอนการปิดท้าย
ใน HotPDF งานปิดท้ายเอกสารจะเกิดขึ้นภายใน EndDoc ขั้นตอนการทำ subsetting เป็นรูทีนภายในชื่อ BuildAndApplyUnicodeFontSubset มันจะอ่านชุดของรหัสอักขระที่ใช้ในแต่ละเอกสาร ซึ่งเก็บไว้ในบิตแมปที่พาธการส่งข้อความจะเติมค่าเมื่อมีแสดง glyph จากนั้นจับคู่รหัสอักขระแต่ละตัวที่ใช้ผ่านตารางแคชรหัสอักขระไปยัง glyph จริง เพื่อเปลี่ยนเป็นตัวระบุ glyph จริง และเขียนโปรแกรมฟอนต์ใหม่รอบ ๆ โครงสร้างนั้น เมื่อฟอนต์ Unicode TrueType ได้รับการลงทะเบียน พาธการส่งข้อความจะตั้งค่าบิตในชุดรหัสอักขระที่ใช้งานสำหรับทุกตัวอักษรที่เขียน ดังนั้นเมื่อถึงเวลาปิดเอกสาร เอ็นจิ้นจะรู้ทันทีว่าซับเซตต้องเก็บรักษา glyph ใดไว้บ้าง
ข้อบกพร่องที่พบคือ BuildAndApplyUnicodeFontSubset ถูกเรียกใช้งานหลังจาก SaveToStream หรือ SaveToFile ได้แปลงเอกสารเป็นข้อมูลอนุกรมไปเรียบร้อยแล้ว การแก้ไขส่วนของ /FontFile2 ค่า /Length1 ที่ปรับปรุงแล้ว และคำนำหน้า /BaseFont หกตัวอักษร ล้วนคำนวณกับกราฟอ็อบเจกต์ที่แปลงเป็นไบต์ไปแล้ว การแก้ไขคือการจัดลำดับใหม่เพียงบรรทัดเดียว: ย้ายการเรียกใช้ subset ลำดับก่อนการแปลงข้อมูลอนุกรม เพื่อให้ตัวเขียนส่งฟอนต์ที่ทำซับเซตแล้วออกไปแทนที่จะเป็นฟอนต์เดิม ลำดับที่ถูกต้องจะรันกระบวนการ subsetter ก่อนและทำ serialization ตามหลัง
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;
เมื่อแก้ไขลำดับขั้นตอนแล้ว โค้ดที่เรียกใช้งานก็ไม่มีอะไรเปลี่ยนแปลง การทำ subsetting จะเปิดใช้งานเป็นค่าเริ่มต้นทันทีเมื่อลงทะเบียนฟอนต์ Unicode TrueType คุณเพียงแค่ลงทะเบียนฟอนต์ เริ่มต้นเอกสาร วาดรูป และสิ้นสุดเอกสาร ซับเซตจะถูกสร้างขึ้นจาก glyphs ที่คุณใช้งานก่อนที่ข้อมูลไบต์จะถูกเขียนออกจากหน่วยความจำ
ทำไมการจัดลำดับผิดเพียงขั้นตอนเดียวจึงเป็นปัญหาประเภทใหญ่
เหตุผลที่ปัญหานี้ควรค่าแก่การเป็นบทเรียนมากกว่าเป็นแค่บันทึกท้ายหน้ากระดาษก็คือ EndDoc มีขั้นตอนการปิดงานหลายอย่าง และทุกขั้นตอนต่างอ่อนไหวต่อตำแหน่งที่สัมพันธ์กับการเขียนข้อมูล การทำ Font subsetting ก็เป็นหนึ่งในนั้น ผลลัพธ์ในรูปแบบ PDF/A จำเป็นต้องมีสตรีม /CIDSet ที่ระบุรหัสของ glyph ที่มีอยู่ในซับเซตอย่างถูกต้อง ซึ่งเป็นข้อกำหนดที่มาตรฐาน ISO 19005 บังคับใช้เพื่อให้โปรแกรมตรวจสอบยืนยันได้ว่าโปรแกรมที่ฝังอยู่ตรงกับที่ตัวอธิบายฟอนต์ระบุ สตรีมนั้นจะถูกส่งออกไปในขั้นตอนการปิดท้ายเดียวกันและต้องพึ่งพาซับเซตที่ถูกสร้างขึ้นก่อน ส่วนมาตรฐาน PDF/UA-1 ตามข้อกำหนด ISO 14289-1 §7.18.3 กำหนดว่าทุกหน้าที่มีการคำอธิบายประกอบ (annotation) จะต้องประกาศ /Tabs ด้วยค่า /S ซึ่งรูทีนภายในชื่อ EnsurePDFUATabsOnAnnotatedPages จะประทับตราคีย์นั้นในระหว่างขั้นตอนเดียวกันนี้ การตรวจสอบเจตนาของผลลัพธ์ (output intent) ก็ทำงานในส่วนนี้เช่นกัน
ความผิดพลาดในการจัดลำดับแบบเดียวกันที่ปิดการใช้งาน subsetting ยังส่งผลให้คีย์ลำดับแท็บ (tab-order) ของ PDF/UA หายไปจากหน้าที่ระบุคำอธิบายประกอบด้วย เนื่องจากขั้นตอนนี้วางผิดตำแหน่งไปอยู่หลังการเขียนเช่นกัน โปรแกรม veraPDF และ PAC จะรายงานกรณีไม่มี /Tabs /S ว่าเป็นการละเมิดโปรโตคอล Matterhorn จุดตรวจสอบที่ 21-001 ดังนั้น การเรียกใช้งานที่วางตำแหน่งผิดเพียงจุดเดียวไม่ได้เพียงแค่ทำให้ไฟล์มีขนาดใหญ่ขึ้นเท่านั้น แต่มันยังทำลายข้อกำหนดการปฏิบัติตามมาตรฐานการเข้าถึงเอกสาร (accessibility conformance) อย่างเงียบเชียบในเวลาเดียวกันโดยไม่มีข้อผิดพลาดแจ้งเตือนใด ๆ นี่คืออันตรายของขั้นตอนการปิดท้ายเอกสาร: ทุกขั้นตอนแชร์เงื่อนไขร่วมกัน และความผิดพลาดในการจัดลำดับเพียงจุดเดียวสามารถส่งผลเสียต่อหลายขั้นตอนพร้อมกันได้ในขณะที่การเรียกใช้งานทุกครั้งยังคงรายงานว่าสำเร็จ
วิธีตรวจจับความล้มเหลวในการส่งข้อมูลแบบเงียบ ๆ
บั๊กที่ไม่แสดงข้อผิดพลาด (exception) จะไม่มีทางถูกตรวจจับได้ด้วยการรันโปรแกรมเพียงอย่างเดียว แต่มันจะถูกตรวจพบได้โดยการตรวจสอบไฟล์ผลลัพธ์และเปรียบเทียบกับสิ่งที่ควรจะเกิดขึ้นจากการป้อนข้อมูล สำหรับการทำ font subsetting การตรวจสอบสามารถทำได้จริง โดยให้เปรียบเทียบขนาดไฟล์ผลลัพธ์กับขนาดที่คาดไว้คร่าว ๆ: เอกสารที่ใช้ glyph เพียงไม่กี่ตัวไม่ควรมีขนาดเท่ากับแบบอักษรทั้งหมด ให้เปิดอ็อบเจกต์ฟอนต์ที่ฝังอยู่แล้วอ่านความยาวไบต์ สตรีม /FontFile2 ที่ทำซับเซตสำหรับแบบอักษรละตินควรเป็นเพียงเศษเสี้ยวเล็ก ๆ ของไฟล์ต้นฉบับ จากนั้นให้อ่านชื่อ /BaseFont และยืนยันว่ามีคำนำหน้าหกตัวอักษรอยู่ด้วย เนื่องจากหากไม่มีคำนำหน้านี้ จะเป็นสัญญาณโดยตรงว่าไม่มีการทำซับเซต
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;
การปฏิบัติตามข้อกำหนด PDF/A
สำหรับการสร้างไฟล์ PDF/A การตรวจสอบจะเข้มงวดยิ่งขึ้นเนื่องจากมีโปรแกรมตรวจสอบช่วยทำงานให้คุณ เพียงกำหนดระดับการปฏิบัติตามข้อกำหนดและส่งไฟล์ผลลัพธ์ผ่านโปรแกรม veraPDF: ข้อมูล /CIDSet ที่หายไป หรือซับเซตที่ไม่ตรงกับตัวอธิบาย จะถูกรายงานเป็นข้อกำหนดที่ล้มเหลวแทนที่จะปล่อยให้คุณต้องสังเกตด้วยตาเปล่า ตัวสวิตช์ข้อกำหนดที่ขับเคลื่อนงานปิดท้ายเอกสารเหล่านี้คือพร็อพเพอร์ตี้บนตัวเอกสาร โดย PDFACompliance จะรับค่าสตริง เช่น '2B' สำหรับ PDF/A-2 Level B และ PDFUACompliance เป็นค่าบูลีนที่จะเปิดใช้งานข้อกำหนดสำหรับ PDF แบบมีแท็ก (tagged-PDF) และลำดับแท็บ
Pdf := THotPDF.Create(nil);
try
Pdf.PDFACompliance := '2B';
Pdf.PDFUACompliance := True;
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;
บทเรียนทางวิศวกรรม
มีสองกฎที่เราได้เรียนรู้จากสิ่งนี้ ข้อแรกคือขั้นตอนการปิดท้ายใด ๆ ที่ปรับเปลี่ยนอ็อบเจกต์จะต้องทำงานก่อนที่อ็อบเจกต์เหล่านั้นจะถูกแปลงเป็นอนุกรม และขั้นตอนสุดท้ายของเอ็นจิ้นสร้างเอกสารควรถูกจัดเตรียมเป็นไพป์ไลน์ที่มีการเรียงลำดับอย่างเคร่งครัดโดยที่การแปลงข้อมูลเป็นอนุกรมจะเป็นกิจกรรมสุดท้ายเสมอ ไม่ใช่เป็นเพียงหนึ่งในกิจกรรมทั่วไป ข้อสองซึ่งเป็นสิ่งที่สร้างความสูญเสียเวลามากที่สุดคือ: สำหรับขั้นตอนการส่งข้อมูลออก การที่ไม่มีข้อผิดพลาดเกิดขึ้นไม่ได้แปลว่าเป็นหลักฐานของความสำเร็จ รูทีนที่สร้างซับเซตที่ถูกต้องและนำไปใช้กับกราฟที่เขียนเสร็จเรียบร้อยและผิดตำแหน่งจะไม่รายงานข้อผิดพลาดใด ๆ เนื่องจากในมุมมองของมันเองไม่มีอะไรผิดพลาด การตรวจสอบจึงต้องดูที่ชิ้นงานที่ผลิตออกมา (artifact) ไม่ใช่แค่รหัสส่งกลับ (return code) ให้ตรวจสอบขนาดไฟล์ผลลัพธ์ อ่านความยาวไบต์ของฟอนต์ที่ฝังและคำนำหน้าชื่อ /BaseFont และใช้โปรแกรม veraPDF เพื่อตัดสินความถูกต้องของไฟล์ PDF/A ซึ่งข้อบกพร่องของ /CIDSet ที่หายไปจะถูกระบุว่าเป็นความล้มเหลวอย่างชัดเจน
ข้อมูลฝั่งผู้ผลิตเกี่ยวกับการจัดการฟอนต์ วิธีลงทะเบียนฟอนต์และการฝังฟอนต์สำหรับรายงาน มีรายละเอียดอธิบายไว้ใน บทความของเราเกี่ยวกับฟอนต์และรูปภาพในรายงานผลลัพธ์ ส่วนฝั่งการตรวจสอบความถูกต้อง ซึ่งขั้นตอนการปิดท้ายเหล่านี้จะได้รับการตรวจสอบเทียบกับมาตรฐาน มีอยู่ใน บทความการตรวจสอบมาตรฐาน PDF/A และ PDF/UA ทั้งสองอย่างนี้ทำงานร่วมกับการทำ subsetting และการปฏิบัติตามมาตรฐานที่อธิบายไว้ที่นี่ ซึ่งพร้อมใช้งานเป็นส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder ร่วมกับ API สำหรับการโหลด การแก้ไข การเข้ารหัส และการลงนามที่ครอบคลุมในส่วนอื่น ๆ ของบล็อกนี้