Briefly: What’s a Font?
A font represents the style how the letters of a word or sentence appear (e.g. Arial, New Times Roman, and so on). These style informations are saved in so-called font files. There are different file standards how to save these information into a file, but the most important font file standard is the TrueType Font standard, thus the name SDL3_ttf. SDL3_ttf is capable to work with TrueType Fonts. The typical file extension of TrueType font files is “.ttf”.
FreeType 2 and HarfBuzz are open source and free software libraries to render text with respect to a TrueType font in high quality.
Texts and Fonts in Applications
In some previous chapters you have read about the creation of textures from image files and how to draw primitives to the renderer. But there is nearly no application out there where you go out without some text messages. Of course, you could print some text to an image and load this image to your application to introduce text to it. That would be very static, what if you like to implement a chat feature to your application where text messages are created dynamically? Or you like the user to insert his name for a highscore list? Or you need to change all texts of the whole application according to a different language. And there are thousands of other circumstances where you need dynamically generated texts.
Not only that they need to be dynamically generated, they need to be different in style, color, size, and so on according to the situation. A chat text appears differently than the caption of a button.
The Solution: SDL3_ttf
Here comes the SDL3_ttf Unit into play. It provides an official extension to SDL3 to render texts by TrueType fonts.
Installing SDL3_ttf
- Get your files here: SDL3_ttf library releases
- Windows:
- Download the most recent version of the Runtime Binaries (DLL files) of the SDL3_image library for your system (32 bit or 64 bit) as a zip file
- extract the zip file
- Copy all these files, especially SDL3_ttf.dll and the other library files, to your system folder (system or system32)
- Linux:
- Download the most recent version of the source code and compile it (until the repos are providing a pre-compiled version to install via the paket manager)
- Copy these files, especially libSDL3_ttf.so, to your library folder; often /usr/local
The Road to Texts in SDL3
In SDL3_ttf there are several functions to load a text as a surface (details later). Hence we need to convert the surface into a texture as known. Unfortunately there is no native way to create a texture directly by a function provided by SDL3_ttf. Look at the following diagram to understand the way we need to go:

So according to the diagram the way to go is as follows, if you have a text in mind (left square in diagram), first create a SDL3 Surface by using one of the functions SDL3_ttf provides (lower path to square at the bottom in the diagram). Next you convert this SDL_Surface into a SDL_Texture by the native SDL3 function SDL_CreateTextureFromSurface (go straight from the buttom square to the upper square in the diagram). Finally render the text as you like to the screen.
Let’s jump into the code now .
program SDL_Fonts;
uses SDL3, SDL3_ttf;
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlSurface1: PSDL_Surface;
sdlTexture1: PSDL_Texture;
ttfFont: PTTF_Font;
sdlColor1: TSDL_Color;
sdlRect1: TSDL_FRect;
begin
// initilization of video subsystem
if not SDL_Init(SDL_INIT_VIDEO) then Halt;
if not SDL_CreateWindowAndRenderer('Use SDL3_Fonts', 500, 500, 0, @sdlWindow1, @sdlRenderer) then Halt;
// initialization of TrueType font engine and loading of a font
if not TTF_Init then Halt;
ttfFont := TTF_OpenFont('font/stadium.ttf', 96);
if ttfFont = nil then Halt;
// font settings
TTF_SetFontStyle(ttfFont, TTF_STYLE_ITALIC);
TTF_SetFontOutline(ttfFont, 1);
TTF_SetFontHinting(ttfFont, TTF_HINTING_NORMAL);
// define colors by RGB values
sdlColor1.r := 255; sdlColor1.g := 0; sdlColor1.b := 0;
// rendering a text to a SDL_Surface
sdlSurface1 := TTF_RenderText_Blended_Wrapped(ttfFont, 'Hello'+LineEnding+' World!', 0, sdlColor1, 0);
// convert SDL_Surface to SDL_Texture and get dimensions
sdlTexture1 := SDL_CreateTextureFromSurface(sdlRenderer, sdlSurface1);
sdlRect1.x := 30;
sdlRect1.y := 100;
SDL_GetTextureSize(sdlTexture1, @sdlRect1.w, @sdlRect1.h);
// rendering of grey background and the text texture
SDL_SetRenderDrawColor(sdlRenderer, 50, 50, 50, SDL_ALPHA_OPAQUE);
SDL_RenderClear(sdlRenderer);
SDL_RenderTexture(sdlRenderer, sdlTexture1, nil, @sdlRect1);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(5000);
// quit TTF engine and clear memory
TTF_CloseFont(ttfFont);
TTF_Quit;
SDL_DestroySurface(sdlSurface1);
SDL_DestroyTexture(sdlTexture1);
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow(sdlWindow1);
// quit SDL3
SDL_Quit;
end.
The final result should behave like this: The text “Hello World!” appears for five seconds in italics and underlined. The text is red and the background is grey. The following screenshot gives an impression what is to be expected.

Let’s begin with the initial lines of code.
program SDL_Fonts;
uses SDL3, SDL3_ttf;
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlSurface1: PSDL_Surface;
sdlTexture1: PSDL_Texture;
ttfFont: PTTF_Font;
sdlColor1: TSDL_Color;
sdlRect1: TSDL_FRect;
begin
// initilization of video subsystem
if not SDL_Init(SDL_INIT_VIDEO) then Halt;
if not SDL_CreateWindowAndRenderer('Use SDL3_Fonts', 500, 500, 0, @sdlWindow1, @sdlRenderer) then Halt;
The program is called “SDL_Fonts” and we will need unit SDL3_ttf additional to SDL3 to have access to the TrueType font engine.
Then we are preparing a window, renderer, surface and texture variable as known.
Next we find a PTTF_Font variable called “ttfFont” which points at TTF_Font that holds the font’s data. The font’s data itself is usually coming from a font file.
Finally there is a color variable “sdlColor1” of the new type TSDL_Color (more on this later) and a rectangle variable “sdlRect1” of the known type TSDL_Rect.
// initialization of TrueType font engine and loading of a font
if not TTF_Init then Halt;
ttfFont := TTF_OpenFont('font/stadium.ttf', 96);
if ttfFont = nil then Halt;
// font settings
TTF_SetFontStyle(ttfFont, TTF_STYLE_ITALIC);
TTF_SetFontOutline(ttfFont, 1);
TTF_SetFontHinting(ttfFont, TTF_HINTING_NORMAL);
To inialize the TrueType font engine the function TTF_Init without any arguments is called. If it returns true, the initialization was successful.
Function TTF_OpenFont(file path and file name, desired point size) returns a pointer to the font data gathered from the font file. If the result is nil, the loading of the font data failed. Most probably something is wrong with the path or file name.
The second argument here is the point size, which asks for an integer value which determines the size of the font. The larger the value, the larger the letters will appear finally (just as known from text editors). Anyway, if you choose too large size the largeste size will be chosen.
Styling the Text in SDL3
By the procedures
TTF_SetFontStyle(font, style),
TTF_SetFontOutline(font, outline)
and
TTF_SetFontHinting(font, hinting)
further shaping of the appearence of the text can be performed. All the procedures need to know as first argument to which font they should be applied, so “ttfFont” in our case. Then the style can be set by the constants shown in the following table, which are kind of self-explanatory. By OR’ing the style constants you can even combine them, e. g. if you need the text to be in italics and underlined.
- TTF_STYLE_NORMAL
- No application of a certain style
- TTF_STYLE_BOLD
- Set a bold style
- TTF_STYLE_ITALIC
- Set letters in italics
- TTF_STYLE_UNDERLINE
- Have the text underlined
- TTF_STYLE_STRIKETHROUGH
- Have the text stroken through
The outline is simply set by an integer number. The larger the number, the larger the outline of the letters will appear. If you don’t need an outline, the argument has to be 0.
The hinting is set similar to the style by pre-defined constants shown in the following table. The hinting setting influences the appearance of the letters and text. In general it should lead to sharper letters, anyway, which setting is best for a certain situation and display device may differ. So if you are unsure what setting to use you should choose TTF_HINTING_NORMAL. If you don’t call this procedure this setting is the default anyway.
- TTF_HINTING_NORMAL
- Normal hinting applies standard grid-fitting.
- TTF_HINTING_LIGHT
- Light hinting applies subtle adjustments to improve rendering.
- TTF_HINTING_MONO
- Monochrome hinting adjusts the font for better rendering at lower resolutions.
- TTF_HINTING_NONE
- No hinting, the font is rendered without any grid-fitting.
- TTF_HINTING_LIGHT_SUBPIXEL
- Light hinting with subpixel rendering for more precise font edges.
All of these three procedures have counter-functions which return the set style, outline and hinting as integer number (the style and hinting constants above are actually just integer numbers), defined as
TTF_GetFontStyle(font): Style constant as Integer,
TTF_GetFontOutline(font): Integer,
TTF_GetFontHinting(font): Hinting constant as Integer.
Obviously the only parameter to be set is the font whose style, outline or hinting you like to know. These functions aren’t demonstrated in the sample though.
TSDL_Color: Colouring the Text by RGB Triples
The next line of code is waiting for us.
// define colors by RGB values
sdlColor1.r := 255; sdlColor1.g := 0; sdlColor1.b := 0;
This line of code represents the color red. Why? – Let’s have a look at the definition of TSDL_Color:
TSDL_Color = record
r: cuint8;
g: cuint8;
b: cuint8;
a: cuint8;
end;
The TSDL_Color record has four fields. For additive color mixing you often have a red share, a green share and a blue share (RGB triple). These three fundamental colors are capable of generating any other color by mixing in the respective proportions. E.g. 100% red and 0% green and 0% blue will lead to red (that is what is done in the code!). But 100% red, 100% green and 0% blue will lead to yellow because of additive mixing. If all three colours are 100% you will get white. If all of them are 0% you will get black. You may notice the variable type cuint8 which means 8bit unsigned integer. From this follows that the values can range between 0 and 255 where 0 equals 0% and 255 equals 100% of the corresponding color. So 2563 = 16,777,216 individual colors can be created.
The a field is for the alpha value, which determines the share of transparency. It is set to opaque (value 255) by default.
Text (Surface) Creation
// rendering a text to a SDL_Surface
sdlSurface1 := TTF_RenderText_Blended_Wrapped(ttfFont, 'Hello'+LineEnding+' World!', 0, sdlColor1, 0);
Finally a text, e.g. the famous “Hello World!” with the chosen colors has to be created. There are several functions to do so and just one of them is chosen for demonstration how the creation generally works. The chosen function is
TTF_RenderText_Blended_Wrapped(font, text, length, color, wrap width): PSDL_Surface
It requires five arguments and returns a SDL3 Surface (no SDL3 Texture!). The first argument is the font you would like to use (it sets the style, outline, size, …). That is “ttfFont” here.
Next is the actual text, obviously, “Hello World”. The text can be directly written as shown in the example but also could come from a variable of type PAnsiChar. Also we placed a LineEnding character in between “Hello” and “World”. This will split the text into two lines at this point. This is possibly only because we used the Wrapped version of the function. The related function TTF_RenderText_Blended() will just ignore the LineEnding character.
The LineEnding character is a native platform-independent control character provided by Free Pascal. Delphi users should use the sLineBreak character instead.
The last argument, the wrap width, is related to this. It is the width in pixels for the text being wrapped (broken into the new line) automatically. This argument and the line ending characters can be combined.
The third argument is the length. Here you can set a length for the text being rendered. Usually you set it to 0, if the text is null-terminated, which it is if you use a PAnsiChar here.
The fourth color argument is setting the color, obviously. We use the “sdlColor1” variable here that we defined before to create a red color.
Text Quality and Rendering Speed
As mentioned before there are more functions to create a SDL3 Surface with text. In general there are four groups of functions according to their properties. The general naming scheme is as follows:
TTF_Render[Text or Glyph]_[suffix]
Those which have the suffix “Solid” are very fast created but their quality is low. Use them when rendering fast changing text (e.g. the score of a pinball simulation). The second group has the suffix “Shaded”. They are slower rendered and have a background color but have much better quality. The third group of functions have the suffix “Blended”. They are of high quality but slow to be rendered. Use them for more static text which doesn’t change a lot. The last group has the suffix “LCD”, if you want sub-pixel rendered text, which may improve quality for LCD screens.
For rendering a single character by its Unicode code(point), use the function which contains “Glyph” as suffix to “TTF_Render”.
As seen in the example code, the TTF_RenderText functions also have a further version where “_Wrapped” is added. Additional to the usual parameters there is a parameter wrapLength of type cint (integer number). Here you can have an integer value which determines the amount of pixels until the text will be text-wrapped. So in our case with a width of the window of 500 pixels the setting of wrapLength to 250 for example would result in a word-wrap when a text would exceed 250 pixels.
Overview: Rendering Modes
The following list summarizes all the functions and most important properties for the three differen rendering modes.
- Solid
- transparency by colorkey (0 pixel)
- very fast but low quality
- 8-bit palettized RGB surface
- Functions
- TTF_RenderText_Solid(font: PTTF_Font; text: PAnsiChar; length: csize_t; fg: TSDL_Color): PSDL_Surface
- TTF_RenderText_Solid_Wrapped(font: PTTF_Font; text: PAnsiChar; length: csize_t; fg: TSDL_Color; wrapLength: cint): PSDL_Surface
- TTF_RenderGlyph_Solid(font: PTTF_Font; ch: cuint32; fg: TSDL_Color): PSDL_Surface
- Shaded
- antialiasing
- slower than solid rendering, but high quality
- 8-bit palettized RGB surface
- Functions
- TTF_RenderText_Shaded(font: PTTF_Font; text: PAnsiChar; length: csize_t; fg: TSDL_Color; bg: TSDL_Color): PSDL_Surface
- TTF_RenderText_Shaded_Wrapped(font: PTTF_Font; text: PAnsiChar; length: csize_t; fg: TSDL_Color; bg: TSDL_Color; wrap_width: cint): PSDL_Surface
- TTF_RenderGlyph_Shaded(font: PTTF_Font; ch: cuint32; fg: TSDL_Color; bg: TSDL_Color): PSDL_Surface
- Blended
- transparency (alpha channel)
- antialiasing
- slow but very high quality
- 32-bit unpalettized (RGBA) surface
- Functions
- TTF_RenderText_Blended(font: PTTF_Font; text: PAnsiChar; length: csize_t; fg: TSDL_Color): PSDL_Surface
- TTF_RenderText_Blended_Wrapped(font: PTTF_Font; text: PAnsiChar; length: csize_t; fg: TSDL_Color; wrap_width: cint): PSDL_Surface
- TTF_RenderGlyph_Blended(font: PTTF_Font; ch: cuint32; fg: TSDL_Color): PSDL_Surface
- LCD
- sub-pixel rendering
- slow but very high quality
- 32-bit unpalettized (RGBA) surface
- Functions
- TTF_RenderText_LCD(font: PTTF_Font; text: PAnsiChar; length: csize_t; fg: TSDL_Color; bg: TSDL_Color): PSDL_Surface
- TTF_RenderText_LCD_Wrapped(font: PTTF_Font; text: PAnsiChar; length: csize_t; fg: TSDL_Color; bg: TSDL_Color; wrap_width: cint): PSDL_Surface
- TTF_RenderGlyph_LCD(font: PTTF_Font; ch: cuint32; fg: TSDL_Color; bg: TSDL_Color): PSDL_Surface
All the functions will return nil on failure to create a SDL3 Surface.
Converting into SDL3 Texture and getting its size
Okay, now let’s proceed to the next lines of code.
// convert SDL_Surface to SDL_Texture and get dimensions
sdlTexture1 := SDL_CreateTextureFromSurface(sdlRenderer, sdlSurface1);
sdlRect1.x := 30;
sdlRect1.y := 100;
SDL_GetTextureSize(sdlTexture1, @sdlRect1.w, @sdlRect1.h);
In the previous list you saw all the functions to generate SDL3 Surfaces with a certain text. Now we need to transform it into a SDL3 Texture by SDL_CreateTextureFromSurface as known.
Then we prepare a rectangle and set the x and y coordinate to 30 and 100. At this point the new text should be rendered in the window.
The dimensions of the new text texture “sdlTexture1” are different from the windows dimensions. If we would render it directly to the window, it would be stretched to fit the window. Hence we need a way to get the dimensions (the width and height) of the new text texture.
SDL_GetTextureSize(texture: PSDL_Texture; w: pcfloat; h: pcfloat): cbool
helps us. This function returns the width and height of a texture into the second and third pointer argument. We want the width and height of “sdlTexture1” and want the result be returned into the width and height field of our rectangle variable “sdlRect1”. So we need to use the @-operator here to get the pointer address of the width and height field.
// rendering of grey background and the text texture
SDL_SetRenderDrawColor(sdlRenderer, 50, 50, 50, SDL_ALPHA_OPAQUE);
SDL_RenderClear(sdlRenderer);
SDL_RenderTexture(sdlRenderer, sdlTexture1, nil, @sdlRect1);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(5000);
Next, we prepare a grey background to be rendered.
Then the text texture “sdlTexture1” is prepared to be rendered to the rectangle “sdlRect1”.
Then the result will be rendered to the window. The rendered result is shown for 5000 ms (5 seconds).
We can proceed to the cleaning process.
// quit TTF engine and clear memory
TTF_CloseFont(ttfFont);
TTF_Quit;
SDL_DestroySurface(sdlSurface1);
SDL_DestroyTexture(sdlTexture1);
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow(sdlWindow1);
// quit SDL3
SDL_Quit;
end.
All the allocated memory has to be free’d now. The font is free’d by procedure
TTF_CloseFont(font: PTTF_Font)
and the TrueType engine is quit by procedure
TTF_Quit.
“sdlSurface1” is free’d by procedure
SDL_FreeSurface(surface: PSDL_Surface)
and the surface, texture, renderer and window as known. After that SDL3 is shut down.
Remark: DO NOT create Text Surfaces on the fly!
As for the loading of bitmap files, while it is possible to directly load the generated text surface to the texture without declaring a surface by combining the surface creation with the texture creation as follows:
sdlTexture1 := SDL_CreateTextureFromSurface(sdlRenderer, TTF_RenderText_Blended(ttfFont, 'Hello World!', 0, sdlColor1, 0);
Do NOT do this, because you have no handle (the surface pointer) to free the surface memory afterwards.
Have fun working with texts :-).
Previous Chapter | Next Chapter