Teknisk artikel

CFF2 variable fonts i PDF med HotPDF til Delphi

HotPDF indlejrer OpenType variable fonts med CFF2-outlines ved at konvertere dem, ved de valgte akseværdier, til et CID-keyed CFF1-fontprogram, som PDF-læsere allerede forstår. Almindelig native tekst registreret gennem RegisterUnicodeTTF bruger fontens egne cmap- og hmtx-tabeller, og writeren indlejrer det konverterede program under /FontFile3 med /Subtype /CIDFontType0C, så hver CID er lig det oprindelige glyph ID

HotPDF-pipeline til indlejring af en CFF2 variable font i Delphi som CID-keyed CFF1 under FontFile3, med den lydløse GDI-substitutionsomvej, som registered-bytes-vejen undgår
HotPDF fryser variationen ved de valgte akseværdier og skriver et CID-keyed CFF1-program, hvor hver CID er lig det oprindelige glyph ID, så intet lag oversætter mellem nummereringer

Den åbenlyse vej er værd at beskrive, for den fejler uden at fortælle nogen noget. Parse og cache CFF2-tabellerne, og lad derefter den almindelige tekststi løse fonten gennem Windows GDI med en TrueType-only match: GDI har ingen anelse om, hvad den skal stille op med CFF2-outlines, og substituerer i stilhed ArialUnicodeMS. Alt downstream, der læser substituttet, får de forkerte outlines, de forkerte bredder og den forkerte ekstraktion af supplementary-plane-tegn. PDF'en åbner fint. Den er bare ikke sat i den font, du bad om, og derfor skal tekststien arbejde ud fra de registrerede byte i stedet for et GDI-opslag

Hvorfor kan en PDF ikke indlejre en CFF2-font direkte?

PDF har ingen plads til et CFF2-program. ISO 32000-2 §9.9 lister de indlejrede fontprogram-typer, en læser skal kunne håndtere: Type 1 under /FontFile, TrueType under /FontFile2 og under /FontFile3 et rent CFF-program (Type1C eller CIDFontType0C) eller en OpenType-wrapper omkring TrueType- eller CFF-outlines. Alle CFF-varianter i listen betyder CFF version 1, det format, læsere har parset i to årtier

De tre PDF-pladser til indlejrede fonte, alle begrænset til CFF version 1, og den manglende CFF2-plads, der får HotPDF til at konvertere før indlejring i Delphi
alle de CFF-varianter, PDF-læsere skal kunne håndtere, er CFF version 1, så en CFF2 variable font har ingen plads, før den er konverteret

CFF2 er et andet format, der tilfældigvis deler et navn. Det dropper Name- og String INDEX, dropper charsettet, tager glyph advances ud af CharStrings og overlader dem til hmtx og tilføjer blend-operatoren, så én enkelt outline kan beskrive hele designrummet. En CFF1-parser, der møder en CFF2-tabel, har intet grundlag for at gætte, og intet i PDF-specifikationen forpligter den til at prøve. At pakke CFF2-fonten ind i en OpenType-stream og håbe på det bedste er præcis den slags fil, der renderer på én maskine og fejler på den næste, så HotPDF gør det ikke

Konverteringen skal også være statisk. Som artiklen om statisk instansiering af variable fonts forklarer, er en PDF-font et fast sæt outlines med faste metrics, og ingen læser er forpligtet til at anvende aksekoordinater. HotPDF løser derfor variationen først og skriver først derefter CFF1

Native tekst i en CFF2-font

For programkode opfører en CFF2-font sig som enhver anden registreret Unicode-font: vælg akseværdier, registrér filen, vælg familien ved navn, og tegn. RegisterUnicodeTTF beholder en separat kopi af den oprindelige nametabel, så SetFont matcher familienavnet, det fulde navn, PostScript-navnet eller det typografiske familienavn og vælger de frosne statiske fontbyte, uden Windows-installation eller GDI-opslag

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure.pdf';
    Pdf.BeginDoc;
    // Akseværdier gælder for fonts registreret efter dette kald
    Pdf.SetVariableFontAxis('wght', 650);
    Pdf.RegisterUnicodeTTF('C:\Fonts\AcmeSerifVF.otf');
    // Familie, fuldt navn, PostScript- eller typografisk familienavn fra nametabellen
    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'er kommer fra den registrerede cmap og advances fra den registrerede hmtx. Et glyph med en advance, der reelt er nul, forbliver nul i stedet for at falde tilbage til en GDI-måling, og et UTF-16 surrogate-par giver præcis én advance, ikke to. BMP-tegn, supplementary-tegn og syntetiske shaping-bærere beholder per-font Unicode-mappings, så et skift til en anden indlejret font eller en ny side ikke lader den anden font opdatere denne fonts subset-tilstand eller låne dens glyph-ID'er

Målingerne følger de samme byte. THPDFPage.GetWideTextWidth og THPDFPage.GetWideCharAdvances læser metrics for den valgte byte-baserede font, så linjebrydning og højrejustering stemmer med det, der tegnes. Én rækkefølge-detalje fanger folk: SetVariableFontAxis-ændringer efter registrering påvirker den næste font, du registrerer, ikke den allerede frosne instans. Vil du have en anden weight, så sæt den nye værdi og registrér igen

Hvordan bygger HPDFConvertCFF2ToCFF det CID-keyed program?

HPDFConvertCFF2ToCFF producerer et råt CID-keyed CFF1-program med en enkelt Font DICT, hvor CID er lig det oprindelige glyph ID for hver plads, også tomme og ubrugte. Den identitet er hele designet. Fontnavnet, PDF /W-bredderne, CFF-advances og ToUnicode CMap refererer alle til samme nummerering, så intet lag skal oversætte mellem glyph-ID'er og CID'er, og intet kan komme ud af trit

På PDF-siden er efterkommeren en CIDFontType0-font med Identity-H- eller Identity-V-encoding, og programstreamen ligger under /FontFile3 med /Subtype /CIDFontType0C. Fordi CID allerede er lig GID, har efterkommeren ingen /CIDToGIDMap, og streamen er udelukket fra det senere stream-replacement-hook brugt til registrerede TrueType-fonte. I modsætning til /Subtype /OpenType-wrapperen udløser den konverterede stream ikke PDF 1.6-versionskravet

Bredderne krævede et lille trick. En CFF1 Type 2 CharString gemmer advance som en valgfri første operand relativt til nominalWidthX, og heltalsoperanden er begrænset til det signerede 16-bit-område. En OpenType-advance er en unsigned 16-bit-værdi, op til 65535. HotPDF sætter nominalWidthX til 32768 og defaultWidthX til 0 og skriver derefter hver advance eksplicit som advance - 32768. En advance på nul bliver -32768, og en advance på 65535 bliver 32767, så hele uint16-området kan repræsenteres uden at clampes ét eneste glyph

Hvordan HotPDF mapper unsigned 16-bit advances ind i signede CFF1-operanden med nominalWidthX 32768 og holder hver font-advance i Delphi PDF-output uklemt
nominalWidthX 32768 flytter operandvinduet, så en advance på nul gemmes som -32768 og 65535 gemmes som 32767 uden at klemme nogen glyph

Geometrien får samme omhu. FontMatrix er [1/UnitsPerEm 0 0 1/UnitsPerEm 0 0], så en font med 2048 units beholder sine native koordinater. Relative moves og linjer, der er for store til én operand, deles op i repræsentérbare trin, og kubiske kurver subdivideres på en måde, der bevarer deres form. De oprindelige copyright- og licens-poster, den oprindelige nametabel og embedding-flagene følger med i CFF Notice-strengen

At kalde konverteren uden for et dokument

Konverteren er en offentlig funktion i HPDFOpenTypeAdvanced-unit'en, skrevet i Pascal, uden HarfBuzz-afhængighed og uden trin til installation af systemfonte. Det gør den brugbar på egen hånd, for eksempel til at pre-konvertere en font på build time eller til at inspicere, hvad PDF-writeren vil indlejre

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;

Variable input går gennem geometry-only statisk instansiering ved de givne koordinater og dækker både outlines og advances. En statisk CFF2-font accepterer kun et tomt valg; at give den akseværdier behandles som en fejl i stedet for at blive ignoreret. Resultatet er et rent CFF1-program, ikke en sfnt-container, så giv det ikke til kode, der forventer en OpenType-fil

Fejl er en tom TBytes, ikke en exception, i de forventede tilfælde: input, der ikke er CFF2, et aksevalg, som instansiereren ikke accepterer, ikke-finite værdier som NaN og fonts, hvis OS/2 fsType sætter restricted-license embedding (0x0002) eller bitmap-only embedding (0x0200). Tjek altid længden. Indeni EndDoc gør writeren det for dig og udløser "StoreFont: CFF2 outline conversion failed", hvilket er det rigtige valg: et tomt fontprogram i en afsendt PDF er langt værre end en synlig fejl i din log

Budgets og cancellation til serverworkloads

Den valgfrie tredje parameter, THPDFVariableFontControl, begrænser hukommelsen og lader dig annullere en lang konvertering. Uden den bruger konverteren sine defaults: en grænse på 128 MiB på font-input og -output, et samlet arbejdsbudget på 1 GiB og 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;  // propagerer ud af HPDFConvertCFF2ToCFF
end;

function TFontJob.Convert(const Source: TBytes): TBytes;
var
  Control: THPDFVariableFontControl;
begin
  Control := THPDFVariableFontControl.Create(
    32 * 1024 * 1024,           // maksimalt antal output-byte
    Int64(256) * 1024 * 1024,   // samlet arbejdsbudget
    CheckCancel);
  try
    // nil-valg: default-instans af en variable font eller en statisk CFF2-font
    Result := HPDFConvertCFF2ToCFF(Source, nil, Control);
  finally
    Control.Free;
  end;
end;

Budgetoverskridelser og annullering rapporteres som exceptions, bevidst adskilt fra den tomme-resultat-vej. Et tomt array betyder, at fonten ikke kan konverteres; en exception betyder, at dette forsøg blev standset. Hold de to adskilt i din fejlhåndtering, for at prøve en ukonvertérbart font igen med et større budget hjælper ikke

Hvor meget vejer en indlejret CFF2-font?

Det vigtigste at planlægge efter er filstørrelse: HotPDF indlejrer det komplette konverterede glyph-sæt for CFF2-fonte, uanset om EnableFontSubsetting er til eller fra. En font med flere tusinde glyphs tilføjer dem alle til hvert dokument, der bruger den, selv hvis siden viser én overskrift. Til en brochure er det acceptabelt. Til en rapportserver, der skriver tusindvis af små PDF'er, er det en reel omkostning, du bør måle, før du binder dig

De relaterede størrelsesfeatures følger samme regel. Konverterede CFF2-fonte indlejres hele, så hverken de kompakte font subsets aktiveret af CompressDocument eller den persistente font subset-cache gælder dem. TrueType-fonte i samme dokument subsettes og caches stadig som sædvanlig. Den ene fordel ved at beholde hvert glyph er, at fonts, hvis licens forbyder subsetting, indlejres i en form, der respekterer det

Ressourcegrænserne er endelige konverteringsgrænser, og CFF1 sætter selv loft over, hvad en enkelt CharString kan holde. Et glyph, hvis konverterede CharString overstiger 65535 byte, eller en font, der brænder igennem arbejdsbudgettet, fejler synligt i stedet for at blive skrevet med en tom outline. Det er den tiltænkte trade-off: en højlydt fejl på et eksotisk font er bedre end et dokument med lydløst manglende glyphs

Native CFF2-tekst og den portable CFF1-konverter følger med i HotPDF Delphi PDF component til Delphi og C++Builder, sammen med variable font-instansiering, color glyph- og font subsetting-features dækket andre steder på denne blog