Artykuł techniczny

Czcionki internetowe w PDF: WOFF2 i osie zmienne w Delphi

PDF Library for Delphi akceptuje czcionki, które projekt webowy już zawiera. Importuje kontenery WOFF2, wylicza osie projektowe czcionki zmiennej jako JSON i przekształca wybrany zestaw współrzędnych osi w statyczną czcionkę, którą zwykłe ścieżki osadzania, tworzenia podzbioru i ekstrakcji obsługują, nie wiedząc niczego o wariacjach. Obejmuje to zarówno kontury TrueType, jak i kontury CFF2

Sytuacja, którą to rozwiązuje, jest częsta i irytująca. Typografia marki jest zdefiniowana w design systemie, design system dostarcza zmienne pliki WOFF2, a generator PDF nie potrafi odczytać ani kontenera, ani danych wariacji. Zwykłe obejście — poproszenie odlewni czcionek o statyczne instancje TTF każdej faktycznie używanej grubości — zajmuje tygodnie i daje pliki, których nikt nie utrzymuje w synchronizacji

Dlaczego WOFF2 to nie tylko skompresowana czcionka?

Potraktowanie WOFF2 jako opakowania Brotli i jego zwykłe zdekompresowanie daje bajty, które nie są prawidłowym sfnt. Format pozwala też na przekształcone i przestawione tabele: glyf, loca i hmtx mogą być przechowywane w przestawionej formie, którą trzeba odwrócić, zanim czcionka stanie się możliwa do sparsowania. Dekompresor ogólnego przeznaczenia tego nie potrafi

W Windows platforma zapewnia analizę i rozpakowywanie kontenera od Windows 10 wersji 1703, więc biblioteka korzysta z implementacji systemowej zamiast dostarczać kolejny dekoder. Ta decyzja utrzymuje wdrożenie wolne od dodatkowej DLL i oznacza, że odwracanie transformacji obsługuje ten sam kod, którego używa własny stos tekstowy systemu operacyjnego. Na innych platformach WOFF1 wciąż przechodzi czystą ścieżką Pascal, a WOFF2 jawnie zgłasza się jako nieobsługiwany, co jest uczciwą odpowiedzią zamiast po cichu uszkodzonej czcionki

Ścieżka importu waliduje zarówno przed dekodowaniem, jak i po: 48-bajtowy nagłówek, długość kontenera, zarezerwowane pola, zadeklarowany rozmiar sfnt i limit 512 MiB są sprawdzane najpierw, a rozmiar zdekodowanego strumienia jest potem porównywany z deklaracją. Czcionka internetowa to pobrana treść, a dwa tryby błędu warte zablokowania to niezgodność rozmiaru i bomba dekompresyjna

Ten diagram potoku pokazuje, jak biblioteka PDF dla Delphi importuje internetową czcionkę WOFF2 przez walidację nagłówka, dekodowanie systemowe, odwrócenie transformacji sfnt i audyt rozmiaru po dekodowaniu
Kontenery WOFF2 przychodzą jako pobrana treść, więc import waliduje nagłówek i zadeklarowane rozmiary przed dekodowaniem i po nim
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 i TTC wchodzą wszystkie przez to samo wywołanie
    FontID := Lib.AddOpenTypeFontFromFile('Inter.woff2', 0);
    if FontID <= 0 then
      raise Exception.Create('font could not be imported');

    // Co ta czcionka faktycznie potrafi?
    if Lib.GetFontVariationAxes(FontID, AxesJSON) = 1 then
      Log(AxesJSON);   // tagi, minimum, wartość domyślna, maksimum dla każdej osi

    // Wypal jedną lokalizację projektową w statyczną czcionkę
    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;

Co tworzenie instancji musi odbudować i co usuwa

Czcionka zmienna przechowuje jeden domyślny kontur na glif plus delty indeksowane pozycją w przestrzeni projektowej. Wyprodukowanie statycznej instancji oznacza interpolację tych delt przy żądanych współrzędnych, przesunięcie punktów, a następnie ponowne obliczenie wszystkiego, co z nich wynika

Dla konturów TrueType oznacza to parsowanie fvar, avar, gvar, HVAR i MVAR, zastosowanie delt krotek ze współdzieloną i prywatną numeracją punktów oraz interpolację punktów niemających jawnej delty. Poziome punkty widmowe (phantom points) razem z HVAR określają szerokość przesunięcia i lewy margines boczny. Glify złożone wymagają najpierw rozwiązania zależności swoich komponentów i mapowań punktów, a potem zastosowania translacji, skali, macierzy i przesunięć dołączenia — z limitami głębokości i wykrywaniem cykli, ponieważ złośliwy graf komponentów w przeciwnym razie rekurowałby bez końca

Metadane też się przesuwają, i to jest ta część, którą pomija większość implementacji. MVAR aktualizuje globalne metryki w hhea, OS/2 i post, więc odstęp linii, pozycja podkreślenia i kąt pochylenia pasują do instancji, a nie do wartości domyślnej. Osie wght, wdth, ital i slnt dodatkowo aktualizują klasę grubości, klasę szerokości, flagi selekcji, macStyle i kąt pochylenia, więc narzędzie inspekcjonujące statyczną czcionkę zgłasza styl, o który poprosiłeś. Czcionka, której geometria jest pogrubiona, a metadane mówią „regular”, będzie błędnie oznaczona wszędzie dalej w potoku

Wyjście następnie spłaszcza kontury złożone i usuwa tabele, które już nie mają zastosowania: tabele wariacji, hinty, tabele urządzeń i wszelkie sygnatury. Granice glifów, zbiorcze metryki hhea, limity hintów maxp, sumy kontrolne poszczególnych tabel i suma kontrolna całej czcionki są ponownie obliczane

Ten diagram pokazuje tworzenie instancji czcionki zmiennej, odbudowujące krój TrueType przez interpolację delt gvar, rozwijanie glifów złożonych, ponowne obliczanie przesunięć i przepisywanie metadanych stylu przed usunięciem tabel wariacji
Tworzenie instancji odbudowuje wszystko, czego dotknęły delty, a następnie usuwa mechanizm wariacji, dzięki czemu zwykłe osadzanie widzi zwykłą, statyczną czcionkę

CFF2 dochodzi do tego samego miejsca inną drogą

CFF2 nie można osadzić bezpośrednio jako programu czcionki PDF: jego charstrings nie niosą szerokości i polega on na otaczających go tabelach OpenType co do kolejności glifów i metryk. Dlatego tworzenie instancji czcionki CFF2 daje zamiast tego czcionkę OpenType CFF indeksowaną przez CID, którą istniejące ścieżki rysowania, osadzania i tworzenia podzbioru już obsługują

Wczytanie czcionki CFF2 od razu tworzy statyczną instancję o domyślnych współrzędnych dla bieżącego dokumentu, podczas gdy oryginalny sfnt CFF2 jest zachowywany w obiekcie czcionki, więc wyliczanie osi i późniejsze tworzenie instancji wciąż widzą pełną przestrzeń projektową. Każdy glif jest indywidualnie pozbawiany podprogramów (de-subroutinised) podczas tworzenia instancji, co całkowicie usuwa arytmetykę wariacji i pozwala istniejącemu narzędziu do tworzenia podzbiorów działać bez zmian. Kompromisem jest to, że statyczna instancja może być nieco większa niż taka, która zachowała współdzielone podprogramy — uczciwa cena za brak konieczności drugiej implementacji tworzenia podzbiorów

Weryfikacja instancji, zanim jej zaufasz

Tworzenie instancji to rodzaj funkcji, która może być subtelnie błędna w sposób, o którym żaden wyjątek nie poinformuje. Trzy kontrole wyłapują prawie wszystko

Wyeksportuj instancję i otwórz ją w narzędziu do czcionek. SaveFontToFile zapisuje zachowany program czcionki, więc instancję w pamięci można zrzucić bez wcześniejszego wytwarzania PDF. Wyeksportowany plik powinien parsować się bezbłędnie, nie zawierać żadnych tabel wariacji i zgłaszać metadane stylu, o które poprosiłeś

Porównaj przesunięcia w lokalizacji innej niż domyślna. Jeśli twój potok potrafi wyprodukować tę samą instancję innym sposobem, porównaj szerokości przesunięć i granice krzywych glif po glifie; implementacja referencyjna zgadzała się tutaj z niezależnym tworzeniem instancji czcionki o 313 glifach i dwóch osiach z maksymalną różnicą zero na każdym glifie

I wyodrębnij tekst z powrotem z wygenerowanego PDF. Instancja, która się osadza, ale nie daje możliwego do wyodrębnienia tekstu, utraciła swoje mapowanie ToUnicode, co ma znaczenie dla wyszukiwania i dostępności długo po tym, jak kontrola wizualna przeszła. Szerszą ścieżkę ekstrakcji opisano w ekstrakcji tekstu, obrazów i czcionek

Ten diagram pokazuje trzy kontrole weryfikacyjne dla wygenerowanej instancji czcionki zmiennej: zrzut wyeksportowanego pliku czcionki, porównanie przesunięć glifów z niezależnym tworzeniem instancji i wyodrębnienie tekstu z powrotem z PDF
Subtelnie błędna instancja nie zgłasza żadnego wyjątku, więc wyeksportowany plik, przesunięcia glifów i wyodrębniony tekst są weryfikowane po kolei
// Zrzuć wygenerowaną instancję do inspekcji albo ponownego użycia
Lib.SaveFontToFile('Inter-620-87.ttf');

// Poproszenie o współrzędne poza zadeklarowanym zakresem powoduje ich obcięcie,
// więc odczytaj JSON osi zamiast zakładać limity czcionki
// {"axes":[{"tag":"wght","min":100,"default":400,"max":900}, ...]}

Praktyczne uwagi dla potoku czcionek marki

Buforuj instancje według zestawu współrzędnych. Każdy odrębny zestaw współrzędnych to odrębny program czcionki, więc dokument mieszający grubość 400 i grubość 620 osadza dwa podzbiory zamiast jednego współdzielonego kroju. Wielokrotne generowanie tej samej instancji to zmarnowana praca; generowanie kilkudziesięciu niemal identycznych grubości to zmarnowany rozmiar pliku

Mierz instancją, którą będziesz rysować. Instancja wąska (condensed) ma węższe przesunięcia niż domyślna, więc każdy układ, który zmierzył szerokości przed utworzeniem instancji, zmierzył złe szerokości. To pojedyncza najczęstsza przyczyna tekstu, który przelewa się poza swój box tylko na produkcji

Utrzymuj ścieżkę zapasową dla glifów, których brakuje twojej czcionce marki. Zmienna czcionka marki zwykle obejmuje łacinkę i niewiele więcej, a raport zawierający cyrylicką nazwę klienta w przeciwnym razie pokaże brakujące glify; automatyczny mechanizm zapasowy opisano w automatycznym zastępowaniu czcionek dla brakujących glifów. A gdy celem jest mniejszy plik, a nie konkretna grubość, tworzenie podzbioru daje więcej efektu niż tworzenie instancji, co opisano w optymalizacji rozmiaru pliku PDF i tworzeniu podzbiorów czcionek

Import czcionek internetowych, tworzenie instancji zmiennych, tworzenie podzbiorów i mechanizm zapasowy są częścią tego samego potoku czcionek dla Delphi, C++Builder i Free Pascal; pełną listę funkcji znajdziesz na stronie PDF Library for Delphi