Artikel Teknis

Shaping Teks Pluggable: Uniscribe dan HarfBuzz di Delphi

Shaping teks di komponen PDFium melewati satu objek yang dapat dipasang. ConfigureTextShaper memasang shaper yang dilalui setiap entry point shaping, mengganti dan membebaskan apa pun yang ada di sana; ActiveTextShaper mengembalikan yang terpasang dan membuat default platform pada pemakaian pertama; ActiveTextShaperName melaporkan backend mana yang hidup; ClearTextShaper menjatuhkan pemasangannya dan membiarkan default dibuat lagi. Di Windows default-nya adalah TPdfUniscribeTextShaper. Di bawah Free Pascal ada TPdfHarfBuzzTextShaper, yang mengikat libharfbuzz pada run time sehingga pustaka yang hilang adalah kondisi yang dilaporkan alih-alih kegagalan muat

Arsitektur shaping teks pluggable di komponen Delphi PDFium: ConfigureTextShaper, ActiveTextShaper, dan ClearTextShaper mengelola satu backend terpasang, Uniscribe di Windows dan HarfBuzz terikat runtime di bawah Free Pascal
Setiap panggilan shaping melewati satu objek shaper terpasang, dengan default platform pada setiap target

Satu interface, dua backend yang membagi pekerjaan dengan cara yang sama sekali berbeda. Memahami asimetri itulah yang menghentikan jalur portabel menghasilkan teks yang ter-shaping dengan benar tetapi diposisikan dengan keliru

Mengapa backend Windows satu kelas dan yang portabel tiga bagian?

Karena Uniscribe adalah empat API yang berpura-pura menjadi satu. ScriptItemize mensegmentasi string berdasarkan script dan menyelesaikan level bidireksional; ScriptShape memetakan karakter ke glyph; ScriptPlace menghitung advance dan offset; ScriptLayout menata run yang dihasilkan ke urutan visual. Backend yang dibangun di atasnya karenanya tidak punya apa pun lagi untuk ditambahkan, dan itulah mengapa shaper Windows adalah satu kelas dengan satu method

HarfBuzz mencakup dua di tengah. Ia men-shaping dan menempatkan run yang arah dan script-nya sudah diputuskan caller, dan ia tidak punya pendapat tentang bagaimana paragraf terbelah menjadi run atau urutan apa run-run itu muncul. Jadi backend portabel menyediakan sisanya: algoritma bidireksional menyelesaikan embedding level, fungsi Unicode HarfBuzz mensegmentasi teks berdasarkan script, dan run-run ditata dalam urutan visual yang dihasilkan aturan L2 UAX #9. Separuh bidireksional itu cukup substansial untuk menjadi unitnya sendiri, dijelaskan dalam artikel embedding level UAX #9

Perbandingan pipeline shaping untuk teks PDF: Uniscribe menyediakan ScriptItemize, ScriptShape, ScriptPlace, dan ScriptLayout di dalam satu kelas, sementara HarfBuzz hanya mencakup shaping dan penempatan di sekitar tahap UAX #9 milik komponen sendiri
Uniscribe mencakup keempat tahap; jalur portabel harus menyediakan itemisasi dan urutan visual sendiri

Shaper tidak menyelesaikan font, dan itu disengaja

Uniscribe membaca binary font dari sebuah device context GDI. Tidak ada padanan portabel untuk itu, dan mengarang satu di dalam unit shaping berarti memutuskan, mewakili setiap aplikasi, apakah font datang dari fontconfig, dari CoreText, dari folder font aplikasi, atau dari database. Jadi backend HarfBuzz menerima resolver: callback yang memetakan nama font ke byte TrueType atau OpenType. Mengembalikan False menggagalkan permintaan shaping dengan cara yang sama seperti font GDI yang tak terbaca menggagalkannya di Windows

uses
  FPdfTextShaping
{$IFDEF FPC}
  , FPdfTextShapingHb
{$ENDIF}
  ;

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Kebijakan Anda: fontconfig, CoreText, folder font aplikasi, database
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // Kepemilikan berpindah ke unit; panggil sekali saat start-up,
  // sebelum ada yang men-shaping teks
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // Di Delphi default platform (Uniscribe) dibuat sesuai permintaan,
  // sehingga tidak perlu pemasangan sama sekali
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Menjaga penemuan font di luar shaper punya manfaat kedua yang muncul di server: proses yang sama bisa men-shaping dengan set font tertanam yang tidak ada sangkut pindanya dengan apa yang terpasang di mesin, yang Anda inginkan ketika output harus dapat direproduksi byte lintas host. Komponen juga memaparkan provider font sistem host untuk kasus-kasus di mana Anda memang menginginkan font terpasang, dibahas dalam artikel provider font sistem

Record hasil bersifat netral-backend, dan cluster adalah alasannya

Kedua backend mengisi TPdfShapedText yang sama: teks sumber, nama font, ukuran, byte font, array run, lebar total, jumlah glyph, dan jumlah karakter logis. Setiap TPdfShapedRun membawa rentangnya di teks sumber, posisi X visualnya, lebarnya, level bidireksionalnya, dan flag kanan-ke-kiri, plus glyph-glyphnya. Setiap TPdfShapedGlyph membawa identifier glyph, advance, offset X dan Y, dan cluster tempatnya berada sebagai awal dan panjang di teks sumber

Field cluster itulah yang membuat record dapat dipakai, bukan sekadar informatif. Shaping bukan pemetaan satu-ke-satu: satu suku kata Devanagari menjadi satu glyph dari empat karakter, satu ligatur Arab menggabungkan dua, dan satu karakter bisa menghasilkan beberapa tanda. Tanpa rentang cluster Anda tidak bisa menempatkan caret, hit-test sebuah klik, atau menyorot seleksi, karena Anda tidak bisa mengatakan glyph itu milik karakter mana. Dengan itu, aritmetikanya lokal dan kode yang sama bekerja untuk kedua backend

Rentang cluster glyph di TPdfShapedText: satu glyph suku kata Devanagari dari empat karakter, satu ligatur Arab dari dua, dan satu dasar plus tanda dari satu karakter, masing-masing dipetakan kembali lewat ClusterStart dan ClusterLength
Rentang cluster memetakan setiap glyph kembali ke karakter sumbernya sehingga caret, hit test, dan seleksi bekerja
var
  Shaped: TPdfShapedText;
  R, G: Integer;
begin
  if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
    for R := 0 to High(Shaped.Runs) do
    begin
      // Run sudah datang dalam urutan visual dengan VisualX terisi
      X := Shaped.Runs[R].VisualX;
      for G := 0 to High(Shaped.Runs[R].Glyphs) do
      begin
        EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
          X + Shaped.Runs[R].Glyphs[G].OffsetX,
          Shaped.Runs[R].Glyphs[G].OffsetY);
        X := X + Shaped.Runs[R].Glyphs[G].Advance;
      end;
    end;
end;

Budget berada di record options

TPdfTextShapingOptions membawa arah plus tiga batas: karakter maksimum, glyph maksimum, dan run maksimum, dengan class function Default yang mengisi nilai masuk akal. Batas-batas itu bukan paranoia tentang input cacat; mereka aritmetika. Shaping mengembang: font dengan substitusi kontekstual yang agresif bisa memancarkan lebih banyak glyph daripada karakter input, dan paragraf yang berganti script setiap beberapa karakter menghasilkan satu run per pergantian. Dokumen yang dirakit untuk memaksimalkan keduanya mengubah string sederhana menjadi alokasi besar, dan layanan yang men-shaping teks dari PDF tak tepercaya membutuhkan batas yang ia pilih alih-alih batas yang dipaksakan mesin

Menetapkan arah secara eksplisit alih-alih membiarkannya otomatis layak dilakukan kapan pun Anda sudah mengetahuinya. Otomatis menerapkan aturan arah paragraf untuk menebak dari karakter kuat pertama, yang benar untuk teks bebas dan keliru untuk field form yang arahnya properti field, bukan properti nilai yang diketik seseorang ke dalamnya

Binding run time, bukan dependensi build

Backend HarfBuzz memuat pustaka secara dinamis. Itu keputusan deployment dengan konsekuensi nyata: satu binary berjalan di mesin dengan HarfBuzz dan di mesin tanpa itu, melaporkan kemampuan yang berkurang pada kasus kedua alih-alih gagal memulai. Untuk pustaka yang dikirim ke developer lain itu susunan satu-satunya yang bisa dikerjakan, karena Anda tidak bisa mensyaratkan setiap konsumen komponen PDF mengakuisisi dan mencocokkan versi pustaka shaping yang mungkin tidak mereka butuhkan

Aturan yang bersesuaian bagi caller adalah memeriksa. ActiveTextShaper mengembalikan nil ketika platform tidak punya default dan tidak ada yang dikonfigurasi, dan entry point shaping melaporkannya sebagai shaper tidak tersedia alih-alih kegagalan shaping. Itu masalah yang berbeda dan layak pesan berbeda: satu adalah celah deployment, yang lain masalah font atau teks

Pasang sekali, sebelum ada yang men-shaping

Pemasangan mengganti dan membebaskan shaper sebelumnya, sehingga memanggilnya berulang kali aman tetapi tidak bermakna, dan memanggilnya saat thread lain sedang men-shaping sama sekali tidak aman. Lakukan saat start-up. Jika Anda perlu jatuh kembali ke default platform nanti, berikan nil, yang juga cara Anda membatalkan test double di akhir uji

Setelah backend terpasang, pengukuran dan pembungkusan berperilaku sama di kedua platform, karena keduanya mengonsumsi metrik run dan glyph alih-alih memanggil platform langsung; model pembungkusannya dijelaskan dalam artikel pengukuran teks dan word wrap. Platform dan toolchain yang didukung untuk komponen tercantum di halaman produk PDFium Delphi component