📜 ⬆️ ⬇️

Modern text rendering on Linux: part 1

Welcome to the first part of Modern Linux Text Rendering. In each article in this series, we will develop a self-contained C program to visualize a character or sequence of characters. Each of these programs will implement a function that I consider necessary for modern text rendering.

In the first part we will configure FreeType and write a simple symbol renderer in the console.


')
This is what we will write. And here is the code.

System Setup



Install FreeType


On Ubuntu, you need to install FreeType and libpng.

 $ sudo apt install libfreetype6 libfreetype6-dev $ sudo apt install libpng16-16 libpng-dev 


Console renderer


Create a C file ( main.c in my case)


 #include <stdio.h> int main() { printf("Hello, world\n"); return 0; } 

 $ clang -Wall -Werror -o main main.c $ ./main Hello, world 

We connect libraries of FreeType


To search for the include path (i.e., directories that the compiler goes through when searching for files in #include ) for FreeType, run:

 $ pkg-config --cflags freetype2 -I/usr/include/freetype2 -I/usr/include/libpng16 

The line -I/usr/include/freetype2 -I/usr/include/libpng16 contains the compilation flags necessary to enable FreeType in the C program.

 #include <stdio.h> #include <freetype2/ft2build.h> #include FT_FREETYPE_H int main() { printf("Hello, world\n"); return 0; } 

 $ clang -I/usr/include/freetype2 \ -I/usr/include/libpng16 \ -Wall -Werror \ -o main \ main.c $ ./main Hello, world 

We print the version of FreeType


Inside main() initialize FreeType with FT_Init_FreeType(&ft) and check for errors (FreeType functions return 0 if successful).

(From now on, all the functions that I will use are taken from the help for the FreeType API ).

 FT_Library ft; FT_Error err = FT_Init_FreeType(&ft); if (err != 0) { printf("Failed to initialize FreeType\n"); exit(EXIT_FAILURE); } 

Then using FT_Library_Version we get the version number.

 FT_Int major, minor, patch; FT_Library_Version(ft, &major, &minor, &patch); printf("FreeType's version is %d.%d.%d\n", major, minor, patch); 

If compiled using the last command, a linker error will pop up:

 /tmp/main-d41304.o: In function `main': main.c:(.text+0x14): undefined reference to `FT_Init_FreeType' main.c:(.text+0x54): undefined reference to `FT_Library_Version' clang: error: linker command failed with exit code 1 (use -v to see invocation) 

To fix, add -lfreetype .

 $ clang -I/usr/include/freetype2 \ -I/usr/include/libpng16 \ -Wall -Werror \ -o main \ -lfreetype \ main.c $ ./main FreeType's version is 2.8.1 

Font Download


The first step for rendering a character is to download a font file. I am using ubuntu mono .

To understand the exact difference between a font face construct, a font family, and individual fonts, see the FreeType documentation .

The third argument is called face index . It is designed to allow font creators to embed multiple faces in the same font size. Since each font has at least one face, a value of 0 will always work, choosing the first option.

  FT_Face face; err = FT_New_Face(ft, "./UbuntuMono.ttf", 0, &face); if (err != 0) { printf("Failed to load face\n"); exit(EXIT_FAILURE); } 

Set pixel size for face


Using this instruction, we tell FreeType the desired width and height for the displayed characters.

If you pass zero for the width, FreeType interprets this as “the same as the others,” in this case 32px. This can be used to display a character, for example, with a width of 10px and a height of 16px.

This operation may fail on a fixed-size font, as in the case of emoji.

 err = FT_Set_Pixel_Sizes(face, 0, 32); if (err != 0) { printf("Failed to set pixel size\n"); exit(EXIT_FAILURE); } 

Getting index for character


First of all, back to the FreeType documentation and establish a naming convention. A symbol is not the same as a glyph . A character is what char says, and a glyph is an image that is somehow associated with that character. This relationship is rather complicated because char can correspond to several glyphs: i.e. accents. A glyph can correspond to many characters: that is, ligatures, where -> is represented as a single image.

To get the glyph index corresponding to the character, we use FT_Get_Char_Index . As you can understand, this involves matching characters and glyphs only one to one. In a future article in this series, we will solve the problem using the HarfBuzz library.

  FT_UInt glyph_index = FT_Get_Char_Index(face, 'a'); 

Loading a glyph from face


Having received glyph_index, we can load the corresponding glyph from our face.

In a future installment, we will discuss in detail the various download flags and how they allow you to use features such as hinting and bitmap fonts.

 FT_Int32 load_flags = FT_LOAD_DEFAULT; err = FT_Load_Glyph(face, glyph_index, load_flags); if (err != 0) { printf("Failed to load glyph\n"); exit(EXIT_FAILURE); } 

Display a glyph in its container (glyph slot)


Now we can finally display our glyph in its container (slot) specified in face->glyph .

We will also discuss rendering flags in the future, because they allow the use of LCD- (or sub-pixel) rendering and grayscale antialiasing.

 FT_Int32 render_flags = FT_RENDER_MODE_NORMAL; err = FT_Render_Glyph(face->glyph, render_flags); if (err != 0) { printf("Failed to render the glyph\n"); exit(EXIT_FAILURE); } 

Character output to the console


The bitmap of the rendered glyph can be obtained from face->glyph->bitmap.buffer , where it is presented as an array of unsigned char values, so its values ​​range from 0 to 255.

The buffer is returned as a one-dimensional array, but is a 2D image. To access the i-th row of the j-th column, we calculate column * row_width + row , as in bitmap.buffer[i * face->glyph->bitmap.pitch + j] .

You can see that when accessing the array we used bitmap.width in a loop and bitmap.pitch , because the length of each line of pixels is equal to bitmap.width , but the “width” of the buffer is bitmap.pitch .

In the following code, all rows and columns are sorted, and different characters are drawn depending on the brightness of the pixel.

 for (size_t i = 0; i < face->glyph->bitmap.rows; i++) { for (size_t j = 0; j < face->glyph->bitmap.width; j++) { unsigned char pixel_brightness = face->glyph->bitmap.buffer[i * face->glyph->bitmap.pitch + j]; if (pixel_brightness > 169) { printf("*"); } else if (pixel_brightness > 84) { printf("."); } else { printf(" "); } } printf("\n"); } 

Console output.

 $ clang -I/usr/include/freetype2 \ -I/usr/include/libpng16 \ -Wall -Werror \ -o main \ -lfreetype \ main.c && ./main FreeType's version is 2.8.1 .*****. .********. .********* . ***. *** *** .******** *********** .**. *** *** *** *** *** ***. *** .*********** *********** .*******.. 

→ The full code can be seen here

Conclusion


We have created a basic character renderer in the console. This example can (and will) be expanded to render characters into OpenGL textures to support emoji, sub-pixel rendering, ligatures, and more. In the next part, we’ll talk about LCD subpixel smoothing compared to shades of gray, their pros and cons.

See you soon.

Source: https://habr.com/ru/post/461497/


All Articles