Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

LaTeX & Typst (papers, theses, resumes and slides)  Module 2 – Document structure, text and page layout

Fonts, engines and Unicode

Choose between pdfLaTeX, XeLaTeX and LuaLaTeX, load OpenType fonts in LaTeX and Typst, catch the characters a font lacks and check the PDF's text.

  • Intermediate
  • 25 minutes
  • Examples run with TeX Live 2026, TinyTeX 2026.10, Typst 0.15.1, Python 3.14.8, Pyodide 314.0.7, poppler 26.09.0 and mupdf 1.28.3
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Choose between pdfLaTeX, XeLaTeX and LuaLaTeX for a document's fonts and its venue
  • Load an OpenType font with fontspec in both Unicode engines and with Typst's text function
  • Detect the characters a font lacks with the log, \tracinglostchars and Typst's font list
  • Check with pdffonts and two extractors that the PDF's text can be copied and searched

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose between pdfLaTeX, XeLaTeX and LuaLaTeX for a document's fonts and its venue
  • Load an OpenType font with fontspec in both Unicode engines and with Typst's text function
  • Detect the characters a font lacks with the log, \tracinglostchars and Typst's font list
  • Check with pdffonts and two extractors that the PDF's text can be copied and searched

Before you start

On this page

A seminar notice that names its speaker, Ирина Петрова, came out of LuaLaTeX with a gap where the 12 letters of the name should be, and the build ended with status 0. The only trace was 36 lines in the middle of the run’s output.

Below you will choose a LaTeX engine for the fonts a document needs, load OpenType fonts in LaTeX and in Typst, make a character the font lacks stop the build instead of vanishing, and check that the PDF’s text copies and searches the way it reads. Most results come from one seminar notice, built by every engine and read back by two text extractors.

pdfLaTeX, XeLaTeX and LuaLaTeX read fonts in two different ways

pdfLaTeX works with 8-bit fonts: each font holds at most 256 characters, arranged by a font encoding: The table that says which character each slot of a font holds, such as OT1 or T1 for pdfLaTeX. OT1, the default, lacks some ASCII characters, so LaTeX draws them and they copy out wrongly. More in “Special characters, quotes, dashes and spaces” such as T1, and a package such as lmodern picks the font files. XeLaTeX and LuaLaTeX are Unicode engines: they read every character as its code point: The number Unicode gives a character, written U+ and four or more hexadecimal digits, such as U+2013 for the en dash. Two characters that look alike can have different code points. More in “Special characters, quotes, dashes and spaces” and set it with an OpenType font: A font file, ending in .otf or .ttf, that holds glyphs for Unicode characters together with rules for ligatures and other features. XeLaTeX and LuaLaTeX load such fonts through fontspec; Typst uses them directly., which the fontspec package loads. Its CTAN description calls fontspec a package for XeLaTeX and LuaLaTeX, so it is the first thing to fail when the engine is wrong. Here is the notice, once for the Unicode engines and once for pdfLaTeX:

The notice for the Unicode engines and for pdfLaTeX

notice/notice.tex

% A seminar notice for LuaLaTeX or XeLaTeX. fontspec loads its default, Latin Modern, which has no Cyrillic letters.
\documentclass{article}
\usepackage{fontspec}
\begin{document}
Speaker: Ирина Петрова, from the field station. The office is efficient:
official badges are issued within a day.
\end{document}

notice/notice-pdf.tex

% The same notice for pdfLaTeX, which cannot load fontspec: T1-encoded Latin Modern.
\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\begin{document}
Speaker: Ирина Петрова, from the field station. The office is efficient:
official badges are issued within a day.
\end{document}

This session builds them with all three engines and lists the fonts each PDF embeds with poppler’s pdffonts:

One notice, three engines, two kinds of font Python · engines_session.py
from session import files, read, run, write

# The notice through the three LaTeX engines. pdfLaTeX gets notice-pdf.tex, which loads no fontspec, and then a copy
# with the speaker's name in Latin letters; LuaLaTeX and XeLaTeX get notice.tex. pdffonts lists the fonts each PDF
# embeds.
files("notice/notice.tex", "notice/notice-pdf.tex")
write("notice-latin.tex", read("notice-pdf.tex").replace("Ирина Петрова", "Irina Petrova"))
run(r"""
!pdflatex -interaction=nonstopmode -halt-on-error notice.tex > /dev/null
grep -A1 "^! Fatal" notice.log
!pdflatex -interaction=nonstopmode -halt-on-error notice-pdf.tex > /dev/null
grep -A1 "^! LaTeX Error" notice-pdf.log
pdflatex -interaction=nonstopmode notice-latin.tex | grep Output
pdffonts notice-latin.pdf
lualatex -interaction=nonstopmode notice.tex | grep Output
pdffonts notice.pdf
xelatex -interaction=nonstopmode notice.tex | grep Output
pdffonts notice.pdf
""")

Output

$ pdflatex -interaction=nonstopmode -halt-on-error notice.tex > /dev/null
[exit status 1]
$ grep -A1 "^! Fatal" notice.log
! Fatal Package fontspec Error: The fontspec package requires either XeTeX or
(fontspec)                      LuaTeX.
$ pdflatex -interaction=nonstopmode -halt-on-error notice-pdf.tex > /dev/null
[exit status 1]
$ grep -A1 "^! LaTeX Error" notice-pdf.log
! LaTeX Error: Unicode character И (U+0418)
               not set up for use with LaTeX.
$ pdflatex -interaction=nonstopmode notice-latin.tex | grep Output
Output written on notice-latin.pdf (1 page, 29696 bytes).
$ pdffonts notice-latin.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
LPQTCA+LMRoman10-Regular             Type 1            Custom           yes yes yes      4  0
$ lualatex -interaction=nonstopmode notice.tex | grep Output
Output written on notice.pdf (1 page, 5621 bytes).
$ pdffonts notice.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
VFSOVW+LMRoman10-Regular             CID Type 0C       Identity-H       yes yes yes      4  0
$ xelatex -interaction=nonstopmode notice.tex | grep Output
Output written on notice.pdf (1 page).
$ pdffonts notice.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
NUWFSN+LMRoman10-Regular-Identity-H  CID Type 0C       Identity-H       yes yes yes      4  0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 engines_session.py

pdfLaTeX stopped twice. With fontspec it gave the fatal error that fontspec requires XeTeX or LuaTeX; without it, it refused И, the first letter of the name, because no font it had loaded was set up for that character. With the name in Latin letters, it embedded LMRoman10-Regular as a Type 1 font. LuaLaTeX and XeLaTeX built notice.tex with no error and embedded the same design as a CID Type 0C font, which the pdffonts manual describes as a 16-bit PostScript CFF font: the OpenType version.

Which engine fits depends on the fonts and on where the source goes. Answer two questions about your document:

Interactive · answer and compare Which engine for this document's fonts? Answer two questions about your document and the table marks the engines that fit it.
Which fonts does the document need?
Will the LaTeX source go to arXiv?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

pdfLaTeX, XeLaTeX, LuaLaTeX and Typst against the font jobs this lesson tried
Engine Loads OpenType fonts Finds TeX tree fonts by name Reports a missing glyph
pdfLaTeX Fonts it uses: Type 1 fonts, chosen by encoding Poor fontspec stops it: it requires XeTeX or LuaTeX Mixed loads fonts through packages such as lmodern, not by name Good stops at И: Unicode character not set up for use with LaTeX
XeLaTeX Fonts it uses: OpenType, through fontspec Good any .otf or .ttf through fontspec Poor not on macOS or arXiv: Libertinus Serif cannot be found Mixed one log line per letter, an error only with \tracinglostchars=3
LuaLaTeX Fonts it uses: OpenType, through fontspec Good any .otf or .ttf through fontspec Good luaotfload found Libertinus Serif in the TeX tree Mixed three log lines per letter, an error only with \tracinglostchars=3
Typst Fonts it uses: OpenType, through text(font:) Good its own four, system fonts and --font-path folders Mixed not the TeX tree: point --font-path at a font folder Poor draws a box and prints nothing; a named family warns
How the answers decide
  • “Which fonts does the document need?” An OpenType font, such as Libertinus Serif: rules out pdfLaTeX. fontspec, which loads OpenType fonts in LaTeX, stops pdfLaTeX with a fatal error.
  • “Will the LaTeX source go to arXiv?” Yes: rules out LuaLaTeX and Typst. arXiv lists tex, pdftex, latex, pdflatex and xelatex as its processors: no LuaLaTeX, and a Typst file is not TeX.
  • “Which fonts does the document need?” An OpenType font, such as Libertinus Serif and “Will the LaTeX source go to arXiv?” Yes: adds a caveat to XeLaTeX. XeLaTeX on arXiv finds fonts of the TeX tree only by file name, as its TeX Live page explains.

In short: pdfLaTeX when a template or venue asks for it and its fonts suffice, XeLaTeX for OpenType fonts that must also build on arXiv, whose list of processors includes xelatex but not LuaLaTeX, and LuaLaTeX or Typst otherwise.

Checkpoint

Check: How the three LaTeX engines use fonts

  1. Question 1 of 2 What happened when pdfLaTeX read notice.tex, which loads fontspec?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It stopped with a fatal error, because fontspec requires XeTeX or LuaTeX

    fontspec is a package for XeLaTeX and LuaLaTeX, as its CTAN description says, and it checks the engine before doing anything else.

  2. Question 2 of 2 pdffonts listed LMRoman10-Regular as Type 1 in one PDF and as CID Type 0C in another. Which engines made them?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: pdfLaTeX made the Type 1 one, LuaLaTeX or XeLaTeX the CID Type 0C one

    Same design, two kinds of font file: pdfLaTeX works with 8-bit Type 1 fonts chosen by a font encoding, the Unicode engines with OpenType fonts.

fontspec loads a font by name in LuaLaTeX, by file in XeLaTeX

\setmainfont{Libertinus Serif} asks for a font by the name you would pick from a menu. Libertinus Serif ships with TeX Live, so its files sit in the TeX tree: The folders where a TeX distribution keeps its classes, packages, fonts and settings, such as texmf-dist. Engines find each file in it by name, and kpsewhich prints where a file is. More in “Install TeX Live, TinyTeX or MiKTeX, and Typst”. A second copy of the notice names the same font by its files instead, with fontspec’s Extension and UprightFont keys, so that *-Regular becomes LibertinusSerif-Regular.otf:

Libertinus Serif by its name, and by its file names

notice/notice-name.tex

% The notice in Libertinus Serif, a font of the TeX tree, asked for by its name.
\documentclass{article}
\usepackage{fontspec}
\setmainfont{Libertinus Serif}
\begin{document}
Speaker: Ирина Петрова, from the field station. The office is efficient:
official badges are issued within a day.
\end{document}

notice/notice-file.tex

% The notice in Libertinus Serif, asked for by its file names, which LuaLaTeX and XeLaTeX both find.
\documentclass{article}
\usepackage{fontspec}
\setmainfont{LibertinusSerif}[
  Extension      = .otf,
  UprightFont    = *-Regular,
  BoldFont       = *-Bold,
  ItalicFont     = *-Italic,
  BoldItalicFont = *-BoldItalic]
\begin{document}
Speaker: Ирина Петрова, from the field station. The office is efficient:
official badges are issued within a day.
\end{document}
A font name in LuaLaTeX and in XeLaTeX, and a file name in XeLaTeX Python · lookup_session.py
from session import files, read, run, write

# Libertinus Serif, which lives in the TeX tree, asked for by name and by file name. luaotfload-tool asks LuaLaTeX's
# font database where a name leads. XeLaTeX builds a copy of notice-name.tex under its own name, so that its PDF does
# not replace LuaLaTeX's.
files("notice/notice-name.tex", "notice/notice-file.tex")
write("notice-name-xe.tex", read("notice-name.tex"))
run(r"""
luaotfload-tool --find="Libertinus Serif"
lualatex -interaction=nonstopmode notice-name.tex | grep -E "rror|Output"
pdffonts notice-name.pdf
!xelatex -interaction=nonstopmode notice-name-xe.tex > /dev/null
grep -A2 "^! Package fontspec Error" notice-name-xe.log | head -3
grep -c "in font nullfont" notice-name-xe.log
pdftotext notice-name-xe.pdf - | wc -w
xelatex -interaction=nonstopmode notice-file.tex | grep -E "rror|Output"
pdffonts notice-file.pdf
""")

Output

$ luaotfload-tool --find="Libertinus Serif"
luaotfload | resolve : Font "Libertinus Serif" found!
luaotfload | resolve : Resolved file name "…/texmf-dist/fonts/opentype/public/libertinus-fonts/LibertinusSerif-Regular.otf"
$ lualatex -interaction=nonstopmode notice-name.tex | grep -E "rror|Output"
Output written on notice-name.pdf (1 page, 7057 bytes).
$ pdffonts notice-name.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
KPJYNR+LibertinusSerif-Regular       CID Type 0C       Identity-H       yes yes yes      4  0
$ xelatex -interaction=nonstopmode notice-name-xe.tex > /dev/null
[exit status 1]
$ grep -A2 "^! Package fontspec Error" notice-name-xe.log | head -3
! Package fontspec Error: 
(fontspec)                The font "Libertinus Serif" cannot be found; this
(fontspec)                may be but usually is not a fontspec bug. Either
$ grep -c "in font nullfont" notice-name-xe.log
97
$ pdftotext notice-name-xe.pdf - | wc -w
       0
$ xelatex -interaction=nonstopmode notice-file.tex | grep -E "rror|Output"
Output written on notice-file.pdf (1 page).
$ pdffonts notice-file.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
ZFUMQQ+LibertinusSerif-Regular-Identity-H CID Type 0C       Identity-H       yes yes yes      4  0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lookup_session.py

LuaLaTeX’s font loader, luaotfload, keeps its own database of fonts, and luaotfload-tool --find showed that it includes the TeX tree: the name led to LibertinusSerif-Regular.otf. fontspec’s manual explains the difference: in LuaTeX, fonts of the TeX tree can be loaded by name, while XeTeX can do so on Windows and Linux but not on macOS, where this lesson ran; by file name, both always find them. So XeLaTeX failed with The font "Libertinus Serif" cannot be found, and in nonstopmode it went on to report 97 characters missing from nullfont, a font with no glyphs: the run ended with status 1 and a PDF with no text at all. By file name, XeLaTeX built the notice. Follow both lookups:

Interactive · step by step Where a font name leads in LuaLaTeX and in XeLaTeX Step through \setmainfont{Libertinus Serif} in both engines, then the file name that works in both.
Where a font name leads in LuaLaTeX and in XeLaTeX, step 6 of 6. Showing A font by its name, LuaLaTeX, XeLaTeX, Found in the TeX tree, Not found, By its file name. In focus: By its file name.the fixA font by its nameLibertinus SerifLuaLaTeXasks luaotfloadXeLaTeXasks the system's fontsFound in the TeX treeLibertinusSerif-Regular.otfNot foundan error, then nullfontBy its file nameLibertinusSerif-Regular.otf

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A name. notice-name.tex asks for Libertinus Serif by its family name, the way you would pick it from a menu. The font's files are in the TeX tree that TinyTeX installed.
  2. LuaLaTeX looks it up. LuaLaTeX's font loader, luaotfload, keeps a database of fonts that includes the TeX tree. luaotfload-tool found the name and resolved it to LibertinusSerif-Regular.otf.
  3. The PDF has it. pdffonts listed LibertinusSerif-Regular, embedded and with a Unicode map, and the build printed no error.
  4. XeLaTeX looks elsewhere. fontspec's manual says XeTeX can load fonts of the TeX tree by name on Windows and Linux but not on macOS, where this ran. XeLaTeX looked for the name and did not find it.
  5. An error, then nothing. fontspec reported The font "Libertinus Serif" cannot be found. In nonstopmode the run went on, and the log then reported 97 characters missing from nullfont, a font with no glyphs at all.
  6. The file name works. notice-file.tex names the files, LibertinusSerif with the extension .otf and Regular, Bold and Italic faces. XeLaTeX found them in the TeX tree by file name, as arXiv's TeX Live page tells XeLaTeX users to do.

arXiv’s TeX Live page gives the same rule for XeLaTeX on its servers, where practically no fonts are registered with fontconfig: fonts of the TeX Live distribution need the file name lookup. A file name works in both engines, so use it for any font a document must find on other machines.

A font's licence travels with your template

Fonts come with their own licences: CTAN lists the GUST Font License for Latin Modern and the SIL Open Font License for Libertinus Serif. The OFL lets a font be bundled and redistributed with your files as long as it is not sold by itself, and it does not apply to documents made with the font. Read the licence before you put a font into a template you share, and keep the licence file beside it, as this lesson’s fonts folder does for Noto Sans Tamil.

Checkpoint

Check: Loading OpenType fonts with fontspec

  1. Question 1 of 2 notice-name.tex builds with LuaLaTeX but fails in XeLaTeX with: The font "Libertinus Serif" cannot be found. What do you change for XeLaTeX?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Ask for the font by its file names, as notice-file.tex does

    arXiv's TeX Live page gives the same advice for its servers: with XeLaTeX, fonts of the TeX tree need the file name lookup.

  2. Question 2 of 2 After the fontspec error, XeLaTeX's log had 97 lines like: Missing character: There is no S ("53) in font nullfont! What do they mean?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Every character of the text fell into nullfont, a font with no glyphs, because no font was loaded

    An error in nonstopmode lets the run go on, which is how 97 missing characters can follow one missing font. Read the first error, not the last lines.

A character the font lacks is only a warning in the log

Back to the notice in Latin Modern, the font fontspec loads when you choose none. Latin Modern has no Cyrillic letters. LaTeX News 33 says what the Unicode engines then do: they typeset nothing and write a Missing character warning to the log. This session builds the notice, then a copy with \tracinglostchars=3, then the copy in Libertinus Serif, and lists which fonts of the TeX tree have И at all:

The lost name, made fatal, and fonts that have its letters Python · missing_session.py
from session import files, run, shell

# The speaker's name in Latin Modern: how many Missing character lines LuaLaTeX and XeLaTeX print, the first of them,
# what the PDF's text then says, and what \tracinglostchars=3 changes, with -halt-on-error and without it. Then the
# notice in Libertinus Serif, the fee notice of the India note, and the fonts of the TeX tree that have И (U+0418) and
# ₹ (U+20B9), by fc-query's list of the characters each font covers.
files("notice/notice.tex", "notice/notice-strict.tex", "notice/notice-file.tex", "notice/rupee.tex")
run(r"""
lualatex -interaction=nonstopmode notice.tex | grep -c "Missing character"
grep -m1 "Missing character" notice.log
pdftotext notice.pdf - | head -1
xelatex -interaction=nonstopmode notice.tex | grep -E "Output"
grep -c "Missing character" notice.log
pdftotext notice.pdf - | head -1
!lualatex -interaction=nonstopmode -halt-on-error notice-strict.tex > /dev/null
grep -m1 -E "^Missing character" notice-strict.log
grep "Fatal error" notice-strict.log
!xelatex -interaction=nonstopmode -halt-on-error notice-strict.tex > /dev/null
grep -A1 "^! Missing character" notice-strict.log
!ls notice-strict.pdf
!lualatex -interaction=nonstopmode notice-strict.tex > /dev/null
pdftotext notice-strict.pdf - | head -1
lualatex -interaction=nonstopmode notice-file.tex | grep -E "Missing|Output"
pdftotext notice-file.pdf - | head -1
lualatex -interaction=nonstopmode rupee.tex | grep -c "Missing character"
pdftotext rupee.pdf - | head -1
""")


def covers(font, code):
    """Whether fc-query lists the character among the ones the font file has."""
    _, ranges = shell(f'fc-query -f "%{{charset}}" "$(kpsewhich {font})"')
    for part in ranges.split():
        low, _, high = part.partition("-")
        if int(low, 16) <= code <= int(high or low, 16):
            return True
    return False


print("# fonts of the TeX tree that have И (U+0418) and ₹ (U+20B9)")
for font in ["lmroman10-regular.otf", "texgyretermes-regular.otf", "XCharter-Roman.otf", "LibertinusSerif-Regular.otf",
             "NewCM10-Book.otf", "STIXTwoText-Regular.otf", "FiraSans-Regular.otf"]:
    marks = ["yes" if covers(font, code) else "no " for code in (0x418, 0x20B9)]
    print(f"{font:30} И {marks[0]}  ₹ {marks[1]}")

Output

$ lualatex -interaction=nonstopmode notice.tex | grep -c "Missing character"
36
$ grep -m1 "Missing character" notice.log
Missing character: There is no И (U+0418) in font [lmroman10-regular]:+tlig;!
$ pdftotext notice.pdf - | head -1
Speaker: ����� �������, from the field station. The office is efficient: official
$ xelatex -interaction=nonstopmode notice.tex | grep -E "Output"
Output written on notice.pdf (1 page).
$ grep -c "Missing character" notice.log
12
$ pdftotext notice.pdf - | head -1
Speaker: ����� �������, from the field station. The office is efficient: official
$ lualatex -interaction=nonstopmode -halt-on-error notice-strict.tex > /dev/null
[exit status 1]
$ grep -m1 -E "^Missing character" notice-strict.log
Missing character: There is no И (U+0418) in font [lmroman10-regular]:+tlig;!
$ grep "Fatal error" notice-strict.log
!  ==> Fatal error occurred, no output PDF file produced!
$ xelatex -interaction=nonstopmode -halt-on-error notice-strict.tex > /dev/null
[exit status 1]
$ grep -A1 "^! Missing character" notice-strict.log
! Missing character: There is no И (U+0418) in font [lmroman10-regular]:mapping=tex-text;.
l.6 Speaker: Ирина 
$ ls notice-strict.pdf
ls: notice-strict.pdf: No such file or directory
[exit status 1]
$ lualatex -interaction=nonstopmode notice-strict.tex > /dev/null
[exit status 1]
$ pdftotext notice-strict.pdf - | head -1
Speaker: ����� �������, from the field station. The office is efficient: official
$ lualatex -interaction=nonstopmode notice-file.tex | grep -E "Missing|Output"
Output written on notice-file.pdf (1 page, 7057 bytes).
$ pdftotext notice-file.pdf - | head -1
Speaker: Ирина Петрова, from the field station. The office is efficient: official
$ lualatex -interaction=nonstopmode rupee.tex | grep -c "Missing character"
3
$ pdftotext rupee.pdf - | head -1
Hostel fee for the term: �4,500.
# fonts of the TeX tree that have И (U+0418) and ₹ (U+20B9)
lmroman10-regular.otf          И no   ₹ no 
texgyretermes-regular.otf      И no   ₹ no 
XCharter-Roman.otf             И yes  ₹ no 
LibertinusSerif-Regular.otf    И yes  ₹ yes
NewCM10-Book.otf               И yes  ₹ yes
STIXTwoText-Regular.otf        И yes  ₹ yes
FiraSans-Regular.otf           И yes  ₹ yes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_session.py

Both engines ended with status 0 and wrote the PDF. LuaLaTeX wrote 36 Missing character lines for the 12 letters, the first There is no И (U+0418) in font [lmroman10-regular], XeLaTeX wrote 12, and nothing at the end of either run repeats them. The text of both PDFs read Speaker: ����� �������: a replacement character stands for each lost letter, so the loss also reaches search and copy. With \tracinglostchars=3 the first missing letter became an error. Run with -halt-on-error, both engines stopped there with status 1 and wrote no PDF; run without it, LuaLaTeX also ended with status 1 but still wrote the PDF, gap included, so a script has to stop on the status.

The rupee sign

Latin Modern has no ₹ either. The session’s rupee.tex, a fee of ₹4,500 in fontspec’s default font, gave three Missing character lines and the text �4,500. Of the fonts in the table, Libertinus Serif, New Computer Modern, STIX Two Text and Fira Sans have ₹; XCharter does not.

Step through what happened to the first letter of the name:

Interactive · step by step What happens to И in Latin Modern Step from the letter in the source to what the PDF and the log end up with, with and without the strict setting.
What happens to И in Latin Modern, step 7 of 7. Showing The letter И, The current font, Has it a glyph for И?, Glyph on the page, A line in the log, Nothing on the page, tracinglostchars=3. In focus: tracinglostchars=3.yesnoThe letter ИU+0418 in the sourceThe current fontLatin Modern RomanHas it a glyph for И?Glyph on the pageas in notice-file.pdfA line in the logMissing character: …Nothing on the pagestatus 0, the text has �tracinglostchars=3an error, status 1

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The letter. notice.tex contains the name Ирина Петрова, which starts with И, the code point U+0418. LuaLaTeX and XeLaTeX read it as one character, as they read every Unicode character.
  2. The font in use. fontspec's default font is Latin Modern Roman, so the letter goes to lmroman10-regular, the font the log names.
  3. The question. The engine asks the font for a glyph of U+0418. fc-query's list of the characters in lmroman10-regular.otf has no Cyrillic letters at all.
  4. When the font has it. In Libertinus Serif, which has Cyrillic letters, the glyph is placed, as notice-file.pdf showed with the whole name in its text.
  5. When it does not. LuaLaTeX wrote 36 Missing character lines for the 12 letters and XeLaTeX 12, among the other lines of the run and in the log. Nothing at the end of the run repeats them.
  6. A gap and a status of 0. Both runs ended with Output written and status 0. The page has a gap where the name was, and pdftotext read a replacement character for each lost letter.
  7. The strict setting. With \tracinglostchars=3 in the preamble the first missing letter became an error, and both engines ended with status 1. Run with -halt-on-error, they stopped there and wrote no PDF at all.

The fix is a font that has the characters. fc-query, which lists every character a font file covers, found И in XCharter, Libertinus Serif, New Computer Modern, STIX Two Text and Fira Sans, and not in Latin Modern or TeX Gyre Termes. The notice in Libertinus Serif built with no Missing character line and read the name back in full.

Make every unattended build strict

Put \tracinglostchars=3 into the preamble of every document that a script builds, such as badges or reports made each night, and run the engine with -halt-on-error. A character the font lacks then stops the build before it writes a PDF, instead of a gap reaching readers.

Checkpoint

Check: Characters a font lacks

  1. Question 1 of 2 LuaLaTeX built notice.tex with exit status 0. Where was the only sign that the speaker's name was lost?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In Missing character lines, part-way through the run's output and in the log

    LaTeX News 33 says Unicode engines typeset nothing for a character the font lacks and write a Missing character warning to the log, which is exactly what both engines did.

  2. Question 2 of 2 Which line in the preamble made the lost letters stop the build in both engines?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: \tracinglostchars=3

    Put \tracinglostchars=3 in every LuaLaTeX and XeLaTeX preamble you build unattended, and run with -halt-on-error: a lost character then fails the build and leaves no PDF to ship.

Typst falls back to any font it knows, then draws a box

Typst’s text reference gives its default font as Libertinus Serif, one of four fonts built into the command-line tool, and lists where it finds others: folders given with --font-path first, then the system’s fonts, then the built-in ones. typst fonts prints what it found. Typst tries the families of the font list in order for each character and, with fallback on by default, then any font it knows. To see what happens when none has the character, the session uses a Tamil word, with system fonts switched off:

The notice, and a Tamil word with and without a named font

notice/notice.typ

// The notice in Typst, in its default font, Libertinus Serif.
Speaker: Ирина Петрова, from the field station. The office is efficient:
official badges are issued within a day.

tamil/thanks.typ

// A Tamil word with no font for Tamil script asked for.
Thank you: நன்றி.

tamil/thanks-named.typ

// The same word with the font list naming a Tamil font after the Latin one.
#set text(font: ("Libertinus Serif", "Noto Sans Tamil"))
Thank you: நன்றி.
Typst's fonts, a missing script and a fonts folder Python · typst_fonts_session.py
from session import files, run

# Typst's fonts with system fonts ignored: the notice in the default font, a Tamil word with no Tamil font, the same
# word with a font list that names one, and both again with a folder that holds Noto Sans Tamil, which the second
# file names and the first finds by fallback.
files("notice/notice.typ", "tamil/thanks.typ", "tamil/thanks-named.typ",
      "fonts/NotoSansTamil-Regular.ttf=fonts/NotoSansTamil-Regular.ttf")
run(r"""
typst fonts
typst compile notice.typ
pdffonts notice.pdf
pdftotext notice.pdf - | head -1
typst compile thanks.typ
pdftotext thanks.pdf - | head -1
mutool draw -F txt thanks.pdf 2>/dev/null | head -1
typst compile thanks-named.typ
typst fonts --font-path fonts
typst compile --font-path fonts thanks-named.typ
pdffonts thanks-named.pdf
pdftotext thanks-named.pdf - | head -1
typst compile --font-path fonts thanks.typ thanks-fallback.pdf
pdftotext thanks-fallback.pdf - | head -1
""")

Output

$ typst fonts
DejaVu Sans Mono
Libertinus Serif
New Computer Modern
New Computer Modern Math
$ typst compile notice.typ
$ pdffonts notice.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
WNBPYW+LibertinusSerif-Regular-Identity-H CID Type 0C       Identity-H       yes yes yes      8  0
$ pdftotext notice.pdf - | head -1
Speaker: Ирина Петрова, from the field station. The office is efficient: official badges are issued
$ typst compile thanks.typ
$ pdftotext thanks.pdf - | head -1
Thank you: ன்றி.
$ mutool draw -F txt thanks.pdf 2>/dev/null | head -1
Thank you: �ன்றி.
$ typst compile thanks-named.typ
warning: unknown font family: noto sans tamil
  ┌─ thanks-named.typ:2:16
  │
2 │ #set text(font: ("Libertinus Serif", "Noto Sans Tamil"))
  │                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

$ typst fonts --font-path fonts
DejaVu Sans Mono
Libertinus Serif
New Computer Modern
New Computer Modern Math
Noto Sans Tamil
$ typst compile --font-path fonts thanks-named.typ
$ pdffonts thanks-named.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
QBLHXJ+LibertinusSerif-Regular-Identity-H CID Type 0C       Identity-H       yes yes yes      8  0
BTLLDN+NotoSansTamil-Regular         CID TrueType      Identity-H       yes yes yes     11  0
$ pdftotext thanks-named.pdf - | head -1
Thank you: நன்றி.
$ typst compile --font-path fonts thanks.typ thanks-fallback.pdf
$ pdftotext thanks-fallback.pdf - | head -1
Thank you: நன்றி.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 typst_fonts_session.py

The notice needed nothing: Libertinus Serif has Cyrillic letters, and the text read the name back. With only the four built-in fonts, no font had Tamil letters, so Typst drew a box, a tofu: The empty box drawn where no font has a glyph for a character, named after its shape. Typst draws one when no font it knows has the character, and prints no warning., for each, printed nothing, and the text read back as ன்றி from pdftotext, one letter short. thanks-named.typ names Noto Sans Tamil in its font list, and that did draw a warning, unknown font family: noto sans tamil. With --font-path fonts, typst fonts listed Noto Sans Tamil, both files drew the word, and the text read நன்றி. Step through one letter:

Interactive · stage by stage How Typst finds a glyph for ந Step through the families Typst tries for one Tamil letter, with and without a folder of fonts.
How Typst finds a glyph for ந, step 6 of 6. Showing The letter ந, The first family, The next family named, Any font Typst knows, The glyph is drawn, A box, and no warning. In focus: A box, and no warning.named and foundThe letter நTamilThe first familyLibertinus Serif: no நThe next family namedNoto Sans Tamil, if foundAny font Typst knowsfallback, on by defaultThe glyph is drawnfrom Noto Sans TamilA box, and no warningwhen no font has it

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One letter. thanks.typ holds the Tamil word நன்றி. Typst finds a glyph for each character in the families of the font list, in order.
  2. The first family. The default list is Libertinus Serif alone, and Libertinus Serif has no Tamil letters.
  3. A family by name. thanks-named.typ names Noto Sans Tamil next. Without a font folder Typst could not find that family and warned, unknown font family, which is the warning a missing font gets.
  4. Found by name. With --font-path fonts, typst fonts listed Noto Sans Tamil, and the named family drew the word. pdffonts showed NotoSansTamil-Regular embedded, and pdftotext read நன்றி back.
  5. The last resort. For a letter that no named family has, Typst's fallback, on by default, tries any font it knows. thanks.typ, built with the fonts folder, found Noto Sans Tamil this way.
  6. Nothing has it. With only Typst's own four fonts, nothing has ந. Typst drew a box for each letter and printed no warning at all, and the text read back as ன்றி from poppler.

Key idea

Typst warns about a font family it cannot find, not about a glyph that no font has: name the fonts your text depends on, and keep them in a folder that every build passes with --font-path.

Checkpoint

Check: Typst's fonts and its fallback

  1. Question 1 of 2 What did typst compile print for thanks.typ, built with only Typst's own four fonts?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing at all, though no font had the Tamil letters

    Typst's text reference describes the boxes as tofus and says warnings for them are planned: today it is up to you to look.

  2. Question 2 of 2 How did the lesson make Typst warn when the Tamil font was missing?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: By naming it in the font list, #set text(font: ("Libertinus Serif", "Noto Sans Tamil"))

    A family you name is checked; a glyph no font has is not. Name every font your document depends on.

Copying, search and screen readers get the PDF’s text, not its look

A PDF draws glyphs; the characters behind them come from a map that each font carries. The pdffonts manual describes its uni column as yes when the PDF has an explicit ToUnicode map: The table in a PDF that says which characters each glyph of a font stands for, so that its text can be copied, searched and read aloud. pdffonts prints yes in its uni column when a font has one. for the font, and every PDF in this lesson said yes. ligature: One glyph that stands for two or more letters, such as ffi or fl. The PDF must map it back to those letters, or copied and searched text comes out wrong. test that map: “efficient” and “official” are set with one ffi glyph, which must read back as three letters. LaTeX News 33 says pdfTeX’s PDFs now get that information automatically, so the most common ligatures copy as intended. This session builds the notice with all four engines and reads it back with poppler’s pdftotext and MuPDF’s mutool:

Two extractors read four engines' ligatures Python · extract_session.py
from session import files, read, run, shell, write

# The words "efficient" and "official" from the notice, set with their ffi ligatures by four engines, and read back
# by two text extractors: poppler's pdftotext and MuPDF's mutool. The LaTeX copies spell the speaker's name in Latin
# letters, which Latin Modern has. The table shows the start of the second sentence as each extractor reads it.
files("notice/notice-pdf.tex", "notice/notice.tex", "notice/notice.typ")
write("notice-latin.tex", read("notice-pdf.tex").replace("Ирина Петрова", "Irina Petrova"))
write("notice-lm.tex", read("notice.tex").replace("Ирина Петрова", "Irina Petrova"))
builds = {
    "pdfLaTeX": "pdflatex -interaction=nonstopmode notice-latin.tex > /dev/null && mv notice-latin.pdf pdflatex.pdf",
    "LuaLaTeX": "lualatex -interaction=nonstopmode notice-lm.tex > /dev/null && mv notice-lm.pdf lualatex.pdf",
    "XeLaTeX": "xelatex -interaction=nonstopmode notice-lm.tex > /dev/null && mv notice-lm.pdf xelatex.pdf",
    "Typst": "typst compile notice.typ typst.pdf",
}
for engine, command in builds.items():
    status, output = shell(command)
    if status:
        raise SystemExit(f"{engine} failed: {output}")
print("# the start of the second sentence, as each extractor reads it from each PDF")
for engine in builds:
    pdf = engine.lower() + ".pdf"
    for tool, command in [("poppler", f"pdftotext {pdf} -"), ("MuPDF", f"mutool draw -F txt {pdf} 2>/dev/null")]:
        _, text = shell(command)
        line = " ".join(text.split())
        sentence = line[line.index("The o"):]
        print(f"{engine:9} {tool:8} {sentence[:48]}")
run(r"""
pdffonts xelatex.pdf
""")

Output

# the start of the second sentence, as each extractor reads it from each PDF
pdfLaTeX  poppler  The office is efficient: official badges are iss
pdfLaTeX  MuPDF    The office is efficient: official badges are iss
LuaLaTeX  poppler  The office is efficient: official badges are iss
LuaLaTeX  MuPDF    The office is efficient: official badges are iss
XeLaTeX   poppler  The office is efficient: official badges are issued
XeLaTeX   MuPDF    The office is efficient: official badges are iss
Typst     poppler  The office is efficient: official badges are iss
Typst     MuPDF    The office is efficient: official badges are iss
$ pdffonts xelatex.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
SLWVZF+LMRoman10-Regular-Identity-H  CID Type 0C       Identity-H       yes yes yes      4  0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 extract_session.py

Seven of the eight pairs read the sentence exactly. poppler read XeLaTeX’s PDF as “office”, “efficient” and “official”, with the single character ff, U+FB00, standing for two letters, while MuPDF read the same file correctly. A portal or search that does not normalise that character would not find the word “efficient” in the PDF.

Checking the page instead of the text

Wrong: reading the PDF on screen and calling it done. A missing glyph, a tofu and a misread ligature can all look acceptable or go unnoticed. Right: extract the text with two tools and compare it with the source, and search it for U+FFFD, the replacement character.

PDF to Text Extractor Extract the text of a PDF in your browser and compare it with what your source says. Unicode Character Inspector Paste text copied from a PDF to see each character's code point, such as U+FB00 or U+FFFD.

Checkpoint

Check: The text that copying and search get

  1. Question 1 of 2 In the extraction table, which engine and extractor read "efficient" as "efficient", with the single character ff?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: XeLaTeX's PDF read by poppler's pdftotext

    The same PDF can read differently in two extractors, so check the text with the tools your readers' portals, search engines and screen readers are likely to use, more than one of them.

  2. Question 2 of 2 What does the uni column of pdffonts tell you?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Whether the font carries a ToUnicode map, which says which characters its glyphs stand for

    A no in uni means copied text may come out as nonsense; a no in emb means the PDF looks different on a computer without that font.

Practise: a font check for every build

Two checks catch most of what this lesson showed before a reader does: a scan of the log for lost characters, and a read of pdffonts for fonts that are not embedded or have no Unicode map. The sample tests use the log lines and tables this page recorded, and the first hint of each step is open:

Code lab · 2 steps

A font check for every build

List the characters a log says were lost, then find the fonts of a PDF that are not embedded or have no Unicode map.

  1. List the characters a LaTeX log says were lost To do
  2. Find the fonts that are not embedded or cannot be copied To do

Exercise · Medium · Python

List the characters a LaTeX log says were lost

Write missing_characters(log) in glyphs.py. It reads the text of a LuaLaTeX or XeLaTeX log and returns every character that a font had no glyph for, so that a build script can fail when the list is not empty.

The log reports each lost character on a line like the ones in the lesson:

  • Missing character: There is no И (U+0418) in font [lmroman10-regular]:+tlig;! (LuaLaTeX)
  • Missing character: There is no И (U+0418) in font [lmroman10-regular]:mapping=tex-text;! (XeLaTeX)
  • ! Missing character: There is no И (U+0418) in font [lmroman10-regular]:mapping=tex-text;. (an error, with \tracinglostchars=3)
  • Missing character: There is no S ("53) in font nullfont! (an old-style code, in hexadecimal after ")

Return a list of (code, character, font) tuples, each once, in the order the log first reports them: the code as U+ and at least four upper-case hexadecimal digits (U+0418, U+0053), and the font's name without its brackets and features (lmroman10-regular, nullfont). The same character in two fonts is two entries.

Starter code · glyphs.py

def missing_characters(log):
    """Return (code, character, font) for every character the log says a font had no glyph for, once each."""
    # Replace this comment: find each "There is no … in font …" report and collect what it names.
    return []
The sample tests · test_glyphs.py
from glyphs import missing_characters

LUALATEX = """Missing character: There is no И (U+0418) in font [lmroman10-regular]:+tlig;!
Missing character: There is no И (U+0418) in font [lmroman10-regular]:+tlig;!
Missing character: There is no р (U+0440) in font [lmroman10-regular]:+tlig;!
Missing character: There is no И (U+0418) in font [lmroman10-regular]:+tlig;!
Output written on notice.pdf (1 page, 5621 bytes).
"""


def test_lualatex_repeats_a_character():
    """lists each lost letter once, in the order the log first reports it"""
    assert missing_characters(LUALATEX) == [("U+0418", "И", "lmroman10-regular"), ("U+0440", "р", "lmroman10-regular")]


def test_xelatex_and_the_strict_error():
    """reads XeLaTeX's warning and the error that \\tracinglostchars=3 turns it into"""
    log = ("Missing character: There is no И (U+0418) in font [lmroman10-regular]:mapping=tex-text;!\n"
           "! Missing character: There is no ✓ (U+2713) in font [lmroman10-regular]:mapping=tex-text;.\n")
    assert missing_characters(log) == [("U+0418", "И", "lmroman10-regular"), ("U+2713", "✓", "lmroman10-regular")]


def test_old_style_codes_in_nullfont():
    """turns "53 into U+0053 and keeps the order of the log"""
    log = ('Missing character: There is no S ("53) in font nullfont!\n'
           'Missing character: There is no p ("70) in font nullfont!\n'
           'Missing character: There is no S ("53) in font nullfont!\n')
    assert missing_characters(log) == [("U+0053", "S", "nullfont"), ("U+0070", "p", "nullfont")]


def test_the_same_character_in_two_fonts():
    """keeps one entry for each font that lacks the character"""
    log = ("Missing character: There is no И (U+0418) in font [lmroman10-regular]:+tlig;!\n"
           "Missing character: There is no И (U+0418) in font [lmroman10-bold]:+tlig;!\n")
    assert missing_characters(log) == [("U+0418", "И", "lmroman10-regular"), ("U+0418", "И", "lmroman10-bold")]


def test_a_clean_log():
    """returns an empty list when nothing was lost"""
    assert missing_characters("Output written on notice-file.pdf (1 page, 7057 bytes).\n") == []
A hint

One regular expression can read every form: There is no (.) \((?:U\+([0-9A-F]+)|"([0-9A-F]+))\) in font (\[[^\]]*\]|[^\s!.:;]+). With re.findall, each match gives the character, the new-style code or the old-style code (one of the two is empty) and the font; int(code, 16) and f"U+{number:04X}" write the code, and font.strip("[]") drops the brackets.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Medium · Python

Find the fonts that are not embedded or cannot be copied

Write font_problems(table) in fonts.py. It reads what pdffonts printed for a PDF, two header lines and then one line per font, and returns the problems a reader would meet:

  • "<name>: not embedded" for a font whose emb column says no: the PDF then looks different on a computer without that font;
  • "<name>: no Unicode map" for a font whose uni column says no: copied text may then come out wrong.

Return them in the order of the table, the embedding problem first when a font has both. Watch the lesson's output: a long name, such as QBLHXJ+LibertinusSerif-Regular-Identity-H, pushes the other columns to the right, so the columns do not always start where the header says. Skip blank lines.

Starter code · fonts.py

def font_problems(table):
    """Return "<name>: not embedded" and "<name>: no Unicode map" for the fonts of a pdffonts table that need them."""
    # Replace this comment: read each font line after the two header lines, find its emb and uni values.
    return []
The sample tests · test_fonts.py
from fonts import font_problems

HEADER = ("name                                 type              encoding         emb sub uni object ID\n"
          "------------------------------------ ----------------- ---------------- --- --- --- ---------\n")


def test_the_lessons_notice():
    """finds nothing wrong with LuaLaTeX's notice.pdf"""
    table = HEADER + "VFSOVW+LMRoman10-Regular             CID Type 0C       Identity-H       yes yes yes      4  0\n"
    assert font_problems(table) == []


def test_a_long_name_shifts_the_columns():
    """reads both fonts of thanks-named.pdf, whose first name is longer than its column"""
    table = HEADER + (
        "QBLHXJ+LibertinusSerif-Regular-Identity-H CID Type 0C       Identity-H       yes yes yes      8  0\n"
        "BTLLDN+NotoSansTamil-Regular         CID TrueType      Identity-H       yes yes yes     11  0\n")
    assert font_problems(table) == []


def test_a_font_that_is_not_embedded():
    """reports a font whose emb column says no"""
    table = HEADER + "Arial-BoldMT                         TrueType          WinAnsi          no  no  yes      9  0\n"
    assert font_problems(table) == ["Arial-BoldMT: not embedded"]


def test_a_font_without_a_unicode_map():
    """reports a font whose uni column says no"""
    table = HEADER + "QWERTY+NotoSansTamil-Regular         CID TrueType      Identity-H       yes yes no      11  0\n"
    assert font_problems(table) == ["QWERTY+NotoSansTamil-Regular: no Unicode map"]


def test_both_problems_and_a_blank_line():
    """lists the embedding problem first and skips a blank line at the end"""
    table = HEADER + "Helvetica                            Type 1            Custom           no  no  no       5  0\n\n"
    assert font_problems(table) == ["Helvetica: not embedded", "Helvetica: no Unicode map"]
A hint

Split each font line into words with line.split(). The name is the first word, and the last five words are always emb, sub, uni, the object number and the generation number, whatever the type and encoding columns hold, so words[-5] is emb and words[-3] is uni.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Afterwards, the self-check below covers engines, font loading, lost characters, Typst’s fallback and extracted text, beginning with whichever of them you are least sure of:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 What did pdfLaTeX say about the name in notice-pdf.tex, with T1 and lmodern?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: LaTeX Error: Unicode character И (U+0418) not set up for use with LaTeX

    pdfLaTeX fails loudly on a character it cannot set, which is the opposite of the Unicode engines' quiet log line.

  2. Question 2 of 15 A paper must use an OpenType font, and its LaTeX source goes to arXiv. Which engine, and how do you load the font?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: XeLaTeX, loading the font by file name

    Check the venue's list of engines before you choose fonts, and prefer fonts that ship with TeX Live, as arXiv asks.

  3. Question 3 of 15 What did luaotfload-tool --find="Libertinus Serif" show?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: The font file LuaLaTeX would load for that name, in the TeX tree

    When a name works in LuaLaTeX and fails in XeLaTeX, the two engines are looking in different places.

  4. Question 4 of 15 In notice-file.tex, what do Extension = .otf and UprightFont = *-Regular do?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: They build the file names, with * standing for LibertinusSerif, so the upright face is LibertinusSerif-Regular.otf

    With file names, both Unicode engines find the same files, which makes the document build the same way in each.

  5. Question 5 of 15 A brand font that is not in TeX Live must build on every co-author's computer and on a server. What is the most reliable set-up?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: Keep the font files in the project, with their licence, and load them by file name or path

    Check the font's licence first: the OFL lets you bundle a font with your files as long as it is not sold by itself.

  6. Question 6 of 15 pdftotext read the speaker in LuaLaTeX's notice.pdf as ����� �������. What do the � tell you?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: The PDF's text holds a replacement character for each lost letter, so the loss shows up in search and copy too

    Extracting the text of the PDF is a quick test for lost characters: search the output for U+FFFD, the replacement character.

  7. Question 7 of 15 Which of these TeX Live fonts can set И, by fc-query's list of the characters each covers?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: XCharter, Libertinus Serif, New Computer Modern, STIX Two Text and Fira Sans

    Check a font's coverage before you choose it: fc-query -f "%{charset}" on the font file lists every character it has.

  8. Question 8 of 15 A build server shows only the last ten lines of each LuaLaTeX run. How do you make sure a lost character never reaches a printed badge?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: Put \tracinglostchars=3 in the preamble, so the run fails, and check the PDF's text for U+FFFD as well

    Make the problem fatal where you can, and test the result where you cannot.

  9. Question 9 of 15 A nightly script runs lualatex -interaction=nonstopmode on a badge file that has \tracinglostchars=3 but no -halt-on-error, ignores the exit status and prints whatever PDF it finds. A name in Cyrillic is lost. What reaches the printer?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: The badge with a gap where the name should be, because the run still wrote a PDF

    \tracinglostchars=3 turns a lost character into an error, which sets the exit status. Either stop the script on the status or run with -halt-on-error, so that no PDF with a gap is left to print.

  10. Question 10 of 15 Which font did Typst use for notice.typ, with no font set?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: Libertinus Serif, its default, which has Cyrillic letters

    Typst's text reference gives the default font as libertinus serif, one of the four fonts the CLI has built in.

  11. Question 11 of 15 A family exists in a --font-path folder, among the system fonts and among Typst's built-in fonts. Which one does Typst use?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: The one in the --font-path folder

    A fonts folder in the project, passed with --font-path, therefore wins on every machine.

  12. Question 12 of 15 Why does a project build Typst documents with --ignore-system-fonts and --font-path fonts?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Every machine then uses the same font files, so the PDFs do not depend on what each computer has installed

    The lesson's sessions run with system fonts ignored for the same reason: the outputs are the same on any machine.

  13. Question 13 of 15 Why did the ffi ligatures of pdfLaTeX's PDF copy correctly without any package?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: LaTeX adds glyphtounicode information to pdfTeX's PDFs automatically, as LaTeX News 33 announced

    Older advice to add \input{glyphtounicode} or the cmap package by hand dates from before this change.

  14. Question 14 of 15 A university portal rejects a thesis because its text check finds "efficient" and other odd words. What do you do first?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: Extract the text with two different tools, and if one misreads the ligatures, rebuild with an engine or font whose PDF reads correctly in both

    The text layer is part of the deliverable: test it with the kind of extractor the portal is likely to use.

  15. Question 15 of 15 poppler read thanks.pdf, built with no Tamil font, as "Thank you: ன்றி.". What does that show?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: The boxes broke the text layer too, so a letter of நன்றி is gone from copy and search

    A box on the page is not only ugly: the text layer under it is wrong, so check both the page and the text.

Interview questions

Warm-up (fresher to 2–5 years): when would you build a LaTeX document with XeLaTeX or LuaLaTeX instead of pdfLaTeX?

When it needs fonts or characters that pdfLaTeX cannot give it. pdfLaTeX uses 8-bit Type 1 fonts chosen through a font encoding and packages such as lmodern, and it stops on a character that its fonts are not set up for, as it did on the И of a Russian name. XeLaTeX and LuaLaTeX read Unicode and load any OpenType font through fontspec, which pdfLaTeX cannot load at all. Between the two: LuaLaTeX finds fonts of the TeX tree by name, while XeLaTeX needs their file names on macOS and on arXiv; and arXiv’s list of processors includes xelatex but not LuaLaTeX. Keep pdfLaTeX when a template or a venue asks for it and its fonts are enough. With either Unicode engine, add \tracinglostchars=3, because a character the font lacks is otherwise only a line in the log.

Two harder questions follow, about names that vanished from conference badges and about a font that a build server cannot find. Decide where you would start before you open each first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    Conference badges are built each night with LuaLaTeX from a list of speakers. The badges of speakers whose names are written in Cyrillic come out with a blank where the name should be, and every build exits with status 0. Find the cause, fix the template, and make the next such loss fail the build.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A report template builds on your laptop, but on the build server XeLaTeX stops with The font "Libertinus Serif" cannot be found, and the PDF has no text. Explain why it worked on the laptop, fix the template, and make the build independent of each machine's fonts, in LaTeX and in Typst.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • pdfLaTeX sets 8-bit Type 1 fonts through font encodings, while XeLaTeX and LuaLaTeX set OpenType fonts through fontspec, which stops pdfLaTeX with a fatal error.
  • LuaLaTeX finds a TeX tree font such as Libertinus Serif by name, while XeLaTeX on macOS and on arXiv finds it only by file name, so file names keep a document building on every machine.
  • A character the font lacks costs only a Missing character line and a replacement character in the text, until \tracinglostchars=3 makes it an error; choose a font that has the character.
  • Typst falls back to any font it knows and then draws a tofu without a word, but warns about a named family it cannot find, so name your fonts and pass their folder with --font-path.
  • Copying and search read the PDF’s text map, not the page: check it with pdffonts and two extractors, because poppler misread XeLaTeX’s ligatures where MuPDF did not.

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How do pdfLaTeX and the Unicode engines differ in their fonts?

    pdfLaTeX sets 8-bit fonts of at most 256 characters through a font encoding; XeLaTeX and LuaLaTeX read every character as its code point and set OpenType fonts loaded with fontspec.

  2. What does fontspec do in pdfLaTeX?

    It stops the build with a fatal error: fontspec requires XeTeX or LuaTeX.

  3. Why give fontspec a font's file names rather than its family name?

    LuaLaTeX finds fonts of the TeX tree by name, but XeLaTeX on macOS and on arXiv finds them only by file name; file names work in both engines everywhere.

  4. What do LuaLaTeX and XeLaTeX do with a character the font lacks?

    They set nothing, write a Missing character line to the log and end with status 0; the PDF's text holds a replacement character in its place.

  5. How do you make a missing character stop the build?

    Put \tracinglostchars=3 in the preamble and run with -halt-on-error, so the first lost character is an error and no PDF is written.

  6. What does Typst do when no font it knows has a character?

    It tries every font it knows, then draws a box, a tofu, without a warning; it warns only about a named font family it cannot find.

  7. How do you give Typst a font that is not installed on the computer?

    Keep the font file in a folder of the project and pass it with --font-path; typst fonts then lists the family.

  8. Why check a PDF's text with two extractors?

    Copying and search read the PDF's text map, not the page: poppler read XeLaTeX's ff ligature as the single character U+FB00, while MuPDF read the same words correctly.

Terms in this lesson

ligature
One glyph that stands for two or more letters, such as ffi or fl. The PDF must map it back to those letters, or copied and searched text comes out wrong.
OpenType font
A font file, ending in .otf or .ttf, that holds glyphs for Unicode characters together with rules for ligatures and other features. XeLaTeX and LuaLaTeX load such fonts through fontspec; Typst uses them directly.
tofu
The empty box drawn where no font has a glyph for a character, named after its shape. Typst draws one when no font it knows has the character, and prints no warning.
ToUnicode map
The table in a PDF that says which characters each glyph of a font stands for, so that its text can be copied, searched and read aloud. pdffonts prints yes in its uni column when a font has one.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.