Headers are text rendered in larger font sizes, optionally in bold, italic, or bold italic. Following Markdown, we support six heading levels: #..######. This article continues and extends the work from the sixth article. The final PDF produced here renders all natural headers using distinct, externally configured font settings.

🦀 Index of the Complete Series.

158-feature-image.png
Rust: PDFs — Pango and Cairo Layout — Supporting Headers

🚀 The code for this post is in the following GitHub repository: pdf_05_header.

💡 Please note that Pango also supports HTML markup. I am not taking that route because I prefer to retain as much control as possible over how the input text is processed.

Repository Layout

💡 Please note: on both Windows and Ubuntu, I’m running Rust version rustc 1.90.0 (1159e78c4 2025-09-14).

This is once again a one‑off project—I don’t plan to update it in future development. I want to keep a log of progress exactly as it occurred. Future code may copy this and make changes to it. I’ve placed the project under the pdf_05_header directory. The structure is:

.
├── Cargo.toml
├── set_env.bat
├── config
│   ├── config-linux.toml
│   └── config-windows.toml
├── src
│   ├── config.rs
│   ├── document.rs
│   ├── main.rs
│   └── page_geometry.rs
└── text
    └── essay.txt

The config.rs, document.rs, and main.rs modules under src/ are new code, which we will discuss in a later section.

⓵ The src/page_geometry.rs module is copied unchanged from the last article.
👉 Changing any margin value in the A4_DEFAULT_MARGINS constant will change the layout of the text in the PDF.

⓶ 💡 The code requires the Pango, HarfBuzz, Cairo, etc. libraries. 🐧 On Ubuntu, all required libraries are globally recognised. 🪟 On Windows, I haven’t added the paths for the libraries’ DLLs to the PATH environment variable. In each new Windows terminal session, I run the following once:

set PATH=C:\PF\harfbuzz\dist\bin\;%PATH%
set PATH=C:\PF\vcpkg\installed\x64-windows\bin\;%PATH%
set PATH=C:\PF\pango\dist\bin;C:\PF\cairo-1.18.4\dist\bin;C:\PF\fribidi\dist\bin;%PATH%

Alternatively, you can simply run set_env.bat.
After that, cargo run works as expected.

⓷ The text/essay.txt file — this is the same text file used in the fourth article and sixth article. However, the actual intended content is only about one‑fifth of the versions used in those two articles. I have just realised that both contained a lot of duplicated text—some paragraphs appeared several times. This was purely a copy‑and‑paste mistake.

Also, all Unicode characters “═” (U+2550) have been replaced with the = (equal sign) character. 🙏 We discuss these updates further in a later section.

⓸ 💥 The build.rs module is not required—the code for the last article does not include a build.rs module. I did not notice this until finishing that article. It appears that this module is no longer necessary, even though Pango still uses the HarfBuzz library underneath.

Download and Install the Be Vietnam Pro Font Program

As mentioned at the outset, for heading text we support bold, italic, and bold italic. The font program used must include faces for bold, italic, and bold italic; otherwise, Pango will simply fall back to the default face. The Arial Unicode MS font program has only a single face, so it cannot be used to demonstrate the features we are going to implement.

The Be Vietnam Pro font program includes all the required faces and is well suited for our needs. Regarding copyright, we are not using it for commercial purposes, so I hope that is okay 😂

Download the Font Program

Download Be_Vietnam_Pro.zip from
https://fonts.google.com/specimen/Be+Vietnam+Pro.

Install On 🪟 Windows

Unpack all individual ttf files from Be_Vietnam_Pro.zip.
Right‑click each ttf file and select Install from the pop‑up menu.

You can verify the installation using Windows Font settings, as illustrated in the screenshot below:

158-01.png

Install On 🐧 Ubuntu

Unpack all ttf files from Be_Vietnam_Pro.zip into /home/behai/Public/, then copy them to /usr/local/share/fonts/ using:

$ sudo mv /home/behai/Public/*.* /usr/local/share/fonts/.

We can verify they are present using sudo ls -l /usr/local/share/fonts.
The result is shown in the screenshot below:

158-02.png

Next, update the system font cache using:

$ sudo fc-cache -f -v

This command produces a long output. To confirm that the new font program Be Vietnam Pro has been installed, run:

$ fc-list | grep "Be Vietnam Pro"

It appears that Be Vietnam Pro has indeed been installed, as shown in the screenshot below:

158-03.png

The Main Code

I did struggle a bit while developing the code. It took several iterations to arrive at the final version presented in this article. The final code feels straightforward enough that I decided not to include any intermediate versions.

The Updated pdf_05_header/text/essay.txt File

Further to the fixes discussed earlier, we also made additional updates to support headers. The first lines now look like this:

# Thỏa Hiệp Án Fontainebleau 14/09/1946: ông Hồ cấu kết với Pháp để tiêu diệt các đảng quốc gia. 

###Tác Giả: Hứa Hoành.

## Kéo rốc sang Pháp làm gì?
...

As mentioned at the outset, we are using Markdown format: # indicates header 1, ## indicates header 2, and so on. The input text file uses headers only up to header 3.

Font Configuration

We now discuss the configuration files
pdf_05_header/config/config-windows.toml and
pdf_05_header/config/config-linux.toml, where we specify font information for the different text types; and their management module
pdf_05_header/src/config.rs.

Instead of having both config-windows.toml and config-linux.toml, we could use a single config.toml. For now, let’s look at config-windows.toml:

[fonts]
paragraph = { family = "Be Vietnam Pro", size = 12, weight = "normal", style = "normal" }

# Headers 1, 2, 3, 4, 5, 6.
headers = [
    { family = "Be Vietnam Pro", size = 20, weight = "bold",   style = "italic" },
    { family = "Be Vietnam Pro", size = 16, weight = "bold",   style = "normal" },
    { family = "Be Vietnam Pro", size = 14, weight = "bold", style = "italic" },
    { family = "Be Vietnam Pro", size = 15, weight = "bold",   style = "italic" },
    { family = "Be Vietnam Pro", size = 14, weight = "normal", style = "normal" },
    { family = "Be Vietnam Pro", size = 13, weight = "bold",   style = "normal" }
]

page_number = { family = "Arial", size = 10, weight = "bold", style = "normal" }

paragraph — the font applied to normal text. The family is Be Vietnam Pro; size is 12 points; weight = "normal" means regular (not bold); style = "normal" means upright (not italic).

headers — loosely speaking, headers[0] is the font applied to header 1, and headers[5] is the font applied to header 6.

page_number — the font applied to the page number. This is the only difference from config-linux.toml, where we also use Be Vietnam Pro instead of Arial.

The pdf_05_header/src/config.rs module is simple and should be self‑explanatory. We take advantage of the serde and toml crates to load the configuration. 🦀 We define the struct Config so that deserialisation can take place automatically.

The pdf_05_header/src/document.rs Module

This module defines the structures that hold the parsed contents of the input text file.

● The enum Block — each Block represents a single line from the input text file. Consider the first few lines shown earlier:

# Thỏa Hiệp Án Fontainebleau 14/09/1946: ông Hồ cấu kết với Pháp để tiêu diệt các đảng quốc gia. 

###Tác Giả: Hứa Hoành.

## Kéo rốc sang Pháp làm gì?
...

This results in five Blocks:

  1. Headerlevel: 1; text: “Thỏa...gia.”.
  2. Paragraphtext: blank line.
  3. Headerlevel: 3; text: “Tác Giả: Hứa Hoành.”.
  4. Paragraphtext: blank line.
  5. Headerlevel: 2; text: “Kéo rốc sang Pháp làm gì?”.

● The
struct PositionedBlock — we apply the appropriate font to each Block and ask Pango to lay out the text in memory. As we have studied before, Pango breaks the text into lines. Each PositionedBlock contains the measured and broken lines that fit on a single page. It follows that a single Block may span multiple PositionedBlocks. Refer to the inline documentation for a detailed description of each field.

We discard the layout memory after measuring. When writing to the PDF, we iterate over the PositionedBlock vector, apply the same font again (ensuring identical line breaking), and write the lines to the PDF document.

The pdf_05_header/src/main.rs Module

We cover some of the new code here; the rest is very similar to what we have already discussed in previous articles in this series.

● The to_pango_description() method — creates and returns a pango::FontDescription based on the struct FontSpec information defined in config.rs. As mentioned earlier, the selected font must provide the faces for the weights and styles referenced in this method.

● The parse_blocks_from_file() function — reads the input text file line by line and parses each line into a Block, as described in this section. It returns a vector of Blocks representing the entire input text file.

● The measure_block() function — this is perhaps the most tricky part of the entire codebase. For each Block in the vector, we create a memory layout using the nominated font. Then, for each layout line, we determine whether there is enough vertical space remaining on the current page to fit it; if not, we advance to the next page. This process is very similar to writing to the actual PDF document, except that we do not draw anything yet. Instead, we store the relevant layout information in PositionedBlock, as discussed earlier.

🦀 The same font is applied to the entire Block’s text, which means all layout lines have the same height. However, inside the loop we still retrieve the height of each layout line:

196
197
        for line_index in 0..layout.line_count() {
            line_height = measure_line_height(line_index, &layout);

I am aware of this oddity. In the future, we may support additional features that could vary text height within a single Block, so keeping this logic in place is safer.

I was caught by the following detail:

209
210
211
                // Advance y so the next line does not overlap.
                // `line_height` of the line that `line_index` points to.
                y += line_height;

Without advancing y by line_height, under certain conditions the first blank line on the next page is lost.

This method returns a vector of PositionedBlock. There can be more PositionedBlocks than Blocks — this is not important for the implementation, but it is an interesting detail from a logical perspective.

● The output_positioned_block() function — it should be clear by now that a PositionedBlock contains either the entire text or a partial segment of only a single Block. It uses the Block information to create the font for the layout, and the PositionedBlock information to write the corresponding text to the PDF.

● The remaining code that we have not discussed should be straightforward and self‑explanatory.

The screenshots below show some PDF pages generated on 🐧 Ubuntu:


What’s Next

This heading feature feels very useful, and I’m really glad I took the time to explore it. In the future, I would like to support additional features—for example, bold, italic, and bold italic within normal paragraphs. That will be a story for another day.

Thanks for reading! I hope this post helps others who are looking to deepen their understanding of PDF technology. As always—stay curious, stay safe 🦊

✿✿✿

Feature image sources:

🦀 Index of the Complete Series.