مطالب
ترفندهای یونیکد برای زبان‌های راست به چپ
آشنایی با RLE

الگوریتم پردازش دوطرفه‌ی یونیکد، جهت و سمت نمایش متن را بر اساس خواص جهتی هر حرف مشخص می‌کند. در این حالت اگر متن مورد نمایش، انگلیسی و یا فارسی خالص باشند به خوبی عمل می‌کند؛ اما اگر ترکیب این دو را در یک  رشته داشته باشیم، نیاز است نحوه‌ی جهت گیری و نمایش حروف را به Unicode bidirectional algorithm معرفی کنیم. این نوع مشکلات را فارسی زبان‌ها در حین نمایش ترکیبی از متن فارسی و انگلیسی در Tooltips، برنامه‌های نمایش زیرنویس‌های فیلم‌ها، برنامه‌های گزارشگیری و امثال آن به وفور مشاهده می‌کنند.
راه حل استاندارد یونیکد آن، استفاده از حروف نامرئی یونیکد است که جهت نمایشی متن جاری را بازنویسی می‌کنند:
U+202A:   LEFT-TO-RIGHT EMBEDDING (LRE)
U+202B:   RIGHT-TO-LEFT EMBEDDING (RLE)
U+202D:   LEFT-TO-RIGHT OVERRIDE (LRO)
U+202E:   RIGHT-TO-LEFT OVERRIDE (RLO)
U+202C:   POP DIRECTIONAL FORMATTING (PDF)
برای مثال حرف یونیکد نامرئی U202B به این معنا است: «از این لحظه به بعد تا اطلاع ثانوی، متن نمایش داده شده راست به چپ است؛ صرفنظر از خواص جهتی حروف مورد استفاده».
این تا اطلاع ثانوی یا POP نیز توسط حرف U202C مشخص شده و به پایان می‌رسد. به عبارتی یونیکد شبیه به یک پشته یا Stack عمل می‌کند.


مثال اول
عبارت «متن فارسی به همراه جمله‌ی this is a test انگلیسی» را در نظر بگیرید. اکنون فرض کنید می‌خواهیم از آن جهت ارائه یک فایل readme مخصوص GitHub با فرمت mark down یا md استفاده کنیم:


همانطور که ملاحظه می‌کنید، جمله معکوس شده‌است. برای رفع این مشکل می‌توان از کاراکتر نامرئی یونیکد 202b استفاده کرد. البته در mark down امکان تعریف ساده‌تر این کاراکتر به صورت ذیل نیز پیش بینی شده‌است:
 ‫



مثال دوم

اغلب نمایشگرهای چپ به راست متون نیز در حالت پیش فرض، عبارت مثال اول را معکوس نمایش می‌دهند:


اگر از notepad استفاده کنید، به صورت توکار امکان افزودن RLE را به ابتدای جمله دارد:


مثال سوم

در زبان‌های دات نتی نیز جهت نمایش صحیح متون ترکیبی، می‌توان حرف RLE را به صورت ذیل به ابتدای یک جمله اضافه کرد:
public const char RightToLeftEmbedding = (char)0x202B;
این مورد خصوصا در ابزارهای گزارشگیری یا کار با API ویندوز می‌تواند مفید باشد.



تشخیص راست به چپ بودن متن

در محیط وب جهت نمایش صحیح یک متن نیز می‌توان به مرورگرها کمک کرد. تعریف dir=rtl تفاوتی با قرار دادن RLE در ابتدای یک متن ندارد. در این حالت نیاز است بدانیم حروف RTL در چه بازه‌ای از شماره حروف یونیکد قرار می‌گیرند:
  Right-to-left Unicode blocks for modern scripts are:

 Consecutive range of the main letters:
 U+0590 to U+05FF - Hebrew
 U+0600 to U+06FF - Arabic
 U+0700 to U+074F - Syriac
 U+0750 to U+077F - Arabic Supplement
 U+0780 to U+07BF - Thaana
 U+07C0 to U+07FF - N'Ko
 U+0800 to U+083F - Samaritan

 Arabic Extended:
 U+08A0 to U+08FF - Arabic Extended-A

 Consecutive presentation forms:
 U+FB1D to U+FB4F - Hebrew presentation forms
 U+FB50 to U+FDFF - Arabic presentation forms A

 More Arabic presentation forms:
 U+FE70 to U+FEFF - Arabic presentation forms B
که یک نمونه‌ی ساده شده‌ی این بازه‌ها، به صورت ذیل است:
private static readonly Regex _matchArabicHebrew =
new Regex(@"[\u0600-\u06FF,\u0590-\u05FF]", RegexOptions.IgnoreCase | RegexOptions.Compiled);

  public static bool ContainsRtlFarsi(this string txt)
  {
       return !string.IsNullOrEmpty(txt) && _matchArabicHebrew.IsMatch(txt);
  }
و حالت پیشرفته‌تر آن‌را که سایت توئیتر برای ارائه‌ی یک جعبه متنی به صورت خودکار راست به چپ شونده، مورد استفاده قرار می‌دهد، در اینجا می‌توانید مطالعه کنید:
RTLText.module.js


نمایش صحیح عبارات ممیز دار در یک گزارش راست به چپ


استاندارد یونیکد یک سری کاراکتر را «کاراکتر ضعیف» معرفی کرده‌است. برای مثال کاراکتر اسلش بکار رفته در یک تاریخ هم از این دست است. بنابراین اگر در یک گزارش تولیدی، شماره کد ممیز دار و یا یک تاریخ را معکوس مشاهده می‌کنید به این علت است که یک «نویسه ضعیف» مثل اسلش نمی‌تواند جهت را تغییر دهد؛ مگر اینکه از یک «نویسه قوی» برای دستکاری آن استفاده شود (مانند RLE و POP که در ابتدای بحث معرفی شدند).
یک مطلب تکمیلی در این مورد: «iTextSharp و نمایش صحیح تاریخ در متنی راست به چپ»
این اصول در تمام محیط‌هایی که از یونیکد پشتیبانی می‌کنند صادق است و تفاوتی نمی‌کند که ویندوز باشد یا Adobe reader و یا یک ابزار گزارشگیری که اصلا برای محیط‌های راست به چپ طراحی نشده‌است.


کار با اعراب در متون راست به چپ

در یونیکد یک حرف می‌تواند از یک یا چند code point تشکیل شود. در حالت FormC، هر حرف، با اعراب آن یک code point را تشکیل می‌دهند. در حالت FormD، حرف با اعراب آن دو code point را تشکیل خواهند داد. به همین جهت نیاز است رشته را تبدیل به حالت D کرد تا بتوان اعراب آن‌را مجزای از حروف پایه، حذف نمود.
البته اعراب در اینجا به اعراب عربی ختم نمی‌شود. یک سری حروف اروپایی مانند  "ä" ،"ö" و "ü" را نیز شامل می‌شود.
یک مطلب تکمیلی در این مورد:  «حذف اعراب از حروف و کلمات»
نظرات اشتراک‌ها
نگاهی به Sketch Design
راستش دنبال برنامه ای بودم که بتونه من رو به چیزی شبیه این عکس برسونه اما مثل اینکه چاره ای جز دست به قلم شدن نیست!
مطالب
فارسی نویسی با SkiaSharp
تا نگارش 4x دات نت که فقط از ویندوز پشتیبانی می‌کند، از وابستگی System.Drawing.Common برای انجام امور روزمره‌ی گرافیکی استفاده می‌شد؛ چون در پشت صحنه، محصور کننده‌ی امکانات بومی گرافیکی ویندوز است. همچنین از زمان ارائه‌ی دات نت Core چندسکویی، تا نگارش 5 دات نت، این وابستگی، در لینوکس، به کمک کتابخانه‌ی جانبی به نام libgdiplus پشتیبانی می‌شد که البته هیچگاه پشتیبانی رسمی را از طرف مایکروسافت پیدا نکرد؛ چون libgdiplus متشکل از چند ده‌هزار سطر کد نوشته شده‌ی به زبان C است که به‌خوبی آزمایش نشده و همچنین برای کارکرد کامل آن نیز به کتابخانه‌های جانبی دیگری مانند pango نیاز است تا برای مثال از نمایش متون فارسی پشتیبانی کند. Libgdiplus در حقیقت بازمانده‌ای از دوران Mono است که مایکروسافت در نگارش 6 دات نت، آن‌را منسوخ شده اعلام کرد و در نگارش 7 دات نت، دیگر از آن پشتیبانی نمی‌کند. یعنی تمام برنامه‌هایی که از وابستگی System.Drawing.Common استفاده می‌کنند، قابل انتقال به دات نت 7 چندسکویی نیستند؛ البته هنوز هم می‌توان از System.Drawing.Common در ویندوز، بدون مشکل استفاده کرد. اما در صورت استفاده‌، برنامه‌ی شما در لینوکس اجرا نخواهد شد و یک چنین برنامه‌هایی با استثناهای TypeInitializationException و PlatformNotSupportedException در زمان اجرا، خاتمه خواهند یافت.
در حال حاضر توصیه‌ی مایکروسافت ، عدم استفاده‌ی از System.Drawing.Common و جایگزینی آن با یکی از کتابخانه‌های زیر است:
- SkiaSharp
- Microsoft.Maui.Graphics

البته پیشتر در این لیست توصیه شده، کتابخانه‌ی SixLabors.ImageSharp.Drawing هم وجود داشت که به علت تغییر مجوز آن، به یک مجوز نیمه تجاری، نیمه سورس باز، از لیست فوق حذف شده‌است.


مشکل فارسی نویسی با SkiaSharp

اگر سعی کنیم با استفاده از مثال‌های متداول SkiaSharp، یک متن فارسی را نمایش دهیم، به خروجی زیر خواهیم رسید:
// crate a surface
var info = new SKImageInfo(256, 256);
using var surface = SKSurface.Create(info);
// the the canvas and properties
var canvas = surface.Canvas;

// make sure the canvas is blank
canvas.Clear(SKColors.White);

// draw some text
using var typeface = SKTypeface.FromFamilyName("Tahoma");
using var paint = new SKPaint
    {
      Color = SKColors.Black,
      IsAntialias = true,
      Style = SKPaintStyle.Fill,
      TextAlign = SKTextAlign.Center,
      TextSize = 24,
      Typeface = typeface,
    };
var coord = new SKPoint(info.Width / 2, (info.Height + paint.TextSize) / 2);
canvas.DrawText("آزمایش", coord, paint);

// save the file
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var stream = File.OpenWrite("farsi-text-1.png");
data.SaveTo(stream);
قطعه کد فوق برای اجرا، نیاز به وابستگی زیر را دارد:
<ItemGroup>
   <PackageReference Include="SkiaSharp" Version="2.88.3" />
</ItemGroup>
که در آن، در ابتدا یک Canvas برای نقاشی ایجاد شده و سپس متنی بر روی آن نمایش داده می‌شود و در آخر این نتیجه را در یک فایل ذخیره می‌کنیم؛ با این خروجی:

همانطور که مشاهده می‌کنید، حروف فارسی در آن از هم جدا هستند و همچنین از چپ به راست نمایش داده شده‌است.


رفع مشکل فارسی نویسی با SkiaSharp

برای رفع مشکل فوق، نیاز است از افزونه‌ی «حرف باز» این کتابخانه استفاده کرد که روش نصب آن به صورت زیر است:
<ItemGroup>
   <PackageReference Include="SkiaSharp" Version="2.88.3" />
   <PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
</ItemGroup>
اینبار تنها تفاوت مورد نیاز جهت نمایش صحیح حروف فارسی، استفاده از SKShaper جهت شکل دادن به متن نهایی است و استفاده از متد DrawShapedText آن به صورت زیر:
// crate a surface
var info = new SKImageInfo(256, 256);
using var surface = SKSurface.Create(info);
// the the canvas and properties
var canvas = surface.Canvas;

// make sure the canvas is blank
canvas.Clear(SKColors.White);

// draw some text
using var typeface = SKTypeface.FromFamilyName("Tahoma");
using var shaper = new SKShaper(typeface);
using var paint = new SKPaint
  {
      Color = SKColors.Black,
      IsAntialias = true,
      Style = SKPaintStyle.Fill,
      TextAlign = SKTextAlign.Center,
      TextSize = 24,
      Typeface = typeface,
  };
var coord = new SKPoint(info.Width / 2, (info.Height + paint.TextSize) / 2);
canvas.DrawShapedText(shaper, "آزمایش", coord, paint);

// save the file
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
using var stream = File.OpenWrite("farsi-text-2.png");
data.SaveTo(stream);
که خروجی صحیح زیر را تولید می‌کند:

مطالب
تاثیر فرهنگ جاری سیستم بر روی اعداد در دات نت
در ویندوز 8، مایکروسافت سعی کرده‌است تا تنظیمات بومی مرتبط با ایران، با واقعیت انطباق بیشتری داشته باشد. برای مثال در فرهنگ فارسی سیستم، علامت ممیز آن / است؛ بجای . معمول.


برای آزمایش آن، سعی کنید چنین برنامه‌ای را در ویندوز 8 اجرا کنید:
using System;

namespace CultureAndNumbers
{
    class Program
    {
        static void Main(string[] args)
        {
            var number = Convert.ToDecimal("12.32");
            Console.WriteLine(number);
        }
    }
}
در اینجا سعی شده‌است یک عدد دسیمال رشته‌ای به معادل عددی آن تبدیل شود.
خروجی آن به نحو ذیل است:


بله! چون در فرهنگ جاری سیستم، علامت ممیز دیگر . نیست، رشته‌ی 12.32 نیز بی‌معنا است و قابل تبدیل به یک عدد دسیمال نخواهد بود.
همچنین باید دقت داشت تاثیر فرهنگ جاری سیستم بر روی متدهای Convert.ToDecimal و decimal.Parse یکسان است.


روشی برای آزمایش موقت فرهنگ‌های مختلف

برای اینکه بتوان فرهنگ‌های مختلف را به سادگی مورد آزمایش قرار داد، نیاز است خاصیت CurrentCulture ترد جاری برنامه را تغییر داد و پس از پایان کار، مجددا این ترد را به فرهنگ پیش از آزمایش تنظیم کرد. برای این منظور می‌توان از پیاده سازی الگوی IDisposable کمک گرفت:
    public class CultureScope : IDisposable
    {
        private readonly CultureInfo _originalCulture;

        public CultureScope(string culture)
        {
            _originalCulture = Thread.CurrentThread.CurrentCulture;
            Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
        }

        public void Dispose()
        {
            Thread.CurrentThread.CurrentCulture = _originalCulture;
        }
    }
در این حالت برای آزمایش فرهنگ فارسی نصب شده در سیستم می‌توان به صورت ذیل عمل کرد. این فرهنگ تنها در چارچوب قطعه کد using، تنظیم می‌شود و پس از آن، مجددا برنامه با فرهنگ اصلی پیش از اجرای این قطعه کد به کار خود ادامه خواهد داد:
using (var cultureScope = new CultureScope("fa-IR"))
{
   Console.WriteLine(Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator);
   var number = decimal.Parse("12.32");
   Console.WriteLine(number);
}


تعیین صریح فرهنگ مورد استفاده

یک راه حل برای رفع این مشکل، قید صریح فرهنگ مورد استفاده است. برای مثال اگر اعداد در بانک اطلاعاتی به صورت 12.32 ثبت شده‌اند، می‌توان نوشت:
 using (var cultureScope = new CultureScope("fa-IR"))
{
   Console.WriteLine(Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator);
   var number = decimal.Parse("12.32", new CultureInfo("en"));
   Console.WriteLine(number);
}
در اینجا فرهنگ انگلیسی به صورت صریح ذکر شده‌است و دیگر فرهنگ تنظیم شده‌ی fa-IR مورد استفاده قرار نخواهد گرفت.
اما این روش هم قابل اطمینان نیست. زیرا کاربر می‌تواند در کنترل پنل سیستم، به سادگی علامت ممیز را مثلا به # تغییر دهد و در این حالت باز هم برنامه کرش خواهد کرد. راه حلی که برای این مساله در دات نت وجود دارد، فرهنگی است به نام Invariant که یک کپی فقط خواندنی از فرهنگ انگلیسی را به همراه دارد و در این حالت تنظیمات اختصاصی کاربر در کنترل پنل، ندید گرفته خواهند شد:
 using (var cultureScope = new CultureScope("fa-IR"))
{
   Console.WriteLine(Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator);
   var number = decimal.Parse("12.32", CultureInfo.InvariantCulture);
   Console.WriteLine(number);
}
اینبار هر چند فرهنگ ترد جاری به fa-IR تنظیم شده‌است اما چون فرهنگ مورد استفاده CultureInfo.InvariantCulture است، از یک فرهنگ انگلیسی فقط خواندنی که تنظیمات محلی کاربر بر روی آن بی تاثیر است، استفاده خواهد شد. یک چنین کدی در تمام سیستم‌ها بدون مشکل کار می‌کند.
مطالب
تعریف رنگ در iTextSharp

در کتابخانه‌ی iTextSharp به جهت سازگاری با کتابخانه‌ی اصلی، رنگ‌ها را بر اساس کلاسی به نام BaseColor تعریف کرده‌اند؛ که ای‌کاش به جای این‌کار، همه را با کلاس Color فضای نام استاندارد System.Drawing جایگزین می‌کردند. همین مشکل با فونت هم هست. یک کلاس فونت در فضای نام iTextSharp.text وجود دارد به علاوه کلاس فونت تعریف شده در فضای نام استاندارد System.Drawing دات نت؛ که خیلی سریع می‌تواند به خطای کامپایل زیر ختم شود:

'Font' is an ambiguous reference between 'iTextSharp.text.Font' and 'System.Drawing.Font'	


و در نهایت مجبور خواهیم شد که به صورت صریح علام کنیم، iTextSharp.text.Font منظور ما است و نه آن یکی.
در کل اگر با کلاس Color فضای نام استاندارد System.Drawing بیشتر راحت هستید به صورت زیر هم می‌توان رنگ‌های متداول را مورد استفاده قرار داد:

تعریف رنگ‌ها بر اساس نام آن‌ها:

var color = new BaseColor(Color.LightGray);


تعریف رنگ‌ها بر اساس مقادیر Hex متداول در المان‌های HTML :

var color = new BaseColor(ColorTranslator.FromHtml("#1C5E55"));


این نکته‌ای است که شاید خیلی‌ها از وجود آن بی‌اطلاع باشند. به صورت پیش فرض در کلاس استاندارد ColorTranslator، امکان دریافت رنگ‌های بکاررفته در المان‌های HTML به کمک متد ColorTranslator.FromHtml مهیا است.
البته اگر زمانی خواستید خودتان این متد را پیاده سازی کنید، نکته‌ی آن به صورت زیر است:

string htmlColor = "#1C5E55";

int x = Convert.ToInt32(htmlColor.Replace("#", "0x"), 16);
byte red   = (byte)((x & 0xff0000) >> 16);
byte green = (byte)((x & 0xff00) >> 8);
byte blue  = (byte)(x & 0xff);

سپس کلاس‌های Color و همچنین BaseColor امکان پذیرش این اجزای حاصل را دارند (به کمک متد Color.FromRgb و یا سازنده‌ی BaseColor).
علت ذکر ColorTranslator.FromHtml به این بر می‌گردد که ترکیبات رنگ جالبی را می‌توان از جداول HTML ایی موجود در سایت‌های مختلف ایده گرفت و یا حتی از قالب‌های پیش فرض GridView در ASP.NET مثلا.


اکنون برای ساخت جدولی مانند شکل فوق، به ازای هر سلولی که مشاهده می‌کنید باید یکبار BorderColor و BackgroundColor تنظیم شوند. رنگ متن هم از رنگ فونت دریافت می‌شود:

var pdfCell = new PdfPCell(new Phrase(Text, Font))

{
RunDirection = ...,
BorderColor = ...,
BackgroundColor = ...
};


نظرات مطالب
قرار دادن نمودارهای MS Chart در گزارشات PdfReport
بله. از فونت‌های فارسی تغییر یافته استفاده کنید. برای مثال فونت irsans ایی که در پوشه bin مثال‌های پروژه هست، از این نوع است. فونت‌های معمولی رو در یک فونت ادیتور باز می‌کنند و بجای اعداد انگلیسی آن، معادل اعداد فارسی موجود در همان فایل فونت را کپی و پیست می‌کنند. این روش از قدیم برای ساخت گزارشات فارسی کاربرد داشته.
ضمنا اگر فونت مورد نظر بر روی سرور نصب نیست، به این صورت هم قابل بارگذاری است:
var privateFontCollection = new PrivateFontCollection();                            
privateFontCollection.AddFontFile(fontPath);
var fontFamily = privateFontCollection.Families[0];
var font = new Font(fontFamily);