Search code examples
c++opencvtextdrawingright-to-left

Write Farsi (persian) text using opencv c++


I want to write persian (Farsi) text on an image in C++, preferably in OpenCV.

I tried cv::putText which leads to writing ????...? instead the text. Also I tested cv::addText but I got not implemented exception. Additional I also checked opencv docs here which seems problematic. When I install opencv 4.8 via "VC package manager" there is no freetype class on it.

Finally I find a solution using this link. It writes persian strings 1 by 1 (characters are divided and are in reverse order).

#include "opencv2/opencv.hpp"
#include "../UTL/Util.h"

#include "ft2build.h"
#include FT_FREETYPE_H
FT_Library  library;
FT_Face     face;

using namespace cv;
using namespace std;
//-----------------------------------------------------------------------
void my_draw_bitmap(Mat& img, FT_Bitmap* bitmap, int x, int y, Scalar color)
{
    Scalar src_col, dst_col;
    for (int i = 0; i < bitmap->rows; i++)
    {
        for (int j = 0; j < bitmap->width; j++)
        {
            unsigned char val = bitmap->buffer[j + i * bitmap->pitch];
            float mix = (float)val / 255.0;
            if (val != 0)
            {
                src_col = Scalar(img.at<Vec3b>(i + y, j + x));
                dst_col = mix * color + (1.0 - mix) * src_col;
                img.at<Vec3b>(i + y, j + x) = Vec3b(dst_col[0], dst_col[1], dst_col[2]);
            }
        }
    }
}
//-----------------------------------------------------------------------
float PrintString(Mat& img, std::wstring str, int x, int y, Scalar color)
{
    FT_Bool       use_kerning = 0;
    FT_UInt       previous = 0;
    use_kerning = FT_HAS_KERNING(face);
    float prev_yadv = 0;
    float posx = 0;
    float posy = 0;
    float dx = 0;
    for (int k = 0; k < str.length(); k++)
    {
        int glyph_index = FT_Get_Char_Index(face, str.c_str()[k]);
        FT_GlyphSlot  slot = face->glyph;  // a small shortcut 
        if (k > 0) { dx = slot->advance.x / 64; }
        FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT);
        FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL);
        prev_yadv = slot->metrics.vertAdvance / 64;
        if (use_kerning && previous && glyph_index)
        {
            FT_Vector  delta;
            FT_Get_Kerning(face, previous, glyph_index, FT_KERNING_DEFAULT, &delta);
            posx += (delta.x / 64);
        }
        posx += (dx);
        my_draw_bitmap(img, &slot->bitmap, posx + x + slot->bitmap_left, y - slot->bitmap_top + posy, color);
        previous = glyph_index;
    }
    return prev_yadv;
}
//-----------------------------------------------------------------------
void PrintText(Mat& img, std::wstring str, int x, int y, Scalar color)
{
    float posy = 0;
    for (int pos = str.find_first_of(L'\n'); pos != wstring::npos; pos = str.find_first_of(L'\n'))
    {
        std::wstring substr = str.substr(0, pos);
        str.erase(0, pos + 1);
        posy += PrintString(img, substr, x, y + posy, color);
    }
    PrintString(img, str, x, y + posy, color);
}
//-----------------------------------------------------------------------
int main(int argc, char* argv[])
{
    FT_Init_FreeType(&library);
    auto path_to_font_file = "arial.ttf";
    FT_New_Face(library, path_to_font_file, 0, &face);
    FT_Set_Pixel_Sizes(face, 36, 0);
    FT_Select_Charmap(face, FT_Encoding::FT_ENCODING_UNICODE);
    
    Mat im(100, 300, CV_8UC3, Scalar(0,0,0));
    wstring str = L"خلیج فارس";

    PrintText(im, str, 100, 50, Scalar(255, 255, 255));
    cv::imshow("win", im);
    cv::waitKey(0);
    return 0;
}

but the result is:

bad result

which should be:

enter image description here

how can fix the problem in the above? any other approach will be appreciated.


Solution

  • Improving on my previous answer. Since you appear to be on Windows, my highest recommandation is to leverage the Win32 libraries to draw that string for you to an in-memory bitmap. Then transfer over the 32-bit ARGB values of that Bitmap to your OpenCV surface.

    Here's a code sample that blits the text string to a Gdiplus::Bitmap object and saves it to a PNG file. Your code wouldn't likely save it to PNG, but instead just use the Bitmap object to get at the raw bytes and blit that onto your surface.

    Here's the code sample:

    int main()
    {   
        Gdiplus::GdiplusStartupInput gdiplusStartupInput = {};
        ULONG_PTR gdiplusToken = {};
        GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    
        Gdiplus::Bitmap bmp(300, 100, PixelFormat32bppARGB);
        Gdiplus::Graphics graphics(&bmp);
        graphics.Clear(Gdiplus::Color(0, 0, 0, 0)); // transparent
    
        Gdiplus::SolidBrush brush(Gdiplus::Color(255, 255, 255, 255)); // white
        Gdiplus::PointF origin(0, 0);
    
        Gdiplus::Font font(L"Arial", 50);
    
        const wchar_t* pwsz = L"خلیج فارس";
        graphics.DrawString(pwsz, -1, &font, origin, &brush);
    
        GUID guidBmp = {};
        GetGdiplusEncoderClsid(L"image/png", &guidBmp);
    
        bmp.Save(L"D:/output.png", &guidBmp);
    
        return 0;
    }
    

    Here's what it produces. The black background is what I artificially added as an extra layer in Paint.net. The real background is transparent. You can easily tweak the foreground and background colors above as well:

    enter image description here

    I hardcoded the font sizes. The most likely Win32 function you'd need in addition to the above is to call DrawText with the DT_CALCRECT flag to pre-determine the width and height such that the Bitmap is sized exactly to hold your string.

    The rest of the code includes the helper function that I leveraged from a previous answer and the required header files.

    #include <windows.h>
    #include <gdiplus.h>
    #include <iostream>
    #include <vector>
    
    HRESULT GetGdiplusEncoderClsid(const std::wstring& format, GUID* pGuid)
    {
        HRESULT hr = S_OK;
        UINT  nEncoders = 0;          // number of image encoders
        UINT  nSize = 0;              // size of the image encoder array in bytes
        std::vector<BYTE> spData;
        Gdiplus::ImageCodecInfo* pImageCodecInfo = NULL;
        Gdiplus::Status status;
        bool found = false;
    
        if (format.empty() || !pGuid)
        {
            hr = E_INVALIDARG;
        }
    
        if (SUCCEEDED(hr))
        {
            *pGuid = GUID_NULL;
            status = Gdiplus::GetImageEncodersSize(&nEncoders, &nSize);
    
            if ((status != Gdiplus::Ok) || (nSize == 0))
            {
                hr = E_FAIL;
            }
        }
    
        if (SUCCEEDED(hr))
        {
    
            spData.resize(nSize);
            pImageCodecInfo = (Gdiplus::ImageCodecInfo*)&spData.front();
            status = Gdiplus::GetImageEncoders(nEncoders, nSize, pImageCodecInfo);
    
            if (status != Gdiplus::Ok)
            {
                hr = E_FAIL;
            }
        }
    
        if (SUCCEEDED(hr))
        {
            for (UINT j = 0; j < nEncoders && !found; j++)
            {
                if (pImageCodecInfo[j].MimeType == format)
                {
                    *pGuid = pImageCodecInfo[j].Clsid;
                    found = true;
                }
            }
    
            hr = found ? S_OK : E_FAIL;
        }
    
        return hr;
    }