مقاله فنی

RtLTextOut در HotPDF: متن PDF راست‌به‌چپ در Delphi

اگر جمله عربی يوضح ملف PDF هذا را به TextOut معمولی بدهید، صفحه خروجی هم‌زمان از دو جهت اشتباه خواهد بود. کلمات به جای راست‌به‌چپ، از چپ به راست می‌روند و حروف به جای پیوستن در قالب واژه‌های متصل، جدا از هم می‌افتند. هیچ خطایی هم رخ نمی‌دهد. Delphi کامپایل می‌شود، فایل باز می‌شود، اما بازبینی که عربی می‌خواند به شما می‌گوید خروجی قابل استفاده نیست. راه‌حل یک فراخوانی متفاوت است، نه تعویض کتابخانه: HotPDF متن راست‌به‌چپ را از مسیر یک متد جداگانه به نام RtLTextOut عبور می‌دهد تا بازچینی‌ای را انجام دهد که TextOut عادی هرگز انجام نمی‌دهد. این صفحه مرجع عملی همین متد است: امضا و پارامترها، آرگومان charset که اسکریپت را تعیین می‌کند، اثر جانبی در سطح سند، تنظیم فونتی که باید پیش از هر چیز انجام شود، و خطاهای واقعی پشتیبانی همراه با راه‌حل آن‌ها

امضا و پارامترها

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

X و Y اجرای متن را در دستگاه مختصات خود صفحه لنگر می‌دهند؛ همان دستگاهی که مبدأ آن گوشه پایین چپ است و محور Y به سمت بالا رشد می‌کند، دقیقاً همان مبدأیی که هر فراخوانی TextOut از آن استفاده می‌کند. RtLTextOut فقط ترتیب گلیف‌ها را عوض می‌کند، نه این‌که صفحه از کجا اندازه‌گیری می‌شود. angle خط مبنا را دقیقاً مانند TextOut می‌چرخاند، بنابراین مقدار 0 یک خط افقی رسم می‌کند. Text باید به ترتیب منطقی داده شود، همان ترتیبی که آن را تایپ می‌کنید. overload دوم همان داده UTF-16 را به صورت یک بافر خام PWORD همراه با تعداد صریح code unit می‌گیرد، و زمانی مناسب است که متن از یک API برسد نه از یک رشته Delphi. در نسخه‌های قدیمی‌تر Delphi که برای این نوع‌ها overload resolution ندارند، فرم رشته‌ای با نام RtLTextOutStr و با همین فهرست پارامترها عرضه می‌شود

تقسیم کار بین دو فراخوانی خروجی کاملاً سخت‌گیرانه است. TextOut codepointها را دقیقاً به همان ترتیبی که می‌فرستید رسم می‌کند؛ این برای لاتین، سیریلیک و CJK درست است و برای عربی و عبری غلط. RtLTextOut ابتدا هر خط را به ترتیب دیداری راست‌به‌چپ بازچینی می‌کند و بعد آن را رسم می‌کند، در حالی که واژه‌های لاتین و ارقام تعبیه‌شده را درون همان خط همچنان چپ‌به‌راست نگه می‌دارد. HotPDF عمداً این دو متد را از هم جدا نگه می‌دارد و جهت را از روی نویسه‌ها حدس نمی‌زند، بنابراین انتخاب متد یعنی انتخاب رفتار اسکریپت. برای بخش‌های راست‌به‌چپ از RtLTextOut استفاده کنید، برای بقیه از TextOut، و هرگز یکی را از مسیر دیگری عبور ندهید. این‌که اصلاً چرا بازچینی لازم است، الگوریتم دوطرفه Unicode و اتصال بافتی عربی دقیقاً چه می‌کنند، و HotPDF در شکل‌دهی کجا متوقف می‌شود، در مقاله همراه شکل‌دهی متن عربی و RTL با HotPDF آمده است؛ آنچه در ادامه می‌آید صرفاً تنظیم عملی است

نموداری که نشان می‌دهد RtLTextOut چگونه پیش از رسم در PDF یک خط ترکیبی عربی و لاتین را به ترتیب دیداری راست‌به‌چپ بازچینی می‌کند
RtLTextOut پیش از رسم، هر خط را به ترتیب دیداری بازچینی می‌کند: بخش‌های راست‌به‌چپ توالی خود را حفظ می‌کنند و واژه‌های لاتین و ارقام تعبیه‌شده درون همان خط از چپ به راست خوانده می‌شوند

آرگومان charset اسکریپت را تعیین می‌کند

چیزی که به RtLTextOut می‌گوید در حال چیدن عربی است یا عبری، خود متد نیست بلکه فونت است. SetFont در آرگومان چهارم خود یک Windows charset می‌گیرد، و همان مقدار قواعد اسکریپت را به فراخوانی راست‌به‌چپ منتقل می‌کند: 178 عربی را انتخاب می‌کند و 177 عبری را. charset را تنظیم کنید و بعد رسم کنید؛ دو خط زیر بدون هیچ پیکربندی اضافی با ترتیب درست خواندن بیرون می‌آیند

// Arabic: charset 178 tells RtLTextOut to apply Arabic rules
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebrew: charset 177 switches the rules to Hebrew
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

یک نکته ترتیبی را به‌راحتی می‌شود از قلم انداخت: SetFont باید اول اجرا شود و بعد از هر AddPage نیز باید تکرار شود، چون فونت جاری، همراه با charset آن، از یک page break عبور نمی‌کند. اگر این تکرار را فراموش کنید، صفحه دوم به هر فونتی که فعال بوده برمی‌گردد، و برای عربی این معمولاً یعنی ردیف‌هایی از مربع خالی

متنی را که خودتان قبلاً معکوس کرده‌اید دوباره به آن ندهید

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

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

اثر جانبی Direction که باید بدانید

فراخوانی RtLTextOut فقط همان خطی را که می‌کشید تغییر نمی‌دهد. این متد ترجیح جهت خواندن سند را هم به راست‌به‌چپ برمی‌گرداند؛ همان چیزی که در غیر این صورت خودتان از راه ویژگی Direction تنظیم می‌کردید. آن setter مقدار vpDirection را به ViewerPreferences سند اضافه می‌کند، و به viewer می‌گوید spreads دوصفحه‌ای را چگونه بچیند و چیدمان صفحات روبه‌رو از کدام سمت شروع شود. وقتی کل سند عربی یا عبری است، دقیقاً همین چیزی است که می‌خواهید و آن را رایگان می‌گیرید

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

// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;

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

فونتی را ثبت کنید که با محصول خود توزیع می‌کنید، نه فونتی که امیدوارید نصب شده باشد

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

// Ship a known Arabic font and register it before drawing
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

دو مرز دیگر هم همراه ثبت فونت می‌آید. فونتی که از راه RegisterUnicodeTTF وارد می‌شود embed می‌شود، و هندلینگ Unicode تعبیه‌شده HotPDF به PDF 1.5 یا بالاتر نیاز دارد؛ این موضوع فقط زمانی گاز می‌گیرد که چیزی در پایین‌دست روی PDF 1.4 اصرار داشته باشد، اما وقتی چنین شود شکست کاملاً بی‌صدا است. مرز دیگر فنی نیست، حقوقی است: فایل‌های TrueType بیت‌های مجوز embedding دارند، و فونتی که روی صفحه عالی به نظر می‌رسد ممکن است مجوزی داشته باشد که توزیع آن در اسناد مشتری را ممنوع کند. مجوز را پیش از embedding بررسی کنید، نه بعد از شکایت

یک نمونه کنسول کامل

اگر همه قطعات را کنار هم بگذاریم، برنامه خودبسنده زیر یک صفحه می‌نویسد که شامل یک خط عربی، یک خط عبری و یک خط ترکیبی با یک نام محصول لاتین است. هر بلوک charset خودش را تنظیم می‌کند و بعد با ترتیب منطقی رسم می‌شود

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // HotPDF main unit

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'RtLTextOut.pdf';
    Pdf.BeginDoc;

    // A Latin heading goes through the ordinary TextOut path
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Arabic: charset 178, logical order, RtLTextOut does the reordering
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // Hebrew: charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // Mixed line: the embedded Latin word still reads left to right
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 640, 0,
      'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');

    Pdf.EndDoc;
    Writeln('Wrote RtLTextOut.pdf');
  finally
    Pdf.Free;
  end;
end.

آن را اجرا کنید و نتیجه را باز کنید. خطوط عربی و عبری از راست به چپ خوانده می‌شوند، حروف در جاهایی که اسکریپت آن‌ها را به هم وصل می‌کند به‌درستی متصل می‌شوند، و در خط آخر، توکن HotPDF درون اجرای عربی همچنان از چپ به راست دیده می‌شود. این تو‌در‌تویی نتیجه درست bidirectional است، نه bug، هرچند بازبینانی که برای اولین بار آن را می‌بینند مرتب آن را bug گزارش می‌کنند. مقاله شکل‌دهی که در بالا پیوند شد توضیح می‌دهد چرا قواعد Unicode دقیقاً این رفتار را می‌طلبند و چطور معیارهای پذیرش را طوری بنویسید که اصلاً چنین گزارش اشتباهی ثبت نشود

خطاهای رایج و راه‌حل آن‌ها

هر شکست زیر در یک تیکت واقعی پشتیبانی دیده شده و هرکدام مستقیماً به یکی از بخش‌های بالا برمی‌گردد

  • خروجی در خطوط ترکیبی برعکس یا درهم می‌ریزد — رشته پیش از فراخوانی دستی معکوس شده بود، معمولاً به‌عنوان باقی‌مانده یک workaround مربوط به TextOut. تمام وارونگی‌های دستی را حذف کنید و متن را به ترتیب منطقی بدهید؛ RtLTextOut خودش درونی معکوس می‌کند
  • حروف به‌صورت جدا و منفصل چاپ می‌شوند — متن از مسیر TextOut عادی عبور کرده، یا SetFont بدون یک charset راست‌به‌چپ فراخوانی شده است. با RtLTextOut رسم کنید و در آرگومان چهارم SetFont برای عربی مقدار 178 و برای عبری مقدار 177 را بدهید
  • روی ماشین مشتری مربع خالی ظاهر می‌شود — Windows فونتی را جایگزین کرده که پوشش عربی یا عبری ندارد. نام فونت‌های نصب‌شده را کنار بگذارید؛ یک font face را که خودتان توزیع می‌کنید با RegisterUnicodeTTF ثبت کنید و بعد با همان نام در SetFont به‌کار ببرید
  • صفحه دوم با فونت اشتباه رندر می‌شود — فونت جاری از AddPage عبور نمی‌کند. بعد از هر page break، فراخوانی SetFont را همراه با charset آن تکرار کنید
  • اسپردهای دورو در سندی که عمدتاً LTR است آینه‌ای چاپ می‌شوند — نخستین فراخوانی RtLTextOut مقدار Direction سند را به‌عنوان یک اثر جانبی برگردانده است. بعد از اجرای بخش راست‌به‌چپ مقدار Pdf.Direction := LeftToRight را تنظیم کنید
  • متن Unicode تعبیه‌شده در پایین‌دست بی‌سروصدا خراب می‌شود — چیزی در pipeline سند را به PDF 1.4 وادار می‌کند، در حالی که هندلینگ Unicode تعبیه‌شده HotPDF به 1.5 یا بالاتر نیاز دارد. نسخه سند را بالا ببرید یا محدودیت پایین‌دست را حذف کنید

پیش از آن‌که فرمت را منتشر کنید، فراتر از نگاه‌کردن صرف اعتبارسنجی کنید: متن را از viewer دوباره کپی کنید، جست‌وجوی درون سند را اجرا کنید، فایل را روی ماشینی بدون فونت‌های توسعه خود باز کنید، و یک سند واقعی را جلوی یک خواننده بومی بگذارید. چک‌لیست کامل اعتبارسنجی، نقشه پوشش per-script، و مجموعه رشته‌های تستی که واقعاً ارزش ساختن دارند، همگی در مقاله همراه درباره شکل‌دهی متن عربی و RTL با HotPDF آمده‌اند

فراخوانی‌های RtLTextOut، SetFont و RegisterUnicodeTTF که در اینجا نشان داده شد بخشی از HotPDF Component برای Delphi و C++Builder هستند