Teknisk artikel

Webskrifttyper i PDF: WOFF2 og variable akser i Delphi

PDF Library for Delphi accepterer de skrifttyper, et webprojekt allerede leverer. Den importerer WOFF2-containere, opremser designakserne for en variabel skrifttype som JSON, og instantierer et valgt sæt akse-koordinater til en statisk skrifttype, som de almindelige indlejrings-, delmængdedannelses- og udtrækningsstier håndterer uden at vide noget om variationer. Både TrueType-omridser og CFF2-omridser er dækket

Den situation, dette adresserer, er almindelig og irriterende. Et brands typografi er defineret i et designsystem, designsystemet leverer variable WOFF2-filer, og PDF-generatoren kan hverken læse containeren eller variationsdataene. Den sædvanlige workaround — at bede skrifttypehuset om statiske TTF-instanser af hver faktisk brugt vægt — tager uger og giver filer, ingen holder synkroniseret

Hvorfor er WOFF2 ikke bare en komprimeret skrifttype?

At behandle WOFF2 som en Brotli-wrapper og dekomprimere den giver dig bytes, der ikke er en gyldig sfnt. Formatet tillader også transformerede og omordnede tabeller: glyf, loca og hmtx kan gemmes i en omarrangeret form, der skal vendes tilbage, før skrifttypen kan parses. En generel dekompressor kan ikke det

På Windows har platformen leveret container-analyse og udpakning siden Windows 10 version 1703, så biblioteket bruger systemimplementeringen frem for at levere endnu en dekoder. Den beslutning holder deploymentet fri for en ekstra DLL og betyder, at transform-tilbagevendingen håndteres af den samme kode, operativsystemets egen tekststak bruger. På andre platforme rejser WOFF1 stadig ad en ren Pascal-sti, og WOFF2 rapporterer eksplicit som ikke-understøttet, hvilket er et ærligt svar frem for en stiltiende korrumperet skrifttype

Importstien validerer både før og efter afkodning: 48-byte-headeren, container-længden, de reserverede felter, den deklarerede sfnt-størrelse og et loft på 512 MiB tjekkes alle først, og den afkodede stream-størrelse sammenlignes med deklarationen bagefter. En webskrifttype er downloadet indhold, og de to fejltilstande, det er værd at blokere, er en størrelsesuoverensstemmelse og en dekomprimeringsbombe

Dette pipeline-diagram viser, hvordan Delphi PDF-biblioteket importerer en WOFF2-webskrifttype gennem headervalidering, systemafkodning, sfnt-transformtilbagevending og et størrelsestjek efter afkodning
WOFF2-containere ankommer som downloadet indhold, så importen validerer headeren og de deklarerede størrelser både før og efter afkodning
uses
  PDFlibrary;

var
  Lib: TPDFlib;
  FontID, InstanceID: Integer;
  AxesJSON: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.SetPageSize('A4');
    Lib.NewPage;

    // WOFF2, WOFF1, TTF, OTF og TTC går alle ind gennem det samme kald
    FontID := Lib.AddOpenTypeFontFromFile('Inter.woff2', 0);
    if FontID <= 0 then
      raise Exception.Create('font could not be imported');

    // Hvad kan denne skrifttype egentlig?
    if Lib.GetFontVariationAxes(FontID, AxesJSON) = 1 then
      Log(AxesJSON);   // tags, minimum, standard, maksimum pr. akse

    // Bag én designposition ind i en statisk skrifttype
    InstanceID := Lib.CreateOpenTypeFontVariation(FontID,
      'wght=620,wdth=87.5');

    Lib.DrawOpenTypeText(56, 760, InstanceID, 24, 'Quarterly results', 0);
    Lib.SaveToFile('report.pdf');
  finally
    Lib.Free;
  end;
end;

Hvad instantiering skal genopbygge, og hvad den fjerner

En variabel skrifttype gemmer ét standardomrids pr. glyf plus deltaer indekseret efter position i designrummet. At producere en statisk instans betyder at interpolere de deltaer ved de ønskede koordinater, flytte punkterne og derefter genberegne alt, der er udledt af dem

For TrueType-omridser betyder det at parse fvar, avar, gvar, HVAR og MVAR, anvende tuple-deltaer med delt og privat punktnummerering, og interpolere de punkter, der ikke bærer noget eksplicit delta. Vandrette phantom-punkter sammen med HVAR bestemmer advance width og venstre sidebæring. Sammensatte glyffer skal have deres komponentafhængigheder og punktkortlægninger opløst først, hvorefter translation, skalering, matrix og fastgørelsesoffsets anvendes — med dybdegrænser og cyklusdetektion, fordi en ondsindet komponentgraf ellers ville rekursere uden ende

Metadata flytter også med, og det er den del, de fleste implementeringer springer over. MVAR opdaterer de globale metrikker i hhea, OS/2 og post, så linjeafstand, understregningsposition og kursivvinkel matcher instansen frem for standarden. Akserne wght, wdth, ital og slnt opdaterer desuden vægtklassen, breddeklassen, selektionsflagene, macStyle og kursivvinklen, så et værktøj, der inspicerer den statiske skrifttype, rapporterer den stil, du bad om. En skrifttype, hvis geometri er fed, og hvis metadata siger almindelig, vil blive fejlmærket alle steder downstream

Outputtet flader derefter sammensatte omridser og fjerner de tabeller, der ikke længere gælder: variationstabellerne, hints, device-tabeller og enhver signatur. Glyfgrænser, hhea-sammenfatningsmetrikker, maxp-hint-lofterne, checksummer pr. tabel og checksummen for hele skrifttypen genberegnes alle

Dette diagram viser variabel skrifttype-instantiering, der genopbygger en TrueType-face ved at interpolere gvar-deltaer, udfolde sammensatte glyffer, genberegne advances og omskrive stil-metadata, før variationstabellerne fjernes
Instantiering genopbygger alt, deltaerne rørte ved, og fjerner derefter variationsmaskineriet, så almindelig indlejring ser en almindelig statisk skrifttype

CFF2 tager en anden rute til samme sted

CFF2 kan ikke indlejres direkte som et PDF-skrifttypeprogram: dens charstrings bærer ingen bredder, og den er afhængig af de omgivende OpenType-tabeller for glyfrækkefølge og metrikker. Så at instantiere en CFF2-skrifttype producerer i stedet en CID-nøglet CFF OpenType-skrifttype, som de eksisterende tegne-, indlejrings- og delmængdedannelsesstier allerede håndterer

At indlæse en CFF2-skrifttype producerer straks en statisk instans ved standardkoordinaterne til det aktuelle dokument, mens den originale CFF2-sfnt bevares på skrifttypeobjektet, så akseopremsning og senere instantiering stadig ser det fulde designrum. Hver glyf de-subrutiniseres individuelt under instantiering, hvilket fjerner variationsaritmetikken helt og lader den eksisterende delmængdedanner arbejde uændret. Prisen er, at en statisk instans kan være lidt større end én, der beholdt delte subrutiner — en fair pris for ikke at skulle have en anden delmængdedannelsesimplementering

Verificering af en instans, før du stoler på den

Instantiering er den slags funktion, der kan være subtilt forkert på måder, ingen exception rapporterer. Tre tjek fanger næsten alt

Eksportér instansen, og åbn den i et skrifttypeværktøj. SaveFontToFile skriver det bevarede skrifttypeprogram, så en in-memory-instans kan dumpes uden først at producere en PDF. Den eksporterede fil bør parses rent, indeholde ingen variationstabeller, og rapportere den stil-metadata, du bad om

Sammenlign advances ved en ikke-standard position. Hvis din pipeline kan producere den samme instans på en anden måde, skal du sammenligne advance widths og kurvegrænser glyf for glyf; referenceimplementeringen her matchede en uafhængig instantiering af en 313-glyf, to-akset skrifttype med en maksimal forskel på nul på tværs af hver glyf

Og udtræk teksten tilbage ud af den genererede PDF. En instans, der indlejres, men ikke producerer udtrækbar tekst, har mistet sin ToUnicode-kortlægning, hvilket betyder noget for søgning og tilgængelighed længe efter, at det visuelle tjek bestod. Den bredere udtrækningssti er dækket i tekst-, billed- og skrifttypeudtrækning

Dette diagram viser tre verifikationstjek for en genereret variabel skrifttype-instans: at dumpe den eksporterede skrifttypefil, sammenligne glyf-advances mod en uafhængig instantiering og udtrække tekst tilbage ud af PDF'en
En subtilt forkert instans kaster ingen exception, så den eksporterede fil, glyf-advances og udtrukket tekst verificeres hver for sig
// Dump en genereret instans til inspektion eller genbrug
Lib.SaveFontToFile('Inter-620-87.ttf');

// Beder du om koordinater uden for det deklarerede interval, klemmes de fast,
// så læs akse-JSON'en frem for at antage en skrifttypes grænser
// {"axes":[{"tag":"wght","min":100,"default":400,"max":900}, ...]}

Praktiske noter til en brand-skrifttype-pipeline

Cache instanser efter koordinatsæt. Hvert distinkt sæt koordinater er et distinkt skrifttypeprogram, så et dokument, der blander vægt 400 og vægt 620, indlejrer to delmængder frem for én delt face. At generere den samme instans gentagne gange er spildt arbejde; at generere dusinvis af næsten identiske vægte er spildt filstørrelse

Mål med den instans, du vil tegne med. En kondenseret instans har smallere advances end standarden, så ethvert layout, der målte før instantiering, målte de forkerte bredder. Dette er den enkeltstående mest almindelige årsag til tekst, der kun løber over sin boks i produktion

Behold en fallback-sti til de glyffer, dit brand-skrifttype mangler. En variabel brand-skrifttype dækker typisk latin og lidt andet, og en rapport, der indeholder et kyrillisk kundenavn, vil ellers vise manglende glyffer; den automatiske fallback-mekanisme er beskrevet i automatisk skrifttypefallback for manglende glyffer. Og når målet er en mindre fil frem for en bestemt vægt, udretter delmængdedannelse mere arbejde end instantiering, som dækket i optimering af PDF-filstørrelse og skrifttype-delmængdedannelse

Webskrifttype-import, variabel instantiering, delmængdedannelse og fallback er del af den samme skrifttype-pipeline til Delphi, C++Builder og Free Pascal; den komplette funktionsliste findes på PDF Library for Delphi-siden