Technical Article

Warianty stylistyczne OpenType GSUB w czystym Delphi

Projektant wybiera czcionkę z jednopoziomowym a do nagłówków, przekreślonym zerem do tabel lub zestawem ozdobnych wielkich liter (swash capitals) na okładkę. Te glify znajdują się już w czcionce. Po prostu nie są domyślne. Domyślne a jest mapowane ze znaku poprzez tabelę cmap na jeden glif, a wariant alternatywny leży o kilka identyfikatorów glifów dalej, dostępny wyłącznie za pośrednictwem reguły substytucji. Wygenerowanie tego wariantu w PDF wymaga odczytania reguły i umieszczenia zastępczego glifu w strumieniu zawartości. Ten artykuł opisuje odczytywanie tych reguł, w szczególności substytucji pojedynczych (single-substitution), w języku Object Pascal bez użycia natywnej biblioteki kształtowania pod spodem.

Zakres artykułu jest celowo wąski. Zestawy stylistyczne i warianty alternatywne to substytucje typu pojedynczy glif na wejściu, pojedynczy glif na wyjściu. Są to elementy układu OpenType, które można rozwiązać za pomocą krótkiego, deterministycznego przejścia po tabeli, co czyni je idealnymi dla silnika napisanego w Pascalu, który ma pozostać wolny od zależności w języku C.

Dlaczego czyste Delphi zamiast HarfBuzz

Biblioteka HarfBuzz to oczywista odpowiedź na pytanie jak ukształtować tekst i w przypadku pełnego kształtowania tekstu dwukierunkowego, indyjskiego czy arabskiego jest to właściwy wybór. Jest to jednak również biblioteka w języku C. Powiązanie jej z produktem w Delphi lub C++Builder oznacza konieczność dostarczania natywnego obiektu dla każdej platformy docelowej i architektury, dopasowania konwencji wywołań, śledzenia cyklu wydań i analizowania warunków licencyjnych w odniesieniu do własnych. Żadna z tych rzeczy z osobna nie jest trudna. Wszystko to stanowi jednak narzut pracy, który nigdy nie znika i nie przynosi żadnych korzyści, gdy rzeczywistym wymaganiem jest po prostu pobranie formy ss01 danej litery.

Pojedyncza substytucja nie wymaga silnika kształtowania tekstu. Wymaga parsera dla kilku formatów podtabel GSUB oraz jednego lub dwóch wyszukiwań binarnych. Napisanie tego w Pascalu pozwala zachować cały łańcuch narzędzi w obrębie jednego kompilatora. Uczciwym ograniczeniem jest to, że to podejście obsługuje wyłącznie wyszukiwanie substytucji glifów i nic więcej. Nie jest to obsługa tekstu dwukierunkowego (bidi), reorganizacja pisma indyjskiego ani automatyczne kształtowanie kontekstowe. Tam, gdzie te funkcje są potrzebne, są po prostu niezastąpione i zapytanie o pojedynczą substytucję ich nie zastąpi.

Hierarchia GSUB od góry do dołu

Tabela substytucji glifów (Glyph Substitution table) jest zorganizowana jako łańcuch przekierowań, a zapytanie o substytucję przeszukuje ten łańcuch od samej góry. Na szczycie znajduje się ScriptList. Znacznik pisma, taki jak latn, wybiera odpowiedni wpis, a specjalny znacznik DFLT to domyślne pismo stosowane, gdy nie pasuje żadne bardziej szczegółowe pismo. Wpis pisma wskazuje na system językowy LangSys, z domyślnym systemem LangSys dla typowych przypadków oraz opcjonalnymi nazwanymi systemami dla języków wymagających innego zachowania. Klasycznym przykładem jest język turecki, gdzie litery i z kropką i bez kropki wymagają osobnej obsługi.

System LangSys wskazuje zestaw indeksów funkcji (features). Każdy indeks prowadzi do listy FeatureList, gdzie rekord funkcji niesie czterobajtowy znacznik, w tym ss01, oraz listę indeksów wyszukiwania (lookup indices). Te indeksy wskazują ostatecznie na LookupList, gdzie znajdują się właściwe podtabele substytucji. Rozwiązanie ss01 oznacza zatem: znajdź pismo, znajdź jego system LangSys, znajdź funkcję o znaczniku ss01, zbierz wskazane wyszukiwania i zastosuj je. HotPDF domyślnie korzysta z pisma DFLT i domyślnego systemu LangSys, co odpowiada zdecydowanej większości projektów tekstów łacińskich, oraz udostępnia sposób na nadpisanie znacznika pisma, gdy czcionka wiąże swoje funkcje pod konkretnym pismem.

var
  Pdf: THotPDF;
  BaseGID, AltGID: Word;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.RegisterUnicodeTTF('C:\Fonts\MyStylisticFace.ttf');
    Pdf.SetFont('My Stylistic Face', 12, []);

    // Default glyph for 'a' through the font's cmap.
    BaseGID := Pdf.GetUnicodeGlyphForCodepoint(Ord('a'));

    // Stylistic Set 1: resolve the alternate via GSUB LookupType 1.
    AltGID := Pdf.GetSingleSubstituteGlyph(BaseGID, 'ss01');

    // AltGID = BaseGID means the feature did not touch this glyph.
    if AltGID <> BaseGID then
      { emit AltGID in the content stream };
  finally
    Pdf.Free;
  end;
end;

Kontraktem wartym odnotowania jest przekazywanie wartości w przypadku braku dopasowania. Funkcja GetSingleSubstituteGlyph zwraca niezmieniony identyfikator glifu wejściowego przy każdym braku dopasowania: brak czcionki, brak tabeli GSUB, brak pasującej funkcji czy brak pokrycia. Oznacza to, że wywołanie można bezpiecznie wykonywać bezwarunkowo. Pytasz o wariant alternatywny i jeśli go nie ma, otrzymujesz dokładnie to, co przekazałeś na wejściu, dzięki czemu kod wywołujący nigdy nie musi obsługiwać czcionki bez tej funkcji jako przypadku szczególnego.

Co oznaczają znaczniki funkcji stylistycznych

Znacznik funkcji to całe słownictwo określające, o jaki wariant alternatywny pytasz, a znaczniki związane z pracami stylistycznymi to krótka lista. Główną parą jest salt (stylistic alternates) — ogólny dostęp do alternatywnych form glifu, oraz ss01 do ss20 — dwadzieścia numerowanych zestawów stylistycznych, które czcionka może zdefiniować, z których każdy jest nazwanym pakietem substytucji zgrupowanych przez projektanta. Czcionka może na przykład umieścić jednopoziomowe a i R z prostą nóżką w zestawie ss03, więc włączenie tego jednego zestawu zmieni styl obu znaków.

// Try a stylistic-set feature, then fall back to plain alternates.
function ResolveAlternate(Pdf: THotPDF; BaseGID: Word;
  const PreferredTag: AnsiString): Word;
begin
  Result := Pdf.GetSingleSubstituteGlyph(BaseGID, PreferredTag);
  if Result = BaseGID then
    Result := Pdf.GetSingleSubstituteGlyph(BaseGID, 'salt');
  // Still BaseGID if neither feature covers this glyph.
end;

Obok nich występuje jeszcze kilka innych znaczników pojedynczych substytucji. aalt (access-all-alternates) to suma wszystkich wariantów alternatywnych, jakie posiada glif, zazwyczaj prezentowana jako paleta glifów. Znacznik titl wybiera wersaliki tytułowe przeznaczone do dużych rozmiarów. Znaczniki subs i sups podmieniają cyfry na rzeczywisty indeks dolny i górny zamiast skalowania wartości domyślnych. Znacznik ordn tworzy formy liczebników porządkowych, czyli uniesione litery w zapisach typu 1st i 2nd. Znacznik frac buduje ułamki, choć pełne ułamki po przekątnej opierają się również na logice ligatur i kontekstu, wykraczającej poza zwykłą pojedynczą substytucję. W przypadkach pojedynczych glifów mechanizm jest identyczny jak dla ss01: przekaż znacznik do zapytania o substytucję i odczytaj alternatywny glif.

Format 12 tabeli cmap i płaszczyzny uzupełniające

Zanim uruchomiona zostanie jakikolwiek substytucja, znak musi zostać zamieniony na glif — i to jest zadanie tabeli cmap. Zapytanie o substytucję rozpoczyna się od identyfikatora glifu, więc ścieżka zawsze prowadzi od znaku do glifu przez cmap, a następnie od glifu do wariantu alternatywnego przez GSUB. Ciekawą cechą cmap jest jej zasięg. Podtabela formatu 4 obejmuje podstawową płaszczyznę wielojęzyczną BMP (Basic Multilingual Plane), czyli pierwsze 65536 punktów kodowych, co wystarcza dla większości tekstów łacińskich. Nie jest to jednak wystarczające dla punktów kodowych od U+10000 w górę, czyli płaszczyzn uzupełniających, na których znajdują się m.in. znaki matematyczne alfanumeryczne, wiele symboli oraz różne współczesne pisma.

Format 12 to podtabela obejmująca pełny zakres od U+0000 do U+10FFFF. Jest to posortowana lista grup, z których każda zawiera początkowy punkt kodowy, końcowy punkt kodowy oraz początkowy identyfikator glifu, dzięki czemu ciągła seria punktów kodowych mapuje się na ciągłą serię glifów. HotPDF rozstrzyga punkty kodowe za pomocą strategii hybrydowej dopasowanej do kształtu danych. Punkty kodowe w obszarze BMP są obsługiwane z bezpośredniej tablicy indeksowanej punktem kodowym — to pojedyncze wyszukanie bez przeszukiwania. Punkty kodowe w płaszczyznach uzupełniających są obsługiwane z rzadkiej tabeli posortowanej według punktów kodowych i przeszukiwanej za pomocą wyszukiwania binarnego. W rezultacie funkcja GetUnicodeGlyphForCodepoint przyjmuje pełny typ Cardinal i odpowiada poprawnie w całym zakresie, zwracając identyfikator glifu 0, czyli glif .notdef, dla każdego punktu kodowego, którego czcionka nie mapuje.

var
  Pdf: THotPDF;
  Cp: Cardinal;
  GID, StyledGID: Word;
begin
  // A supplementary-plane code point: U+1D49C MATHEMATICAL SCRIPT CAPITAL A.
  Cp := $1D49C;
  GID := Pdf.GetUnicodeGlyphForCodepoint(Cp);  // format 12 lookup
  if GID <> 0 then
    StyledGID := Pdf.GetSingleSubstituteGlyph(GID, 'ss01')
  else
    StyledGID := 0;  // font has no glyph for this code point
end;

Gdzie te zapytania się kończą

Interfejsy API pojedynczych substytucji odpowiadają na pytania o określonym kształcie i warto jasno powiedzieć, na co nie odpowiadają. LookupType 1 to jeden z ośmiu typów substytucji. Zapytanie to nie obsługuje substytucji wielokrotnej LookupType 2, w której jeden glif zamienia się w kilka, ani substytucji ligatur LookupType 4, gdzie kilka glifów tworzy jeden. Nie obsługuje również typów kontekstowych i łańcuchowo-kontekstowych (LookupTypes 5 i 6), które uruchamiają się tylko wtedy, gdy glif pojawia się w określonym sąsiedztwie, ani typów rozszerzeń i substytucji odwrotnych. Ułamek po przekątnej, spójka w piśmie Devanagari czy arabska kaskada form początkowych-środkowych-końcowych to problem sekwencji, a przeszukiwanie pojedynczych substytucji dla pojedynczych glifów nie pozwala na jego wyrażenie.

Nie realizuje również automatycznego kształtowania tekstu. Nic w tym miejscu nie bada fragmentu tekstu, nie decyduje, które funkcje włączyć, ani nie aplikuje ich w kolejności wymaganej przez pismo. Wywołujący wybiera znacznik funkcji i stosuje go glif po glifie. Jest to dokładnie to narzędzie, którego potrzebujemy do zestawów i wariantów stylistycznych, które są lokalne i wymagają jawnego włączenia, i zupełnie niewłaściwe dla pism wymagających zmiany kolejności znaków. Utrzymanie wyraźnej granicy pozwala zachować prostotę i przewidywalność ścieżki substytucji.

Dla przypadków wymagających operacji na poziomie sekwencji, kwestie pism złożonych opisano w naszym artykule na temat kształtowania tekstów złożonych w Delphi. Jeśli Twoje substytucje są częścią większego zadania raportowania, które umieszcza również obrazy i inne czcionki na stronie, przewodnik po generowaniu raportów z czcionkami i obrazami omawia sposób łączenia tych elementów. Wszystko to działa na tym samym silniku, czyli pakiecie HotPDF Component dla Delphi i C++Builder, który realizuje zapytania o substytucję GSUB obok interfejsów API do osadzania czcionek, tworzenia podzbiorów i obsługi tekstu, o których mowa w innych artykułach na tym blogu.