Teknik Makale

Uniscribe'siz PDF Metni için BiDi Gömme Düzeyleri

Uniscribe, çoğu çağıranın fark ettiğinden daha çok iş yapar. ScriptItemize, tek geçişte çift yönlü analiz ile yazı sistemi parçalamasını yerine getirir ve ScriptLayout, sonuçlanan çalıştırmaların görsel sırasını üretir. İnsanların başvurduğu taşınabilir yedek HarfBuzz ise ikisini de yapmaz: yönü ve yazı sistemi başkasınca belirlenmiş tek bir çalıştırmayı şekillendirir. Dolayısıyla bir Windows PDF metin hattını Linux'a ya da macOS'a taşımanın zor kısmı bir şekillendirme motoru bağlamak değildir. Uniscribe'ın sessizce sağladığı çift yönlü algoritmayı sağlamaktır ve PDFium bileşeninde FPdfBidi birimi tam olarak bunun içindir

Birim, UAX #9'u doğrudan uygular: paragraf yönü için P2 ve P3, açık gömmeler ve izolatlar için X1'den X10'a, zayıf türler için W1'den W7'ye, nötrler ve parantezler için N0'dan N2'ye, örtük düzeyler için I1 ve I2 ve son yeniden sıralama için L1 ile L2. İki işlev onu taşır: PdfResolveBidiLevels, UTF-16 kod birimi başına bir gömme düzeyi döndürür ve PdfBidiVisualOrder, o düzeyleri kod birimlerini soldan sağa yerleştiren permütasyona çevirir

Algoritmanın verdikleri ve vermedikleri

Size sayılar verir. Çift düzeyler soldan sağadır, tek düzeyler sağdan soladır ve her karakterin düzeyi, o karakterin içinde oturduğu yönlü çalıştırmaların iç içeliğini kodlar. L2, o sayılardan bir permütasyon türetir. Algoritmanın bilinçli olarak yapmadığı şey hangi fontun kullanılacağına karar vermek, ligatür kurmak ya da bir küme içindeki glifleri yeniden sıralamaktır; onlar şekillendirme meseleleridir ve bu aşamadan sonraki aşamaya aittir

Uniscribe'siz PDF metni için FPdfBidi hattı: PdfResolveBidiLevels UTF-16 kod birimi başına bir UAX #9 gömme düzeyi atar ve PdfBidiVisualOrder görsel sırayı üretmek için L2 kuralını uygular
Düzeyler çalıştırma iç içeliğini kodlar ve L2 kuralı onları soldan sağa okunan permütasyona çevirir
uses
  FPdfBidi;

var
  Levels: TPdfBidiLevels;
  Order: TPdfBidiOrder;
  ParagraphLevel: Byte;
  Text, Visual: WideString;
  I: Integer;
begin
  Text := SourceLine;
  // pbdAuto P2-P3 kurallarını uygular: ilk güçlü karakter karar verir
  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 artık soldan sağa okunur; Levels dizisi hangi
    // çalıştırmaların RTL olduğunu hâlâ söyler, böylece bir
    // şekillendiriciye doğru yönler verilebilir
  end;
end;

Karakter sınıfı tablosu üretilir, elle yazılmaz

Her kod noktasının bir Bidi_Class özelliği vardır ve algoritma ona sürekli başvurur; dolayısıyla tablo, her şeyin üzerine kurulduğu temeldir. Unicode Karakter Veritabanından üretilir, elle bakım görmez: UnicodeData.txt dosyasının beşinci alanı atanmış sınıfları verir ve DerivedBidiClass.txt içindeki @missing bildirimleri, veritabanının atamadığı kod noktaları için varsayılanları verir; atanmamış blokların bu yüzden L'ye değil doğru biçimde R, AL, ET ya da BN'ye varsaydığı budur

Sıkıştırma hilesi, sınıfı L olmayan aralıkları yaymaktır. Hiçbir aralığın dışında kalan her şey L'dir; o hem Unicode varsayılanı hem de kod noktalarının ezici çoğunluğunun sınıfıdır. Bu, yoksa binlerce girdiye ulaşacak bir tabloyu 745 aralığa ve yaklaşık 6,7 KB'a indirir. İşlemsel sonucu söylemeye değer: yeni bir Unicode sürümüne geçtiğinizde üreteci yeniden çalıştırın. Include dosyasını elle düzenlemek çalışır ve bir sonraki yükseltmede veritabanından sessizce sapmaya da başlar

L2 kod birimlerini değil kod noktalarını yeniden sıralamalıdır

Gerçekten bozuk çıktı üreten hata budur ve ilk uygulama onu yaptı. L2, her düzeyde en yüksekten en düşük tek düzeye dek bitişik çalıştırmaları tersine çevirmeyi söyler. Bir UTF-16 dizisi üzerinde yazıldığında "bir çalıştırmayı tersine çevir" doğal olarak içindeki kod birimlerini tersine çevirmek demektir. Temel Çok Dilli Düzlemdeki karakterler için bu sorun değildir. Astral düzlemde bir RTL karakteri için, U+10800 yakınındaki Kıbrıs ya da Eski Güney Arabistan bloklarındakiler gibi, değildir: karakter bir yedek çiftidir, çalıştırmayı tersine çevirmek düşük yedeği yükseğin önüne koyar ve dize artık bir karakter yerine iki eşleşmemiş yedek içerir. Aşağı akışta hiçbir şey onu kurtaramaz

Düzeltme, L2'yi kod noktası birimleri üzerinde yapmaktır. Uygulama, kod birimlerini kod noktası birimlerine birleştirir, ters çevirmeleri o birimler üzerinde yapar ve sonunda sonucu kod birimi indekslerine geri genişletir. PdfBidiVisualOrder işlevinin yalnızca düzeyler dizisini değil metni almasının nedeni budur: yedek sınırlarının nerede olduğunu düzeylerden anlayamaz. Aynı yedek çifti disiplini, metin API'lerinde genel olarak da sürer; emoji, CJK ve yedek çifti makalesinde anlatılır

Bidi yeniden sıralamasında yedek çifti bozulması: UTF-16 kod birimlerini tersine çevirmek U+10800 yakınındaki bir astral karakteri eşleşmemiş yedeklere böler; birleştirilmiş kod noktası birimlerini tersine çevirmek onu bütün tutar
L2 kuralı, tersine çevirmeden önce kod birimlerini kod noktalarında birleştirmeli ve sonrasında onları geri genişletmelidir

Düzeylerdeki iniş, hiç gerçekleşmeyen düzeyleri de kapsamalıdır

İkinci hata daha incedir ve çökme üretmez; yalnızca yeniden sıralanmamış metin üretir. L2, bulunan en yüksek düzeyden başlayıp en düşük tek düzeye dek inilmeyi söyler. Doğal iyileştirme, gerçekte gerçekleşen düzeyler kümesini toplamak ve o küme üzerinde yinelemektir. Yanlıştır

Sağdan sola bir gömme içindeki bir Latince metin satırını düşünün. Paragraf düzeyi 0'dır, gömme Latince karakterleri 2. düzeye iter ve hiçbir karakter 1. düzeyde oturmaz. Gerçekleşen düzeyler üzerinde yinelemek yalnızca 0 ile 2'yi bulur ve hiç tek düzey yoktur; dolayısıyla döngü hiçbir ters çevirme yapmaz. O cevap doğrudur, ama iyileştirmenin bilmediği bir nedenden: 2. düzeyde bir ters çevirmenin ardından 1. düzeyde bir ters çevirme tam olarak birbirini sileceğinden ikisinin de yapılmaması doğru sonuçtur. Girdiyi biraz değiştirin, 1 ve 3. düzey karakterler bulunsun ama 2. düzey bulunmasın; küme tabanlı döngü, algoritmanın gerektirdiği 2. düzey ters çevirmesini atlar

// Doğru: en yüksekten en düşük tek düzeye dek her düzeyi gezin;
// hiçbir karakterin gerçekten sahip olmadığı düzeyler dahil
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
  ReverseRunsAtOrAbove(Level);   // nitelikli çalıştırma yoksa no-op
  Dec(Level);
end;

Düz bir azalan döngü olarak yazıldığında davranış bedavaya düşer ve no-op yinelemeleri ölçülebilir bir şey maliyet etmez. Burada bariz iyileştirme hafifçe yanlış değildir; küçük bir test derlemesinin asla açığa çıkaramayacağı, girdiye bağlı bir şekilde yanlıştır

UAX #9'da bidi düzey inişi tuzağı: yalnızca gerçekleşen düzeyleri yinelemek gereken 2. düzey ters çevirmesini atlar; MaxLevel'den en düşük tek düzeye düz bir azalan döngü ise her zaman doğru yeniden sıralar
En düşük tek düzeye dek her düzeyi gezinmek hiçbir şey maliyet etmez ve gereken bir ters çevirmeyi asla atlamaz

Parantezler: pragmatik bir tabloyla BD16

N0 kuralı ve BD16 parantez çifti algoritması, karışık yönlü metindeki bir parantezin, yanı tesadüfen bitişik olana değil sardığı şeyin yönüne çözülmesi için vardır. Bu, bir parantez çifti tablosu gerektirir. Uygulama, Unicode parantez dosyasının tüm içeriği yerine yaygın kullanımdaki çiftleri taşır: ASCII, CJK, tam genişlikli, matematiksel ve süsleme parantezleri

Listede olmayan bir parantez bir hata değildir. N1 ile N2 üzerinden sıradan bir nötr olarak çözülür; Unicode 6.3'ün N0'ı getirmesinden önce her uygulamanın davranışı tam olarak buydu. Dolayısıyla sınır "nadir parantezler için daha az rafine", "yanlış" değildir. Bir ayrıntının açık ele alınması gerekir: U+2329 ile U+232A'daki açılı parantezlerle U+3008 ile U+3009'dakiler arasındaki kanonik eşdeğerlik, çiftler eşleştirilirken katlanmalıdır; yoksa bir biçimde yazılmış açma parantezi öbür biçimde yazılmış bir kapatma paranteziyle eşleşemez

Otuz etkileşen kuralı nasıl test edersiniz

Büyük bir derlemle değil, en azından önce değil. Verimli yaklaşım, her biri belirli bir kuralı çalıştırmak için seçilmiş ve her biri UAX #9'un üretmesi gerektiğini söylediği düzeylerle denetlenmiş on altı elle doğrulanmış durumdur: P2 ve P3 altında paragraf yönü saptaması, W2, W3 ve W7 zayıf tür kuralları, I1 ve I2 örtük düzey kuralları, X2 ile X7 üzerinden açık gömme, X5a ile X6a üzerinden izolatlar, sondaki boşluk ve ayraçların L1 sıfırlaması, bir N0 parantez durumu ve yedek ele alışını sabitlemek için astral karakterli bir durum

Bilinen-doğru beklenen düzeyli on altı durum, inandırıcı görünen çıktılı bin altı yüz durumdan fazlasını yakalar; çünkü bir çift yönlü uygulamanın hata modu, neredeyse doğru okunan metindir. Onlar geçtikten sonra derlem, tablo boşluklarını ve performans sorunlarını bulmak için faydalıdır; onlar farklı hata sınıflarıdır

PDFium bileşeninin içinde düzeyler iki tüketiciyi besler. Yazma tarafında şekillendirme arka ucuna her çalıştırmanın yönünü söyler; HarfBuzz'ın gerektirdiği girdi odur. Okuma tarafında seçim geometrisini ve okuma sırasını bilgilendirir; çünkü RTL metindeki bir tıklama görsel değil mantıksal bir konuma eşlenmek zorundadır. O eşleme görsel satır seçimi makalesinde, okuma sırası modeli ise yapılandırılmış metin blokları ve okuma sırasında kapsanır. Bileşenin platform desteği ayrıntıları PDFium Delphi component ürün sayfasındadır