Font recipes

Measurement vs rendering

Using fonts with ‘munch’ involves two independent steps:

  1. Measurement, ‘munch’ calls gdtools::strings_sizes() to compute text metrics (width, ascent, descent). Under the hood, ‘systemfonts’ locates the font file, then the metrics are read from this file with FreeType. Any font registered via systemfonts::register_font(), gdtools::register_gfont(), gdtools::font_set(), gdtools::font_set_liberation() or gdtools::font_set_auto() is found at measurement time.

  2. Rendering, the graphic device draws the text. Each device has its own way of resolving font names. This is where mismatches happen: a font that measurement found may be invisible to the device, or vice versa.

When both steps find the same font, the output is correct. When they disagree, text is positioned using one font’s metrics but drawn with another, leading to overlapping, clipping or misaligned labels.

Even with the same font, measurement does not apply kerning while devices do: see the section “Alignment accuracy” of vignette("using-ggplot2").

Which devices agree with measurement?

Device family Font lookup Agrees with measurement?
ragg, svglite, ggiraph ‘systemfonts’ Always, same lookup on both sides
cairo_pdf(), png(type = "cairo") fontconfig Only for system-installed fonts
pdf() Own engine No guarantee, needs ‘extrafont’
png(), jpeg() (default type) Depends on the system Only for system-installed fonts

Cairo devices

With Cairo devices (cairo_pdf(), png(type = "cairo")), font lookup differs between the two steps: measurement goes through ‘systemfonts’, rendering goes through fontconfig. Fontconfig only scans system-installed fonts.

This means:

Use font_family_exists(system_only = TRUE) to check whether a font is installed at the system level:

library(gdtools)

# Found by measurement (systemfonts + registry)?
font_family_exists("sans")
#> [1] TRUE

# Installed at the system level?
font_family_exists("sans", system_only = TRUE)
#> [1] TRUE

‘munch’ recognizes cairo_pdf(), but not png(type = "cairo"): its device name, "png", is the same as with other types. Set options(munch.skip_device_check = TRUE) to avoid the compatibility warning with this device.

Bitmap devices png() and jpeg()

With their default type, png() and jpeg() use the graphics engine of the system: Cairo on Linux (see above), Quartz on macOS, GDI on Windows. They find fonts installed at the system level, not fonts registered with ‘systemfonts’ only. Prefer ragg::agg_png() and ragg::agg_jpeg(), which are drop-in replacements.

The pdf() device and extrafont

The standard pdf() device uses its own font engine and ignores ‘systemfonts’ entirely.

‘munch’ uses ‘gdtools’ (which reads the font files found by ‘systemfonts’) for text measurement. For the output to be correct, the fonts used by ‘munch’ must also be known to pdf().

The ‘extrafont’ package bridges this gap: it imports TrueType fonts into R’s PDF device by generating font metric files that pdf() can use.

The key requirement: every font used in the plot must be available to both ‘systemfonts’ (for measurement) and pdf() (for rendering). If a font is registered with ‘systemfonts’ but not with pdf(), the text metrics will not match the rendering and the output will have positioning errors.

Example: exporting a munch plot to PDF

library(gdtools)
library(munch)
library(ggplot2)

df <- data.frame(
  x = 1:3,
  y = 1:3,
  label = c("**Bold**", "*Italic*", "`Code`")
)

p <- ggplot(df, aes(x, y, label = label)) +
  # the theme font does not apply to geoms: set it in the geom too
  geom_label_md(family = "Arial", code_font_family = "Courier New") +
  theme_minimal(base_family = "Arial")

# Make Arial and Courier New available to pdf()
library(extrafont)
# font_import()
# Run font_import() once to scan system fonts,
# then loadfonts() in each session.
loadfonts()

# Suppress the device warning since we ensured font concordance
options(munch.skip_device_check = TRUE)

pdf(file = "munch-example.pdf", width = 7, height = 7)
print(p)
dev.off()

The steps are:

  1. Install fonts at the system level, both “Arial” and “Courier New” must be installed as system fonts.
  2. Import fonts once with extrafont::font_import(), this scans system fonts and creates metric files for pdf(). Only needed once.
  3. Load fonts each session with extrafont::loadfonts(), this registers the imported fonts with pdf() for the current session.
  4. Disable the device check with options(munch.skip_device_check = TRUE), since you have ensured font concordance manually, the warning is no longer needed.

Note: cairo_pdf() is a simpler alternative. It finds system-installed fonts with fontconfig, so they work without ‘extrafont’. Use pdf() + ‘extrafont’ only when cairo_pdf() is not available.

Font sources at a glance

Font source Measurement ragg / svglite / ggiraph Cairo devices, png(), jpeg() pdf()
System font Yes Yes Yes Via extrafont
register_gfont() Yes Yes Depends on cache location No
font_set_liberation() Yes Yes No (unless OS-installed) No
font_set_auto() Yes Yes System fonts only No
font_set() Yes Yes System fonts only No

Diagnosing font problems

When text looks wrong (wrong font, misaligned labels), the cause is almost always a measurement/rendering disagreement. Here is a quick checklist:

  1. Is the font found by measurement?

    gdtools::font_family_exists("My Font")

    If FALSE: the font is not registered with ‘systemfonts’. Register it with register_gfont(), font_set(), or systemfonts::register_font().

  2. Is the font found by the device?

    • For Cairo devices, png() and jpeg():

      gdtools::font_family_exists("My Font", system_only = TRUE)

      If FALSE: the font is not system-installed. Either install it at the OS level or switch to a ‘systemfonts’ device (ragg, svglite).

    • For pdf(): check that extrafont::fonts() includes the family.

    • For ragg/svglite/ggiraph: if step 1 passed, this always works. With svglite and ggiraph, the browser that displays the SVG also needs the font (see above).

  3. Do both sides find the same font? Even when both find a font, it may not be the same file (e.g. a different weight or a substitution). Compare:

    # What systemfonts resolves
    systemfonts::match_fonts("My Font")
    
    # What's installed at the system level
    subset(systemfonts::system_fonts(), family == "My Font")