Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iText PDF Persian text rendering issue — characters appear broken

I’m generating a PDF using iText (version 9.2.0) in C#. The PDF contains Persian (Farsi) text, but the characters do not display correctly — they appear separated or broken.

here is my code :

public async Task<byte[]> GenerateOrderPdf(Guid orderId, CancellationToken cancellationToken)
{
    var order = await _orderRepository.GetOrderById(orderId, cancellationToken);
    var orderServicesList = await _orderServicesRepository.GetOrderServices(orderId, cancellationToken);
    var serviceIds = orderServicesList.Select(x => x.ServiceId);
    var services = await _serviceRepository.GetServices(serviceIds, cancellationToken);

    using var stream = new MemoryStream();
    var writer = new PdfWriter(stream);
    var pdf = new PdfDocument(writer);
    var document = new Document(pdf);

    var vazirFontPath = Path.Combine(AppContext.BaseDirectory, "Resources", "Fonts", "Vazirmatn-Regular.ttf");
    var vazirFont = PdfFontFactory.CreateFont(vazirFontPath, PdfEncodings.IDENTITY_H);

    document.Add(new Paragraph($"جزئیات سفارش - کد #{order.OrderCode}")
        .SetFontSize(20)
        .SetTextAlignment(TextAlignment.RIGHT)
        .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
        .SetFont(vazirFont));

    document.Add(new Paragraph("وضعیت : " + order.Status)
        .SetTextAlignment(TextAlignment.RIGHT)
        .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
        .SetFont(vazirFont));

    document.Add(new Paragraph("تاریخ ایجاد : " + order.CreateDate.ToString("yyyy-MM-dd"))
        .SetTextAlignment(TextAlignment.RIGHT)
        .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
        .SetFont(vazirFont));

    document.Add(new Paragraph("تاریخ پرداخت : " + order.ProcessedAt?.ToString("yyyy-MM-dd"))
        .SetTextAlignment(TextAlignment.RIGHT)
        .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
        .SetFont(vazirFont));

    document.Add(new Paragraph("آخرین تغییر وضعیت : " + order.LastStateChange?.ToString("yyyy-MM-dd"))
        .SetTextAlignment(TextAlignment.RIGHT)
        .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
        .SetFont(vazirFont));

    document.Add(new Paragraph(" ")
        .SetFontSize(15));

    var table = new Table(new float[] { 1, 1, 1, 1 });
    table.SetWidth(UnitValue.CreatePercentValue(100))
         .SetFont(vazirFont)
         .SetTextAlignment(TextAlignment.CENTER);

    var headerBgColor = new iText.Kernel.Colors.DeviceRgb(238, 238, 238);
    table.AddHeaderCell(new Cell()
        .SetBackgroundColor(headerBgColor)
        .Add(new Paragraph("خدمت")
            .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));
    table.AddHeaderCell(new Cell()
        .SetBackgroundColor(headerBgColor)
        .Add(new Paragraph("سیستم خدمت")
            .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));
    table.AddHeaderCell(new Cell()
        .SetBackgroundColor(headerBgColor)
        .Add(new Paragraph("وضعیت")
            .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));
    table.AddHeaderCell(new Cell()
        .SetBackgroundColor(headerBgColor)
        .Add(new Paragraph("جزئیات")
            .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

    foreach (var os in orderServicesList)
    {
        var serviceName = services.FirstOrDefault(x => x.Id == os.ServiceId)?.Name?.GetValue() ?? "";

        table.AddCell(new Cell().Add(new Paragraph(serviceName)
            .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

        table.AddCell(new Cell().Add(new Paragraph(os.ServiceSystem.ToString())
            .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

        var statusCell = new Cell().Add(new Paragraph(os.Status.ToString())
            .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont));
        if (os.Status.ToString() == "ناموفق")
        {
            var redBgColor = new iText.Kernel.Colors.DeviceRgb(255, 204, 204);
            statusCell.SetBackgroundColor(redBgColor);
        }
        table.AddCell(statusCell);

        table.AddCell(new Cell().Add(new Paragraph(os.DeleteStatus.ToString())
            .SetBaseDirection(iText.Layout.Properties.BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));
    }

    document.Add(table);
    document.Close();

    return stream.ToArray();
}

Here is a part of pdf that shows the issues : broken characters

I tried embedding Persian fonts (Vazirmatn, IRANSans, etc.) I tried using PdfFontFactory.createFont() with PdfEncodings.IDENTITY_H I saw that iText has something called PdfCalligraph, but I’m not sure how to use it

like image 274
Mobin Gholizadeh Avatar asked Sep 08 '25 15:09

Mobin Gholizadeh


1 Answers

You can manually convert characters

Step 1: Create Persian Text Shaper Class

using System.Text;

public class PersianTextShaper
{
    private static readonly Dictionary<char, char[]> PersianForms = new Dictionary<char, char[]>
    {
        { 'ا', new char[] { '\u0627', '\uFE8E', '\u0627', '\u0627' } },
        { 'ب', new char[] { '\uFE8F', '\uFE90', '\uFE91', '\uFE92' } },
        { 'پ', new char[] { '\uFB56', '\uFB57', '\uFB58', '\uFB59' } },
        { 'ت', new char[] { '\uFE95', '\uFE96', '\uFE97', '\uFE98' } },
        { 'ث', new char[] { '\uFE99', '\uFE9A', '\uFE9B', '\uFE9C' } },
        { 'ج', new char[] { '\uFE9D', '\uFE9E', '\uFE9F', '\uFEA0' } },
        { 'چ', new char[] { '\uFB7A', '\uFB7B', '\uFB7C', '\uFB7D' } },
        { 'ح', new char[] { '\uFEA1', '\uFEA2', '\uFEA3', '\uFEA4' } },
        { 'خ', new char[] { '\uFEA5', '\uFEA6', '\uFEA7', '\uFEA8' } },
        { 'د', new char[] { '\uFEA9', '\uFEAA', '\uFEA9', '\uFEA9' } },
        { 'ذ', new char[] { '\uFEAB', '\uFEAC', '\uFEAB', '\uFEAB' } },
        { 'ر', new char[] { '\uFEAD', '\uFEAE', '\uFEAD', '\uFEAD' } },
        { 'ز', new char[] { '\uFEAF', '\uFEB0', '\uFEAF', '\uFEAF' } },
        { 'ژ', new char[] { '\uFB8A', '\uFB8B', '\uFB8A', '\uFB8A' } },
        { 'س', new char[] { '\uFEB1', '\uFEB2', '\uFEB3', '\uFEB4' } },
        { 'ش', new char[] { '\uFEB5', '\uFEB6', '\uFEB7', '\uFEB8' } },
        { 'ص', new char[] { '\uFEB9', '\uFEBA', '\uFEBB', '\uFEBC' } },
        { 'ض', new char[] { '\uFEBD', '\uFEBE', '\uFEBF', '\uFEC0' } },
        { 'ط', new char[] { '\uFEC1', '\uFEC2', '\uFEC3', '\uFEC4' } },
        { 'ظ', new char[] { '\uFEC5', '\uFEC6', '\uFEC7', '\uFEC8' } },
        { 'ع', new char[] { '\uFEC9', '\uFECA', '\uFECB', '\uFECC' } },
        { 'غ', new char[] { '\uFECD', '\uFECE', '\uFECF', '\uFED0' } },
        { 'ف', new char[] { '\uFED1', '\uFED2', '\uFED3', '\uFED4' } },
        { 'ق', new char[] { '\uFED5', '\uFED6', '\uFED7', '\uFED8' } },
        { 'ک', new char[] { '\uFED9', '\uFEDA', '\uFEDB', '\uFEDC' } },
        { 'گ', new char[] { '\uFB92', '\uFB93', '\uFB94', '\uFB95' } },
        { 'ل', new char[] { '\uFEDD', '\uFEDE', '\uFEDF', '\uFEE0' } },
        { 'م', new char[] { '\uFEE1', '\uFEE2', '\uFEE3', '\uFEE4' } },
        { 'ن', new char[] { '\uFEE5', '\uFEE6', '\uFEE7', '\uFEE8' } },
        { 'و', new char[] { '\uFEED', '\uFEEE', '\uFEED', '\uFEED' } },
        { 'ه', new char[] { '\uFEE9', '\uFEEA', '\uFEEB', '\uFEEC' } },
        { 'ی', new char[] { '\uFEF1', '\uFEF2', '\uFEF3', '\uFEF4' } }
    };

    private static readonly HashSet<char> NonConnectors = new HashSet<char>
    {
        'ا', 'د', 'ذ', 'ر', 'ز', 'ژ', 'و'
    };

    public static string ShapePersianText(string text)
    {
        if (string.IsNullOrEmpty(text))
            return text;

        var result = new StringBuilder();
        var chars = text.ToCharArray();

        for (int i = 0; i < chars.Length; i++)
        {
            char currentChar = chars[i];

            if (!PersianForms.ContainsKey(currentChar))
            {
                result.Append(currentChar);
                continue;
            }

            bool hasLeft = i > 0 && IsConnectable(chars[i - 1]);
            bool hasRight = i < chars.Length - 1 && IsConnectable(chars[i + 1]) && !NonConnectors.Contains(currentChar);

            int formIndex = 0; // isolated
            if (hasLeft && hasRight) formIndex = 3; // medial
            else if (hasLeft) formIndex = 1; // final
            else if (hasRight) formIndex = 2; // initial

            result.Append(PersianForms[currentChar][formIndex]);
        }

        return result.ToString();
    }

    private static bool IsConnectable(char c)
    {
        return PersianForms.ContainsKey(c) && !char.IsWhiteSpace(c) && !char.IsPunctuation(c);
    }
}

Step 2: Updated PDF Generation Method

public async Task<byte[]> GenerateOrderPdf(Guid orderId, CancellationToken cancellationToken)
{
    var order = await _orderRepository.GetOrderById(orderId, cancellationToken);
    var orderServicesList = await _orderServicesRepository.GetOrderServices(orderId, cancellationToken);
    var serviceIds = orderServicesList.Select(x => x.ServiceId);
    var services = await _serviceRepository.GetServices(serviceIds, cancellationToken);

    using var stream = new MemoryStream();
    var writer = new PdfWriter(stream);
    var pdf = new PdfDocument(writer);
    var document = new Document(pdf);

    var vazirFontPath = Path.Combine(AppContext.BaseDirectory, "Resources", "Fonts", "Vazirmatn-Regular.ttf");
    var vazirFont = PdfFontFactory.CreateFont(vazirFontPath, PdfEncodings.IDENTITY_H);

    Paragraph CreatePersianParagraph(string text, float fontSize = 12)
    {
        var shapedText = PersianTextShaper.ShapePersianText(text);
        return new Paragraph(shapedText)
            .SetFontSize(fontSize)
            .SetTextAlignment(TextAlignment.RIGHT)
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont);
    }

    document.Add(CreatePersianParagraph($"جزئیات سفارش - کد #{order.OrderCode}", 20));
    document.Add(CreatePersianParagraph("وضعیت : " + order.Status));
    document.Add(CreatePersianParagraph("تاریخ ایجاد : " + order.CreateDate.ToString("yyyy-MM-dd")));
    document.Add(CreatePersianParagraph("تاریخ پرداخت : " + order.ProcessedAt?.ToString("yyyy-MM-dd")));
    document.Add(CreatePersianParagraph("آخرین تغییر وضعیت : " + order.LastStateChange?.ToString("yyyy-MM-dd")));

    document.Add(new Paragraph(" ").SetFontSize(15));

    var table = new Table(new float[] { 1, 1, 1, 1 });
    table.SetWidth(UnitValue.CreatePercentValue(100))
         .SetFont(vazirFont)
         .SetTextAlignment(TextAlignment.CENTER);

    var headerBgColor = new DeviceRgb(238, 238, 238);

    table.AddHeaderCell(new Cell()
        .SetBackgroundColor(headerBgColor)
        .Add(new Paragraph(PersianTextShaper.ShapePersianText("خدمت"))
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

    table.AddHeaderCell(new Cell()
        .SetBackgroundColor(headerBgColor)
        .Add(new Paragraph(PersianTextShaper.ShapePersianText("سیستم خدمت"))
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

    table.AddHeaderCell(new Cell()
        .SetBackgroundColor(headerBgColor)
        .Add(new Paragraph(PersianTextShaper.ShapePersianText("وضعیت"))
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

    table.AddHeaderCell(new Cell()
        .SetBackgroundColor(headerBgColor)
        .Add(new Paragraph(PersianTextShaper.ShapePersianText("جزئیات"))
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

    foreach (var os in orderServicesList)
    {
        var serviceName = services.FirstOrDefault(x => x.Id == os.ServiceId)?.Name?.GetValue() ?? "";

        table.AddCell(new Cell().Add(new Paragraph(PersianTextShaper.ShapePersianText(serviceName))
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

        table.AddCell(new Cell().Add(new Paragraph(PersianTextShaper.ShapePersianText(os.ServiceSystem.ToString()))
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));

        var statusCell = new Cell().Add(new Paragraph(PersianTextShaper.ShapePersianText(os.Status.ToString()))
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont));
        
        if (os.Status.ToString() == "ناموفق")
        {
            var redBgColor = new DeviceRgb(255, 204, 204);
            statusCell.SetBackgroundColor(redBgColor);
        }
        table.AddCell(statusCell);

        table.AddCell(new Cell().Add(new Paragraph(PersianTextShaper.ShapePersianText(os.DeleteStatus.ToString()))
            .SetBaseDirection(BaseDirection.RIGHT_TO_LEFT)
            .SetFont(vazirFont)));
    }

    document.Add(table);
    document.Close();

    return stream.ToArray();
}
like image 103
Ali Hemmatnia Avatar answered Sep 10 '25 05:09

Ali Hemmatnia