Artikel Teknis

Level Embedding BiDi untuk Teks PDF tanpa Uniscribe

Uniscribe mengerjakan lebih banyak hal daripada yang disadari kebanyakan caller. ScriptItemize melakukan analisis bidireksional dan segmentasi script dalam satu lintasan, dan ScriptLayout menghasilkan urutan visual dari run yang dihasilkannya. HarfBuzz, pengganti portabel yang dicari orang, tidak melakukan keduanya: ia men-shaping satu run yang arah dan script-nya sudah diputuskan orang lain. Jadi bagian sulit dari membawa pipeline teks PDF Windows ke Linux atau macOS bukanlah mengikat engine shaping. Melainkan menyediakan algoritma bidireksional yang diam-diam disediakan Uniscribe, dan di komponen PDFium itulah gunanya FPdfBidi

Unit itu mengimplementasikan UAX #9 secara langsung: aturan P2 dan P3 untuk arah paragraf, X1 sampai X10 untuk embedding dan isolate eksplisit, W1 sampai W7 untuk tipe lemah, N0 sampai N2 untuk netral dan tanda kurung, I1 dan I2 untuk level implisit, serta L1 dan L2 untuk penataan ulang akhir. Dua fungsi menggendongnya: PdfResolveBidiLevels mengembalikan satu embedding level per code unit UTF-16, dan PdfBidiVisualOrder mengubah level-level itu menjadi permutasi yang menempatkan code unit kiri ke kanan

Apa yang diberikan algoritma, dan apa yang tidak

Ia memberi Anda angka. Level genap kiri-ke-kanan, level ganjil kanan-ke-kiri, dan level setiap karakter mengodekan sarang run arah yang dihuninya. Dari angka-angka itu L2 menurunkan sebuah permutasi. Yang sengaja tidak dilakukan algoritma adalah memutuskan font mana yang dipakai, membentuk ligatur, atau menata ulang glyph dalam cluster; itu urusan shaping dan menjadi milik tahap setelah ini

Pipeline FPdfBidi untuk teks PDF tanpa Uniscribe: PdfResolveBidiLevels menugaskan satu embedding level UAX #9 per code unit UTF-16 dan PdfBidiVisualOrder menerapkan aturan L2 untuk menghasilkan urutan visual
Level mengodekan sarang run, dan aturan L2 mengubahnya menjadi permutasi yang dibaca kiri ke kanan
uses
  FPdfBidi;

var
  Levels: TPdfBidiLevels;
  Order: TPdfBidiOrder;
  ParagraphLevel: Byte;
  Text, Visual: WideString;
  I: Integer;
begin
  Text := SourceLine;
  // pbdAuto menerapkan P2-P3: karakter kuat pertama yang memutuskan
  if PdfResolveBidiLevels(Text, pbdAuto, Levels, ParagraphLevel) then
  begin
    Order := PdfBidiVisualOrder(Text, Levels);
    SetLength(Visual, Length(Order));
    for I := 0 to High(Order) do
      Visual[I + 1] := Text[Order[I] + 1];
    // Visual kini terbaca kiri ke kanan; Levels[] masih menyatakan run
    // mana yang RTL sehingga shaper bisa diberi arah yang benar
  end;
end;

Tabel kelas karakter digenerate, bukan ditulis

Setiap code point punya properti Bidi_Class, dan algoritma memeriksanya terus-menerus, sehingga tabel adalah fondasi tempat segala hal lain berdiri. Ia digenerate dari Unicode Character Database alih-alih dirawat dengan tangan: field lima dari UnicodeData.txt memberikan kelas yang ditugaskan, dan deklarasi @missing di DerivedBidiClass.txt memberikan default untuk code point yang tidak ditugaskan database, dan begitulah blok yang belum dialokasikan dengan benar menjadi default R, AL, ET, atau BN alih-alih L

Trik kompresinya adalah memancarkan hanya rentang yang kelasnya bukan L. Apa pun yang jatuh di luar setiap rentang adalah L, yang merupakan default Unicode sekaligus kelas dari mayoritas mutlak code point. Itu membawa tabel yang kalau tidak akan memanjang ke ribuan entri turun menjadi 745 rentang dan sekitar 6,7 KB. Konsekuensi operasionalnya layak dinyatakan: ketika Anda berpindah ke versi Unicode baru, jalankan ulang generatornya. Menyunting file include dengan tangan akan bekerja, dan juga akan menyimpang diam-diam dari database pada peningkatan berikutnya

L2 harus menata ulang code point, bukan code unit UTF-16

Inilah kesalahan yang menghasilkan output yang benar-benar terkorupsi, dan implementasi pertama melakukannya. L2 memerintahkan membalik run yang bersebelahan pada setiap level dari tertinggi turun ke level ganjil terendah. Ditulis terhadap string UTF-16, "membalik sebuah run" secara alami berarti membalik code unit di dalamnya. Untuk karakter di Basic Multilingual Plane itu baik-baik saja. Untuk karakter RTL di bidang astral, seperti yang di blok Cypriot atau Old South Arabian dekat U+10800, tidak: karakter itu adalah surrogate pair, membalik run menaruh surrogate rendah sebelum yang tinggi, dan string kini memuat dua surrogate tanpa pasangan alih-alih satu karakter. Tidak ada yang di hilir bisa memulihkannya

Perbaikannya adalah melakukan L2 pada unit code point. Implementasinya menggabungkan code unit menjadi unit code point, melakukan pembalikan pada unit-unit itu, dan mengembangkan hasilnya kembali ke indeks code unit di akhir. Itulah mengapa PdfBidiVisualOrder menerima teks dan bukan hanya array level: ia tidak bisa mengetahui di mana batas surrogate hanya dari level. Disiplin surrogate pair yang sama mengalir melalui API teks secara umum, seperti dijelaskan dalam artikel emoji, CJK, dan surrogate pair

Korupsi surrogate pair dalam penataan ulang bidi: membalik code unit UTF-16 membelah karakter astral dekat U+10800 menjadi surrogate tanpa pasangan, sementara membalik unit code point gabungan menjaganya utuh
Aturan L2 harus menggabungkan code unit menjadi code point sebelum membalik, lalu mengembangkannya kembali setelahnya

Penurunan melalui level harus mencakup level yang tidak muncul

Kesalahan kedua lebih halus dan tidak menghasilkan crash, hanya teks yang tidak ditata ulang. L2 memerintahkan memulai dari level tertinggi yang ada dan bekerja turun ke level ganjil terendah. Optimasi yang alami adalah mengumpulkan set level yang benar-benar muncul dan mengiterasi set itu. Itu keliru

Pertimbangkan satu baris teks Latin di dalam embedding kanan-ke-kiri. Level paragraf 0, embedding mendorong karakter Latin ke level 2, dan tidak ada karakter yang duduk di level 1. Mengiterasi level yang muncul menemukan hanya 0 dan 2, dan tidak ada level ganjil sama sekali, sehingga loop tidak melakukan pembalikan. Jawaban itu benar, tetapi dengan alasan yang tidak diketahui optimasi itu: pembalikan di level 2 yang diikuti pembalikan di level 1 akan saling menghapus persis, sehingga tidak melakukan keduanya adalah hasil yang benar. Ubah inputnya sedikit, sehingga karakter level 1 dan level 3 ada tetapi level 2 tidak, dan loop berbasis set melewatkan pembalikan level 2 yang disyaratkan algoritma

// Benar: telusuri setiap level dari maksimum turun ke level ganjil
// terendah, termasuk level yang tidak dimiliki karakter mana pun
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
  ReverseRunsAtOrAbove(Level);   // no-op ketika tidak ada run yang memenuhi syarat
  Dec(Level);
end;

Ditulis sebagai loop penurun biasa, perilakunya jatuh begitu saja, dan iterasi no-op-nya tidak berbiaya terukur. Ini kasus di mana optimasi yang jelas bukan keliru sedikit, melainkan keliru dengan cara yang bergantung input yang tidak akan pernah terungkap oleh korpus uji kecil

Jebakan penurunan level bidi di UAX #9: mengiterasi hanya level yang muncul melewatkan pembalikan level 2 yang disyaratkan, sementara loop penurun biasa dari MaxLevel ke level ganjil terendah selalu menata ulang dengan benar
Menelusuri setiap level turun ke level ganjil terendah tidak berbiaya dan tidak pernah melewatkan pembalikan yang disyaratkan

Tanda kurung: BD16 dengan tabel pragmatis

Aturan N0 dan algoritma pasangan tanda kurung BD16 ada agar tanda kurung dalam teks arah campuran terselesaikan ke arah dari apa yang dilingkupinya alih-alih ke apa pun yang kebetulan bersebelahan. Itu membutuhkan tabel pasangan tanda kurung. Implementasinya membawa pasangan yang umum dipakai alih-alih isi lengkap berkas tanda kurung Unicode: ASCII, CJK, fullwidth, matematis, dan ornamen

Tanda kurung yang tidak tercantum bukan error. Ia terselesaikan sebagai netral biasa melalui N1 dan N2, yang persis adalah perilaku yang dimiliki setiap implementasi sebelum Unicode 6.3 memperkenalkan N0. Jadi batasnya "kurang terhalus untuk tanda kurung langka", bukan "tidak tepat". Satu detail memang butuh penanganan eksplisit: kesetaraan kanonis antara angle bracket di U+2329 dan U+232A dengan yang di U+3008 dan U+3009 harus dilipat saat mencocokkan pasangan, atau tanda kurung pembuka yang ditulis satu cara akan gagal berpasangan dengan tanda kurung penutup yang ditulis cara lain

Bagaimana Anda menguji tiga puluh aturan yang saling berinteraksi

Bukan dengan korpus besar, setidaknya bukan yang pertama. Pendekatan yang produktif adalah enam belas kasus yang diverifikasi tangan, masing-masing dipilih untuk menjalankan aturan tertentu dan masing-masing diperiksa terhadap level yang kata UAX #9 seharusnya dihasilkan: deteksi arah paragraf di bawah P2 dan P3, aturan tipe lemah W2, W3, dan W7, aturan level implisit I1 dan I2, embedding eksplisit lewat X2 dan X7, isolate lewat X5a dan X6a, reset L1 atas whitespace dan pemisah di ekor, satu kasus tanda kurung N0, dan satu kasus dengan karakter astral untuk mengunci penanganan surrogate

Enam belas kasus dengan level harapan yang diketahui benar menangkap lebih banyak daripada seribu enam ratus kasus dengan output yang tampak masuk akal, karena mode kegagalan sebuah implementasi bidireksional adalah teks yang terbaca hampir benar. Begitu semuanya lolos, korpus berguna untuk menemukan celah tabel dan masalah kinerja, yang merupakan kelas defek yang berbeda

Di dalam komponen PDFium level-level itu memberi makan dua konsumen. Di sisi penulisan mereka memberi tahu backend shaping arah setiap run, yang merupakan input yang disyaratkan HarfBuzz. Di sisi pembacaan mereka menginformasikan geometri seleksi dan urutan baca, karena klik di teks RTL harus terpetakan ke posisi logis alih-alih visual; pemetaan itu dibahas dalam artikel seleksi baris visual dan model urutan baca dalam blok teks terstruktur dan urutan baca. Detail dukungan platform untuk komponen ada di halaman produk PDFium Delphi component