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ها درون یک خوشه؛ آنها دغدغههای شکلدهیاند و به مرحله بعد از این تعلق دارند
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 و جفت جانشین توصیف شده است
فرورفتن در سطوح باید سطوحی را هم شامل شود که رخ نمیدهند
اشتباه دوم ظریفتر است و 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;
نوشته بهشکل یک حلقه کاهنده ساده رفتار بیهزینه از خودش بیرون میآید، و تکرارهای بدون اثر هیچ چیز قابل اندازهگیری خرج نمیگیرند. این موردی است که بهینهسازی بدیهی کمی غلط نیست، به شکلی وابسته-به-ورودی غلط است که یک پیکره تست کوچک هرگز آشکارش نمیکند
کمانکها: 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 است