مقاله فنی

سطوح تعبیه دوسویه برای متن PDF بدون Uniscribe

Uniscribe بیشتر از آنچه بیشتر فراخوانندگان می‌فهمند کار می‌کند. ScriptItemize تحلیل دوسویه و بخش‌بندی خط نوشتاری را در یک گذر انجام می‌دهد، و ScriptLayout ترتیب بصری ران‌های حاصل را تولید می‌کند. HarfBuzz، جایگزین پرتابلی‌ای که مردم سراغش می‌روند، هیچ‌کدام را نمی‌کند: یک ران منفرد را که جهت و خط نوشتاری‌اش از قبل توسط کس دیگری تصمیم گرفته شده شکل می‌دهد. پس بخش سخت بردن یک خط لوله متن PDF ویندوزی به Linux یا macOS اتصال یک موتور شکل‌دهی نیست. فراهم‌کردن الگوریتم دوسویه‌ای است که Uniscribe بی‌سروصدا فراهم می‌کرد، و در مؤلفه PDFium کار FPdfBidi همین است

یونیت UAX #9 را مستقیماً پیاده می‌کند: قواعد P2 و P3 برای جهت بند، X1 تا X10 برای تعبیه‌های صریح و isolateها، W1 تا W7 برای نوع‌های ضعیف، N0 تا N2 برای خنثی‌ها و کمانک‌ها، I1 و I2 برای سطوح ضمنی، و L1 و L2 برای بازترتیب نهایی. دو تابع آن را حمل می‌کنند: PdfResolveBidiLevels یک سطح تعبیه به‌ازای هر یگان کد UTF-16 برمی‌گرداند، و PdfBidiVisualOrder آن سطوح را به جایگشتی تبدیل می‌کند که یگان‌های کد را چپ‌به‌راست می‌نشاند

الگوریتم چه می‌دهد و چه نمی‌دهد

اعداد می‌دهد. سطوح زوج چپ‌به‌راست‌اند، سطوح فرد راست‌به‌چپ، و سطح هر نویسه تودرتویی ران‌های جهتی را که آن نویسه درونشان نشسته رمزگذاری می‌کند. از آن اعداد L2 یک جایگشت استخراج می‌کند. کاری که الگوریتم عمداً نمی‌کند تصمیم‌گرفتن این است که از کدام فونت استفاده شود، ساختن لیگاچرها، یا بازترتیب glyphها درون یک خوشه؛ آن‌ها دغدغه‌های شکل‌دهی‌اند و به مرحله بعد از این تعلق دارند

خط لوله FPdfBidi برای متن PDF بدون Uniscribe: PdfResolveBidiLevels یک سطح تعبیه UAX #9 به‌ازای هر یگان کد UTF-16 تخصیص می‌دهد و PdfBidiVisualOrder قاعده L2 را برای تولید ترتیب بصری اعمال می‌کند
سطوح تودرتویی ران را رمزگذاری می‌کنند، و قاعده L2 آن‌ها را به جایگشتی تبدیل می‌کند که چپ‌به‌راست خوانده می‌شود
uses
  FPdfBidi;

var
  Levels: TPdfBidiLevels;
  Order: TPdfBidiOrder;
  ParagraphLevel: Byte;
  Text, Visual: WideString;
  I: Integer;
begin
  Text := SourceLine;
  // pbdAuto قواعد P2-P3 را اعمال می‌کند: اولین نویسه قوی تصمیم می‌گیرد
  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 اکنون چپ‌به‌راست خوانده می‌شود؛ Levels[] همچنان می‌گوید
    // کدام ران‌ها RTL هستند تا شکل‌دهنده جهت‌های درست تحویل بگیرد
  end;
end;

جدول کلاس نویسه تولیدشده است، نه نوشته‌شده

هر نقطه کد یک پراپرتی Bidi_Class دارد، و الگوریتم مدام به آن ارجاع می‌دهد، پس جدول بنیانی است که همه‌چیز دیگر رویش ایستاده. از پایگاه داده نویسه یونیکد تولید می‌شود نه با دست نگهداری: فیلد پنجم UnicodeData.txt کلاس‌های تخصیص‌یافته را می‌دهد، و اعلان‌های @missing در DerivedBidiClass.txt پیش‌فرض‌های نقطه‌های کدی را می‌دهند که پایگاه داده تخصیصشان نمی‌دهد، که همان چیزی است که بلوک‌های تخصیص‌نیافته را درست به R، AL، ET یا BN پیش‌فرض می‌کند نه به L

ترفند فشرده‌سازی این است که فقط بازه‌هایی را ساطع کنید که کلاسشان L نیست. هر چیزی که بیرون از همه بازه‌ها بیفتد L است، که هم پیش‌فرض یونیکد است هم کلاس اکثریت قاطع نقطه‌های کد. آن جدولی را که وگرنه تا هزاران entry می‌رفت به ۷۴۵ بازه و حدود ۶.۷ کیلوبایت می‌رساند. پیامد عملی ارزش گفته‌شدن دارد: وقتی به یک نسخه جدید یونیکد کوچ می‌کنید، مولد را دوباره اجرا کنید. ویرایش دستی فایل include کار خواهد کرد، و در ارتقای بعدی هم بی‌سروصدا از پایگاه داده واگرا خواهد شد

L2 باید نقطه‌های کد را بازترتیب کند، نه یگان‌های کد UTF-16 را

این اشتباهی است که خروجی واقعاً خراب تولید می‌کند، و پیاده‌سازی اول مرتکبش شد. L2 می‌گوید ران‌های پیوسته را در هر سطح از بالاترین تا پایین‌ترین سطح فرد وارونه کن. وقتی روی یک رشته UTF-16 نوشته شود، «وارونه‌کردن یک ران» طبیعتاً یعنی وارونه‌کردن یگان‌های کد درونش. برای نویسه‌های صفحه چندزبانه پایه آن خوب است. برای یک نویسه RTL در یک صفحه astral، مثل آن‌ها در بلوک‌های قبرسی یا کهن-عربی-جنوبی نزدیک U+10800، نیست: نویسه یک جفت جانشین است، وارونه‌کردن ران جانشین کم‌ارزش را پیش از پرارزش می‌گذارد، و رشته اکنون به‌جای یک نویسه دو جانشین جفت‌نشده دارد. هیچ چیز پایین‌دست نمی‌تواند بازیابی‌اش کند

fix این است که L2 روی یگان‌های نقطه-کد انجام شود. پیاده‌سازی یگان‌های کد را در یگان‌های نقطه-کد ادغام می‌کند، وارونه‌سازی‌ها را روی آن یگان‌ها انجام می‌دهد، و نتیجه را در انتها به اندیس‌های یگان-کد باز می‌گسترد. دلیل اینکه PdfBidiVisualOrder متن را می‌گیرد نه فقط آرایه سطوح همین است: از سطوح تنها نمی‌تواند بگوید مرزهای جانشین کجا هستند. همان نظم جفت-جانشین به‌طور کلی از APIهای متنی می‌گذرد، همان‌طور که در مقاله ایموجی، CJK و جفت جانشین توصیف شده است

خرابی جفت جانشین در بازترتیب دوسویه: وارونه‌کردن یگان‌های کد UTF-16 یک نویسه astral نزدیک U+10800 را به جانشین‌های جفت‌نشده می‌شکند، در حالی که وارونه‌کردن یگان‌های ادغام‌شده نقطه-کد سالمش نگه می‌دارد
قاعده L2 باید پیش از وارونه‌کردن یگان‌های کد را در نقطه‌های کد ادغام کند، بعدشان را دوباره باز بگسترد

فرورفتن در سطوح باید سطوحی را هم شامل شود که رخ نمی‌دهند

اشتباه دوم ظریف‌تر است و crash تولید نمی‌کند، فقط متنی که بازترتیب نشده. L2 می‌گوید از بالاترین سطح حاضر شروع و تا پایین‌ترین سطح فرد پایین بیا. یک بهینه‌سازی طبیعی جمع‌کردن مجموعه سطوحی است که واقعاً رخ می‌دهند و پیمایش روی آن مجموعه. غلط است

یک سطر متن لاتین را درون یک تعبیه راست‌به‌چپ در نظر بگیرید. سطح بند 0 است، تعبیه نویسه‌های لاتین را به سطح 2 می‌راند، و هیچ نویسه‌ای در سطح 1 نمی‌نشیند. پیمایش روی سطوح رخ‌داده فقط 0 و 2 را پیدا می‌کند، و اصلاً هیچ سطح فردی نیست، پس حلقه هیچ وارونه‌سازی انجام نمی‌دهد. آن پاسخ درست است، اما به دلیلی که بهینه‌سازی نمی‌داند: یک وارونه‌سازی در سطح 2 به‌دنبالش با یک وارونه‌سازی در سطح 1 دقیقاً یکدیگر را حذف می‌کردند، پس انجام‌ندادن هیچ‌کدام نتیجه درست است. ورودی را کمی تغییر دهید، طوری که هم نویسه‌های سطح 1 و هم سطح 3 موجود باشند ولی سطح 2 نه، و حلقه مبتنی-بر-مجموعه وارونه‌سازی سطح-2 را که الگوریتم لازم دارد رد می‌کند

// درست: هر سطحی را از ماکسیمم تا پایین‌ترین سطح
// فرد بپیمای، شامل سطوحی که هیچ نویسه‌ای واقعاً ندارد
Level := MaxLevel;
while Level >= LowestOddLevel do
begin
  ReverseRunsAtOrAbove(Level);   // بدون اثر وقتی هیچ رانی واجد نیست
  Dec(Level);
end;

نوشته به‌شکل یک حلقه کاهنده ساده رفتار بی‌هزینه از خودش بیرون می‌آید، و تکرارهای بدون اثر هیچ چیز قابل اندازه‌گیری خرج نمی‌گیرند. این موردی است که بهینه‌سازی بدیهی کمی غلط نیست، به شکلی وابسته-به-ورودی غلط است که یک پیکره تست کوچک هرگز آشکارش نمی‌کند

تله فرورفتن در سطح دوسویه در UAX #9: پیمایش فقط سطوح رخ‌داده وارونه‌سازی لازم سطح-2 را رد می‌کند، در حالی که یک حلقه کاهنده ساده از MaxLevel تا پایین‌ترین سطح فرد همیشه درست بازترتیب می‌کند
پیمودن هر سطح تا پایین‌ترین سطح فرد هیچ خرجی ندارد و هرگز یک وارونه‌سازی لازم را رد نمی‌کند

کمانک‌ها: BD16 با یک جدول عمل‌گرا

قاعده N0 و الگوریتم جفت-کمانک BD16 وجود دارند تا یک پرانتز در متن جهت‌آمیخته به جهت آنچه دربرمی‌گیرد حل شود نه به هر چه اتفاقاً مجاورش باشد. آن به یک جدول جفت کمانک نیاز دارد. پیاده‌سازی جفت‌های رایج را حمل می‌کند نه محتوای کامل فایل کمانک یونیکد را: کمانک‌های ASCII، CJK، تمام‌پهنا، ریاضی و تزئینی

یک کمانک فهرست‌نشده یک خطا نیست. به‌عنوان یک خنثی عادی از طریق N1 و N2 حل می‌شود، که دقیقاً رفتاری است که هر پیاده‌سازی پیش از آنکه Unicode 6.3 قاعده N0 را معرفی کرد داشت. پس مرز «کمتر پخته برای کمانک‌های نادر» است، نه «نادرست». یک جزئیات واقعاً به برخورد صریح نیاز دارد: هم‌ارزی هنجار میان کمانک‌های زاویه‌دار در U+2329 و U+232A و آن‌ها در U+3008 و U+3009 باید هنگام تطبیق جفت‌ها تا شود، وگرنه یک کمانک بازشونده که یک‌جور نوشته شده در جفت‌شدن با یک کمانک بسته‌شونده که جور دیگر نوشته شده شکست می‌خورد

چطور سی قاعده درهم‌کنش‌گر را تست می‌کنید

نه با یک پیکره بزرگ، حداقل اول نه. رویکرد کارا شانزده مورد دستی-راستی‌آزمایی‌شده بود، که هر یک برای ورز دادن یک قاعده مشخص انتخاب و هر یک در برابر سطوحی که UAX #9 می‌گوید باید تولید کند بررسی شد: آشکارسازی جهت بند زیر P2 و P3، قواعد نوع-ضعیف W2، W3 و W7، قواعد سطح ضمنی I1 و I2، تعبیه صریح از طریق X2 و X7، isolateها از طریق X5a و X6a، بازنشانی L1 از فاصله‌های سفید و جداکننده‌های انتهایی، یک مورد کمانک N0، و یک مورد با یک نویسه astral تا برخورد جانشین قفل شود

شانزده مورد با سطوح مورد انتظار معلوم‌الصحت بیشتر از هزار و ششصد مورد با خروجی موجه‌نما می‌گیرند، چون حالت شکست یک پیاده‌سازی دوسویه متنی است که تقریباً درست خوانده می‌شود. وقتی آن‌ها پاس شدند، یک پیکره برای پیدا کردن شکاف‌های جدول و مشکل‌های کارایی مفید است، که رده‌های متفاوتی از عیب‌اند

درون مؤلفه PDFium سطوح به دو مصرف‌کننده تغذیه می‌شوند. در سمت نوشتن به بک‌اند شکل‌دهی جهت هر ران را می‌گویند، که ورودی لازم HarfBuzz است. در سمت خواندن هندسه انتخاب و ترتیب خواندن را خبر می‌کنند، چون یک کلیک در متن RTL باید به یک جایگاه منطقی نگاشت شود نه بصری؛ آن نگاشت در مقاله انتخاب سطر بصری و مدل ترتیب خواندن در بلوک‌های متن ساختاریافته و ترتیب خواندن پوشش داده شده است. جزئیات پشتیبانی پلتفرم مؤلفه در صفحه محصول PDFium Delphi component است