Teknisk artikel

Bädda in CFF2-variable fonts i PDF med HotPDF

HotPDF bäddar in OpenType-variable fonts med CFF2-konturer genom att konvertera dem, vid de valda axelvärdena, till ett CID-keyed CFF1-fontprogram som PDF-läsare redan förstår. Vanlig nativ text registrerad via RegisterUnicodeTTF använder fontens egna cmap- och hmtx-tabeller, och skrivaren bäddar in det konverterade programmet under /FontFile3 med /Subtype /CIDFontType0C, med varje CID lika med det ursprungliga glyph-ID:t

HotPDF-pipeline för att bädda in ett CFF2-variable font i Delphi som CID-keyed CFF1 under FontFile3, med den tysta GDI-substitutionsomvägen som registrerade-byte-vägen undviker
HotPDF fryser variationen vid de valda axelvärdena och skriver ett CID-keyed CFF1-program där varje CID är lika med det ursprungliga glyph-ID:t, så inget lager översätter mellan numreringar

Den uppenbara vägen är värd att beskriva, för den fallerar utan att säga till någon. Parsa och cachaa CFF2-tabellerna, och låt sedan den vanliga textvägen slå upp fonten via Windows GDI med en TrueType-only-matchning: GDI har inte en aning om vad det ska göra med CFF2-konturer och byter i smyg ut mot ArialUnicodeMS. Allt nedströms som läser substitutet får fel konturer, fel bredder och fel extrahering för tecken på supplementalplanen. PDF:en öppnas fint. Den är bara inte satt i den font du bad om, och därför måste textvägen jobba från de registrerade bytena i stället för en GDI-uppslagning

Varför kan inte en PDF bädda in en CFF2-font direkt?

PDF har ingen plats för ett CFF2-program. ISO 32000-2 §9.9 listar de inbäddade fontprogramtyper en läsare måste hantera: Type 1 under /FontFile, TrueType under /FontFile2, och under /FontFile3 ett bart CFF-program (Type1C eller CIDFontType0C) eller en OpenType-wrapper runt TrueType- eller CFF-konturer. Varje CFF-flavor i den listan betyder CFF version 1, formatet läsare har parsat i två decennier

De tre PDF-platserna för inbäddade fontprogram, alla begränsade till CFF version 1, och den saknade CFF2-platsen som får HotPDF att konvertera före inbäddning i Delphi
varje CFF-flavor PDF-läsare måste hantera är CFF version 1, så ett CFF2-variable font har ingen plats förrän det konverterats

CFF2 är ett annat format som råkar dela namn. Det släpper Name- och String-INDEX, släpper charset:en, flyttar glyph-advanser ut ur CharStrings och lämnar dem till hmtx, och lägger till blend-operatorer så att en enda kontur kan beskriva hela designrymden. En CFF1-parser som möter en CFF2-tabell har inget underlag för att gissa, och ingenting i PDF-specifikationen ålägger den att försöka. Att svepa in CFF2-fonten i en OpenType-ström och hoppas på det bästa är precis den sortens fil som renderas på en maskin och misslyckas på nästa, så HotPDF gör inte det

Konverteringen måste också vara statisk. Som artikeln om statisk instansiering av variable fonts förklarar är en PDF-font en fix uppsättning konturer med fixa metriker, och ingen läsare krävs tillämpa axelkoordinater. HotPDF löser därför variationen först och skriver CFF1 först därefter

Skriva nativ text i en CFF2-font

För applikationskod beter sig en CFF2-font som vilken annan registrerad Unicode-font som helst: välj axelvärden, registrera filen, välj familjen vid namn, och rita. RegisterUnicodeTTF behåller en separat kopia av den ursprungliga namntabellen, så SetFont matchar familjenamnet, fullständiga namnet, PostScript-namnet eller det typografiska familjenamnet och väljer de frysta statiska fontbytena, utan någon Windows-installation eller GDI-uppslagning

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure.pdf';
    Pdf.BeginDoc;
    // Axelvärden gäller fonts registrerade efter det här anropet
    Pdf.SetVariableFontAxis('wght', 650);
    Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
    // Family-, full-, PostScript- eller typografiskt family-namn från namntabellen
    Pdf.CurrentPage.SetFont('Acme Serif VF', [], 12, DEFAULT_CHARSET);
    Pdf.CurrentPage.UnicodeTextOut(72, 100, 0, 'Variable weight 650, CFF2 outlines');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Glyph-ID:n kommer från den registrerade cmap:en och advanser från den registrerade hmtx:en. En glyph vars advans faktiskt är noll stannar noll i stället för att falla tillbaka till en GDI-mätning, och ett UTF-16-surrogatpar ger exakt en advans, inte två. BMP-tecken, supplementtecken och syntetiska shaping-bärare behåller per-font Unicode-mappningar, så ett byte till en annan inbäddad font eller en ny sida låter inte den andra fonten uppdatera denna fonts subset-tillstånd eller låna dess glyph-ID:n

Mätning följer samma byte. THPDFPage.GetWideTextWidth och THPDFPage.GetWideCharAdvances läser metriken för den valda byte-baserade fonten, så radbrytning och högerjustering stämmer med det som ritas. En ordningsdetalj snärjer folk: SetVariableFontAxis-ändringar efter registreringen påverkar nästa font du registrerar, inte instansen som redan är fryst. För en andra vikt, sätt det nya värdet och registrera igen

Hur bygger HPDFConvertCFF2ToCFF det CID-keyed-programmet?

HPDFConvertCFF2ToCFF producerar ett rått CID-keyed CFF1-program med en enda Font DICT, där CID är lika med det ursprungliga glyph-ID:t för varje plats, även tomma och oanvända. Den identiteten är hela designen. Fontnamnet, PDF:ns /W-bredder, CFF-advanserna och ToUnicode-CMapen syftar alla på samma numrering, så inget lager behöver översätta mellan glyph-ID:n och CIDs, och ingenting kan komma ur steg

På PDF-sidan är ättlingen en CIDFontType0-font med Identity-H- eller Identity-V-kodning, och programströmmen ligger under /FontFile3 med /Subtype /CIDFontType0C. Eftersom CID redan är lika med GID bär ättlingen ingen /CIDToGIDMap, och strömmen undantas från den senare stream-ersättningshaken som används för registrerade TrueType-fonts. I motsats till /Subtype /OpenType-wrappern höjer den konverterade strömmen inte PDF 1.6-versionskravet

Bredderna behövde ett litet trick. En CFF1 Type 2 CharString lagrar advansen som en valfri första operand relativt nominalWidthX, och heltalsoperander är begränsade till det signerade 16-bitarsområdet. En OpenType-advans är ett osignerat 16-bitarsvärde, upp till 65535. HotPDF sätter nominalWidthX till 32768 och defaultWidthX till 0, och skriver sedan varje advans explicit som advance - 32768. En nolladvans blir -32768 och en 65535-advans blir 32767, så hela uint16-området är representerbart utan att klampa en enda glyph

Hur HotPDF mappar osignerade 16-bitars advanser till signerade CFF1-operander med nominalWidthX 32768, så varje fontadvans i Delphi PDF-utdata förblir oklampad
nominalWidthX 32768 skiftar operandfönstret så att en nolladvans lagras som -32768 och 65535 lagras som 32767 utan att klampa någon glyph

Geometrin får samma omsorg. FontMatrix är [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0], så en font med 2048 enheter behåller sina nativa koordinater. Relativa flyttar och linjer för stora för en operand delas upp i representerbara steg, och kubiska kurvor delas på ett sätt som bevarar deras form. De ursprungliga copyright- och licensposterna, den ursprungliga namntabellen och inbäddningsflaggorna följer med i CFF Notice-strängen

Anropa konverteraren utanför ett dokument

Konverteraren är en publik funktion i uniten HPDFOpenTypeAdvanced, skriven i Pascal, utan HarfBuzz-beroende och utan något systemfontinstallationssteg. Det gör den användbar på egen hand, till exempel för att förkonvertera en font vid byggtid eller inspektera vad PDF-skrivaren kommer bädda in

uses
  System.SysUtils, System.IOUtils, HPDFOpenTypeAdvanced;

procedure ConvertWeight650;
var
  Source, Cff: TBytes;
  Axes: THPDFVariableFontAxisValueArray;
begin
  Source := TFile.ReadAllBytes('C:\Fonts\AcmeSerifVF.otf');
  SetLength(Axes, 1);
  Axes[0].Tag := 'wght';
  Axes[0].Value := 650;
  Cff := HPDFConvertCFF2ToCFF(Source, Axes);
  if Length(Cff) = 0 then
    raise Exception.Create('Not convertible: not CFF2, embedding denied or bad axes');
  TFile.WriteAllBytes('AcmeSerif-650.cff', Cff);
end;

Variabel indata går genom geometri-only statisk instansiering vid de givna koordinaterna, som täcker både konturer och advanser. En statisk CFF2-font accepterar bara ett tomt urval; att skicka axelvärden till den behandlas som ett fel i stället för att ignoreras. Resultatet är ett bart CFF1-program, inte en sfnt-container, så ge den inte till kod som väntar sig en OpenType-fil

Fel blir en tom TBytes, inte ett exception, för de förväntade fallen: indata som inte är CFF2, ett axelval som instansieringen inte accepterar, icke-ändliga värden som NaN, och fonts vars OS/2 fsType sätter restricted-license-inbäddning (0x0002) eller bitmap-only-inbäddning (0x0200). Testa alltid längden. Inuti EndDoc gör skrivaren det åt dig och kastar ”StoreFont: CFF2 outline conversion failed”, vilket är rätt beslut: ett tomt fontprogram i en levererad PDF är betydligt värre än ett synligt fel i loggen

Budgeter och avbrytning för serverarbetslaster

Den valfria tredje parametern, THPDFVariableFontControl, sätter gränser för minnet och låter dig avbryta en lång konvertering. Utan den använder konverteraren sina standarder: en gräns på 128 MiB för fontindata och utdata, en kumulativ arbetsbudget på 1 GiB, och CFF1-gränsen på 65535 byte per CharString

type
  TFontJob = class
  private
    FCancelled: Boolean;
    procedure CheckCancel;
  public
    function Convert(const Source: TBytes): TBytes;
    property Cancelled: Boolean read FCancelled write FCancelled;
  end;

procedure TFontJob.CheckCancel;
begin
  if FCancelled then
    Abort;  // propagerar ut ur HPDFConvertCFF2ToCFF
end;

function TFontJob.Convert(const Source: TBytes): TBytes;
var
  Control: THPDFVariableFontControl;
begin
  Control := THPDFVariableFontControl.Create(
    32 * 1024 * 1024,           // maximalt antal utbyte
    Int64(256) * 1024 * 1024,   // kumulativ arbetsbudget
    CheckCancel);
  try
    // nil-val: standardinstans för ett variable font, eller en statisk CFF2-font
    Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
  finally
    Control.Free;
  end;
end;

Budgetöverskridanden och avbrytning rapporteras som exceptions, medvetet åtskilda från vägen med tomt resultat. En tom array betyder att fonten inte går att konvertera; ett exception betyder att det här försöket stoppades. Håll de två isär i felhanteringen, för att försöka igen med en icke-konverterbar font och större budget hjälper inte

Hur mycket väger en inbäddad CFF2-font?

Det viktigaste att planera för är filstorlek: HotPDF bäddar in den kompletta konverterade glyph-uppsättningen för CFF2-fonts, oavsett om EnableFontSubsetting är på eller av. En font med flera tusen glypher lägger till alla i varje dokument som använder den, även om sidan visar en enda rubrik. För en broschyr är det acceptabelt. För en rapportserver som skriver tusentals små PDF:er är det en verklig kostnad du bör mäta innan du bestämmer dig

De relaterade storleksfunktionerna följer samma regel. Konverterade CFF2-fonts bäddas in hela, så varken de kompakta fontsubsets som CompressDocument aktiverar eller den persisterande font-subset-cachen gäller dem. TrueType-fonts i samma dokument subsettas och cachas som vanligt ändå. Den ena fördelen med att behålla varje glyph är att fonts vars licens förbjuder subsetting bäddas in i en form som respekterar den

Resursgränserna är ändliga konverteringsgränser, och CFF1 själv takar vad en enda CharString kan rymma. En glyph vars konverterade CharString överskrider 65535 byte, eller en font som bränner igenom arbetsbudgeten, faller synligt i stället för att skrivas med en tom kontur. Det är den avsedda avvägningen: ett högtligt misslyckande på en exotisk font slår ett dokument med tyst saknade glypher

Nativ CFF2-text och den portabla CFF1-konverteraren följer med HotPDF Delphi PDF-komponenten för Delphi och C++Builder, tillsammans med instansiering av variable fonts, color glyphs och font subsetting som tas upp på annat håll i bloggen