عبارت عربی يوضح ملف PDF را به TextOut بدهید و نتیجه را باز کنید. حروف در جهت نادرست قرار می گیرند و هر حرف در شکل منفصل خود می نشیند، با فاصله ای آشکار پیش از حرف بعدی، انگار کسی انگلیسی را برعکس تایپ کرده و بین هر نویسه فاصله زده است. هیچ exception ای رخ نمی دهد. هیچ هشداری چاپ نمی شود. خروجی فقط غلط است، و این غلط بودن به این دلیل است که دو تبدیل جداگانه که عربی به آن ها وابسته است اصلاً انجام نشده اند. فهمیدن این دو تبدیل و اینکه کدام فراخوانی آن ها را انجام می دهد، تقریباً تمام ماجرای خروجی PDF برای خط های پیچیده است
HotPDF یک کامپوننت بومی VCL برای Delphi و C++Builder است و کار متن راست به چپ را از طریق یک فراخوانی مجزا برای شما انجام می دهد. با این حال در چند نقطه مشخص هم عمداً متوقف می شود که بهتر است پیش از اینکه یک locale را متعهد شوید آن ها را بدانید، بنابراین این نوشته مفاهیم و مرزهای واقعی را نقشه برداری می کند. راه اندازی عملی خود فراخوانی در مقاله مرجع RtLTextOut آمده است
چرا یک رشته صحیح باز هم غلط چاپ می شود
Unicode متن را به ترتیب منطقی نگه می دارد، یعنی همان ترتیبی که آن را تایپ می کنید و با صدای بلند می خوانید. یک renderer باید glyph ها را به ترتیب دیداری روی صفحه بگذارد. برای خط های چپ به راست این دو ترتیب یکی هستند و کسی به آن فکر نمی کند. برای عربی و عبری چنین نیست، و وقتی یک خط جهت ها را مخلوط می کند، مثلاً یک جمله عربی که token لاتین "PDF" یا یک قیمت عددی را در خود دارد، الگوریتم دوسویه Unicode یعنی UAX #9 دقیقاً تعیین می کند که قطعه های چپ به راست چگونه داخل یک خط راست به چپ جا بگیرند. این اولین تبدیل یعنی reordering است، و حذف آن همان چیزی است که خط را وارونه می کند
دومی شکل دهی وابسته به بافت است. یک حرف عربی بسته به اینکه در آغاز، میانه، پایان کلمه یا تنها قرار بگیرد متفاوت رسم می شود. codepoint در تمام این حالات یکسان می ماند، فقط glyph عوض می شود. یک pipeline که هر codepoint را مستقیم به glyph پیش فرضش بدهد دقیقاً همان خروجی گسسته و منفصل پاراگراف آغازین را تولید می کند. عبری این مرحله را لازم ندارد چون حروفش به هم نمی چسبند، اما هنوز به reordering نیاز دارد. عربی به هر دو نیاز دارد، و به همین دلیل رشته ای که با آن تست می کنید باید عربی باشد نه عبری
در دسکتاپ این مسائل مشکل شما نیستند. وقتی یک فرم VCL متن عربی را داخل TEdit رسم می کند، stack متنی سیستم عامل بی سر و صدا آن را reorder و shape می کند، و دقیقاً به همین دلیل رشته ای که روی صفحه بی نقص به نظر می رسد در یک PDF ساده خراب از آب درمی آید. content stream متن قابل ویرایش ذخیره نمی کند؛ glyph های موقعیت گذاری شده ذخیره می کند، بنابراین هرکس stream را می نویسد، کار shape کردن را که قبلاً سیستم عامل انجام می داد به ارث می برد. RtLTextOut همان فراخوانی ای است که این مسئولیت را پس می گیرد
RtLTextOut چه چیزهایی را برای شما shape می کند
HotPDF مسیر لاتین و مسیر complex-script را به شکل دو متد متفاوت نگه می دارد. TextOut چیزی را که به آن می دهید به همان ترتیبی که می دهید چاپ می کند. RtLTextOut نخست هر دو تبدیل را انجام می دهد، یعنی reordering دوسویه در کل خط و تحلیل بافتی برای script های متصل شونده، و بعد چاپ می کند. اینکه قواعد کدام script اعمال شود از طریق charset فونت وارد می شود نه از طریق خود فراخوانی، بنابراین جهت در هر call site یک انتخاب صریح است نه حدسی که از روی نویسه ها زده شود. تنظیم پارامتر به پارامتر، مقادیر charset، گام های ثبت فونت و یک مثال کامل قابل کامپایل همگی در مقاله مرجع RtLTextOut آمده اند؛ این متن فقط بر معنای این تبدیل ها، مرز توقف آن ها و روش اثبات درست کار کردنشان تمرکز دارد
یک قاعده استفاده حتی در این سطح هم مهم است: ورودی باید در ترتیب منطقی باشد، چون RtLTextOut خودش عمل برگرداندن را انجام می دهد، و رشته ای که شما از قبل دستی وارونه کرده اید دوباره وارونه می شود. مقاله مرجع آن دام و نحوه پاکسازی اش را قدم به قدم نشان می دهد. دلیل ذکر آن در اینجا این است که چرا در تست ها زنده می ماند. یک رشته صرفاً عربی که دوبار وارونه شده ممکن است کاملاً درست به نظر برسد و فقط وقتی خراب شود که خط شامل یک واژه لاتین یا عدد شود، زیرا آن run های توکار دیگر مطابق UAX #9 در هم فرو نمی روند. باگ در rendering نیست؛ در تغذیه الگوریتم با متنی است که از قبل نیمه پردازش شده است
همین رفتار جهت های مخلوط بیشتر از کد، بازبین ها را گیر می اندازد. درون یک خط راست به چپ، رقم ها و واژه های لاتین توکار هنوز چپ به راست خوانده می شوند. کسی که با چیدمان دوسویه کار نکرده است، صورت حساب render شده را می بیند، می بیند شماره حساب نسبت به متن عربی اطرافش به ظاهر «در جهت غلط» خوانده می شود، و آن را باگ گزارش می کند. در واقع این نتیجه کاملاً مطابق spec است. یک یادداشت کوتاه در معیارهای پذیرش، پیش از نخستین بازبینی توسط native speaker، این رفت و برگشت را حذف می کند
چه وقت reordering و joining کافی هستند و چه وقت نیستند
برای متن جاری عربی و عبری، یعنی گزارش ها، فاکتورها، قراردادها و نامه ها، reordering به اضافه contextual joining تمام کار است و RtLTextOut به تنهایی آن را بر عهده می گیرد. مرز از جایی پیدا می شود که typography چیزی بیش از joining بخواهد. راه حل HotPDF در سمت عربی یک shaper اختیاری در سمت تولیدکننده است: اگر AutoShapeArabic := True را تنظیم کنید، کامپوننت run با ترتیب منطقی را پیش از گذر دوسویه به Unicode Presentation Forms بازنویسی می کند، تا شکل های joining بر اساس همسایه های منطقی محاسبه شوند و ligature ها در همان codepoint هایی که PDF واقعاً حمل می کند پخته شوند، نه اینکه به viewer واگذار شوند. این سوییچ به طور پیش فرض خاموش است و وقتی خاموش بماند خروجی از نظر بایت پایدار می ماند، بنابراین روشن کردن آن یک تصمیم عمدی برای هر pipeline سند است نه یک ارتقای سراسری. همین مدل opt-in به script های راست به چپ متصل شونده دیگر که HotPDF shape می کند هم گسترش یافته است: Syriac، N'Ko، Adlam و Hanifi Rohingya هر کدام پرچم auto-shape مخصوص به خود را دارند که نسخه عربی را آینه می کنند
ویژگی های اختیاری OpenType ماجرای دیگری هستند. ligature های اختیاری و جانشینی های تک مرحله ای مشابه از مسیر GetSingleSubstituteGlyph(GID, 'liga') عبور می کنند که هر بار فقط یک جانشینی را حل می کند، ابتدا glyph ID ورودی و سپس feature tag را می گیرد، و اگر feature اعمال نشود همان glyph ورودی را برمی گرداند. این مقدار برای راه انداختن یک فهرست محدود و معلوم از ligature هایی که خودتان نگه می دارید کافی است. اما یک موتور کامل GSUB نیست، و همین تفاوت دقیقاً همان جایی است که برنامه های بلندپروازانه locale اشتباه می روند: pipeline ای که عربی را بی نقص handle کرده فقط reordering و joining را ثابت کرده است، نه بیشتر
پوشش در میان script ها
عربی هر دو تبدیل را تمرین می کند، به همین دلیل بهترین رشته برای تست است و گذر موفق آن قوی ترین مدرک تکی برای درست کار کردن pipeline محسوب می شود. عبری به reordering نیاز دارد اما joining لازم ندارد، چون حروفش جدا می مانند؛ اگر عبری درست render شود اما عربی جدا از هم بیرون بیاید، نیمه دوسویه درست است و نیمه بافتی اصلاً اجرا نشده است. فارسی و اردو هم بر script عربی سوارند و همان رفتار را به ارث می برند، هرچند ترجیح اردو برای سبک Nastaliq یک تصمیم فونتی با پیامدهای خوانایی است که بهتر است یک خواننده بومی درباره اش قضاوت کند
تایلندی کاملاً در سوی دیگر ماجرا قرار می گیرد. چپ به راست است، بنابراین به کار دوسویه نیاز ندارد، و حروفش به هم متصل نمی شوند، بنابراین به تحلیل بافتی هم نیاز ندارد؛ رشته های تایلندی مثل لاتین از مسیر معمولی TextOut عبور می کنند. اما تایلندی علامت های انباشته شده دارد، یعنی مصوت ها و tone mark هایی که بالا و پایین صامت پایه می نشینند، و درست نشستن آن ها بستگی به این دارد که فونت chosen combining mark ها را طوری ساخته باشد که بدون کمک موتور shape روی هم بنشینند. بیشتر فونت های تخصصی تایلندی همین طورند. با همان فونتی تست کنید که واقعاً قرار است embed شود، نه با یک نمونه شبیه به آن
دواناگری و بقیه خانواده Indic همان مرز سخت و صادقانه هستند. علائم مصوت آن ها اطراف خوشه های صامت reorder می شوند و conjunct هایشان از زنجیره ای از جانشینی های وابسته به context شکل می گیرند، که کاملاً قلمرو GSUB کامل است، فراتر از reordering و joining. اگر locale ای مبتنی بر Indic در roadmap شماست، پیش از هر قولی یک pilot واقعی روی رشته های واقعی مشتری اجرا کنید؛ موفقیت عربی هیچ مدرکی برای موفقیت دواناگری نیست. رشته های CJK، ویتنامی با diacritic های انباشته، و متن ترکیبی اروپایی همه از مسیر عادی عبور می کنند و بهتر است این دو مسیر را در کد گزارش از نظر فیزیکی جدا نگه دارید، یک routine برای run های RTL و یکی برای همه چیز دیگر، تا منطق locale در همان call site دیده شود نه پشت پرچمی که کسی فراموش کند آن را روشن کند
پوشش glyph پیش از شروع shape شدن تعیین می شود
shape کردن از داخل یک فونت glyph انتخاب می کند. اگر فونت آن ها را نداشته باشد چیزی برای انتخاب وجود ندارد، و به همین دلیل شکست کلاسیک استقرار، یعنی بی نقص بودن روی ماشین برنامه نویس و ظاهر شدن جعبه های خالی روی سرور مشتری بعد از یک font substitution بی سر و صدا، مشکل پوشش است نه مشکل shape کردن. درمان عملی، یعنی ثبت فونتی که همراه نرم افزار خود می فرستید به جای اعتماد به هرچه روی ماشین نصب شده، در مقاله مرجع قدم به قدم توضیح داده شده است. نکته مفهومی این است که پوشش باید پیش از معنادار شدن هر بحث shape برقرار شده باشد، و این کار را می شود programmatically انجام داد نه فقط با نگاه کردن به خروجی
// After RegisterUnicodeTTF, audit coverage for the
// codepoints your data actually uses
GID := Pdf.GetUnicodeGlyphForCodepoint($0628); // U+0628 ARABIC LETTER BEH
LogGlyphAudit($0628, GID);
خود ثبت فونت هم دو محدودیت دارد، یعنی حداقل PDF 1.5 برای handlingِ Unicode embed شده و bit های مجوز embedding فونت، که هر دو در کنار گام های setup در مقاله مرجع RtLTextOut پوشش داده شده اند. چیزی که به اینجا تعلق دارد عادت audit است: GetUnicodeGlyphForCodepoint سیستم هشدار زودهنگام شماست. هنگام راه اندازی سرویس، بازه codepoint هایی را که داده شما واقعاً استفاده می کند پیمایش کنید و glyph ID های برگشتی را log کنید. در این صورت شکاف پوشش به شکل یک خط در startup log هنگام rollout ظاهر می شود، نه به شکل نویسه های گم شده در فاکتوری که از قبل به مشتری رسیده است
ترتیب خواندن متعلق به سند است، نه glyph ها
درست بودن همه glyph ها هنوز یک کار ناتمام باقی می گذارد. ISO 32000-1 §12.2 یک viewer preference به نام /Direction تعریف می کند که ترتیب کلی خواندن سند را اعلام می کند. این ویژگی هیچ glyph ای را دست نمی زند. کاری که انجام می دهد این است که به viewer می گوید spread های دو صفحه ای را چگونه بچیند، صفحه آرایی رو به رو از کدام سمت آغاز شود، و رابط خواندن به کدام سمت تمایل داشته باشد. هیچ یک از این ها روی یک صفحه تنها دیده نمی شوند، و دقیقاً به همین دلیل هم فراموش می شوند
// Declare right-to-left reading order at the document level
Pdf.Direction := RightToLeft; // adds vpDirection to ViewerPreferences
تنظیم کردن Direction تمام کار است: setter این property، vpDirection را به ViewerPreferences سند اضافه می کند، بنابراین همان یک خط ترجیح را داخل فایل می برد. اگر متن را از طریق RtLTextOut خارج کنید این را مجانی می گیرید، چون آن فراخوانی به عنوان side effect جهت سند را هم برمی گرداند. مقاله مرجع توضیح می دهد در چه وضعی یک سند ترکیبی نیاز دارد این اثر را خنثی کنید. حالتی که باید خودتان این را تنظیم کنید سندی راست به چپ است که به هر روش دیگری تولید شده، مثلاً ورودی ای که upstream آن را از پیش shape کرده و از مسیر عادی رسم شده است. اگر آنجا حذفش کنید، مدرک تک صفحه ای که به آن خیره شده اید در هر دو حالت یکسان به نظر می رسد؛ بعد کسی یک booklet دو رو چاپ می کند، spread ها آینه ای بیرون می آیند، و علت یک خط جاافتاده از هفته ها پیش است
بررسی خروجی shape شده
از ابتدا تا انتها verify کنید، چون یک صفحه می تواند درست به نظر برسد و در عین حال برای همه چیز پایین دست بی فایده باشد. سه بررسی بیشتر مشکل ها را پیدا می کنند. متن را دوباره از Acrobat کپی کنید و codepoint ها را با رشته منبع مقایسه کنید. در همان viewer برای واژه ای که روی صفحه می بینید جست وجو کنید. و خروجی را روی ماشینی باز کنید که فونت های توسعه شما را ندارد، یعنی همان جایی که بیشترین احتمال دارد substitution را آشکار کند. هیچ کدام از این ها جای نگاه یک خواننده native به یک سند واقعی را نمی گیرد، چون او چیزهایی را می بیند که هیچ corpus مصنوعی ای نمی بیند. این بازبینی را پیش از انتشار فرمت در تقویم بگذارید
رشته های تست را آگاهانه انتخاب کنید، نه اینکه هرچه مترجم سال گذشته فرستاده را دوباره مصرف کنید. یک حداقل عملی برای هر locale این است: یک جمله کاملاً از همان script، یک جمله با برندهای لاتین توکار، یک خط شامل رقم و واحد پول، و نام هایی با diacritic یا combining mark. نام های واقعی مشتری فرض هایی را می شکنند که filler text هرگز به آن ها دست نمی زند، بنابراین هر بار که یک پرونده پشتیبانی الگوی تازه ای را آشکار می کند، مجموعه regression باید یک رشته دیگر به خود اضافه کند
ثبت فونت، subset سازی و API های عادی رسم متن در مقاله مربوط به خروجی گزارش، فونت ها و تصویرها با HotPDF پوشش داده شده اند. وقتی همین اسناد همزمان باید پروفایل های دسترس پذیری را هم برآورده کنند، قواعد برچسب زبان و ساختار در مقاله اعتبارسنجی PDF/A و PDF/UA بر کار shape کردن اینجا سوار می شوند
API های راست به چپ و فونت Unicode که در بالا توصیف شدند همراه با HotPDF Component برای Delphi و C++Builder ارائه می شوند، و صفحه محصول به مرجع کامل خروجی متن پیوند می دهد