บทความเทคนิค

ฝังฟอนต์ CID Type 2 ใน PDF จาก Delphi

PDFium Component เขียนข้อความลงในหน้า PDF ที่ระดับกลิฟด้วย AddCidType2Text โดยฝังข้อมูล TrueType ที่คุณส่งเข้ามาในรูปฟอนต์ CID Type 2 มันกำหนด CID ให้แต่ละอินสแตนซ์กลิฟตามลำดับ สร้างแผนที่ CID-to-GID อย่างชัดเจนและ ToUnicode CMap และรวมกลิฟที่อยู่บนเส้นฐานเดียวกันเข้าเป็นอ็อบเจกต์ข้อความเนทีฟชิ้นเดียว การทำ subset แบบเลือกได้ช่วยรักษาไฟล์ให้เล็ก พร้อมนโยบายชัดเจนว่าจะเกิดอะไรขึ้นเมื่อการ subset ล้มเหลว

การเขียนระดับกลิฟคือสิ่งที่คุณต้องการเมื่อข้อความผ่านการ shape มาแล้ว ภาษาอาหรับ เทวนาครี และสคริปต์ใดก็ตามที่มีรูปแบบตามบริบทจะสร้างลำดับ glyph identifier ที่ไม่ตรงกันแบบหนึ่งต่อหนึ่งกับตัวอักษรอีกต่อไป ดังนั้น API ที่รับสตริงกับชื่อฟอนต์จึงไม่สามารถแสดงผลลัพธ์นั้นได้ การส่งกลิฟและตำแหน่งเข้าไปโดยตรงเป็นวิธีเดียวที่จะใส่ข้อความสคริปต์ซับซ้อนที่ shape ถูกต้องลงใน PDF

AddCidType2Text ใน Delphi กำหนด CID เรียงลำดับให้ glyph instance ที่จัดรูปแล้วแต่ละตัว, เขียน CIDToGIDMap กับ ToUnicode CMap แล้วควบรวม run เป็น text object ของหน้าหนึ่งตัวที่สกัดออกมาได้สะอาด
glyph แต่ละอินสแตนซ์ใช้ CID ของตัวเอง dictionary ของฟอนต์แมป CID ไปยัง glyph id และย้อนกลับสู่ Unicode และ run ลงจอดบนหน้าเป็น text object หนึ่งชุดที่ดึงกลับได้เป็นข้อความตามที่ตั้งใจ

เหตุใดกลิฟแต่ละอินสแตนซ์จึงได้ CID ของตัวเอง

การปรับให้เหมาะที่น่าดึงดูดใจคือการลดความซ้ำซ้อน คือหนึ่ง CID ต่อหนึ่ง glyph identifier ที่ต่างกัน แล้วใช้ซ้ำทุกที่ที่กลิฟนั้นปรากฏ มันสร้างฟอนต์ที่เล็กลงและมันทำลายการดึงข้อความ เพราะกลิฟเดียวกันสามารถสอดคล้องกับเนื้อหา Unicode ที่ต่างกันได้อย่างถูกต้องในตำแหน่งที่ต่างกัน

กลิฟ ligature เป็นตัวอย่างที่ชัดเจน กลิฟ "ffi" เดียวกันอาจแทนตัวอักษรสามตัวในคำหนึ่งคำ และหลังจากการตัดสินใจ shaping ที่ต่างออกไป อาจแทนบางอย่างอื่นในที่อื่น แผนที่ ToUnicode ถูกกำหนดคีย์ด้วย CID ดังนั้น CID ที่ใช้ร่วมกันจึงพกได้แค่แผนที่เดียว และข้อความไหนก็ตามที่แพ้ในการแย่งชิงนั้นจะกลายเป็นดึงออกมาไม่ได้

ดังนั้นกลิฟแต่ละอินสแตนซ์จึงได้ CID ของตัวเอง และแต่ละแผนที่มีอิสระที่จะเป็นหลาย UTF-16 code unit กลิฟที่ไม่มีข้อความที่มีความหมาย เช่นอิลิเมนต์ตกแต่งหรือกลิฟที่ใช้จัดตำแหน่งอย่างเดียว จะแมปอย่างชัดเจนไปยัง U+200B ซึ่งเป็นช่องว่างความกว้างศูนย์ ดังนั้นทุก CID จึงมีผลลัพธ์การดึงข้อมูลที่สังเกตได้แทนที่จะเป็นช่องว่าง

uses
  PDFium;

var
  Pdf: TPdf;
  Glyphs: TPdfCidGlyphs;
  Options: TPdfCidFontOptions;
  Report: TPdfCidFontReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'label.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;

    // หนึ่งรายการต่อกลิฟที่ shape แล้วหนึ่งตัว: glyph id, ข้อความที่มันแทน
    // และระยะเดินและออฟเซ็ตของมันใน text space
    SetLength(Glyphs, 3);
    Glyphs[0].GlyphID := 402;  Glyphs[0].UnicodeText := 'ffi';
    Glyphs[0].Advance := 18.4;
    Glyphs[1].GlyphID := 71;  Glyphs[1].UnicodeText := 'c';
    Glyphs[1].Advance := 9.8;
    Glyphs[2].GlyphID := 74;   Glyphs[2].UnicodeText := 'e';
    Glyphs[2].Advance := 9.2;

    Options := TPdfCidFontOptions.Default;   // เลือก subset ก่อน
    Options.VerifyExtraction := True;

    if Pdf.AddCidType2Text(LoadFileBytes('NotoSans.ttf'), Glyphs,
         11, 72, 700, Options, Report) then
      Writeln(Format('%d glyphs, %d unique, font %d -> %d bytes, match=%s',
        [Report.GlyphCount, Report.UniqueGlyphCount,
         Report.OriginalFontBytes, Report.EmbeddedFontBytes,
         BoolToStr(Report.ExtractionMatches, True)]));
  finally
    Pdf.Free;
  end;
end;

การทำ subset และการตรวจสอบที่รหัสส่งคืนอย่างเดียวให้ไม่ได้

การทำ subset ฟอนต์ให้เหลือเฉพาะกลิฟที่ใช้จริงคือความแตกต่างระหว่างการฝัง 300 KB กับการฝัง 12 KB และบนเอกสารที่มีหลายฟอนต์ มันตัดสินว่าไฟล์ส่งอีเมลได้หรือไม่ เส้นทาง subsetting ของแพลตฟอร์มรับรายการกลิฟโดยตรง ซึ่งเข้ากันได้พอดีกับ API นี้ เพราะผู้เรียกใช้รู้อยู่แล้วว่ากลิฟใดถูกใช้บ้าง

สิ่งที่มันไม่ให้คือความมั่นใจ การเรียก subsetting อาจรายงานความสำเร็จและคืนเอาต์พุตที่ใช้งานไม่ได้ ดังนั้นคอมโพเนนต์จึงตรวจสอบคุณสมบัติสามอย่างก่อนยอมรับผลลัพธ์ เอาต์พุตต้องเล็กกว่าต้นฉบับ ต้องแยกวิเคราะห์ใหม่ได้เป็น sfnt ที่ถูกต้องพร้อมตาราง maxp ที่อ่านได้ และต้องคงกลิฟไว้จนถึง identifier สูงสุดของต้นฉบับที่ร้องขอ ความล้มเหลวใด ๆ หมายความว่า subset ถูกปฏิเสธ

สิ่งที่เกิดขึ้นต่อไปคือนโยบายของผู้เรียกใช้ ภายใต้ pcfemSubsetPreferred ซึ่งเป็นค่าเริ่มต้น subset ที่ถูกปฏิเสธจะสำรองไปฝังฟอนต์เต็ม ดังนั้นหน้าจึงถูกต้องแค่มีขนาดใหญ่ขึ้น ภายใต้ pcfemSubsetRequired การดำเนินการจะล้มเหลวก่อนหน้าจะถูกแก้ไข ซึ่งเป็นสิ่งที่ไปป์ไลน์ที่จำกัดขนาดต้องการ pcfemFull ข้ามการทำ subset ไปเลย รายงานจะบอกว่าเส้นทางไหนถูกใช้ผ่าน SubsetAttempted, SubsetApplied, UsedFullFontFallback และ SubsetErrorCode

การ subset ฟอนต์ CID Type 2 ใน Delphi ผ่านการตรวจสามข้อคือ ขนาด ความสมบูรณ์ของ sfnt และการครอบคลุม glyph ก่อนการยอมรับ และ subset ที่ถูกปฏิเสธจะถือนโยบาย pcfem คือสำรองด้วยฟอนต์เต็มหรือล้มเหลวถาวร
subset ถูกยอมรับหลังการตรวจขนาด, sfnt และความครอบคลุมของ glyph ผ่านเท่านั้น ส่วน subset ที่ถูกปฏิเสธจะเดินตามนโยบายของผู้เรียก — ตกกลับไปใช้ฟอนต์เต็มหรือล้มเหลวก่อนหน้าจะถูกแก้ไข

การตรวจสอบที่ไม่มีทางให้ผลบวกลวง

เมื่อเปิดใช้ VerifyExtraction คอมโพเนนต์จะยืนยันว่าสิ่งที่มันเขียนอ่านกลับมาได้ วิธีไร้เดียงสาในการทำสิ่งนี้คือดึงทั้งหน้าแล้วค้นหาสตริงที่คาดหวัง และมันผิด เพราะหน้าที่มีข้อความนั้นอยู่แล้วจะผ่านการตรวจสอบแม้เมื่อข้อความใหม่ถูกเขียนผิดพลาด

แทนที่จะทำแบบนั้น text page ถูกสร้างขึ้นใหม่และ object handle ที่ถูกแทรกโดยการเรียกนี้จะถูกอ่านทีละตัวตามลำดับที่แทรก แล้วต่อกัน ผลลัพธ์ถูกเปรียบเทียบกับข้อความที่คาดหวัง และรายงานเปิดเผยทั้งสองสตริงพร้อมค่าบูลีน ดังนั้นความไม่ตรงกันจึงวินิจฉัยได้แทนที่จะแค่ตรวจพบ

เปิดมันไว้ระหว่างการพัฒนาและในไปป์ไลน์ใดที่ความสามารถในการดึงข้อมูลเป็นข้อกำหนด เช่นคลังเอกสารที่ค้นหาได้ การปฏิบัติตามข้อกำหนดการเข้าถึง หรือการขุดข้อความปลายทาง ต้นทุนคือการสร้าง text page ใหม่หนึ่งครั้งต่อการเรียก ซึ่งเป็นเหตุผลที่มันไม่เปิดโดยปริยายในลูปที่แน่นหนา

งบประมาณและความล้มเหลวแบบอะตอมมิก

ไบต์ฟอนต์ อินสแตนซ์กลิฟ และหน่วยรหัส Unicode ล้วนถูกจำกัดเพดานก่อนการจอง และรูปแบบฟอนต์ ดัชนี TTC ช่วง glyph identifier และค่าเรขาคณิตล้วนถูกตรวจสอบก่อนที่จะเขียนอะไรเลย เรขาคณิตต้องเป็นค่าจำกัด ซึ่งฟังดูชัดเจนจนกระทั่งเอนจิน shaping ส่งค่า advance เป็น NaN มาจากฟอนต์ที่ผิดรูป

ความล้มเหลวเป็นแบบอะตอมมิกที่ระดับหน้า หากการโหลดฟอนต์ การเขียนอ็อบเจกต์ การสร้างเนื้อหา หรือการตรวจสอบการดึงข้อมูลล้มเหลว อ็อบเจกต์ทุกชิ้นที่แทรกโดยการเรียกนั้นจะถูกลบตามลำดับย้อนกลับและเนื้อหาของหน้าจะถูกสร้างขึ้นใหม่ การเรียกที่ล้มเหลวจะทิ้งหน้าไว้เหมือนเดิม ไม่ใช่มีข้อความครึ่งบรรทัดอยู่บนนั้น

สิ่งนี้เข้ากับไปป์ไลน์ข้อความตรงไหน

การแบ่งงานคุ้มค่าที่จะระบุให้ชัดเจน การ shaping คือการเปลี่ยนตัวอักษรเป็นกลิฟที่วางตำแหน่งแล้ว ไม่ใช่หน้าที่ของ API นี้ มันเป็นของเอนจิน shaping และการรองรับสองทิศทางและสคริปต์ซับซ้อนของคอมโพเนนต์เองอธิบายไว้ใน การจัดการอิโมจิ CJK และ surrogate pair API นี้คือสิ่งที่คุณเรียกใช้หลังจากนั้น พร้อมกับชุดกลิฟที่ตัว shaper สร้างขึ้น

สำหรับข้อความละตินธรรมดาที่ไม่มีการ shaping ตามบริบท API ข้อความแบบสตริงที่เรียบง่ายกว่าคือเครื่องมือที่ถูกต้องและสร้างโค้ดที่เล็กกว่า เลือกใช้การเขียน CID Type 2 เมื่อคุณมีเอาต์พุตที่ shape แล้ว เมื่อคุณต้องการควบคุม glyph identifier อย่างแม่นยำ หรือเมื่อฟอนต์ต้องถูกฝังจากไบต์ที่คุณมีอยู่แทนที่จะแก้ไขด้วยชื่อ ทางเลือกในการแก้ไขฟอนต์คือกลไก provider ที่อธิบายไว้ใน การควบคุมการทดแทนฟอนต์ PDF

มีข้อสังเกตด้านการติดตั้งใช้งานหนึ่งข้อ การฝังฟอนต์เป็นคำถามด้านลิขสิทธิ์พอ ๆ กับคำถามด้านเทคนิค ฟอนต์แตกต่างกันไปว่าการฝังได้รับอนุญาตหรือไม่ ได้รับอนุญาตเฉพาะสำหรับการดู หรือได้รับอนุญาตสำหรับการแก้ไข ไลบรารีจะฝังไบต์ใดก็ตามที่คุณให้มัน และการตรวจสอบใบอนุญาตเป็นความรับผิดชอบของคุณ ไม่ใช่ของรูปแบบไฟล์

การเขียนข้อความระดับกลิฟ การจัดหาฟอนต์ และการดึงข้อความใช้โมเดลหน้าเดียวกันใน Delphi, C++Builder และ Lazarus API ฉบับเต็มอธิบายไว้ที่หน้า PDFium Component สำหรับ Delphi