Skip to content

Inconsistent expression and font rendering between ggsave(cairo_pdf) and Cairo::CairoPDF + grid.draw() #6480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
louis-heraut opened this issue May 26, 2025 · 13 comments
Labels
bug an unexpected problem or unintended behavior

Comments

@louis-heraut
Copy link

louis-heraut commented May 26, 2025

I encountered a kerning issue when using ggsave() with cairo_pdf in R. While searching for a solution using base Cairo utilities, I ran into a problem with rendering R expression.

What I expected was a simple and reliable way to eliminate kerning issues while still being able to use Cairo's PDF rendering capabilities, especially for mathematical expressions in plots (by using latex2exp package).

Here is a minimal reproducible example illustrating the issue:

library(ggplot2)

plot <- ggplot() + theme_void() + 
    annotate("text",
             x = 0.5, y = 0.5, size = 1.5,
             label = expression('example formula: '*alpha^{2} + beta)) +
    scale_x_continuous(limits = c(0, 1), expand = c(0, 0)) +
    scale_y_continuous(limits = c(0, 1), expand = c(0, 0))

# Expression renders correctly, but with kerning issues
ggsave("ggsave_cairo.pdf", plot = plot, device = cairo_pdf)

# No kerning issues, but expression rendering fails
Cairo::CairoPDF(file = "base_cairo.pdf")
grid::grid.draw(plot)
dev.off()

Note: Rasterizing fonts using the showtext package is not a suitable solution in my view, as I need to keep text fully vectorized in the output PDF.

@teunbrand
Copy link
Collaborator

teunbrand commented May 26, 2025

For those of us without eyes trained to spot kerning problems, can you point out where this goes wrong?
The difference I notice between the plots is that the symbol font differs, but that is separate from kerning.

As an aside:

label = expression('example formula: '*alpha^{2} + beta)

This should not work. One shouldn't be able to put naked expressions into ggplot2 because they are not vectors. You can use label = "'example formula: '*alpha^{2} + beta" with parse = TRUE instead.

@louis-heraut
Copy link
Author

louis-heraut commented May 26, 2025

ggsave cairo (notice the uneven spaces between caracter) :
Image

base cairo (no kerning issue but no expression rendering) :
Image

I still have the issue with :

plot = ggplot() + theme_void() + 
    annotate("text",
             x = 0.5, y = 0.5, size = 1.5,
             label = "'example formula: '*alpha^{2} + beta",
             parse = TRUE) +
    scale_x_continuous(limits = c(0, 1),
                       expand = c(0, 0)) +
    scale_y_continuous(limits = c(0, 1),
                       expand = c(0, 0))

(in the end i use the latex2exp package for expression construction but that doesn't seems to change anything)

@teunbrand
Copy link
Collaborator

teunbrand commented May 26, 2025

Thanks for the example, yeah that is much clearer and objectively terrible. It is different than what I get on my local machine. What version of R and what platform are you using?

As I cannot locally reproduce, does this give you the same problem?

library(grid)
txt <- textGrob(expression('example formula: '*alpha^{2} + beta), 0.5, 0.5)

grDevices::cairo_pdf("grDevices_cairo.pdf")
grid.newpage()
grid.draw(txt)
dev.off()

Cairo::CairoPDF("Cairo_cairo.pdf")
grid.newpage()
grid.draw(txt)
dev.off()

@louis-heraut
Copy link
Author

I kind of remember that on a previous platform running Debian 10, things worked properly but I have changed my config recently and can't manage to make it works.

> sessionInfo()
R version 4.4.3 (2025-02-28)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.2 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0 
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0

locale:
 [1] LC_CTYPE=fr_FR.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=fr_FR.UTF-8        LC_COLLATE=fr_FR.UTF-8    
 [5] LC_MONETARY=fr_FR.UTF-8    LC_MESSAGES=fr_FR.UTF-8   
 [7] LC_PAPER=fr_FR.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=fr_FR.UTF-8 LC_IDENTIFICATION=C       

time zone: Europe/Paris
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
[1] compiler_4.4.3

Cairo_cairo
Image

grDevices_cairo
Image

So the grDevices::cairo_pdf("grDevices_cairo.pdf") seems to work with the txt <- textGrob(expression('example formula: '*alpha^{2} + beta), 0.5, 0.5)

But if I use the first way of plotting with ggplot() and annotate kerning issue reappears even with grDevices::cairo_pdf.
(I am not familiar with differences between gg, ggplot, text, grob, gDesc class in R but if it possible I would prefer to stick to the more common ggplot way of creating plots)

@teunbrand
Copy link
Collaborator

So the grDevices::cairo_pdf("grDevices_cairo.pdf") seems to work with the txt <- textGrob(expression('example formula: '*alpha^{2} + beta), 0.5, 0.5)

Thanks for checking this out! So it does seem that the problem isolates to a ggplot2 if the grDevices cairo renders plain grid normally, but bungles ggplot2 text. Unfortunately, this is hard for me to debug for me personally. Maybe @thomasp85 has been tortured enough by text rendering at this point that he has a glimpse on what might be going on.

@teunbrand teunbrand added the bug an unexpected problem or unintended behavior label May 28, 2025
@teunbrand
Copy link
Collaborator

Ok I got my hands on an R session running on a Linux machine and can now reproduce the issue myself.
I think the kerning goes wrong when the fontsize property is small (<5). For example rendering the following grob shows it:

txt <- textGrob(
  expression('example formula: '*alpha^{2} + beta), 
  x = 0.5, y = 0.5,
  gp = gpar(fontsize = 3)
)

It 'feels' like the device is trying to pixel-snap the letters and gets weird kerning that way, but it wouldn't make sense to pixel-snap in a vector graphic. In any case, ggplot2 wouldn't be able to fix this problem if it is an issue with the device, so I'm leaning towards closing it.

@thomasp85
Copy link
Member

I think the reason you are seeing this is because you are sizing your text extremely small (1.5pt). While glyphs can be scaled down indefinitely, their kerning value and dimensions are aware of the underlying pixel grid and will snap to specific fractional values. This is usually never an issue, except when you render text at the size where these fractional values are a meaningful proportion of the glyph size.

There is nothing ggplot2 can do about this.

@thomasp85
Copy link
Member

(was responding while Teun posted - we are in agreement on the larger parts)

@thomasp85
Copy link
Member

(the reason why you see better kerning in one over the other is probably that the "correct" looking one is calculating glyph positions at a higher resolution than the other (e.g. 72ppi vs 300ppi))

@louis-heraut
Copy link
Author

louis-heraut commented May 28, 2025

OK, thanks @thomasp85 and @teunbrand for your time. But what should I do, since I'm still having problems with these plots ?

library(ggplot2)
library(grid)

# example 1
txt = ggplot() + theme_void() + 
    annotate("text",
             x=0.5, y=0.5, size=1.5,
             label="'example formula: '*alpha^{2} + beta",
             parse=TRUE) +
    scale_x_continuous(limits=c(0, 1),
                       expand=c(0, 0)) +
    scale_y_continuous(limits=c(0, 1),
                       expand=c(0, 0))

# example 2
txt <- textGrob(
  expression('example formula: '*alpha^{2} + beta), 
  x = 0.5, y = 0.5,
  gp = gpar(fontsize = 3)
)

Here I have expression supported but kerning issue

# kerning issue but expression supported
grDevices::cairo_pdf("grDevices_cairo.pdf")
grid.newpage()
grid.draw(txt)
dev.off()

And here, I don't have kerning issue but expression are not supported

# no kerning issue but no expression support
Cairo::CairoPDF(file="base_cairo.pdf")
grid.newpage()
grid::grid.draw(txt)
dev.off()

I need to use expressions and a small font, though perhaps not that small in practice, it's for emphasizing the issue here. But the kerning problem is still visible in my plots with expression. I don’t understand how there isn’t a solution, given that R is a scientific language. The use of mathematical expressions seems fundamental, and using smaller fonts for graphics annotations also seems like a reasonable need. Right now, I just feel stuck without a simple solution.

@teunbrand
Copy link
Collaborator

teunbrand commented May 28, 2025

I also had problems with wiggly kerning in pdf once and I fixed my problem by using svglite::svglite(fix_text_size = FALSE).
An alternative is to use the base PDF device, which should render these expressions OK but may lack modern font support.
In any case, I'd suggest switching to a different device as the solution.

@thomasp85
Copy link
Member

I can understand your frustration. The "brave" solution is to bring this issue to the ones that can fix it (i.e. R Core). A hack could be to scale everything up (size of pdf along with size of text, line width etc). Or you could not use pdf but svg as Teun suggested.

In any case it is outside of the hands of ggplot2

@louis-heraut
Copy link
Author

That is what i wanted to clarify ! Not shure that i am brave enough for that yet haha but i will definitly try to find a solution or a workaround as you suggested.

Thanks again for your time !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug an unexpected problem or unintended behavior
Projects
None yet
Development

No branches or pull requests

3 participants