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

ฟอนต์แบบแปรผันใน PDF: การสร้างอินสแตนซ์แบบสแตติกใน Delphi

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

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

การสร้างอินสแตนซ์ต้องสร้างอะไรขึ้นใหม่บ้างจริงๆ

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

สิ่งที่ถูกลบออกก็สำคัญไม่แพ้กัน อินสแตนซ์แบบสแตติกต้องไม่คงตาราง fvar, avar, gvar, HVAR, VVAR, MVAR, STAT หรือ cvar ไว้ และ DSIG ที่ล้าสมัยก็ต้องถูกลบไปด้วย เพราะไบต์ที่ถูกเซ็นชื่อไว้ไม่มีอยู่แล้ว การปล่อยตารางเหล่านี้ทิ้งไว้จะทำให้ได้ฟอนต์ที่อ้างว่าเป็นแบบแปรผันทั้งที่เส้นขอบถูกเลื่อนไปแล้ว และตัวอ่านที่นำการแปรผันไปใช้จริงก็จะนำไปใช้ซ้ำเป็นครั้งที่สอง

จุดผี และกับดักการใช้ซ้ำสองครั้ง

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

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

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

ตรวจสอบพื้นที่การออกแบบก่อนเลือกค่า

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

var
  Pdf: THotPDF;
  Axes: THPDFVariableFontAxisArray;
  Instances: THPDFVariableFontNamedInstanceArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
    begin
      for I := 0 to High(Axes) do
        Writeln(Format('%s  min=%.1f default=%.1f max=%.1f',
          [string(Axes[I].Tag), Axes[I].MinimumValue,
           Axes[I].DefaultValue, Axes[I].MaximumValue]));
      Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
    end
    else
      Writeln('not a variable font - embed it as an ordinary TrueType face');
  finally
    Pdf.Free;
  end;
end;

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

การเลือกพิกัดและการสร้างเอกสาร

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

begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;

    Pdf.SetVariableFontAxis('wght', 620);   // น้ำหนักกึ่งหนา ไม่ใช่อินสแตนซ์ที่ตั้งชื่อไว้
    Pdf.SetVariableFontAxis('wdth', 87.5);  // แคบลงเล็กน้อย
    Pdf.CurrentPage.SetFont('Inter', [], 11);
    Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');

    Pdf.ClearVariableFontAxes;              // กลับไปยังอินสแตนซ์เริ่มต้น
    Pdf.CurrentPage.SetFont('Inter', [], 10);
    Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

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

การสร้างอินสแตนซ์มีปฏิสัมพันธ์กับการแบ่งชุดย่อยและการเรียงรูปอย่างไร

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

การเรียงรูปไม่ได้รับผลกระทบในทางหลักการ แต่ก็ควรตรวจสอบในทางปฏิบัติ คุณสมบัติเลย์เอาต์อยู่ในตาราง GSUB และ GPOS ซึ่งการสร้างอินสแตนซ์คงไว้ ดังนั้นลิเกเจอร์และรูปแบบทางเลือกเชิงสไตล์จึงยังคงทำงานตามที่อธิบายไว้ในรูปแบบทางเลือกเชิงสไตล์ของ OpenType GSUB สิ่งที่เปลี่ยนไปคือการจัดตำแหน่ง อินสแตนซ์แบบแคบมีระยะเลื่อนที่แคบกว่าค่าเริ่มต้น ดังนั้นเลย์เอาต์ใดๆ ที่วัดข้อความก่อนการสร้างอินสแตนซ์จะวัดความกว้างผิด ให้วัดด้วยการเลือกแกนเดียวกับที่คุณจะใช้เรนเดอร์ แล้วความคลาดเคลื่อนจะหายไป

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

การรองรับฟอนต์แบบแปรผันเข้ากันได้กับไปป์ไลน์ฟอนต์เดียวกันกับการฝัง การแบ่งชุดย่อย และกลุ่มปิดของกลิฟ ซึ่งอธิบายไว้อย่างละเอียดยิ่งขึ้นในกลุ่มปิดของชุดย่อยฟอนต์และกลิฟที่ผ่านการเรียงรูป ชุดคุณสมบัติงานพิมพ์ทั้งหมดสำหรับ Delphi และ C++Builder แสดงไว้ที่หน้าคอมโพเนนต์ PDF ของ HotPDF สำหรับ Delphi