Technical Article

Alternatif Gaya OpenType GSUB di Delphi Murni

Seorang desainer memilih font dengan karakter a berlantai tunggal (single-story) untuk header, atau nol bergaris miring (slashed zero) untuk tabel, atau set huruf kapital hias (swash capitals) untuk sampul. Glif-glif tersebut sudah ada di dalam font. Mereka hanya bukan merupakan nilai default. Karakter default a memetakan dari karakter tersebut melalui tabel cmap ke satu glif, dan alternatifnya berada beberapa ID glif di dekatnya, hanya dapat dijangkau melalui aturan substitusi. Menghasilkan alternatif tersebut dalam PDF berarti membaca aturan tersebut dan mengeluarkan glif pengganti dalam aliran konten. Artikel ini membahas tentang membaca aturan-aturan tersebut, jenis substitusi tunggal (single-substitution), dalam Object Pascal tanpa pustaka pembentukan (shaping library) asli di bawahnya.

Cakupannya sengaja dibuat sempit. Set gaya (stylistic sets) dan alternatifnya adalah substitusi satu-glif-masuk, satu-glif-keluar. Mereka adalah bagian dari tata letak OpenType yang dapat Anda selesaikan dengan penelusuran tabel deterministik yang kecil, menjadikannya sangat cocok untuk mesin Pascal yang ingin tetap bebas dari ketergantungan C.

Mengapa menggunakan Delphi murni alih-alih HarfBuzz

HarfBuzz is jawaban yang jelas untuk "membentuk teks ini" (shape this text), dan untuk pembentukan dua arah (bidirectional), bahasa rumpun India (Indic), atau Arab, itu adalah jawaban yang tepat. Namun, itu juga merupakan pustaka C. Mengikatnya ke dalam produk Delphi atau C++Builder berarti mengirimkan objek asli untuk setiap platform target dan arsitektur, mencocokkan konvensi panggilannya, melacak irama rilisnya, dan membaca ketentuan lisensinya terhadap lisensi Anda sendiri. Tidak ada dari hal tersebut yang sulit jika dilakukan secara terisolasi. Namun, semua itu adalah gesekan yang tidak pernah hilang, dan tidak menghasilkan apa-apa ketika persyaratan sebenarnya hanyalah "beri saya bentuk ss01 dari huruf ini".

Substitusi tunggal tidak memerlukan mesin pembentuk (shaping engine). Ia memerlukan parser untuk beberapa format subtabel GSUB dan satu atau dua pencarian biner (binary search). Menulis hal tersebut di Pascal menjaga seluruh rantai alat (toolchain) tetap berada di dalam satu kompilator. Batasan jujurnya adalah bahwa pendekatan ini hanya menangani pencarian substitusi glif dan tidak ada yang lain. Ini bukan resolusi bidi (dua arah), bukan pengurutan ulang bahasa rumpun India, dan bukan pembentukan kontekstual otomatis. Di mana hal-hal tersebut dibutuhkan, mereka tetap dibutuhkan, dan kueri substitusi tunggal tidak akan dapat menggantikannya.

Hierarki GSUB, dari atas ke bawah

Tabel Substitusi Glif (Glyph Substitution table) diatur sebagai rantai penyimpangan (indirections), dan kueri substitusi menelusuri rantai tersebut dari atas. Di bagian paling atas adalah ScriptList. Tag skrip seperti latn memilih entri, dan tag khusus DFLT adalah skrip default yang berlaku ketika tidak ada skrip yang lebih spesifik yang cocok. Entri skrip menunjuk ke LangSys, sistem bahasa, dengan LangSys default untuk kasus umum dan yang bernama opsional untuk bahasa yang membutuhkan perilaku berbeda. Bahasa Turki adalah contoh umum, di mana karakter i dengan titik dan tanpa titik menuntut penanganannya sendiri.

LangSys menamai sekumpulan indeks fitur. Setiap indeks menunjuk ke FeatureList, di mana rekaman fitur membawa tag empat byte, di antaranya adalah ss01, dan daftar indeks pencarian. Indeks tersebut akhirnya menunjuk ke LookupList, tempat subtabel substitusi yang sebenarnya berada. Jadi, menyelesaikan ss01 berarti: temukan skripnya, temukan LangSys-nya, temukan fitur yang tag-nya adalah ss01, kumpulkan pencarian yang dinamakannya, dan terapkan. HotPDF secara default menggunakan skrip DFLT dan LangSys default, yang merupakan bagian terbesar dari desain teks Latin yang dikirimkan, dan ia mengekspos cara untuk menimpa tag skrip ketika sebuah font menghubungkan fiturnya di bawah skrip tertentu.

Tabel Coverage menentukan siapa yang berpartisipasi

Setiap subtabel substitusi dimulai dengan pertanyaan yang sama: apakah glif masukan ini ikut serta dalam aturan ini, dan jika demikian, di mana posisinya dalam pengindeksan aturan itu sendiri. Pertanyaan tersebut dijawab oleh tabel Cakupan (Coverage table), dan jawabannya adalah indeks cakupan (coverage index), sebuah ordinal kecil yang digunakan oleh bagian subtabel lainnya untuk mencari glif tersebut menjadi apa.

Cakupan (Coverage) hadir dalam dua format. Format 1 adalah daftar ID glif yang diurutkan secara menaik (ascending). Anda menemukan glif dengan pencarian biner, dan posisinya dalam daftar adalah indeks cakupannya. Format 2 adalah daftar rekaman rentang (range records), masing-masing terdiri dari glif awal, glif akhir, dan indeks cakupan yang dipetakan oleh glif awal tersebut. Glif di dalam rentang mendapatkan indeks cakupannya dengan mengimbangi (offsetting) dari awal rentang. Format 1 ringkas ketika glif yang berpartisipasi tersebar, Format 2 ketika glif tersebut berada dalam urutan yang berdekatan. Keduanya diurutkan, sehingga keduanya dicari dalam waktu logaritmik, dan keduanya mengembalikan indeks cakupan atau hasil bersih "tidak tercakup" (not covered) yang membuat mesin membiarkan glif tersebut apa adanya.

Substitusi Tunggal, dua format tersebut

Substitusi Tunggal (Single Substitution) adalah LookupType 1, dan ia memetakan satu glif ke tepat satu pengganti. Ia juga memiliki dua format, dan pembagian ini adalah optimasi ruang. Format 1 menyimpan satu nilai delta bertanda (signed delta). ID glif keluaran adalah ID glif masukan ditambah delta tersebut, modulo 65536. Ini adalah cara font mengodekan substitusi di mana setiap glif yang berpartisipasi berada pada offset tetap yang sama dari glif alternatifnya, misalnya blok angka lining yang ditempatkan pada jarak konstan dari angka oldstyle yang cocok. Tabel Coverage menyatakan glif mana yang memenuhi syarat, dan satu delta tersebut melayani semuanya.

Format 2 menyimpan array eksplisit dari ID glif pengganti. Indeks cakupan dari tabel Coverage adalah indeks ke dalam array tersebut, sehingga glif pada indeks cakupan 0 menjadi entri array pertama, indeks cakupan 1 menjadi entri kedua, dan seterusnya. Format 2 digunakan ketika glif alternatif tidak berada pada offset yang seragam, yang merupakan kasus umum untuk set gaya buatan tangan. Kueri dari sisi pemanggil tetap sama dengan kedua cara tersebut. Ambil glif masukan, jalankan melalui Coverage, dan jika tercakup, terapkan delta atau baca slot array.

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;

Kontrak yang patut diperhatikan adalah pass-through. GetSingleSubstituteGlyph mengembalikan ID glif masukan tanpa perubahan pada setiap kegagalan pencarian: tidak ada font, tidak ada tabel GSUB, tidak ada fitur yang cocok, tidak ada kecocokan cakupan. Itu berarti panggilan tersebut aman dilakukan tanpa syarat. Anda meminta alternatif, dan jika tidak ada, Anda mendapatkan kembali persis apa yang Anda masukkan, sehingga kode pemanggil tidak perlu menangani kasus khusus untuk font yang tidak memiliki fitur tersebut.

Apa arti dari tag fitur gaya

Tag fitur adalah seluruh kosakata dari alternatif mana yang Anda minta, dan tag yang relevan dengan pekerjaan gaya (stylistic) adalah daftar pendek. Pasangan utama adalah salt, stylistic alternates, akses menyeluruh ke bentuk alternatif glif, dan ss01 hingga ss20, dua puluh set gaya bernomor yang dapat ditentukan oleh font, masing-masing merupakan bundel substitusi bernama yang dikelompokkan oleh desainer. Sebuah font mungkin menempatkan karakter a berlantai tunggal dan R kaki lurus di bawah ss03, misalnya, sehingga mengaktifkan set tersebut akan mengubah gaya kedua glif sekaligus.

Di sekitar tag tersebut terdapat beberapa tag substitusi tunggal lainnya. aalt adalah akses-semua-alternatif (access-all-alternates), gabungan dari setiap alternatif yang dimiliki glif, biasanya disajikan sebagai fitur palet glif. titl memilih huruf kapital judul (titling capitals) yang dipotong untuk ukuran besar. subs dan sups menukar angka subscript dan superscript asli alih-alih menggunakan default yang diperkecil skalanya. ordn menghasilkan bentuk ordinal, huruf yang dinaikkan pada ke-1 (1st) dan ke-2 (2nd). frac membangun pecahan (fractions), meskipun pecahan diagonal penuh juga bersandar pada ligatur dan logika kontekstual yang melampaui substitusi tunggal biasa. Untuk kasus glif tunggal, mekanismenya identik dengan ss01: teruskan tag ke kueri substitusi dan baca kembali glif alternatifnya.

// 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;

cmap format 12 dan bidang tambahan (supplementary planes)

Sebelum substitusi apa pun dapat berjalan, karakter harus menjadi glif, dan itu adalah tugas tabel cmap. Kueri substitusi dimulai dari ID glif, jadi jalurnya selalu karakter ke glif melalui cmap, lalu glif ke alternatif melalui GSUB. Bagian menarik dari cmap adalah jangkauannya. Subtabel format 4 mencakup Basic Multilingual Plane, yaitu 65.536 titik kode pertama, dan itu cukup untuk sebagian besar teks Latin. Namun, itu tidak cukup untuk titik kode dari U+10000 ke atas, yaitu bidang tambahan (supplementary planes), yang merupakan tempat huruf alfanumerik matematika, banyak simbol, dan beberapa skrip hidup berada saat ini.

Format 12 adalah subtabel yang mencakup seluruh rentang U+0000 hingga U+10FFFF range. Ini adalah daftar grup yang diurutkan, masing-masing grup terdiri dari titik kode awal, titik kode akhir, dan ID glif awal, sehingga urutan titik kode yang berdekatan memetakan ke urutan glif yang berdekatan. HotPDF menyelesaikan titik kode dengan strategi hibrida yang sesuai dengan bentuk datanya. Titik kode di dalam BMP dilayani dari array langsung yang diindeks oleh titik kode, pencarian tunggal tanpa pencarian rumit. Titik kode di bidang tambahan dilayani dari tabel jarang (sparse table) yang diurutkan berdasarkan titik kode dan dicari dengan pencarian biner. Hasilnya adalah bahwa GetUnicodeGlyphForCodepoint mengambil tipe data Cardinal penuh dan menjawab dengan benar di seluruh rentang, mengembalikan ID glif 0, yaitu glif .notdef, untuk titik kode apa pun yang tidak dipetakan oleh font.

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;

Di mana kueri ini berhenti

API substitusi tunggal menjawab satu jenis pertanyaan, dan perlu dijelaskan tentang apa saja yang tidak mereka jawab. LookupType 1 adalah salah satu dari delapan tipe substitusi. Kueri tidak menangani substitusi berganda LookupType 2, di mana satu glif menjadi beberapa glif, juga tidak menangani substitusi ligatur LookupType 4, di mana beberapa glif menjadi satu. Kueri ini juga tidak menangani tipe kontekstual dan kontekstual-berantai (chaining-contextual), LookupType 5 dan 6, yang hanya aktif ketika glif muncul di sekitar tertentu, atau tipe ekstensi dan pengikatan balik (reverse-chaining). Pecahan diagonal, konjungsi Devanagari, atau kaskade awal-tengah-akhir Arab adalah masalah urutan (sequence problem), dan pencarian substitusi tunggal per glif tidak dapat mengekspresikannya.

Ia juga tidak melakukan pembentukan otomatis (automatic shaping). Tidak ada di sini yang memeriksa rangkaian teks, memutuskan fitur mana yang akan diaktifkan, dan menerapkannya dalam urutan yang disyaratkan oleh skrip. Pemanggil memilih tag fitur dan menerapkannya glif demi glif. Itu adalah alat yang tepat untuk set gaya dan alternatif, yang bersifat opsional (opt-in) dan lokal, dan alat yang salah untuk skrip yang memerlukan pengurutan ulang. Menjaga batas tersebut tetap tajam adalah hal yang memungkinkan jalur substitusi tetap kecil dan terprediksi.

Untuk kasus-kasus yang memerlukan pekerjaan tingkat urutan, cerita skrip kompleks dibahas dalam artikel kami tentang pembentukan teks skrip kompleks di Delphi. Jika substitusi Anda adalah bagian dari pekerjaan laporan yang lebih besar yang juga menempatkan gambar dan font lain di halaman, panduan untuk keluaran laporan dengan font dan gambar membahas bagaimana bagian-bagian tersebut cocok bersama. Semua ini berjalan pada mesin yang sama, HotPDF Component untuk Delphi and C++Builder, yang membawa kueri substitusi GSUB bersama dengan API penyematan font, subsetting, dan teks yang dibahas di bagian lain di blog ini.