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

ควบคุมการแทนที่ฟอนต์ PDF ใน Delphi ด้วย PDFium

PDFium Component ให้แอปพลิเคชัน Delphi ตัดสินใจได้ว่าจะใช้ไบต์ฟอนต์ใดเมื่อ PDF อ้างอิงถึงฟอนต์ที่มันไม่ได้ฝังไว้ ConfigureSystemFontProvider ติดตั้งการนำไปใช้งานจริงของ IPdfSystemFontProvider ซึ่งรับคำขอแม็ปฟอนต์ทุกคำขอที่ PDFium สร้างขึ้น ครบถ้วนพร้อมชื่อฟอนต์ น้ำหนัก แฟล็กตัวเอียง ชุดอักขระ และตระกูลระยะพิมพ์ และตอบกลับด้วยไบต์ TrueType, TrueType Collection หรือ OpenType ที่จะใช้

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

ทำไมไม่ติดตั้งฟอนต์บนเซิร์ฟเวอร์ไปเลย

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

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

การติดตั้งผู้ให้บริการ

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

uses
  PDFium;

type
  TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
  public
    function ResolveFont(const Request: TPdfSystemFontRequest;
      out Font: TPdfSystemFontData): Boolean;
  end;

function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
  out Font: TPdfSystemFontData): Boolean;
var
  Path: string;
begin
  // การแม็ปที่กำหนดผลลัพธ์แน่นอน: ชื่อฟอนต์บวกกับน้ำหนักและตัวเอียงตัดสินใจ
  // ว่าจะส่งไฟล์ใดสำหรับคำขอนี้
  Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
    Request.Italic, Request.Charset);
  Result := Path <> '';
  if not Result then
    Exit;
  Font.FaceName := Request.FaceName;
  Font.FontData := LoadFileBytes(Path);   // ไบต์ sfnt หรือ TTC แบบครบชุด
  Font.Charset := Request.Charset;
  Font.TTCIndex := 0;                     // ดัชนีภายในคอลเลกชัน
end;

var
  Policy: TPdfSystemFontPolicy;
begin
  Policy := TPdfSystemFontPolicy.Default;
  Policy.AllowDefaultFallback := False;   // โฮสต์เป็นผู้ตัดสินใจทุกอย่าง
  Policy.AllowFaceSubstitution := False;  // ปฏิเสธชื่อฟอนต์ที่ต่างออกไป
  Policy.MaxFontBytes := 32 * 1024 * 1024;
  Policy.MaxCacheEntries := 64;

  ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
  // เพิ่งจะโหลดไลบรารีและเปิดเอกสารได้ตอนนี้เท่านั้น
end;

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

แฟล็กนโยบายตัดสินใจอะไรจริงๆ

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

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

คอมโพเนนต์ตรวจสอบความถูกต้องของทุกคำตอบจากผู้ให้บริการก่อนที่จะไปถึง PDFium ข้อมูลว่างเปล่าจะถูกปฏิเสธ ฟอนต์ที่ใหญ่เกินไปจะถูกปฏิเสธเทียบกับ MaxFontBytes ดัชนี TTC ถูกตรวจสอบ และตาราง sfnt แต่ละตัวถูกให้บริการจากไดเรกทอรีฟอนต์เมื่อ PDFium ขอตารางแทนที่จะขอทั้งไฟล์ ความสามารถสุดท้ายนี้หมายความว่าผู้ให้บริการสามารถส่งมอบไฟล์ฟอนต์ที่สมบูรณ์และให้คอมโพเนนต์ตอบคำถามระดับตารางแทนได้ แทนที่จะเปิดเผยอ็อบเจกต์ Pascal ดิบข้าม C ABI

การแคชโดยไม่มีข้อมูลฟอนต์ค้างอยู่

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

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

ผู้ให้บริการถูกเรียกบนเธรดของฉันหรือไม่

ไม่จำเป็นเสมอไป PDFium อาจเรียกตัวแม็ปจากเวิร์กเกอร์เธรดของตัวเอง ดังนั้นการนำไปใช้งานจริงต้องปลอดภัยต่อเธรด ตัวนับที่ใช้ร่วมกัน แคช และการสังเกตการกำหนดค่า ต่างได้รับการป้องกันภายในคอมโพเนนต์ด้วย critical section ของตัวเอง แต่โค้ดภายใน ResolveFont เป็นหน้าที่ของคุณที่จะทำให้ปลอดภัย

รูปแบบที่ปลอดภัยที่สุดคือผู้ให้บริการที่ไม่แตะต้องสถานะที่ใช้ร่วมกันซึ่งเปลี่ยนแปลงได้เลย อ่านจากตารางที่สร้างไว้ตอนเริ่มต้น โหลดไบต์จากไฟล์หรือทรัพยากร แล้วคืนค่า หากการค้นหาต้องใช้แคชที่ใช้ร่วมกันของคุณเอง ให้ป้องกันมันไว้ และเก็บข้อยกเว้นไว้ภายในการนำไปใช้งานจริงของคุณ เพราะข้อยกเว้นของ Pascal ต้องไม่มีวันคลี่กลับผ่านสแตกของ PDFium คอมโพเนนต์ดักจับที่ขอบเขต C ABI และแปลงเป็นความล้มเหลวหรือค่าสำรองเริ่มต้นที่เป็นตัวเลือก แต่การพึ่งพาสิ่งนั้นเป็นลำดับควบคุมปกติมีต้นทุนด้านประสิทธิภาพและซ่อนบั๊ก กฎการทำงานแบบเธรดสำหรับส่วนที่เหลือของคอมโพเนนต์เป็นไปตามหลักการเดียวกับที่อยู่ในวินัยการล็อกการเรนเดอร์

การพิสูจน์การแม็ปในระบบใช้งานจริง

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

var
  Stats: TPdfSystemFontStatistics;
begin
  Stats := GetSystemFontProviderStatistics;
  Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
    [Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
     Stats.DefaultFallbackHits]));
  Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
    [Stats.RejectedProviderResponses, Stats.FailedRequests,
     Stats.ActiveHandles, Stats.CachedFonts]));

  // In a conformance run with fallback disabled, any fallback hit or
  // failed request means a document referenced a font we do not ship
  if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
    raise Exception.Create('unmapped font encountered - update the font set');
end;

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

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