Progress on text support
This commit is contained in:
@@ -15,14 +15,12 @@
|
||||
#include <audio/AudioBuffer.h>
|
||||
#include <graphics/format/JPEG.h>
|
||||
#include <graphics/format/PNG.h>
|
||||
#include <graphics/Font.h>
|
||||
#ifdef LIME_HARFBUZZ
|
||||
#include <graphics/Text.h>
|
||||
#endif
|
||||
#include <graphics/ImageBuffer.h>
|
||||
#include <graphics/Renderer.h>
|
||||
#include <graphics/RenderEvent.h>
|
||||
#include <system/System.h>
|
||||
#include <text/Font.h>
|
||||
#include <text/TextLayout.h>
|
||||
#include <ui/KeyEvent.h>
|
||||
#include <ui/Mouse.h>
|
||||
#include <ui/MouseCursor.h>
|
||||
@@ -137,9 +135,9 @@ namespace lime {
|
||||
}
|
||||
|
||||
|
||||
void lime_font_destroy (value fontHandle) {
|
||||
void lime_font_destroy (value handle) {
|
||||
|
||||
Font *font = (Font*)(intptr_t)val_float (fontHandle);
|
||||
Font *font = (Font*)(intptr_t)val_float (handle);
|
||||
delete font;
|
||||
font = 0;
|
||||
|
||||
@@ -150,7 +148,7 @@ namespace lime {
|
||||
|
||||
#ifdef LIME_FREETYPE
|
||||
Font *font = (Font*)(intptr_t)val_float (fontHandle);
|
||||
return font->GetFamilyName ();
|
||||
return alloc_wstring (font->GetFamilyName ());
|
||||
#else
|
||||
return alloc_null ();
|
||||
#endif
|
||||
@@ -158,25 +156,35 @@ namespace lime {
|
||||
}
|
||||
|
||||
|
||||
value lime_font_load (value fontFace) {
|
||||
value lime_font_load (value data) {
|
||||
|
||||
#ifdef LIME_FREETYPE
|
||||
Font *font = Font::FromFile (val_string (fontFace));
|
||||
Resource resource;
|
||||
|
||||
if (val_is_string (data)) {
|
||||
|
||||
resource = Resource (val_string (data));
|
||||
|
||||
} else {
|
||||
|
||||
ByteArray bytes (data);
|
||||
resource = Resource (&bytes);
|
||||
|
||||
}
|
||||
|
||||
Font *font = new Font (&resource, 0);
|
||||
|
||||
if (font) {
|
||||
|
||||
|
||||
value v = alloc_float ((intptr_t)font);
|
||||
val_gc (v, lime_font_destroy);
|
||||
return v;
|
||||
|
||||
} else {
|
||||
|
||||
return alloc_null ();
|
||||
|
||||
|
||||
}
|
||||
#else
|
||||
return alloc_null ();
|
||||
#endif
|
||||
|
||||
return alloc_null ();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -417,10 +425,10 @@ namespace lime {
|
||||
}
|
||||
|
||||
|
||||
void lime_text_destroy (value textHandle) {
|
||||
void lime_text_layout_destroy (value textHandle) {
|
||||
|
||||
#ifdef LIME_HARFBUZZ
|
||||
Text *text = (Text*)(intptr_t)val_float (textHandle);
|
||||
TextLayout *text = (TextLayout*)(intptr_t)val_float (textHandle);
|
||||
delete text;
|
||||
text = 0;
|
||||
#endif
|
||||
@@ -428,28 +436,36 @@ namespace lime {
|
||||
}
|
||||
|
||||
|
||||
value lime_text_create (value direction, value script, value language) {
|
||||
value lime_text_layout_create (value direction, value script, value language) {
|
||||
|
||||
#if defined(LIME_FREETYPE) && defined(LIME_HARFBUZZ)
|
||||
Text *text = new Text (val_int (direction), val_string (script), val_string (language));
|
||||
|
||||
TextLayout *text = new TextLayout (val_int (direction), val_string (script), val_string (language));
|
||||
value v = alloc_float ((intptr_t)text);
|
||||
val_gc (v, lime_text_destroy);
|
||||
val_gc (v, lime_text_layout_destroy);
|
||||
return v;
|
||||
|
||||
#else
|
||||
|
||||
return alloc_null ();
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
|
||||
value lime_text_from_string (value textHandle, value fontHandle, value size, value textString) {
|
||||
value lime_text_layout_layout (value textHandle, value fontHandle, value size, value textString) {
|
||||
|
||||
#if defined(LIME_FREETYPE) && defined(LIME_HARFBUZZ)
|
||||
Text *text = (Text*)(intptr_t)val_float (textHandle);
|
||||
|
||||
TextLayout *text = (TextLayout*)(intptr_t)val_float (textHandle);
|
||||
Font *font = (Font*)(intptr_t)val_float (fontHandle);
|
||||
return text->FromString(font, val_int (size), val_string (textString));
|
||||
return text->Layout (font, val_int (size), val_string (textString));
|
||||
|
||||
#else
|
||||
|
||||
return alloc_null ();
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -556,8 +572,8 @@ namespace lime {
|
||||
DEFINE_PRIM (lime_renderer_flip, 1);
|
||||
DEFINE_PRIM (lime_render_event_manager_register, 2);
|
||||
DEFINE_PRIM (lime_system_gettimer, 0);
|
||||
DEFINE_PRIM (lime_text_create, 3);
|
||||
DEFINE_PRIM (lime_text_from_string, 4);
|
||||
DEFINE_PRIM (lime_text_layout_create, 3);
|
||||
DEFINE_PRIM (lime_text_layout_layout, 4);
|
||||
DEFINE_PRIM (lime_touch_event_manager_register, 2);
|
||||
DEFINE_PRIM (lime_update_event_manager_register, 2);
|
||||
DEFINE_PRIM (lime_window_close, 1);
|
||||
@@ -575,4 +591,4 @@ extern "C" int lime_register_prims () {
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -217,7 +217,7 @@ namespace lime {
|
||||
|
||||
} else {
|
||||
|
||||
ByteArray data = ByteArray::FromFile (resource->path);
|
||||
ByteArray data = ByteArray (resource->path);
|
||||
MySrcManager manager (data.Bytes (), data.Size ());
|
||||
cinfo.src = &manager.pub;
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace lime {
|
||||
|
||||
} else {
|
||||
|
||||
ByteArray data = ByteArray::FromFile (resource->path);
|
||||
ByteArray data = ByteArray (resource->path);
|
||||
ReadBuffer buffer (data.Bytes (), data.Size ());
|
||||
png_set_read_fn (png_ptr, &buffer, user_read_data_fn);
|
||||
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
#include <graphics/Font.h>
|
||||
#include <text/Font.h>
|
||||
#include <graphics/ImageBuffer.h>
|
||||
#include <system/System.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#ifdef LIME_FREETYPE
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include FT_BITMAP_H
|
||||
#include FT_SFNT_NAMES_H
|
||||
#include FT_TRUETYPE_IDS_H
|
||||
#include FT_GLYPH_H
|
||||
#include FT_OUTLINE_H
|
||||
#endif
|
||||
|
||||
|
||||
// from http://stackoverflow.com/questions/2948308/how-do-i-read-utf-8-characters-via-a-pointer
|
||||
#define IS_IN_RANGE(c, f, l) (((c) >= (f)) && ((c) <= (l)))
|
||||
|
||||
|
||||
unsigned long readNextChar (char*& p)
|
||||
{
|
||||
// TODO: since UTF-8 is a variable-length
|
||||
@@ -259,63 +273,31 @@ namespace lime {
|
||||
}
|
||||
|
||||
|
||||
Font *Font::FromFile (const char *fontFace) {
|
||||
Font::Font (void* face) {
|
||||
|
||||
int error;
|
||||
FT_Library library;
|
||||
|
||||
error = FT_Init_FreeType (&library);
|
||||
|
||||
if (error) {
|
||||
|
||||
printf ("Could not initialize FreeType\n");
|
||||
|
||||
} else {
|
||||
|
||||
FT_Face face;
|
||||
error = FT_New_Face (library, fontFace, 0, &face);
|
||||
|
||||
if (error == FT_Err_Unknown_File_Format) {
|
||||
|
||||
printf ("Invalid font type\n");
|
||||
|
||||
} else if (error) {
|
||||
|
||||
printf ("Failed to load font face %s\n", fontFace);
|
||||
|
||||
} else {
|
||||
|
||||
return new Font(face);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
Font::Font (FT_Face face) {
|
||||
|
||||
this->face = face;
|
||||
|
||||
/* Set charmap
|
||||
*
|
||||
* See http://www.microsoft.com/typography/otspec/name.htm for a list of
|
||||
* some possible platform-encoding pairs. We're interested in 0-3 aka 3-1
|
||||
* - UCS-2. Otherwise, fail. If a font has some unicode map, but lacks
|
||||
* UCS-2 - it is a broken or irrelevant font. What exactly Freetype will
|
||||
* select on face load (it promises most wide unicode, and if that will be
|
||||
* slower that UCS-2 - left as an excercise to check.
|
||||
*/
|
||||
for (int i = 0; i < face->num_charmaps; i++) {
|
||||
|
||||
if (face) {
|
||||
|
||||
FT_UShort pid = face->charmaps[i]->platform_id;
|
||||
FT_UShort eid = face->charmaps[i]->encoding_id;
|
||||
|
||||
if (((pid == 0) && (eid == 3)) || ((pid == 3) && (eid == 1))) {
|
||||
/* Set charmap
|
||||
*
|
||||
* See http://www.microsoft.com/typography/otspec/name.htm for a list of
|
||||
* some possible platform-encoding pairs. We're interested in 0-3 aka 3-1
|
||||
* - UCS-2. Otherwise, fail. If a font has some unicode map, but lacks
|
||||
* UCS-2 - it is a broken or irrelevant font. What exactly Freetype will
|
||||
* select on face load (it promises most wide unicode, and if that will be
|
||||
* slower that UCS-2 - left as an excercise to check.
|
||||
*/
|
||||
for (int i = 0; i < ((FT_Face)face)->num_charmaps; i++) {
|
||||
|
||||
FT_Set_Charmap (face, face->charmaps[i]);
|
||||
FT_UShort pid = ((FT_Face)face)->charmaps[i]->platform_id;
|
||||
FT_UShort eid = ((FT_Face)face)->charmaps[i]->encoding_id;
|
||||
|
||||
if (((pid == 0) && (eid == 3)) || ((pid == 3) && (eid == 1))) {
|
||||
|
||||
FT_Set_Charmap ((FT_Face)face, ((FT_Face)face)->charmaps[i]);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -324,44 +306,87 @@ namespace lime {
|
||||
}
|
||||
|
||||
|
||||
wchar_t *get_familyname_from_sfnt_name (FT_Face face) {
|
||||
Font::Font (Resource *resource, int faceIndex) {
|
||||
|
||||
wchar_t *family_name = NULL;
|
||||
FT_SfntName sfnt_name;
|
||||
FT_UInt num_sfnt_names, sfnt_name_index;
|
||||
int len, i;
|
||||
|
||||
if (FT_IS_SFNT (face)) {
|
||||
if (resource) {
|
||||
|
||||
num_sfnt_names = FT_Get_Sfnt_Name_Count (face);
|
||||
sfnt_name_index = 0;
|
||||
int error;
|
||||
FT_Library library;
|
||||
|
||||
while (sfnt_name_index < num_sfnt_names) {
|
||||
error = FT_Init_FreeType (&library);
|
||||
|
||||
if (error) {
|
||||
|
||||
if (!FT_Get_Sfnt_Name (face, sfnt_name_index++, (FT_SfntName *)&sfnt_name) && sfnt_name.name_id == TT_NAME_ID_FULL_NAME) {
|
||||
printf ("Could not initialize FreeType\n");
|
||||
|
||||
} else {
|
||||
|
||||
FT_Face face;
|
||||
FILE_HANDLE *file = NULL;
|
||||
|
||||
if (resource->path) {
|
||||
|
||||
if (sfnt_name.platform_id == TT_PLATFORM_MACINTOSH) {
|
||||
file = lime::fopen (resource->path, "rb");
|
||||
|
||||
if (file->isFile ()) {
|
||||
|
||||
len = sfnt_name.string_len;
|
||||
family_name = new wchar_t[len + 1];
|
||||
mbstowcs (&family_name[0], &reinterpret_cast<const char*>(sfnt_name.string)[0], len);
|
||||
family_name[len] = L'\0';
|
||||
return family_name;
|
||||
error = FT_New_Face (library, resource->path, faceIndex, &face);
|
||||
|
||||
} else if ((sfnt_name.platform_id == TT_PLATFORM_MICROSOFT) && (sfnt_name.encoding_id == TT_MS_ID_UNICODE_CS)) {
|
||||
} else {
|
||||
|
||||
len = sfnt_name.string_len / 2;
|
||||
family_name = (wchar_t*)malloc ((len + 1) * sizeof (wchar_t));
|
||||
ByteArray data = ByteArray (resource->path);
|
||||
unsigned char *buffer = (unsigned char*)malloc (data.Size ());
|
||||
memcpy (buffer, data.Bytes (), data.Size ());
|
||||
error = FT_New_Memory_Face (library, buffer, data.Size (), faceIndex, &face);
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
unsigned char *buffer = (unsigned char*)malloc (resource->data->Size ());
|
||||
memcpy (buffer, resource->data->Bytes (), resource->data->Size ());
|
||||
error = FT_New_Memory_Face (library, buffer, resource->data->Size (), faceIndex, &face);
|
||||
|
||||
}
|
||||
|
||||
if (file) {
|
||||
|
||||
lime::fclose (file);
|
||||
|
||||
}
|
||||
|
||||
if (error == FT_Err_Unknown_File_Format) {
|
||||
|
||||
printf ("Invalid font type\n");
|
||||
|
||||
} else if (error) {
|
||||
|
||||
printf ("Failed to load font face %s\n", resource->path);
|
||||
|
||||
} else {
|
||||
|
||||
this->face = face;
|
||||
|
||||
/* Set charmap
|
||||
*
|
||||
* See http://www.microsoft.com/typography/otspec/name.htm for a list of
|
||||
* some possible platform-encoding pairs. We're interested in 0-3 aka 3-1
|
||||
* - UCS-2. Otherwise, fail. If a font has some unicode map, but lacks
|
||||
* UCS-2 - it is a broken or irrelevant font. What exactly Freetype will
|
||||
* select on face load (it promises most wide unicode, and if that will be
|
||||
* slower that UCS-2 - left as an excercise to check.
|
||||
*/
|
||||
for (int i = 0; i < ((FT_Face)face)->num_charmaps; i++) {
|
||||
|
||||
FT_UShort pid = ((FT_Face)face)->charmaps[i]->platform_id;
|
||||
FT_UShort eid = ((FT_Face)face)->charmaps[i]->encoding_id;
|
||||
|
||||
if (((pid == 0) && (eid == 3)) || ((pid == 3) && (eid == 1))) {
|
||||
|
||||
family_name[i] = ((wchar_t)sfnt_name.string[i * 2 + 1]) | (((wchar_t)sfnt_name.string[i * 2]) << 8);
|
||||
FT_Set_Charmap ((FT_Face)face, ((FT_Face)face)->charmaps[i]);
|
||||
|
||||
}
|
||||
|
||||
family_name[len] = L'\0';
|
||||
return family_name;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -370,7 +395,16 @@ namespace lime {
|
||||
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
Font::~Font () {
|
||||
|
||||
if (face) {
|
||||
|
||||
FT_Done_Face ((FT_Face)face);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -379,7 +413,7 @@ namespace lime {
|
||||
|
||||
int result, i, j;
|
||||
|
||||
FT_Set_Char_Size (face, em, em, 72, 72);
|
||||
FT_Set_Char_Size ((FT_Face)face, em, em, 72, 72);
|
||||
|
||||
std::vector<glyph*> glyphs;
|
||||
|
||||
@@ -397,20 +431,20 @@ namespace lime {
|
||||
FT_ULong char_code;
|
||||
FT_UInt glyph_index;
|
||||
|
||||
char_code = FT_Get_First_Char (face, &glyph_index);
|
||||
char_code = FT_Get_First_Char ((FT_Face)(FT_Face)face, &glyph_index);
|
||||
|
||||
while (glyph_index != 0) {
|
||||
|
||||
if (FT_Load_Glyph (face, glyph_index, FT_LOAD_NO_BITMAP | FT_LOAD_FORCE_AUTOHINT | FT_LOAD_DEFAULT) == 0) {
|
||||
if (FT_Load_Glyph ((FT_Face)(FT_Face)face, glyph_index, FT_LOAD_NO_BITMAP | FT_LOAD_FORCE_AUTOHINT | FT_LOAD_DEFAULT) == 0) {
|
||||
|
||||
glyph *g = new glyph;
|
||||
result = FT_Outline_Decompose (&face->glyph->outline, &ofn, g);
|
||||
result = FT_Outline_Decompose (&((FT_Face)face)->glyph->outline, &ofn, g);
|
||||
|
||||
if (result == 0) {
|
||||
|
||||
g->index = glyph_index;
|
||||
g->char_code = char_code;
|
||||
g->metrics = face->glyph->metrics;
|
||||
g->metrics = ((FT_Face)face)->glyph->metrics;
|
||||
glyphs.push_back (g);
|
||||
|
||||
} else {
|
||||
@@ -421,7 +455,7 @@ namespace lime {
|
||||
|
||||
}
|
||||
|
||||
char_code = FT_Get_Next_Char (face, char_code, &glyph_index);
|
||||
char_code = FT_Get_Next_Char ((FT_Face)face, char_code, &glyph_index);
|
||||
|
||||
}
|
||||
|
||||
@@ -429,7 +463,7 @@ namespace lime {
|
||||
std::sort (glyphs.begin (), glyphs.end (), glyph_sort_predicate ());
|
||||
|
||||
std::vector<kerning> kern;
|
||||
if (FT_HAS_KERNING (face)) {
|
||||
if (FT_HAS_KERNING (((FT_Face)face))) {
|
||||
|
||||
int n = glyphs.size ();
|
||||
FT_Vector v;
|
||||
@@ -442,7 +476,7 @@ namespace lime {
|
||||
|
||||
int r_glyph = glyphs[j]->index;
|
||||
|
||||
FT_Get_Kerning (face, l_glyph, r_glyph, FT_KERNING_DEFAULT, &v);
|
||||
FT_Get_Kerning ((FT_Face)face, l_glyph, r_glyph, FT_KERNING_DEFAULT, &v);
|
||||
|
||||
if (v.x != 0 || v.y != 0) {
|
||||
|
||||
@@ -457,21 +491,23 @@ namespace lime {
|
||||
}
|
||||
|
||||
int num_glyphs = glyphs.size ();
|
||||
wchar_t* family_name = get_familyname_from_sfnt_name (face);
|
||||
|
||||
Font font = Font (face);
|
||||
wchar_t* family_name = font.GetFamilyName ();
|
||||
|
||||
value ret = alloc_empty_object ();
|
||||
alloc_field (ret, val_id ("has_kerning"), alloc_bool (FT_HAS_KERNING (face)));
|
||||
alloc_field (ret, val_id ("is_fixed_width"), alloc_bool (FT_IS_FIXED_WIDTH (face)));
|
||||
alloc_field (ret, val_id ("has_glyph_names"), alloc_bool (FT_HAS_GLYPH_NAMES (face)));
|
||||
alloc_field (ret, val_id ("is_italic"), alloc_bool (face->style_flags & FT_STYLE_FLAG_ITALIC));
|
||||
alloc_field (ret, val_id ("is_bold"), alloc_bool (face->style_flags & FT_STYLE_FLAG_BOLD));
|
||||
alloc_field (ret, val_id ("has_kerning"), alloc_bool (FT_HAS_KERNING (((FT_Face)face))));
|
||||
alloc_field (ret, val_id ("is_fixed_width"), alloc_bool (FT_IS_FIXED_WIDTH (((FT_Face)face))));
|
||||
alloc_field (ret, val_id ("has_glyph_names"), alloc_bool (FT_HAS_GLYPH_NAMES (((FT_Face)face))));
|
||||
alloc_field (ret, val_id ("is_italic"), alloc_bool (((FT_Face)face)->style_flags & FT_STYLE_FLAG_ITALIC));
|
||||
alloc_field (ret, val_id ("is_bold"), alloc_bool (((FT_Face)face)->style_flags & FT_STYLE_FLAG_BOLD));
|
||||
alloc_field (ret, val_id ("num_glyphs"), alloc_int (num_glyphs));
|
||||
alloc_field (ret, val_id ("family_name"), family_name == NULL ? alloc_string (face->family_name) : alloc_wstring (family_name));
|
||||
alloc_field (ret, val_id ("style_name"), alloc_string (face->style_name));
|
||||
alloc_field (ret, val_id ("em_size"), alloc_int (face->units_per_EM));
|
||||
alloc_field (ret, val_id ("ascend"), alloc_int (face->ascender));
|
||||
alloc_field (ret, val_id ("descend"), alloc_int (face->descender));
|
||||
alloc_field (ret, val_id ("height"), alloc_int (face->height));
|
||||
alloc_field (ret, val_id ("family_name"), family_name == NULL ? alloc_string (((FT_Face)face)->family_name) : alloc_wstring (family_name));
|
||||
alloc_field (ret, val_id ("style_name"), alloc_string (((FT_Face)face)->style_name));
|
||||
alloc_field (ret, val_id ("em_size"), alloc_int (((FT_Face)face)->units_per_EM));
|
||||
alloc_field (ret, val_id ("ascend"), alloc_int (((FT_Face)face)->ascender));
|
||||
alloc_field (ret, val_id ("descend"), alloc_int (((FT_Face)face)->descender));
|
||||
alloc_field (ret, val_id ("height"), alloc_int (((FT_Face)face)->height));
|
||||
|
||||
delete family_name;
|
||||
|
||||
@@ -507,7 +543,7 @@ namespace lime {
|
||||
alloc_field (ret, val_id ("glyphs"), neko_glyphs);
|
||||
|
||||
// 'kerning' field
|
||||
if (FT_HAS_KERNING (face)) {
|
||||
if (FT_HAS_KERNING (((FT_Face)face))) {
|
||||
|
||||
value neko_kerning = alloc_array (kern.size ());
|
||||
|
||||
@@ -537,17 +573,62 @@ namespace lime {
|
||||
}
|
||||
|
||||
|
||||
value Font::GetFamilyName () {
|
||||
wchar_t *Font::GetFamilyName () {
|
||||
|
||||
return alloc_wstring (get_familyname_from_sfnt_name (face));
|
||||
#ifdef LIME_FREETYPE
|
||||
|
||||
wchar_t *family_name = NULL;
|
||||
FT_SfntName sfnt_name;
|
||||
FT_UInt num_sfnt_names, sfnt_name_index;
|
||||
int len, i;
|
||||
|
||||
if (FT_IS_SFNT (((FT_Face)face))) {
|
||||
|
||||
num_sfnt_names = FT_Get_Sfnt_Name_Count ((FT_Face)face);
|
||||
sfnt_name_index = 0;
|
||||
|
||||
while (sfnt_name_index < num_sfnt_names) {
|
||||
|
||||
if (!FT_Get_Sfnt_Name ((FT_Face)face, sfnt_name_index++, (FT_SfntName *)&sfnt_name) && sfnt_name.name_id == TT_NAME_ID_FULL_NAME) {
|
||||
|
||||
if (sfnt_name.platform_id == TT_PLATFORM_MACINTOSH) {
|
||||
|
||||
len = sfnt_name.string_len;
|
||||
family_name = new wchar_t[len + 1];
|
||||
mbstowcs (&family_name[0], &reinterpret_cast<const char*>(sfnt_name.string)[0], len);
|
||||
family_name[len] = L'\0';
|
||||
return family_name;
|
||||
|
||||
} else if ((sfnt_name.platform_id == TT_PLATFORM_MICROSOFT) && (sfnt_name.encoding_id == TT_MS_ID_UNICODE_CS)) {
|
||||
|
||||
len = sfnt_name.string_len / 2;
|
||||
family_name = (wchar_t*)malloc ((len + 1) * sizeof (wchar_t));
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
|
||||
family_name[i] = ((wchar_t)sfnt_name.string[i * 2 + 1]) | (((wchar_t)sfnt_name.string[i * 2]) << 8);
|
||||
|
||||
}
|
||||
|
||||
family_name[len] = L'\0';
|
||||
return family_name;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return NULL;
|
||||
|
||||
}
|
||||
|
||||
bool Font::InsertCodepointFromIndex (unsigned long codepoint) {
|
||||
return InsertCodepoint(codepoint, false);
|
||||
}
|
||||
|
||||
bool Font::InsertCodepoint (unsigned long codepoint, bool b) {
|
||||
|
||||
bool Font::InsertCodepoint (unsigned long codepoint, bool fromIndex) {
|
||||
|
||||
GlyphInfo info;
|
||||
info.codepoint = codepoint;
|
||||
@@ -561,14 +642,18 @@ namespace lime {
|
||||
// if (codepoint < (*first).codepoint ||
|
||||
// (codepoint == (*first).codepoint && mSize != (*first).size)) {
|
||||
|
||||
|
||||
if (b) {
|
||||
info.index = FT_Get_Char_Index (face, codepoint);
|
||||
if (fromIndex) {
|
||||
|
||||
info.index = FT_Get_Char_Index ((FT_Face)face, codepoint);
|
||||
|
||||
} else {
|
||||
info.index = codepoint;
|
||||
|
||||
info.index = codepoint;
|
||||
|
||||
}
|
||||
if (FT_Load_Glyph (face, info.index, FT_LOAD_DEFAULT) != 0) return false;
|
||||
info.height = face->glyph->metrics.height;
|
||||
|
||||
if (FT_Load_Glyph ((FT_Face)face, info.index, FT_LOAD_DEFAULT) != 0) return false;
|
||||
info.height = ((FT_Face)face)->glyph->metrics.height;
|
||||
|
||||
glyphList.insert (first, info);
|
||||
|
||||
@@ -576,7 +661,14 @@ namespace lime {
|
||||
|
||||
// }
|
||||
|
||||
return false;
|
||||
//return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
bool Font::InsertCodepointFromIndex (unsigned long codepoint) {
|
||||
|
||||
return InsertCodepoint (codepoint, false);
|
||||
|
||||
}
|
||||
|
||||
@@ -617,8 +709,8 @@ namespace lime {
|
||||
(int)((1.0) * 0x10000L)
|
||||
};
|
||||
|
||||
FT_Set_Char_Size (face, 0, (int)(size*64), (int)(hdpi * hres), vdpi);
|
||||
FT_Set_Transform (face, &matrix, NULL);
|
||||
FT_Set_Char_Size ((FT_Face)face, 0, (int)(size*64), (int)(hdpi * hres), vdpi);
|
||||
FT_Set_Transform ((FT_Face)face, &matrix, NULL);
|
||||
|
||||
mSize = size;
|
||||
|
||||
@@ -662,14 +754,14 @@ namespace lime {
|
||||
for (std::list<GlyphInfo>::iterator it = glyphList.begin (); it != glyphList.end (); it++) {
|
||||
|
||||
// recalculate the character size for each glyph since it will vary
|
||||
FT_Set_Char_Size (face, 0, (int)((*it).size*64), (int)(hdpi * hres), vdpi);
|
||||
FT_Set_Transform (face, &matrix, NULL);
|
||||
FT_Set_Char_Size ((FT_Face)face, 0, (int)((*it).size*64), (int)(hdpi * hres), vdpi);
|
||||
FT_Set_Transform ((FT_Face)face, &matrix, NULL);
|
||||
|
||||
FT_Load_Glyph (face, (*it).index, FT_LOAD_DEFAULT);
|
||||
FT_Load_Glyph ((FT_Face)face, (*it).index, FT_LOAD_DEFAULT);
|
||||
|
||||
if (FT_Render_Glyph (face->glyph, FT_RENDER_MODE_NORMAL) != 0) continue;
|
||||
if (FT_Render_Glyph (((FT_Face)face)->glyph, FT_RENDER_MODE_NORMAL) != 0) continue;
|
||||
|
||||
FT_Bitmap bitmap = face->glyph->bitmap;
|
||||
FT_Bitmap bitmap = ((FT_Face)face)->glyph->bitmap;
|
||||
|
||||
if (x + bitmap.width > image->width) {
|
||||
|
||||
@@ -733,8 +825,8 @@ namespace lime {
|
||||
alloc_field (v, id_height, alloc_int (bitmap.rows));
|
||||
|
||||
value offset = alloc_empty_object ();
|
||||
alloc_field (offset, id_x, alloc_int (face->glyph->bitmap_left));
|
||||
alloc_field (offset, id_y, alloc_int (face->glyph->bitmap_top));
|
||||
alloc_field (offset, id_x, alloc_int (((FT_Face)face)->glyph->bitmap_left));
|
||||
alloc_field (offset, id_y, alloc_int (((FT_Face)face)->glyph->bitmap_top));
|
||||
alloc_field (v, id_offset, offset);
|
||||
|
||||
alloc_field (v, id_codepoint, alloc_int ((*it).index));
|
||||
@@ -1,82 +1,87 @@
|
||||
#include <graphics/Font.h>
|
||||
#include <graphics/Text.h>
|
||||
#include <text/TextLayout.h>
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include <hb-ft.h>
|
||||
#include <hb.h>
|
||||
|
||||
|
||||
namespace lime {
|
||||
|
||||
Text::Text (hb_tag_t direction, const char *script, const char *language) {
|
||||
|
||||
if (strlen(script) != 4) return;
|
||||
|
||||
|
||||
|
||||
TextLayout::TextLayout (int direction, const char *script, const char *language) {
|
||||
|
||||
if (strlen (script) != 4) return;
|
||||
|
||||
mDirection = (hb_direction_t)direction;
|
||||
mLanguage = hb_language_from_string (language, strlen (language));
|
||||
mLanguage = (int)hb_language_from_string (language, strlen (language));
|
||||
mScript = hb_script_from_string (script, -1);
|
||||
|
||||
|
||||
mBuffer = hb_buffer_create ();
|
||||
hb_buffer_set_direction (mBuffer, mDirection);
|
||||
hb_buffer_set_script (mBuffer, mScript);
|
||||
hb_buffer_set_language (mBuffer, mLanguage);
|
||||
|
||||
hb_buffer_set_direction ((hb_buffer_t*)mBuffer, (hb_direction_t)mDirection);
|
||||
hb_buffer_set_script ((hb_buffer_t*)mBuffer, (hb_script_t)mScript);
|
||||
hb_buffer_set_language ((hb_buffer_t*)mBuffer, (hb_language_t)mLanguage);
|
||||
|
||||
}
|
||||
|
||||
Text::~Text () {
|
||||
|
||||
hb_buffer_destroy (mBuffer);
|
||||
|
||||
|
||||
|
||||
TextLayout::~TextLayout () {
|
||||
|
||||
hb_buffer_destroy ((hb_buffer_t*)mBuffer);
|
||||
|
||||
}
|
||||
|
||||
value Text::FromString (Font *font, size_t size, const char *text) {
|
||||
|
||||
|
||||
|
||||
value TextLayout::Layout (Font *font, size_t size, const char *text) {
|
||||
|
||||
font->SetSize (size);
|
||||
|
||||
|
||||
// reset buffer
|
||||
hb_buffer_reset (mBuffer);
|
||||
hb_buffer_set_direction (mBuffer, mDirection);
|
||||
hb_buffer_set_script (mBuffer, mScript);
|
||||
hb_buffer_set_language (mBuffer, mLanguage);
|
||||
|
||||
hb_buffer_reset ((hb_buffer_t*)mBuffer);
|
||||
hb_buffer_set_direction ((hb_buffer_t*)mBuffer, (hb_direction_t)mDirection);
|
||||
hb_buffer_set_script ((hb_buffer_t*)mBuffer, (hb_script_t)mScript);
|
||||
hb_buffer_set_language ((hb_buffer_t*)mBuffer, (hb_language_t)mLanguage);
|
||||
|
||||
// layout the text
|
||||
hb_buffer_add_utf8 (mBuffer, text, strlen (text), 0, -1);
|
||||
hb_font_t *hb_font = hb_ft_font_create (font->face, NULL);
|
||||
hb_shape (hb_font, mBuffer, NULL, 0);
|
||||
|
||||
hb_buffer_add_utf8 ((hb_buffer_t*)mBuffer, text, strlen (text), 0, -1);
|
||||
hb_font_t *hb_font = hb_ft_font_create ((FT_Face)font->face, NULL);
|
||||
hb_shape (hb_font, (hb_buffer_t*)mBuffer, NULL, 0);
|
||||
|
||||
unsigned int glyph_count;
|
||||
hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos (mBuffer, &glyph_count);
|
||||
hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions (mBuffer, &glyph_count);
|
||||
|
||||
hb_glyph_info_t *glyph_info = hb_buffer_get_glyph_infos ((hb_buffer_t*)mBuffer, &glyph_count);
|
||||
hb_glyph_position_t *glyph_pos = hb_buffer_get_glyph_positions ((hb_buffer_t*)mBuffer, &glyph_count);
|
||||
|
||||
float hres = 100;
|
||||
value pos_info = alloc_array (glyph_count);
|
||||
int posIndex = 0;
|
||||
|
||||
|
||||
for (int i = 0; i < glyph_count; i++) {
|
||||
|
||||
|
||||
font->InsertCodepointFromIndex(glyph_info[i].codepoint);
|
||||
hb_glyph_position_t pos = glyph_pos[i];
|
||||
|
||||
|
||||
value obj = alloc_empty_object ();
|
||||
alloc_field (obj, val_id ("codepoint"), alloc_float (glyph_info[i].codepoint));
|
||||
|
||||
|
||||
value advance = alloc_empty_object ();
|
||||
alloc_field (advance, val_id ("x"), alloc_float (pos.x_advance / (float)(hres * 64)));
|
||||
alloc_field (advance, val_id ("y"), alloc_float (pos.y_advance / (float)64));
|
||||
alloc_field (obj, val_id ("advance"), advance);
|
||||
|
||||
|
||||
value offset = alloc_empty_object ();
|
||||
alloc_field (offset, val_id ("x"), alloc_float (pos.x_offset / (float)(hres * 64)));
|
||||
alloc_field (offset, val_id ("y"), alloc_float (pos.y_offset / (float)64));
|
||||
alloc_field (obj, val_id ("offset"), offset);
|
||||
|
||||
|
||||
val_array_set_i (pos_info, posIndex++, obj);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
hb_font_destroy (hb_font);
|
||||
|
||||
|
||||
return pos_info;
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -5,189 +5,201 @@
|
||||
|
||||
|
||||
namespace lime {
|
||||
|
||||
|
||||
// --- ByteArray -----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
AutoGCRoot *gByteArrayCreate = 0;
|
||||
AutoGCRoot *gByteArrayLen = 0;
|
||||
AutoGCRoot *gByteArrayResize = 0;
|
||||
AutoGCRoot *gByteArrayBytes = 0;
|
||||
|
||||
value lime_byte_array_init(value inFactory, value inLen, value inResize, value inBytes) {
|
||||
|
||||
gByteArrayCreate = new AutoGCRoot(inFactory);
|
||||
gByteArrayLen = new AutoGCRoot(inLen);
|
||||
gByteArrayResize = new AutoGCRoot(inResize);
|
||||
gByteArrayBytes = new AutoGCRoot(inBytes);
|
||||
|
||||
return alloc_null();
|
||||
|
||||
} DEFINE_PRIM(lime_byte_array_init,4);
|
||||
|
||||
|
||||
ByteArray::ByteArray(int inSize)
|
||||
{
|
||||
mValue = val_call1(gByteArrayCreate->get(), alloc_int(inSize) );
|
||||
|
||||
|
||||
ByteArray::ByteArray (int inSize) {
|
||||
|
||||
mValue = val_call1 (gByteArrayCreate->get (), alloc_int (inSize));
|
||||
|
||||
}
|
||||
|
||||
ByteArray::ByteArray() : mValue(0) { }
|
||||
|
||||
ByteArray::ByteArray(const QuickVec<uint8> &inData)
|
||||
{
|
||||
mValue = val_call1(gByteArrayCreate->get(), alloc_int(inData.size()) );
|
||||
uint8 *bytes = Bytes();
|
||||
if (bytes)
|
||||
memcpy(bytes, &inData[0], inData.size() );
|
||||
|
||||
|
||||
ByteArray::ByteArray () : mValue(0) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
ByteArray::ByteArray(const ByteArray &inRHS) : mValue(inRHS.mValue) { }
|
||||
|
||||
ByteArray::ByteArray(value inValue) : mValue(inValue) { }
|
||||
|
||||
void ByteArray::Resize(int inSize)
|
||||
{
|
||||
|
||||
|
||||
ByteArray::ByteArray (const QuickVec<uint8> &inData) {
|
||||
|
||||
mValue = val_call1 (gByteArrayCreate->get (), alloc_int (inData.size ()));
|
||||
uint8 *bytes = Bytes ();
|
||||
if (bytes)
|
||||
memcpy (bytes, &inData[0], inData.size ());
|
||||
|
||||
}
|
||||
|
||||
|
||||
ByteArray::ByteArray (const ByteArray &inRHS) : mValue (inRHS.mValue) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
ByteArray::ByteArray (value inValue) : mValue (inValue) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
ByteArray::ByteArray (const OSChar *inFilename) {
|
||||
|
||||
FILE_HANDLE *file = lime::fopen (inFilename, "rb");
|
||||
|
||||
if (!file) {
|
||||
|
||||
//#ifdef ANDROID
|
||||
//return AndroidGetAssetBytes(inFilename);
|
||||
//#endif
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
lime::fseek (file, 0, SEEK_END);
|
||||
int len = lime::ftell (file);
|
||||
lime::fseek (file, 0, SEEK_SET);
|
||||
|
||||
mValue = val_call1 (gByteArrayCreate->get (), alloc_int (len));
|
||||
|
||||
int status = lime::fread (Bytes (), len, 1, file);
|
||||
lime::fclose (file);
|
||||
|
||||
}
|
||||
|
||||
|
||||
void ByteArray::Resize (int inSize) {
|
||||
|
||||
if (mValue == 0)
|
||||
mValue = val_call1(gByteArrayCreate->get(), alloc_int(inSize) );
|
||||
mValue = val_call1 (gByteArrayCreate->get (), alloc_int (inSize));
|
||||
else
|
||||
val_call2(gByteArrayResize->get(), mValue, alloc_int(inSize) );
|
||||
}
|
||||
|
||||
int ByteArray::Size() const
|
||||
{
|
||||
return val_int( val_call1(gByteArrayLen->get(), mValue ));
|
||||
}
|
||||
|
||||
|
||||
const unsigned char *ByteArray::Bytes() const
|
||||
{
|
||||
value bytes = val_call1(gByteArrayBytes->get(),mValue);
|
||||
if (val_is_string(bytes))
|
||||
return (unsigned char *)val_string(bytes);
|
||||
buffer buf = val_to_buffer(bytes);
|
||||
if (buf==0)
|
||||
{
|
||||
val_throw(alloc_string("Bad ByteArray"));
|
||||
}
|
||||
return (unsigned char *)buffer_data(buf);
|
||||
}
|
||||
|
||||
|
||||
unsigned char *ByteArray::Bytes()
|
||||
{
|
||||
value bytes = val_call1(gByteArrayBytes->get(),mValue);
|
||||
if (val_is_string(bytes))
|
||||
return (unsigned char *)val_string(bytes);
|
||||
buffer buf = val_to_buffer(bytes);
|
||||
if (buf==0)
|
||||
{
|
||||
val_throw(alloc_string("Bad ByteArray"));
|
||||
}
|
||||
return (unsigned char *)buffer_data(buf);
|
||||
}
|
||||
|
||||
|
||||
// Put in asset stubs for now, until we have a final system in place
|
||||
|
||||
#ifdef ANDROID
|
||||
FileInfo AndroidGetAssetFD(const char *inResource)
|
||||
{
|
||||
FileInfo info;
|
||||
info.fd = 0;
|
||||
info.offset = 0;
|
||||
info.length = 0;
|
||||
|
||||
// TODO
|
||||
|
||||
return info;
|
||||
val_call2 (gByteArrayResize->get (), mValue, alloc_int (inSize));
|
||||
|
||||
}
|
||||
|
||||
|
||||
ByteArray AndroidGetAssetBytes(const char *inResource) {
|
||||
int ByteArray::Size () const {
|
||||
|
||||
ByteArray result;
|
||||
|
||||
// TODO
|
||||
|
||||
return result;
|
||||
return val_int (val_call1 (gByteArrayLen->get (), mValue));
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
value lime_byte_array_read_file(value inFilename) {
|
||||
|
||||
ByteArray result = ByteArray::FromFile(val_os_string(inFilename));
|
||||
|
||||
return result.mValue;
|
||||
|
||||
} DEFINE_PRIM(lime_byte_array_read_file,1);
|
||||
|
||||
|
||||
value lime_byte_array_get_native_pointer(value inByteArray) {
|
||||
|
||||
ByteArray bytes(inByteArray);
|
||||
|
||||
if (!val_is_null (bytes.mValue)) {
|
||||
return alloc_int((intptr_t)bytes.Bytes ());
|
||||
}
|
||||
|
||||
return alloc_null();
|
||||
|
||||
} DEFINE_PRIM(lime_byte_array_get_native_pointer,1);
|
||||
|
||||
|
||||
|
||||
|
||||
ByteArray ByteArray::FromFile(const OSChar *inFilename)
|
||||
{
|
||||
FILE_HANDLE *file = lime::fopen (inFilename, "rb");
|
||||
if (!file)
|
||||
{
|
||||
#ifdef ANDROID
|
||||
return AndroidGetAssetBytes(inFilename);
|
||||
#endif
|
||||
return ByteArray();
|
||||
}
|
||||
|
||||
lime::fseek(file,0,SEEK_END);
|
||||
int len = lime::ftell(file);
|
||||
lime::fseek(file,0,SEEK_SET);
|
||||
|
||||
ByteArray result(len);
|
||||
int status = lime::fread(result.Bytes(),len,1,file);
|
||||
lime::fclose(file);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
value lime_byte_array_overwrite_file(value inFilename, value inBytes) {
|
||||
|
||||
// file is created if it doesn't exist,
|
||||
// if it exists, it is truncated to zero
|
||||
FILE_HANDLE *file = lime::fopen (val_os_string(inFilename), "wb");
|
||||
if (!file)
|
||||
{
|
||||
#ifdef ANDROID
|
||||
// [todo]
|
||||
#endif
|
||||
return alloc_null();
|
||||
}
|
||||
|
||||
ByteArray array(inBytes);
|
||||
|
||||
// The function fwrite() writes nitems objects, each size bytes long, to the
|
||||
// stream pointed to by stream, obtaining them from the location given by
|
||||
// ptr.
|
||||
// fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
|
||||
lime::fwrite( array.Bytes() , 1, array.Size() , file);
|
||||
|
||||
lime::fclose(file);
|
||||
return alloc_null();
|
||||
|
||||
} DEFINE_PRIM(lime_byte_array_overwrite_file, 2);
|
||||
|
||||
|
||||
const unsigned char *ByteArray::Bytes () const {
|
||||
|
||||
value bytes = val_call1 (gByteArrayBytes->get (), mValue);
|
||||
|
||||
if (val_is_string (bytes))
|
||||
return (unsigned char *)val_string (bytes);
|
||||
|
||||
buffer buf = val_to_buffer (bytes);
|
||||
|
||||
if (buf == 0) {
|
||||
|
||||
val_throw (alloc_string ("Bad ByteArray"));
|
||||
}
|
||||
|
||||
return (unsigned char *)buffer_data (buf);
|
||||
|
||||
}
|
||||
|
||||
|
||||
unsigned char *ByteArray::Bytes () {
|
||||
|
||||
value bytes = val_call1 (gByteArrayBytes->get (),mValue);
|
||||
|
||||
if (val_is_string (bytes))
|
||||
return (unsigned char *)val_string (bytes);
|
||||
|
||||
buffer buf = val_to_buffer (bytes);
|
||||
|
||||
if (buf == 0) {
|
||||
|
||||
val_throw (alloc_string ("Bad ByteArray"));
|
||||
|
||||
}
|
||||
|
||||
return (unsigned char *)buffer_data (buf);
|
||||
|
||||
}
|
||||
|
||||
|
||||
value lime_byte_array_get_native_pointer (value inByteArray) {
|
||||
|
||||
ByteArray bytes (inByteArray);
|
||||
|
||||
if (!val_is_null (bytes.mValue)) {
|
||||
|
||||
return alloc_int ((intptr_t)bytes.Bytes ());
|
||||
|
||||
}
|
||||
|
||||
return alloc_null ();
|
||||
|
||||
}
|
||||
|
||||
value lime_byte_array_init (value inFactory, value inLen, value inResize, value inBytes) {
|
||||
|
||||
gByteArrayCreate = new AutoGCRoot (inFactory);
|
||||
gByteArrayLen = new AutoGCRoot (inLen);
|
||||
gByteArrayResize = new AutoGCRoot (inResize);
|
||||
gByteArrayBytes = new AutoGCRoot (inBytes);
|
||||
|
||||
return alloc_null ();
|
||||
|
||||
}
|
||||
|
||||
|
||||
value lime_byte_array_overwrite_file (value inFilename, value inBytes) {
|
||||
|
||||
// file is created if it doesn't exist,
|
||||
// if it exists, it is truncated to zero
|
||||
FILE_HANDLE *file = lime::fopen (val_os_string (inFilename), "wb");
|
||||
|
||||
if (!file) {
|
||||
|
||||
#ifdef ANDROID
|
||||
// [todo]
|
||||
#endif
|
||||
return alloc_null();
|
||||
|
||||
}
|
||||
|
||||
ByteArray array (inBytes);
|
||||
|
||||
// The function fwrite() writes nitems objects, each size bytes long, to the
|
||||
// stream pointed to by stream, obtaining them from the location given by
|
||||
// ptr.
|
||||
// fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
|
||||
lime::fwrite (array.Bytes (), 1, array.Size (), file);
|
||||
|
||||
lime::fclose (file);
|
||||
return alloc_null ();
|
||||
|
||||
}
|
||||
|
||||
|
||||
value lime_byte_array_read_file (value inFilename) {
|
||||
|
||||
ByteArray result = ByteArray (val_os_string(inFilename));
|
||||
|
||||
return result.mValue;
|
||||
|
||||
}
|
||||
|
||||
|
||||
DEFINE_PRIM (lime_byte_array_get_native_pointer, 1);
|
||||
DEFINE_PRIM (lime_byte_array_init, 4);
|
||||
DEFINE_PRIM (lime_byte_array_overwrite_file, 2);
|
||||
DEFINE_PRIM (lime_byte_array_read_file, 1);
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user