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

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

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

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

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

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

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

แผนภาพไปป์ไลน์ instancing ฟอนต์แปรผันของ HotPDF: interpolate delta ของ gvar ที่พิกัด wght กับ wdth ที่เลือก, สร้าง metrics กับตารางใหม่ และถอดตาราง variation ออกให้เหลือ instance แบบ static ที่ฝังได้
HotPDF แทรกค่า gvar delta ที่พิกัดที่คุณเลือก สร้างเส้นโครงและเมตริกใหม่ และม้วนตาราง variation ทุกตารางออก ฟอนต์ที่ฝังจึงถูกอ่านเป็นฟอนต์ TrueType แบบ static ธรรมดา

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

กฎที่ละเอียดอ่อนที่สุดในกระบวนการทั้งหมดเกี่ยวข้องกับเมตริก ในตาราง 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 จะฝังชุดย่อยสองชุด ไม่ใช่ฟอนต์ตัวเดียวที่ใช้ร่วมกันซึ่งมีสองอินสแตนซ์

แผนภาพ phantom points ของ gvar ใน HotPDF ที่แบก left side bearing กับ advance width และกับดักการใช้ซ้ำสองชั้น ที่ทำให้ advance ทุกตัวกว้างขึ้นเมื่อ HVAR ถูกใช้ทับบน metrics ที่มาจาก phantom
เมื่อมี gvar อยู่ เมตริกมาจาก phantom point ที่แทรกค่าแล้ว การนำ HVAR หรือ VVAR มาซ้อนจึงนับ variation เดียวกันสองรอบ ข้อความจึงลอยเหลื่อมไปทางขวาตลอดบรรทัด

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

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

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