Text

nakst  —  4 months ago [Edited 0 minutes later]

Click to zoom in.

The OS currently uses FreeType for text rendering. Although I originally used stb_truetype, it unfortunately was not able to produce text of comparable quality to other modern operating systems. This lead me to porting FreeType to the OS. In this blog post I'll explain how building and porting FreeType works, which'll hopefully help people implement it into their own programs.

Downloading FreeType

FreeType can be downloaded from their website, www.freetype.org. I haven't yet updated to version 2.9.1 (still using 2.9), but it shouldn't matter which version you download. Once you've got the source, all you need to do is extract it. The OS's build system automatically downloads and extracts the source in these three UNIX commands:

1
2
3
curl https://mirrors.up.pt/pub/nongnu/freetype/freetype-2.9.tar.gz > freetype-2.9.tar.gz
gunzip freetype-2.9.tar.gz
tar -xf freetype-2.9.tar


Configuring FreeType

FreeType is a large library with support for many different features, most of which you probably don't need. Luckily, the main configuration for the library is split into only 3 files (I'm looking at you, GCC!). The build system automatically patches all three of these files after the source has been downloaded.

The first is modules.cfg, which allows you to enable and disable different features. Features are written in the form FONT_MODULES += .... You can disable the ones you don't need by placing a # at the start of the line. Here is the full list of modules we disable, in addition to the disabled-by-default modules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type1
cff
cid
pfr
type42
winfonts
pcf
bdf
autohint
pshinter
cache
gzip
lzw
bzip2
psaux
fttype1.c
ftwinfnt.c


You don't have to disable all of these if libary size isn't a concern, but I wanted to keep it as lightweight as possible.

The next file is include/freetype/config/ftoption.h. This is a header file, so you can disable features here using //. From this file, I disable FT_CONFIG_OPTION_ENVIRONMENT_PROPERTIES since the OS doesn't have any notion of environment variables (yay!), FT_CONFIG_OPTION_USE_LZW and FT_CONFIG_OPTION_USE_ZLIB to avoid any dependencies on external decompression libraries, FT_CONFIG_OPTION_POSTSCRIPT_NAMES, FT_CONFIG_OPTION_ADOBE_GLYPH_LIST, and FT_CONFIG_OPTION_MAC_FONTS. I also enable FT_CONFIG_OPTION_DISABLE_STREAM_SUPPORT so there's no dependency on the C standard library.

The final file we patch is ports/freetype/src/include/freetype/config/ftstdlib.h, where FreeType gets the names of C runtime functions from. We have a lightweight set of these functions in the OS API, prefixed with OSCRT-, so if a program doesn't want the C standard library, it won't have to link to it. We redirect all the functions in this file to our versions.

At the start of the file, I include the OS API's header file. We have to declare some additional defines so we can statically link FreeType with the API. Sadly, I can't remember how many of these are still necessary since the API has been through several refactors. If you're building FreeType on another platform you shouldn't have to worry about all this.

1
2
3
4
5
6
7
8
#ifndef IncludedEssenceAPIHeader
#define OS_CRT
#define OS_API
#define OS_FORWARD(x) x
#define OS_DIRECT_API
#define OS_EXTERN_FORWARD extern
#include <os.h>
#endif


After that, I comment out all the C standard library header inclusions, e.g. // #include <stdio.h>, and changes all the defines to use the OSCRT- prefix, e.g. #define ft_strrchr OSCRTstrrchr. If you want to use my lightweight reimplementation of all the C standard library functions needed to port FreeType, look at api/crt.c of the OS's source. I'm doubtful whether you can copyright such simple implementations of such generic functions, but it'd be nice if you credited the project if you used these functions.

Building

Now that we've configured FreeType we can build it. This is a simple configure followed by make, although I have to add a few extra flags because I'm cross-compiling.

1
2
./configure --without-zlib --without-bzip2 --without-png --without-harfbuzz CC=x86_64-essence-gcc CFLAGS="-g -ffreestanding -DARCH_X86_64 -Wno-unused-function" LDFLAGS="-nostdlib -lgcc" --host=x86_64-essence
make ANSIFLAGS="" > /dev/null


When configuring I inform it of the cross compiler and all the no-cstdlib releated flags. I then tell it to not use any dependencies. I pass ANSIFLAGS="" to the makefile, so it doesn't complain that my OS's header isn't compatible with whatever version of C they used 100 years ago. I also pass > /dev/null/ so only errors are reported. Much quieter. If everything built correctly, you now have a new shiny static library in objs/.libs/libfreetype.a, and headers in include/. If it didn't build correctly, please don't ask me why, because I have no idea.

Using FreeType

Once you've build FreeType, linked your program to it, and added its include path, you can now start actually using it.

In your soruce file, first include the FreeType headers. For some reason you have to include ft2build.h, sine it defines the path of the header file you actually want to include. There's probably a good reason why (there isn't).

1
2
#include <ft2build.h>
#include FT_FREETYPE_H


Since we built FreeType without access to C's stdio functions, we can load our fonts ourselves.

1
2
3
void *loadedFontRegular = OSFileReadAll(OSLiteral("/OS/Fonts/Noto Sans Regular.ttf"), (size_t *) &nodeRegular.fileSize);
void *loadedFontBold = OSFileReadAll(OSLiteral("/OS/Fonts/Noto Sans Bold.ttf"), (size_t *) &nodeBold.fileSize);
...


We then can initialise FreeType and load our fonts into it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FT_Library freetypeLibrary;
FT_Face fontRegular, fontBold, ...;
FT_Error error;

error = FT_Init_FreeType(&freetypeLibrary);
if (error) ...
error = FT_New_Memory_Face(freetypeLibrary, (uint8_t *) loadedFontRegular, nodeRegular.fileSize, 0, &fontRegular);
if (error) ...
error = FT_New_Memory_Face(freetypeLibrary, (uint8_t *) loadedFontBold, nodeBold.fileSize, 0, &fontBold);
if (error) ...
...


Rendering glyphs

Good news! We are finally render to actually render glyphs from our fonts.

1
2
3
4
5
6
7
int size = 9;
int character = 'a';
FT_Face font = fontRegular;

FT_Set_Char_Size(font, 0, size * 64, 100, 100);
FT_Load_Glyph(font, FT_Get_Char_Index(font, character), FT_LOAD_DEFAULT);
FT_Render_Glyph(font->glyph, FT_RENDER_MODE_LCD);	// Render using subpixel antialiasing.


We can then extract some information about the glyph that we need.

1
2
3
4
5
6
7
8
9
int advanceWidth = font->glyph->advance.x >> 6;		// The amount of pixels to advance to draw the next character.
int lineHeight = font->size->metrics.height >> 6; 	// The height of a line.
int ascent = font->size->metrics.ascender >> 6; 	// Where the line starts, vertically. (Used for drawing carets).

FT_Bitmap *bitmap = &font->glyph->bitmap;
int width = bitmap->width / 3;				// The width of the glyph.
int height = bitmap->rows;					// The height of the glyph.
int xoff = font->glyph->bitmap_left;			// The x offset to use when drawing the glyph.
int yoff = -font->glyph->bitmap_top;			// The y offset to use when drawing the glyph.


Then we can store the glyph's data in our cache. The OS caches the last 256 glyphs it sees, storing their size, character and font. If a glyph is already in the cache, it won't go to FreeType to render it. We need to shift the color channels around to get them into our desired format, and when also reduce the effect of the subpixel antialiasing to prevent the appearance of color fringes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
for (int y = 0; y < height; y++) {
	for (int x = 0; x < width; x++) {
		int32_t r = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 0];
		int32_t g = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 1];
		int32_t b = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 2];

		// Reduce how noticible the colour fringes are.
		int32_t average = (r + g + b) / 3;
		r -= (r - average) / 3;
		g -= (g - average) / 3;
		b -= (b - average) / 3;

		output[(x + y * width) * 4 + 0] = (uint8_t) r;	// Put the colors into the order we want. You may want to change this.
		output[(x + y * width) * 4 + 1] = (uint8_t) g;
		output[(x + y * width) * 4 + 2] = (uint8_t) b;
		output[(x + y * width) * 4 + 3] = 0xFF;
	}
}

#ifdef OS_ALT_BACKEND
BackendStoreGlyph(output, width, height, fontCachePosition); 	// If we're using the SDL backend, upload the glyph to the graphics card.
#endif


We can then draw the glyph to the screen. outputPosition contains the position on the screen we're drawing to. After the glyph's drawn, you'll want to add advanceWidth to outputPosition.x. textColor contains the color of our text.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int xOut = outputPosition.x + xoff;
int yOut = outputPosition.y + yoff;
int xFrom = xOut, xTo = xOut + width;
int yFrom = yOut, yTo = yOut + height;
uint8_t alpha = (textColor & 0xFF000000) >> 24;

for (int oY = yFrom; oY < yTo; oY++) {
	int y = oY - yOut;

	for (int oX = xFrom; oX < xTo; oX++) {
		int x = oX - xOut;

		uint32_t pixel = *((uint32_t *) (output + (x * 4 + y * width * 4)));			// The pixel from the rendered glyph.
		uint32_t *destination = (uint32_t *) ((uint8_t *) bitmap + oX * 4 + oY * stride);	// The destination in our window.
		uint32_t original = *destination;

		uint32_t ra = (((pixel & 0x000000FF) >> 0) * alpha) >> 8;		// Alpha blending.
		uint32_t ga = (((pixel & 0x0000FF00) >> 8) * alpha) >> 8;		// Subpixel antialiasing gives us a seperate alpha value to use for each of the RGB channels.
		uint32_t ba = (((pixel & 0x00FF0000) >> 16) * alpha) >> 8;		
		uint32_t r2 = (255 - ra) * ((original & 0x000000FF) >> 0);
		uint32_t g2 = (255 - ga) * ((original & 0x0000FF00) >> 8);
		uint32_t b2 = (255 - ba) * ((original & 0x00FF0000) >> 16);
		uint32_t r1 = ra * ((textColor & 0x000000FF) >> 0);
		uint32_t g1 = ga * ((textColor & 0x0000FF00) >> 8);
		uint32_t b1 = ba * ((textColor & 0x00FF0000) >> 16);

		uint32_t result = 0xFF000000 | (0x00FF0000 & ((b1 + b2) << 8)) 
			| (0x0000FF00 & ((g1 + g2) << 0)) 
			| (0x000000FF & ((r1 + r2) >> 8));
		*destination = result;
	}
}


Rich text

Instead of implementing HTML5 like other modern UI libraries for rich text, I've started to work on my own simple rich text format. At the moment, the code is fairly work in progress, but let's take a short look at the markup format.

The format uses '\v' (vertical tab) and '\f' (form feed) to open and close a tag by default. Since these characters are non-printable, you don't need to worry about escaping certain characters in your input. And instead of having seperate open and close tags, you put the text your marking inside the tag instead.

For example:
1
2
3
4
5
This word is <em>emphasised</em>.

.. becomes ..

This word is \vem emphasised\f.


A single space after the tag name indicates where the marked text starts. Tags can also be nested, as you'd expect.

1
\vmono This text is monospaced. This word is also \vem emphasised\f\f.


I know I'll definitely want lists, tables and images in the future, but I'm not exactly sure where to draw the line before I end up with something like HTML/CSS. If people have any thoughts about rich text, I'd be interested to here them. I'm fairly happy with how it works already (although the code need a rewrite), and have been using it in the File Manager to change the color of text in list view item labels.



SVG icons

I thought I'd also briefly talk about SVG icons here, since they share the glyph cache with text glyphs. The OS currently uses a mix of bitmap Tango icons, and the SVG elementary icons.

As someone who is morally opposed to XML, I needed a way to render SVGs without introducing an XML parser into the OS. For this reason, I pre-parse all the icons during the build process into a icon_pack file. This is about the 33% of the size of the unzipped SVGs, and 160% of the size of the gzip'd SVGs. (But when gzip'd itself it gets to about 50% the size of the gzip's SVGs). I use nanosvg to parse the files.

The icon_pack file format is incredibly simple. At the start of the file, there's a list of filename hashes and file offsets. After that is the uncompressed pre-parsed data, directly saved from the output of nanosvg. This makes it very quick to load. I can then render the icons using nanosvgrast. You can get nanosvg and nanosvgrast here.

Conclusion

Thank you for reading this blog post! I hope it helps more people get high-quality text into their programs using FreeType. If you have any thoughts about how I should implement rich text, please leave a comment, as I'm interested to here them.
#16971
Mārtiņš Možeiko  —  4 months ago
Few comments:

1) no need for FT_CONFIG_OPTION_USE_LZW - lzw in freetype is self-contained code, does not depend on external libraries.

2) no need for FT_CONFIG_OPTION_USE_ZLIB - freetype has zlib source in its distribution which is used when system zlib is not found.

3) you can do "curl -O https://.../freetype-2.9.tar.gz" to automatically download to same filename as in url.

4) "tar zxf freetype-2.9.tar.gz" will do un-gzipping automatically.

#16972
nakst  —  4 months ago [Edited 10 minutes later]
@mmozeiko

1. 2.
Are these features necessary to render many fonts? I haven't had any problems with the truetype fonts I've tested so far, and I'd rather keep the built library as small as possible.

3. 4.
I don't pretend to understand the Unix command line and I appreciate the help, but it's probably easier for me to know how the code works when I come back to it later if things are a bit more verbose.
#16979
Mārtiņš Možeiko  —  4 months ago [Edited 0 minutes later]

No idea if anybody uses lzw and zlib. Freetype has also bzip2 decompression code (that depends on external libbzip2). Comments in source code of these three components actually suggests that this compression is used in PCF files that comes with/for X11. Not in ttf.
Log in to comment