Просмотр исходного кода

WARNING: BREAKING: `LoadFontData()` redesigned, added parameter

This redesign is a big improvement on font loading time and memory requirements. It only loads glyphs available on font from requested codepoints and only processes those glyphs for packaging. When processing +10K codepoints (CJK), the loading time improves considerably.
Ray 1 неделя назад
Родитель
Сommit
29ce5d8aa9
2 измененных файлов с 108 добавлено и 76 удалено
  1. 3 3
      src/raylib.h
  2. 105 73
      src/rtext.c

+ 3 - 3
src/raylib.h

@@ -1466,11 +1466,11 @@ RLAPI int GetPixelDataSize(int width, int height, int format);              // G
 // Font loading/unloading functions
 RLAPI Font GetFontDefault(void);                                                            // Get the default Font
 RLAPI Font LoadFont(const char *fileName);                                                  // Load font from file into GPU memory (VRAM)
-RLAPI Font LoadFontEx(const char *fileName, int fontSize, int *codepoints, int codepointCount); // Load font from file with extended parameters, use NULL for codepoints and 0 for codepointCount to load the default character set, font size is provided in pixels height
+RLAPI Font LoadFontEx(const char *fileName, int fontSize, const int *codepoints, int codepointCount); // Load font from file with extended parameters, use NULL for codepoints and 0 for codepointCount to load the default character set, font size is provided in pixels height
 RLAPI Font LoadFontFromImage(Image image, Color key, int firstChar);                        // Load font from Image (XNA style)
-RLAPI Font LoadFontFromMemory(const char *fileType, const unsigned char *fileData, int dataSize, int fontSize, int *codepoints, int codepointCount); // Load font from memory buffer, fileType refers to extension: i.e. '.ttf'
+RLAPI Font LoadFontFromMemory(const char *fileType, const unsigned char *fileData, int dataSize, int fontSize, const int *codepoints, int codepointCount); // Load font from memory buffer, fileType refers to extension: i.e. '.ttf'
 RLAPI bool IsFontValid(Font font);                                                          // Check if a font is valid (font data loaded, WARNING: GPU texture not checked)
-RLAPI GlyphInfo *LoadFontData(const unsigned char *fileData, int dataSize, int fontSize, int *codepoints, int codepointCount, int type); // Load font data for further use
+RLAPI GlyphInfo *LoadFontData(const unsigned char *fileData, int dataSize, int fontSize, const int *codepoints, int codepointCount, int type, int *glyphCount); // Load font data for further use
 RLAPI Image GenImageFontAtlas(const GlyphInfo *glyphs, Rectangle **glyphRecs, int glyphCount, int fontSize, int padding, int packMethod); // Generate image font atlas using chars info
 RLAPI void UnloadFontData(GlyphInfo *glyphs, int glyphCount);                               // Unload font chars info data (RAM)
 RLAPI void UnloadFont(Font font);                                                           // Unload font from GPU memory (VRAM)

+ 105 - 73
src/rtext.c

@@ -171,7 +171,7 @@ extern void LoadFontDefault(void)
     // NOTE: Using UTF-8 encoding table for Unicode U+0000..U+00FF Basic Latin + Latin-1 Supplement
     // Ref: http://www.utf8-chartable.de/unicode-utf8-table.pl
 
-    defaultFont.glyphCount = 224;   // Number of chars included in our default font
+    defaultFont.glyphCount = 224;   // Number of glyphs included in our default font
     defaultFont.glyphPadding = 0;   // Characters padding
 
     // Default font is directly defined here (data generated from a sprite font image)
@@ -365,7 +365,7 @@ Font LoadFont(const char *fileName)
     #define FONT_TTF_DEFAULT_FIRST_CHAR     32      // TTF font generation default first char for image sprite font (32-Space)
 #endif
 #ifndef FONT_TTF_DEFAULT_CHARS_PADDING
-    #define FONT_TTF_DEFAULT_CHARS_PADDING   4      // TTF font generation default chars padding
+    #define FONT_TTF_DEFAULT_CHARS_PADDING   4      // TTF font generation default glyphs padding
 #endif
 
     Font font = { 0 };
@@ -404,7 +404,7 @@ Font LoadFont(const char *fileName)
 // Load Font from TTF or BDF font file with generation parameters
 // NOTE: You can pass an array with desired characters, those characters should be available in the font
 // if array is NULL, default char set is selected 32..126
-Font LoadFontEx(const char *fileName, int fontSize, int *codepoints, int codepointCount)
+Font LoadFontEx(const char *fileName, int fontSize, const int *codepoints, int codepointCount)
 {
     Font font = { 0 };
 
@@ -440,8 +440,8 @@ Font LoadFontFromImage(Image image, Color key, int firstChar)
     int x = 0;
     int y = 0;
 
-    // We allocate a temporal arrays for chars data measures,
-    // once we get the actual number of chars, we copy data to a sized arrays
+    // We allocate a temporal arrays for glyphs data measures,
+    // once we get the actual number of glyphs, we copy data to a sized arrays
     int tempCharValues[MAX_GLYPHS_FROM_IMAGE] = { 0 };
     Rectangle tempCharRecs[MAX_GLYPHS_FROM_IMAGE] = { 0 };
 
@@ -520,7 +520,7 @@ Font LoadFontFromImage(Image image, Color key, int firstChar)
     font.glyphCount = index;
     font.glyphPadding = 0;
 
-    // We got tempCharValues and tempCharsRecs populated with chars data
+    // We got tempCharValues and tempCharsRecs populated with glyphs data
     // Now we move temp data to sized charValues and charRecs arrays
     font.glyphs = (GlyphInfo *)RL_MALLOC(font.glyphCount*sizeof(GlyphInfo));
     font.recs = (Rectangle *)RL_MALLOC(font.glyphCount*sizeof(Rectangle));
@@ -549,7 +549,7 @@ Font LoadFontFromImage(Image image, Color key, int firstChar)
 }
 
 // Load font from memory buffer, fileType refers to extension: i.e. ".ttf"
-Font LoadFontFromMemory(const char *fileType, const unsigned char *fileData, int dataSize, int fontSize, int *codepoints, int codepointCount)
+Font LoadFontFromMemory(const char *fileType, const unsigned char *fileData, int dataSize, int fontSize, const int *codepoints, int codepointCount)
 {
     Font font = { 0 };
 
@@ -557,21 +557,21 @@ Font LoadFontFromMemory(const char *fileType, const unsigned char *fileData, int
     strncpy(fileExtLower, TextToLower(fileType), 16 - 1);
 
     font.baseSize = fontSize;
-    font.glyphCount = (codepointCount > 0)? codepointCount : 95;
     font.glyphPadding = 0;
 
 #if defined(SUPPORT_FILEFORMAT_TTF)
     if (TextIsEqual(fileExtLower, ".ttf") ||
         TextIsEqual(fileExtLower, ".otf"))
     {
-        font.glyphs = LoadFontData(fileData, dataSize, font.baseSize, codepoints, font.glyphCount, FONT_DEFAULT);
+        font.glyphs = LoadFontData(fileData, dataSize, font.baseSize, codepoints, (codepointCount > 0)? codepointCount : 95, FONT_DEFAULT, &font.glyphCount);
     }
     else
 #endif
 #if defined(SUPPORT_FILEFORMAT_BDF)
     if (TextIsEqual(fileExtLower, ".bdf"))
     {
-        font.glyphs = LoadFontDataBDF(fileData, dataSize, codepoints, font.glyphCount, &font.baseSize);
+        font.glyphs = LoadFontDataBDF(fileData, dataSize, codepoints, (codepointCount > 0)? codepointCount : 95, &font.baseSize);
+        font.glyphCount = (codepointCount > 0)? codepointCount : 95;
     }
     else
 #endif
@@ -620,7 +620,7 @@ bool IsFontValid(Font font)
 
 // Load font data for further use
 // NOTE: Requires TTF font memory data and can generate SDF data
-GlyphInfo *LoadFontData(const unsigned char *fileData, int dataSize, int fontSize, int *codepoints, int codepointCount, int type)
+GlyphInfo *LoadFontData(const unsigned char *fileData, int dataSize, int fontSize, const int *codepoints, int codepointCount, int type, int *glyphCount)
 {
     // NOTE: Using some SDF generation default values,
     // trades off precision with ability to handle *smaller* sizes
@@ -637,7 +637,8 @@ GlyphInfo *LoadFontData(const unsigned char *fileData, int dataSize, int fontSiz
     #define FONT_BITMAP_ALPHA_THRESHOLD     80      // Bitmap (B&W) font generation alpha threshold
 #endif
 
-    GlyphInfo *chars = NULL;
+    GlyphInfo *glyphs = NULL;
+    int glyphCounter = 0;
 
 #if defined(SUPPORT_FILEFORMAT_TTF)
     // Load font data (including pixel data) from TTF memory file
@@ -646,6 +647,7 @@ GlyphInfo *LoadFontData(const unsigned char *fileData, int dataSize, int fontSiz
     {
         bool genFontChars = false;
         stbtt_fontinfo fontInfo = { 0 };
+        int *requiredCodepoints = (int *)codepoints;
 
         if (stbtt_InitFont(&fontInfo, (unsigned char *)fileData, 0))     // Initialize font for data reading
         {
@@ -662,21 +664,29 @@ GlyphInfo *LoadFontData(const unsigned char *fileData, int dataSize, int fontSiz
 
             // Fill fontChars in case not provided externally
             // NOTE: By default we fill glyphCount consecutively, starting at 32 (Space)
-            if (codepoints == NULL)
+            if (requiredCodepoints == NULL)
             {
-                codepoints = (int *)RL_MALLOC(codepointCount*sizeof(int));
-                for (int i = 0; i < codepointCount; i++) codepoints[i] = i + 32;
+                requiredCodepoints = (int *)RL_MALLOC(codepointCount*sizeof(int));
+                for (int i = 0; i < codepointCount; i++) requiredCodepoints[i] = i + 32;
                 genFontChars = true;
             }
 
-            chars = (GlyphInfo *)RL_CALLOC(codepointCount, sizeof(GlyphInfo));
+            // Check available glyphs on provided font before loading them
+            for (int i = 0, index; i < codepointCount; i++)
+            {
+                index = stbtt_FindGlyphIndex(&fontInfo, requiredCodepoints[i]);
+                if (index > 0) glyphCounter++;
+            }
 
-            // NOTE: Using simple packaging, one char after another
+            // WARNING: Allocating space for maximum number of codepoints
+            glyphs = (GlyphInfo *)RL_CALLOC(glyphCounter, sizeof(GlyphInfo));
+            glyphCounter = 0; // Reset to reuse
+
+            int k = 0;
             for (int i = 0; i < codepointCount; i++)
             {
-                int chw = 0, chh = 0;   // Character width and height (on generation)
-                int ch = codepoints[i];  // Character value to get info for
-                chars[i].value = ch;
+                int cpWidth = 0, cpHeight = 0;   // Codepoint width and height (on generation)
+                int cp = requiredCodepoints[i];  // Codepoint value to get info for
 
                 //  Render a unicode codepoint to a bitmap
                 //      stbtt_GetCodepointBitmap()           -- allocates and returns a bitmap
@@ -685,76 +695,96 @@ GlyphInfo *LoadFontData(const unsigned char *fileData, int dataSize, int fontSiz
 
                 // Check if a glyph is available in the font
                 // WARNING: if (index == 0), glyph not found, it could fallback to default .notdef glyph (if defined in font)
-                int index = stbtt_FindGlyphIndex(&fontInfo, ch);
+                int index = stbtt_FindGlyphIndex(&fontInfo, cp);
 
                 if (index > 0)
                 {
+                    // NOTE: Only storing glyphs for codepoints found in the font
+                    glyphs[k].value = cp;
+
                     switch (type)
                     {
                         case FONT_DEFAULT:
-                        case FONT_BITMAP: chars[i].image.data = stbtt_GetCodepointBitmap(&fontInfo, scaleFactor, scaleFactor, ch, &chw, &chh, &chars[i].offsetX, &chars[i].offsetY); break;
-                        case FONT_SDF: if (ch != 32) chars[i].image.data = stbtt_GetCodepointSDF(&fontInfo, scaleFactor, ch, FONT_SDF_CHAR_PADDING, FONT_SDF_ON_EDGE_VALUE, FONT_SDF_PIXEL_DIST_SCALE, &chw, &chh, &chars[i].offsetX, &chars[i].offsetY); break;
+                        case FONT_BITMAP: glyphs[k].image.data = stbtt_GetCodepointBitmap(&fontInfo, scaleFactor, scaleFactor, cp, &cpWidth, &cpHeight, &glyphs[k].offsetX, &glyphs[k].offsetY); break;
+                        case FONT_SDF:
+                        {
+                            if (cp != 32)
+                            {
+                                glyphs[k].image.data = stbtt_GetCodepointSDF(&fontInfo, scaleFactor, cp,
+                                    FONT_SDF_CHAR_PADDING, FONT_SDF_ON_EDGE_VALUE, FONT_SDF_PIXEL_DIST_SCALE,
+                                    &cpWidth, &cpHeight, &glyphs[k].offsetX, &glyphs[k].offsetY);
+                            }
+                        } break;
+                        //case FONT_MSDF:
                         default: break;
                     }
 
-                    if (chars[i].image.data != NULL)    // Glyph data has been found in the font
+                    if (glyphs[k].image.data != NULL)    // Glyph data has been found in the font
                     {
-                        stbtt_GetCodepointHMetrics(&fontInfo, ch, &chars[i].advanceX, NULL);
-                        chars[i].advanceX = (int)((float)chars[i].advanceX*scaleFactor);
+                        stbtt_GetCodepointHMetrics(&fontInfo, cp, &glyphs[k].advanceX, NULL);
+                        glyphs[k].advanceX = (int)((float)glyphs[k].advanceX*scaleFactor);
 
-                        if (chh > fontSize) TRACELOG(LOG_WARNING, "FONT: Character [0x%08x] size is bigger than expected font size", ch);
+                        if (cpHeight > fontSize) TRACELOG(LOG_WARNING, "FONT: [0x%04x] Glyph height is bigger than requested font size: %i > %i", cp, cpHeight, (int)fontSize);
 
-                        // Load characters images
-                        chars[i].image.width = chw;
-                        chars[i].image.height = chh;
-                        chars[i].image.mipmaps = 1;
-                        chars[i].image.format = PIXELFORMAT_UNCOMPRESSED_GRAYSCALE;
+                        // Load glyph image
+                        glyphs[k].image.width = cpWidth;
+                        glyphs[k].image.height = cpHeight;
+                        glyphs[k].image.mipmaps = 1;
+                        glyphs[k].image.format = PIXELFORMAT_UNCOMPRESSED_GRAYSCALE;
 
-                        chars[i].offsetY += (int)((float)ascent*scaleFactor);
+                        glyphs[k].offsetY += (int)((float)ascent*scaleFactor);
                     }
+                    //else TRACELOG(LOG_WARNING, "FONT: Glyph [0x%08x] has no image data available", cp); // Only reported for 0x20 and 0x3000
 
-                    // NOTE: We create an empty image for space character,
-                    // it could be further required for atlas packing
-                    if (ch == 32)
+                    // We create an empty image for Space character (0x20), useful for sprite font generation
+                    // NOTE: Another space to consider: 0x3000 (CJK - Ideographic Space)
+                    if ((cp == 0x20) || (cp == 0x3000))
                     {
-                        stbtt_GetCodepointHMetrics(&fontInfo, ch, &chars[i].advanceX, NULL);
-                        chars[i].advanceX = (int)((float)chars[i].advanceX*scaleFactor);
+                        stbtt_GetCodepointHMetrics(&fontInfo, cp, &glyphs[k].advanceX, NULL);
+                        glyphs[k].advanceX = (int)((float)glyphs[k].advanceX*scaleFactor);
 
                         Image imSpace = {
-                            .data = RL_CALLOC(chars[i].advanceX*fontSize, 2),
-                            .width = chars[i].advanceX,
+                            .data = RL_CALLOC(glyphs[k].advanceX*fontSize, 2),
+                            .width = glyphs[k].advanceX,
                             .height = fontSize,
                             .mipmaps = 1,
                             .format = PIXELFORMAT_UNCOMPRESSED_GRAYSCALE
                         };
 
-                        chars[i].image = imSpace;
+                        glyphs[k].image = imSpace;
                     }
 
                     if (type == FONT_BITMAP)
                     {
                         // Aliased bitmap (black & white) font generation, avoiding anti-aliasing
                         // NOTE: For optimum results, bitmap font should be generated at base pixel size
-                        for (int p = 0; p < chw*chh; p++)
+                        for (int p = 0; p < cpWidth*cpHeight; p++)
                         {
-                            if (((unsigned char *)chars[i].image.data)[p] < FONT_BITMAP_ALPHA_THRESHOLD) ((unsigned char *)chars[i].image.data)[p] = 0;
-                            else ((unsigned char *)chars[i].image.data)[p] = 255;
+                            if (((unsigned char *)glyphs[k].image.data)[p] < FONT_BITMAP_ALPHA_THRESHOLD) 
+                                ((unsigned char *)glyphs[k].image.data)[p] = 0;
+                            else ((unsigned char *)glyphs[k].image.data)[p] = 255;
                         }
                     }
+
+                    k++;
+                    glyphCounter++;
                 }
                 else
                 {
-                    // TODO: Use some fallback glyph for codepoints not found in the font
+                    // WARNING: Glyph not found on font, optionally use a fallback glyph
                 }
             }
+        
+            if (glyphCounter < codepointCount) TRACELOG(LOG_WARNING, "FONT: Requested codepoints glyphs found: [%i/%i]", k, codepointCount);
         }
         else TRACELOG(LOG_WARNING, "FONT: Failed to process TTF font data");
 
-        if (genFontChars) RL_FREE(codepoints);
+        if (genFontChars) RL_FREE(requiredCodepoints);
     }
 #endif
 
-    return chars;
+    *glyphCount = glyphCounter;
+    return glyphs;
 }
 
 // Generate image font atlas using chars info
@@ -1239,7 +1269,7 @@ void DrawTextCodepoint(Font font, int codepoint, Vector2 position, float fontSiz
                       (font.recs[index].height + 2.0f*font.glyphPadding)*scaleFactor };
 
     // Character source rectangle from font texture atlas
-    // NOTE: We consider chars padding when drawing, it could be required for outline/glow shader effects
+    // NOTE: We consider glyphs padding when drawing, it could be required for outline/glow shader effects
     Rectangle srcRec = { font.recs[index].x - (float)font.glyphPadding, font.recs[index].y - (float)font.glyphPadding,
                          font.recs[index].width + 2.0f*font.glyphPadding, font.recs[index].height + 2.0f*font.glyphPadding };
 
@@ -1292,7 +1322,7 @@ int MeasureText(const char *text, int fontSize)
     // Check if default font has been loaded
     if (GetFontDefault().texture.id != 0)
     {
-        int defaultFontSize = 10;   // Default Font chars height in pixel
+        int defaultFontSize = 10;   // Default Font glyphs height in pixel
         if (fontSize < defaultFontSize) fontSize = defaultFontSize;
         int spacing = fontSize/defaultFontSize;
 
@@ -2394,7 +2424,7 @@ static unsigned char HexToInt(char hex)
 
 // Load font data for further use
 // NOTE: Requires BDF font memory data
-static GlyphInfo *LoadFontDataBDF(const unsigned char *fileData, int dataSize, int *codepoints, int codepointCount, int *outFontSize)
+static GlyphInfo *LoadFontDataBDF(const unsigned char *fileData, int dataSize, const int *codepoints, int codepointCount, int *outFontSize)
 {
     #define MAX_BUFFER_SIZE 256
 
@@ -2428,7 +2458,9 @@ static GlyphInfo *LoadFontDataBDF(const unsigned char *fileData, int dataSize, i
     int charBByoff0 = 0;            // Character bounding box Y0 offset
     int charDWidthX = 0;            // Character advance X
     int charDWidthY = 0;            // Character advance Y (unused)
-    GlyphInfo *charGlyphInfo = NULL; // Pointer to output glyph info (NULL if not set)
+
+    GlyphInfo *glyphs = NULL;       // Pointer to output glyph info (NULL if not set)
+    int *requiredCodepoints = codepoints;
 
     if (fileData == NULL) return glyphs;
 
@@ -2437,10 +2469,10 @@ static GlyphInfo *LoadFontDataBDF(const unsigned char *fileData, int dataSize, i
 
     // Fill fontChars in case not provided externally
     // NOTE: By default we fill glyphCount consecutively, starting at 32 (Space)
-    if (codepoints == NULL)
+    if (requiredCodepoints == NULL)
     {
-        codepoints = (int *)RL_MALLOC(codepointCount*sizeof(int));
-        for (int i = 0; i < codepointCount; i++) codepoints[i] = i + 32;
+        requiredCodepoints = (int *)RL_MALLOC(codepointCount*sizeof(int));
+        for (int i = 0; i < codepointCount; i++) requiredCodepoints[i] = i + 32;
         genFontChars = true;
     }
 
@@ -2466,11 +2498,11 @@ static GlyphInfo *LoadFontDataBDF(const unsigned char *fileData, int dataSize, i
 
             if (charBitmapStarted)
             {
-                if (charGlyphInfo != NULL)
+                if (glyphs != NULL)
                 {
                     int pixelY = charBitmapNextRow++;
 
-                    if (pixelY >= charGlyphInfo->image.height) break;
+                    if (pixelY >= glyphs->image.height) break;
 
                     for (int x = 0; x < readBytes; x++)
                     {
@@ -2480,9 +2512,9 @@ static GlyphInfo *LoadFontDataBDF(const unsigned char *fileData, int dataSize, i
                         {
                             int pixelX = ((x*4) + bitX);
 
-                            if (pixelX >= charGlyphInfo->image.width) break;
+                            if (pixelX >= glyphs->image.width) break;
 
-                            if ((byte & (8 >> bitX)) > 0) ((unsigned char *)charGlyphInfo->image.data)[(pixelY*charGlyphInfo->image.width) + pixelX] = 255;
+                            if ((byte & (8 >> bitX)) > 0) ((unsigned char *)glyphs->image.data)[(pixelY*glyphs->image.width) + pixelX] = 255;
                         }
                     }
                 }
@@ -2514,30 +2546,30 @@ static GlyphInfo *LoadFontDataBDF(const unsigned char *fileData, int dataSize, i
             if (strstr(buffer, "BITMAP") != NULL)
             {
                 // Search for glyph index in codepoints
-                charGlyphInfo = NULL;
+                glyphs = NULL;
 
                 for (int codepointIndex = 0; codepointIndex < codepointCount; codepointIndex++)
                 {
                     if (codepoints[codepointIndex] == charEncoding)
                     {
-                        charGlyphInfo = &glyphs[codepointIndex];
+                        glyphs = &glyphs[codepointIndex];
                         break;
                     }
                 }
 
                 // Init glyph info
-                if (charGlyphInfo != NULL)
+                if (glyphs != NULL)
                 {
-                    charGlyphInfo->value = charEncoding;
-                    charGlyphInfo->offsetX = charBBxoff0 + fontBByoff0;
-                    charGlyphInfo->offsetY = fontBBh - (charBBh + charBByoff0 + fontBByoff0 + fontAscent);
-                    charGlyphInfo->advanceX = charDWidthX;
-
-                    charGlyphInfo->image.data = RL_CALLOC(charBBw*charBBh, 1);
-                    charGlyphInfo->image.width = charBBw;
-                    charGlyphInfo->image.height = charBBh;
-                    charGlyphInfo->image.mipmaps = 1;
-                    charGlyphInfo->image.format = PIXELFORMAT_UNCOMPRESSED_GRAYSCALE;
+                    glyphs->value = charEncoding;
+                    glyphs->offsetX = charBBxoff0 + fontBByoff0;
+                    glyphs->offsetY = fontBBh - (charBBh + charBByoff0 + fontBByoff0 + fontAscent);
+                    glyphs->advanceX = charDWidthX;
+
+                    glyphs->image.data = RL_CALLOC(charBBw*charBBh, 1);
+                    glyphs->image.width = charBBw;
+                    glyphs->image.height = charBBh;
+                    glyphs->image.mipmaps = 1;
+                    glyphs->image.format = PIXELFORMAT_UNCOMPRESSED_GRAYSCALE;
                 }
 
                 charBitmapStarted = true;
@@ -2588,14 +2620,14 @@ static GlyphInfo *LoadFontDataBDF(const unsigned char *fileData, int dataSize, i
             {
                 charStarted = true;
                 charEncoding = -1;
-                charGlyphInfo = NULL;
+                glyphs = NULL;
                 charBBw = 0;
                 charBBh = 0;
                 charBBxoff0 = 0;
                 charBByoff0 = 0;
                 charDWidthX = 0;
                 charDWidthY = 0;
-                charGlyphInfo = NULL;
+                glyphs = NULL;
                 charBitmapStarted = false;
                 charBitmapNextRow = 0;
                 continue;