スポンサーリンク

フォントを交換しながら一つの文字列を描画する

日本語や絵文字などが混同している場合、フォントを切り替えなければ文字を描画できない。

そこで、描画する文字列を書記素単位にし、書記素にスクリプト=用字=文字の種類の情報を付与して、スクリプトからフォントを引ける辞書を作成する。

#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>


#include "u16_to_u32.hpp"
#include "render_font.hpp"


#pragma warning(disable:4996)

//! @brief PBM(1byte,テキスト)を書き込む
//! @param [in] fname ファイル名
//! @param [in] width 画像の幅
//! @param [in] height 画像の高さ
//! @param [in] p 画像のメモリへのアドレス
//! @details 1画素1Byteのメモリを渡すと、0,1テキストでファイル名fnameで書き込む
void pbmP1_Write(const char* const fname, const int width, const int height, const unsigned char* const p) { // PPM ASCII

    FILE* fp = fopen(fname, "wb");
    fprintf(fp, "P1\n%d\n%d\n", width, height);

    size_t k = 0;
    for (size_t i = 0; i < (size_t)height; i++) {
        for (size_t j = 0; j < (size_t)width; j++) {
            fprintf(fp, "%d ", p[k] ? 0 : 1);
            k++;
        }
        fprintf(fp, "\n");
    }

    fclose(fp);
}


// フォントのオブジェクト
struct FT_HB_Font {
    FT_Face *ftface;   
    hb_font_t* hbfont;
    std::string lang;

    FT_HB_Font(FT_Face* face,const std::string& language){
        ftface = face;                                // FreeTypeのフォントオブジェクト
        hbfont = hb_ft_font_create(*ftface, nullptr); // HarfBuzzのフォントオブジェクト
        lang = language;                              // HarfBuzz用の言語
    }
    FT_HB_Font():ftface(nullptr),hbfont(nullptr){}

    void Delete(){
        if (ftface) {
            FT_Done_Face(*ftface);
            ftface = nullptr;
        }
        if (hbfont) {
            hb_font_destroy(hbfont);
            hbfont = nullptr;
        }
    }
};
int main()
{
    FT_Library  library; // handle to library
    FT_Error error;

    error = FT_Init_FreeType(&library);
    if (error)
        return -1;


    // HarfBuzzのオブジェクト作成
    hb_buffer_t* hbbuf;
    hbbuf = hb_buffer_create();

    //////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////

    // Script(用字)に応じたフォントをマップに登録
    std::unordered_map< int, FT_HB_Font> script_map;
    script_map[HB_SCRIPT_HIRAGANA] = FT_HB_Font(my_LoadFonts(library, "C:\\Windows\\Fonts\\msgothic.ttc"),"jp");
    script_map[HB_SCRIPT_HAN]      = script_map[HB_SCRIPT_HIRAGANA];
    script_map[HB_SCRIPT_KATAKANA] = script_map[HB_SCRIPT_HIRAGANA];
    script_map[HB_SCRIPT_LATIN]    = FT_HB_Font(my_LoadFonts(library, "C:\\Windows\\Fonts\\Times.ttf"),"en");
    script_map[EMOJI]              = FT_HB_Font(my_LoadFonts(library, "C:\\Windows\\Fonts\\seguiemj.ttf"),"en");

    //////////////////////////////////////////////////////
    // HarfBuzzのUnicode関数の取得

    std::u16string text = u"漢字あ゙いう👨‍👨‍👧‍👦àbâáăã";

    // 文字列をUTF32で表現した書記素の配列に変換
    std::vector<Grapheme> graphemes;
    u16_to_u32(text.c_str(), graphemes);

    Image image(1024, 300);
    // 描画先をクリア
    std::fill(image.image.begin(), image.image.end(), 0);

    int draw_offset_x = 0;
    for (size_t i = 0; i < graphemes.size(); i++) {

        // 用字を取得
        // ただし絵文字に関しては、HB_SCRIPT_COMMON が入っている
        hb_script_t hbscript = graphemes[i].script;
        FT_HB_Font& face = script_map.at(graphemes[i].is_emoji?EMOJI: hbscript);

        draw_offset_x = render_by_font(
            &image               // 出力先
            ,draw_offset_x       // 描画開始位置
            ,face.ftface         // FreeTypeのフォント
            ,graphemes[i].u32str // 描画する文字列
            ,hbscript            // 描画する文字列の用字
            ,hbbuf               // HarfBuzzのバッファ
            ,face.hbfont         // HarfBuzzのフォント
            ,face.lang           // HarfBuzz用の言語
        );
    }

    pbmP1_Write("freetypetest_xxx.pbm", image.imageWidth, image.imageHeight, &image.image[0]);


}

u16_to_u32.hpp

#include <unicode/ubrk.h>
#include <unicode/ustring.h>

#include <vector>
#include <string>

#include <hb.h>
#include <hb-ft.h>

#ifdef _DEBUG
// デバッグ時リンク
#pragma comment(lib, "icuucd.lib")
#pragma comment(lib, "icuind.lib")
#pragma comment(lib,"harfbuzz.lib")

#else

// 要リンク
#pragma comment(lib, "icuuc.lib")
#pragma comment(lib, "icuin.lib")
#pragma comment(lib,"harfbuzz.lib")

#endif

// 絵文字判定関数
bool is_emoji(char32_t c32) {
    bool _emoji = false;
    _emoji |= (bool)u_getIntPropertyValue(c32, UCHAR_EMOJI);
    _emoji |= (bool)u_getIntPropertyValue(c32, UCHAR_EMOJI_PRESENTATION);
    _emoji |= (bool)u_getIntPropertyValue(c32, UCHAR_EMOJI_MODIFIER);
    _emoji |= (bool)u_getIntPropertyValue(c32, UCHAR_EMOJI_MODIFIER_BASE);
    _emoji |= (bool)u_getIntPropertyValue(c32, UCHAR_EMOJI_COMPONENT);
    return _emoji;
}

struct Grapheme{
    int32_t start;         // 元のutf16文字列のindex
    int32_t end;           // 元のutf16文字列のindex
    hb_script_t script;    // この書記素(書記素なので複数文字の可能性がある)の先頭の文字の書記素
    std::u32string u32str;  // この書記素のコードポイント表現
    bool is_emoji;         // この書記素が絵文字かどうかを表すフラグ
};

//! @brief UTF-16からUTF-32で表現した書記素の配列を作成
//! @param [in] u16str UTF-16文字列
//! @param [out] graphemes 書記素の配列
bool u16_to_u32(
    const char16_t* u16str, 
    std::vector<Grapheme>& graphemes) 
{
    // hb_unicode_scriptに渡す構造体(解放不要)
    hb_unicode_funcs_t* ufuncs = hb_unicode_funcs_get_default();

    UErrorCode status = U_ZERO_ERROR;

    // イテレータ作成
    UBreakIterator* bi = ubrk_open(UBRK_CHARACTER, nullptr /*文字単位の処理にロケールは不要*/, nullptr, 0, &status);

    if (U_FAILURE(status)) {
        return false;// エラーが発生
    }

    int32_t utf16Length = wcslen((wchar_t*)u16str);
    // テキストを設定
    ubrk_setText(bi, (const UChar*)u16str, utf16Length, &status);

    if (U_FAILURE(status)) {
        ubrk_close(bi);
        return false;// エラーが発生
    }

    // 文字境界を使ってUTF-16からUTF-32に変換
    int32_t start = ubrk_first(bi);

    // 文字列(utf16)の最後までループ
    for (int32_t end = ubrk_next(bi); end != UBRK_DONE; start = end, end = ubrk_next(bi)) {

        graphemes.push_back(Grapheme{ start, end });

        int32_t i = start;

        bool is_first = true;
        while(i < end) {

            UChar32 c32;
            // ① u16str[i] 以降の文字を一文字コードポイントに変換してcに格納
            // ② iを次の文字の位置に進める
            U16_NEXT(u16str, i, utf16Length, c32);

            // コードポイントを保存
            graphemes.back().u32str.push_back(c32);

            if(is_first) {
                is_first = false;
                graphemes.back().script =
                    hb_unicode_script(ufuncs, c32); // 用字の取得

                graphemes.back().is_emoji = is_emoji(c32);// 絵文字かどうかのチェック
            }

        }

    }

    // 終了処理
    ubrk_close(bi);

    return true;
};

render_font.hpp

#pragma once

#ifdef _DEBUG
#pragma comment(lib,"freetyped.lib")
#pragma comment(lib,"harfbuzz.lib")
#else
#pragma comment(lib,"freetype.lib")
#pragma comment(lib,"harfbuzz.lib")
#endif

#include <ft2build.h>
#include FT_FREETYPE_H

#include <hb.h>
#include <hb-ft.h>

#include <vector>
#include <string>


constexpr int EMOJI = HB_TAG('0', '0', '0', '0');

struct Image {
    int imageWidth;
    int imageHeight;
    std::vector<unsigned char> image;
    Image(int w, int h) {
        image.resize(w * h);
        imageWidth = w;
        imageHeight = h;
    }
    //! @brief imageへの書き込み時のピクセル計算
    int pixel_pos(const int x, const int y) {
        return y * imageWidth + x;
    }

    bool is_valid_area(int xx, int yy) {
        if (xx < 0)return false;
        if (yy < 0)return false;
        if (xx >= imageWidth)return false;
        if (yy >= imageHeight)return false;
        return true;
    }
};
//! @brief imageへbmpの内容を書き込む
//! @param [in] bmp 文字画像
//! @param [in] startx image画像内の書き込み開始位置
//! @param [in] starty image画像内の書き込み開始位置
void draw(Image* image, const FT_Bitmap& bmp, int startx, int starty) {

    int Width = bmp.width;
    int Height = bmp.rows;

    for (size_t y = 0; y < Height; y++) {
        for (size_t x = 0; x < Width; x++) {

            int xx = startx + x;
            int yy = starty + y;

            if( image->is_valid_area(xx, yy) == false)continue;

            if (bmp.buffer[y * Width + x]) {
                (image->image)[image->pixel_pos(xx, yy)] = 1;
            }
        }
    }

}
 
//! @brief フォントの読込 FreeType2の処理
//! @param [in] library FT_Library
//! @param [in] fontfile フォントファイルへのパス
//! @return フェイスオブジェクト
FT_Face* my_LoadFonts(
    FT_Library& library,
    const char* fontfile
) {
    FT_Face* face = new FT_Face;
    FT_Error error;

    // フォントファイル読み込み
    error = FT_New_Face(
        library,
        fontfile,
        0,
        face
    );

    //文字コード指定
    error = FT_Select_Charmap(
        *face,
        FT_ENCODING_UNICODE // エンコード指定
    );
    if (error == FT_Err_Unknown_File_Format)
        return nullptr;
    else if (error)
        return nullptr;

    int pixel_size_y = 64;
    error = FT_Set_Pixel_Sizes(
        *face,
        0,
        pixel_size_y);

    return face;
}
      
//! @brief HarfBuzzで計算した座標を使いFreeType2で文字列を描画する
//! @param [in] image 描画先
//! @param [in] hbbuf HarfBuzzオブジェクト
//! @param [in] face FreeType2のフェイス
int my_FaceDraw(
    Image* image,
    int draw_offset_x,
    hb_buffer_t* hbbuf,
    FT_Face* face
) {

    //文字数を格納 (書記素数ではない。例えば「あ゙」は2文字)
    unsigned int glyph_count;

    hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(hbbuf, &glyph_count);
    hb_glyph_position_t* glyph_pos = hb_buffer_get_glyph_positions(hbbuf, &glyph_count);

    hb_position_t cursor_x = draw_offset_x;

    // 各文字ごとに描画する
    for (unsigned int i = 0; i < glyph_count; i++) {

        // codepointという変数名だが実際にはグリフインデクスが入っている
        hb_codepoint_t glyphid = glyph_info[i].codepoint;

        // 一文字分のオフセット。本来描画される位置からどれぐらいずれるか
        hb_position_t x_offset = glyph_pos[i].x_offset >> 6;// 結合文字の゛は本来の位置より左側に描画するので
        hb_position_t y_offset = glyph_pos[i].y_offset >> 6;// x_offsetにはマイナスの値が入る

        // 次の文字の描画開始位置までのピクセル数
        hb_position_t x_advance = glyph_pos[i].x_advance >> 6;
        hb_position_t y_advance = glyph_pos[i].y_advance >> 6;


        ///////////////////////////////////////
        ///////////////////////////////////////
        ///////////////////////////////////////
        FT_Error error = FT_Load_Glyph(*face, glyphid, FT_LOAD_RENDER);
        if (error) {
            continue;
        }
        // 文字を画像化
        FT_Render_Glyph((*face)->glyph, FT_RENDER_MODE_NORMAL);

        int cursor_y = 100;



        // 画像書き込み
        // オフセットを加えて座標調整する
        draw(
            image,
            (*face)->glyph->bitmap,
            cursor_x + x_offset + (*face)->glyph->bitmap_left,
            cursor_y + y_offset - (*face)->glyph->bitmap_top
        );
        ///////////////////////////////////////
        ///////////////////////////////////////
        ///////////////////////////////////////

        // 次の文字の描画開始値
        cursor_x += x_advance;
        cursor_y += y_advance;
    }

    return cursor_x;
}

//! @brief 指定したフォントで文字列を描画する
//! @param [out] image          出力先
//! @param [in]  draw_offset_x  描画開始位置
//! @param [in]  ft_face        FreeTypeのフォント
//! @param [in]  text           描画する文字列
//! @param [in]  script         描画する文字列の用字
//! @param [in]  hbbuf            HarfBuzzのバッファ
//! @param [in]  hbfont         HarfBuzzのフォント
//! @param [in]  lang           HarfBuzz用の言語
int render_by_font(
    Image* image,
    int draw_offset_x,
    FT_Face* ft_face,
    const std::u32string& text,
    hb_script_t script,
    hb_buffer_t* hbbuf,
    hb_font_t* hbfont,
    std::string lang
) {
    // バッファの内容をクリア
    // hb_buffer_tを再利用するときは、hb_buffer_clear_contents()でバッファをクリアする
    hb_buffer_clear_contents(hbbuf);

    // バッファにテキストを追加
    hb_buffer_add_utf32(hbbuf, (const std::uint32_t*)text.data(), -1, 0, -1);// 描画したいテキストの設定
    hb_buffer_set_direction(hbbuf, HB_DIRECTION_LTR);                        // 文字の方向を左から右として設定
    hb_buffer_set_script(hbbuf, script);                                     // Unicodeの用字(Script)として日本語を指定
    hb_buffer_set_language(hbbuf, hb_language_from_string(lang.c_str(), -1));// 言語を設定

    ////////////////////////
    hb_shape(hbfont, hbbuf, NULL, 0);
    ////////////////////////
    int next_pos = my_FaceDraw(image, draw_offset_x,hbbuf, ft_face);//FreeType2とHarfBuzzで文字列描画
    ////////////////////////
    return next_pos;
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


この記事のトラックバックURL: