Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support (or at least documentation) for displaying Arabic text #655

Open
ZXTube opened this issue Dec 10, 2024 · 1 comment
Open

Add support (or at least documentation) for displaying Arabic text #655

ZXTube opened this issue Dec 10, 2024 · 1 comment
Labels
enhancement New feature or request pinned exempt from stale bot

Comments

@ZXTube
Copy link

ZXTube commented Dec 10, 2024

I have spent the last week, trying to display arabic text on the screen

Suggested Solution
I would like you to either add a font that comes by default for arabic, just like for chinese, korean, etc.
OR
Add documentation on how to use an arabic font.

Additional Context
I gave up on ChatGPT after it was just making things up. then after searching around everywhere I found this solution.

  1. Download some arabic tft font, from anywhere, example Amiri from google fonts
  2. Download otf2bdf and run: otf2bdf -r 72 -p 32 Amiri.ttf -o amiri.bdf
  3. Download bdfconv and run: bdfconv -v -f 1 -m "0-127,1536-1791,65136-65279" amiri.bdf -o amiri_u8g2.c -n amiri_u8g2 -d amiri.bdf
    This includes the basic english letters, isolated arabic letters, and arabic letters when connected to other letters.
  4. The arabic letters will be all isolated when you simply do for example: lcd.print("السلام عليكم\n"); so you will need to create a function or use a library which gives tells lgfx the specific unicode of each character to print.
    I'll edit say how when I have step 4 done.

Edit:
This is the way I'm currently displaying arabic text with all the logic to make words connected. (I could have used I library to connect letters but I chose not to)

NOTE: This is code for the ESP32, it can be made to work with arduino with a few changes. (hint: chatgpt can do it)

#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_freertos_hooks.h"
#include "lgfx/v1/lgfx_fonts.hpp"
#include <cstdint>

#define LGFX_AUTODETECT
#define LGFX_USE_V1
#include <LovyanGFX.hpp>

#include <amiri_u8g2.c>

LGFX lcd;

lgfx::U8g2font amiri_font (amiri_u8g2);

struct ArabicWord {
    std::u16string word;
    uint16_t wordWidth = 0;
    std::vector<uint8_t> letterWidths;
};
struct ArabicMapping {
    uint16_t base, initial, medial, final;
};
static ArabicMapping arabic_map[] = {
    {0x0621, 0xFE80, 0xFE80, 0xFE80}, // Hamza
    {0x0622, 0xFE81, 0xFE82, 0xFE82}, // Alef Madda
    {0x0623, 0xFE83, 0xFE84, 0xFE84}, // Alef Hamza Above
    {0x0624, 0xFE85, 0xFE86, 0xFE86}, // Waw Hamza Above
    {0x0625, 0xFE87, 0xFE88, 0xFE88}, // Alef Hamza Below
    {0x0626, 0xFE8B, 0xFE8C, 0xFE8A}, // Yeh Hamza
    {0x0627, 0xFE8D, 0xFE8E, 0xFE8E}, // Alef
    {0x0628, 0xFE91, 0xFE92, 0xFE90}, // Ba
    {0x0629, 0xFE93, 0xFE94, 0xFE94}, // Teh Marbuta
    {0x062A, 0xFE97, 0xFE98, 0xFE96}, // Ta
    {0x062B, 0xFE9B, 0xFE9C, 0xFE9A}, // Tha
    {0x062C, 0xFE9F, 0xFEA0, 0xFE9E}, // Jeem
    {0x062D, 0xFEA3, 0xFEA4, 0xFEA2}, // Hah
    {0x062E, 0xFEA7, 0xFEA8, 0xFEA6}, // Khah
    {0x062F, 0xFEA9, 0xFEAA, 0xFEAA}, // Dal
    {0x0630, 0xFEAB, 0xFEAC, 0xFEAC}, // Thal
    {0x0631, 0xFEAD, 0xFEAE, 0xFEAE}, // Ra
    {0x0632, 0xFEAF, 0xFEB0, 0xFEB0}, // Zain
    {0x0633, 0xFEB3, 0xFEB4, 0xFEB2}, // Seen
    {0x0634, 0xFEB7, 0xFEB8, 0xFEB6}, // Sheen
    {0x0635, 0xFEBB, 0xFEBC, 0xFEBA}, // Sad
    {0x0636, 0xFEBF, 0xFEC0, 0xFEBE}, // Dad
    {0x0637, 0xFEC3, 0xFEC4, 0xFEC2}, // Tah
    {0x0638, 0xFEC7, 0xFEC8, 0xFEC6}, // Zah
    {0x0639, 0xFECB, 0xFECC, 0xFECA}, // Ain
    {0x063A, 0xFECF, 0xFED0, 0xFECE}, // Ghain
    {0x0641, 0xFED3, 0xFED4, 0xFED2}, // Fa
    {0x0642, 0xFED7, 0xFED8, 0xFED6}, // Qaf
    {0x0643, 0xFEDB, 0xFEDC, 0xFEDA}, // Kaf
    {0x0644, 0xFEDF, 0xFEE0, 0xFEDE}, // Lam
    {0x0645, 0xFEE3, 0xFEE4, 0xFEE2}, // Meem
    {0x0646, 0xFEE7, 0xFEE8, 0xFEE6}, // Noon
    {0x0647, 0xFEEB, 0xFEEC, 0xFEEA}, // Ha
    {0x0648, 0xFEED, 0xFEEE, 0xFEEE}, // Waw
    {0x0649, 0xFEEF, 0xFEE0, 0xFEF0}, // Alef Layina
    {0x064A, 0xFEF3, 0xFEF4, 0xFEF2}, // Yeh
    {0xFEF5, 0xFEF5, 0xFEF6, 0xFEF5}, // Lam & Alef Hamza Madda
    {0xFEF7, 0xFEF7, 0xFEF8, 0xFEF7}, // Lam & Alef Hamza Above
    {0xFEF9, 0xFEF9, 0xFEFA, 0xFEF9}, // Lam & Alef Hamza Below
    {0xFEFB, 0xFEFB, 0xFEFC, 0xFEFB}, // Lam & Alef
};

static bool isLetter(uint16_t character) {
    for (auto &entry : arabic_map)
        if (entry.base == character) 
            return true;
    return false;
}
static const ArabicMapping* getArabicMapping(uint16_t character) {
    for (auto &entry : arabic_map)
        if (entry.base == character) 
            return &entry;
    return nullptr;
}
static std::vector<ArabicWord> shapeArabicText(const std::u16string &input) {
    lgfx::FontMetrics ch_metric;
    std::vector<ArabicWord> text;
    text.emplace_back();

    bool connectPrevious = false;
    bool connectNext = false;
    for (uint16_t i = 0; i < input.size(); i++) {
        uint16_t ch = input[i];
        // If new word
        if (ch == u' ') {
            if (!text.back().word.empty()) text.emplace_back();
            text.back().word.push_back(u' ');
            text.back().letterWidths.push_back(lcd.textWidth(" "));
            text.back().wordWidth = text.back().letterWidths[0];
            connectPrevious = false;
            text.emplace_back();
        } else {
            // All just for Lam & Alef
            if ((ch == u'ا' || ch == u'أ' || ch == u'إ' || ch == u'آ') && !text.back().word.empty()) {
                uint8_t word_size = text.back().word.size();
                bool lam_found = false;
                uint8_t j = 1;
                while (1) {
                    if (j == word_size) { break; }
                    ESP_LOGI("H", "HI1");
                    if (input[i - j] == u'ل') { 
                        ESP_LOGI("H", "HI2");
                        text.back().wordWidth -= text.back().letterWidths.back();
                        text.back().word.erase(word_size - j); 
                        text.back().letterWidths.erase(text.back().letterWidths.begin() + (word_size - j)); 
                        switch (ch) {
                            case u'ا': ch = 0xFEFB; break;
                            case u'أ': ch = 0xFEF7; break;
                            case u'إ': ch = 0xFEF9; break;
                            case u'آ': ch = 0xFEF5; break;
                        }
                        lam_found = true;
                    }
                    else if (isLetter(input[i - j])) {
                        ESP_LOGI("H", "HI3, %x", input[i-j]);
                        if (lam_found) {
                            ESP_LOGI("H", "HI4");
                            switch (input[i - j]) {
                                case u'أ': case u'إ': case u'آ': case u'ا': case u'و': case u'ز': case u'ر': case u'ذ': case u'د':
                                    connectPrevious = false;
                            }
                        }
                        break;
                    } 
                    j++;
                }
            }

            // Get all possible version of the letter. (If returned null, then ch is not an arabic letter)
            auto mapping = getArabicMapping(ch);
            if (!mapping) {
                text.back().word.push_back(ch);
                amiri_font.updateFontMetric(&ch_metric, ch);
                text.back().wordWidth += ch_metric.x_advance;
                text.back().letterWidths.push_back(ch_metric.x_advance);
                continue;
            }
            
            // Find next letter in word
            uint16_t j = i + 1;
            while (1) {
                if (j > input.size() || input[j] == ' ') { connectNext = false; break; }
                else if (isLetter(input[j])) { connectNext = true; break; }
                j++;
            }

            // Set correct letter version
            uint16_t shapedCh;
            if (!connectPrevious && !connectNext) shapedCh = mapping->base;
            else if (connectPrevious && connectNext) shapedCh = mapping->medial;
            else if (connectPrevious && !connectNext) shapedCh = mapping->final;
            else shapedCh = mapping->initial;

            // Set letter and it's width
            text.back().word.push_back(shapedCh);
            amiri_font.updateFontMetric(&ch_metric, shapedCh);
            text.back().wordWidth += ch_metric.x_advance;
            text.back().letterWidths.push_back(ch_metric.x_advance);

            // Set that the next letter can connect to this letter
            connectPrevious = true;
            switch (ch) {
                case u'أ': case u'إ': case u'آ': case u'ا': case u'و': case u'ز': case u'ر': case u'ذ': case u'د':
                    connectPrevious = false;
            }
            // Same as above, specific to Lam & Alef
            switch (shapedCh) {
                case 0xFEF5: case 0xFEF6: case 0xFEF7: case 0xFEF8: case 0xFEF9: case 0xFEFA: case 0xFEFB: case 0xFEFC:
                    connectPrevious = false;
            }
        }
    }

    return text;
}

void loop(void*)
{
    lcd.init();
    lcd.setTextColor(TFT_WHITE);
    lcd.setRotation(1); // Make screen landscape
    lcd.clear();
    lcd.setFont(&amiri_font);
    lcd.setCursor(20, 30);
    uint16_t scr_width = lcd.width();

    std::u16string text = u"الْحَمْدُ لِلَّهِ رَبِّ الْعَالمينَ، الرَّحْمَٰنِ الرَّحِيمِ، مَلِكِ يَوْمِ الدِّينِ,,,, - السلام عليكم";
    std::vector<ArabicWord> shapedText = shapeArabicText(text);

    // Figure out the line widths (only to centre lines horizontally)
    std::vector<uint16_t> lineWidths;
    lineWidths.emplace_back();
    uint8_t margin = 10;
    for (auto word : shapedText) {
        if (lineWidths.back() + word.wordWidth > scr_width - margin) lineWidths.push_back(margin);
        lineWidths.back() += word.wordWidth;
    }
 
    // Draw letters
    uint8_t j = 0;
    uint16_t y = 60;
    int16_t x = scr_width - margin - ((scr_width - lineWidths[j]) / 2);
    for (auto word : shapedText) {
        if (x - word.wordWidth < margin) {
            j++;
            x = scr_width - margin - ((scr_width - lineWidths[j]) / 2);
            y += 60;
        }

        for (uint16_t i=0; i < word.word.size(); i++) {
            lcd.drawChar(word.word[i], x - word.letterWidths[i], y);
            x -= word.letterWidths[i];
        }
    }

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

extern "C" void app_main(void) {
  xTaskCreate(loop, "app", 8192, NULL, 1, NULL);
}

Thanks in advance.

@ZXTube ZXTube added the enhancement New feature or request label Dec 10, 2024
@tobozo tobozo added the pinned exempt from stale bot label Dec 19, 2024
@tobozo
Copy link
Collaborator

tobozo commented Dec 19, 2024

hi,

thanks for submitting this 👍

given that LVGL supports bidirectional text and can work on top of LovyanGFX, it's unlikely to happen in LGFX core, but this is totally worth an addition in the examples section

feel free to submit a pull request on the develop branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request pinned exempt from stale bot
Projects
None yet
Development

No branches or pull requests

2 participants