How to use Cairo PNGs in R Markdown
Asked Answered
D

2

21

There are many advantages to using Cairo to save R graphics (see here, for example). When saving PDFs, for instance, the cairo_pdf device correctly embeds custom fonts.

Using the cairo_pdf graphics device is easy with ggplot-based graphics with ggsave():

library(ggplot2)

ugly_plot <- ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point() +
  labs(title = "Some data about cars") +
  theme_gray(base_family = "Papyrus")
ugly_plot

ggsave(ugly_plot, filename = "ugly_plot.pdf", 
       width = 4, height = 2.5, device = cairo_pdf)

Using the cairo_pdf device in R Markdown with knitr is also easy—add dev: cairo_pdf to the YAML front matter:

---
title: "Cairo stuff"
output:
  pdf_document:
    dev: cairo_pdf
---

```{r make-ugly-plot, fig.width=4, fig.height=2.5}
library(ggplot2)

ugly_plot <- ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point() +
  labs(title = "Some data about cars") +
  theme_gray(base_family = "Papyrus")
ugly_plot
```

PDF output

There are also advantages to using Cairo-based PNGs, since Cairo correctly deals with DPI. If you place a normally-saved PNG with a high DPI into a Word or PowerPoint file, the dimensions of the figure are exaggerated and not accurate. If you place a Cairo-based PNG with the same high DPI into Word, the dimensions are correct:

Weird Word dimensions

Saving ggplot output as high resolution Cairo PNGs is easy with ggsave(), but the syntax is slightly different from saving as Cairo PDFs. Instead of specifying a device, we specify type:

ggsave(ugly_plot, filename = "ugly_plot.png", 
       width = 4, height = 2.5, dpi = 300, type = "cairo")

Placing that file in Word or PowerPoint works great and everything is sized correctly at high resolution.

This misinterpretation of dimensions carries over into R Markdown when knitting to HTML or Word. It'd be great to have knitr use type = "cairo" when knitting, but replicating this dpi = 300, type = "cairo" in R Markdown, however, is more difficult. The Cairo library includes devices like Cairo::CairoPNG(), but ggsave(..., type = "cairo") doesn't use this device. It uses R's standard PNG device, but with Cairo support turned on.

Making the figure high resolution is easy enough with adding dpi=300 to the chunk options, but I can't get knitr to use the built-in PNG device with type = cairo enabled. I tried naively adding type: cairo to the YAML metadata, but it unsurprisingly doesn't work. The PNG that knitr generates doesn't use Cairo and is much larger than expected (and is gigantic in HTML and Word documents).

---
title: "Cairo stuff"
output:
  html_document: 
    self_contained: no  # to see resulting figure as a file
    dev: png
    type: cairo  # this doesn't do anything
---

```{r make-ugly-plot, fig.width=5, fig.height=3.5, dpi=300}
library(ggplot2)

ugly_plot <- ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point() +
  labs(title = "Some data about cars") +
  theme_gray(base_family = "Papyrus")
ugly_plot
```

In sum, I'm looking for a way to use the same output you'd get from ggsave(..., filename = "blah.png", dpi = 300, type = "cairo") in knitr. Is there a way to do this?

---
title: "Something"
output:
  pdf_document:
    dev: cairo_pdf  # yay Cairo output
  html_document:  # What needs to go here?
    dev: png
    type: cairo
---
Dalessandro answered 5/3, 2019 at 18:27 Comment(1)
Is it maybe due to something to do with dev.args instead of just dev , as in my answer below??Stannite
S
29

Use knitr options, not the YAML header.

You can use knitr options to change the type of a specific device (Yihui's recommendation):

knitr::opts_chunk$set(dev.args = list(png = list(type = "cairo")))

Alternately, you could do it conditionally based on the output:

if (!knitr::is_latex_output()) {
  knitr::opts_chunk$set(dpi = 300, dev.args = list(type = "cairo"))
})

I've used this on a couple of documents now. Note: I've only used this for documents doing rmarkdown::render(...) from the R command line.

Stannite answered 5/3, 2019 at 18:39 Comment(9)
Ah! Good call! I didn't know about the dev.args list thing!Dalessandro
Yeah, it's really useful. I don't know if / how that translates to an option in the yaml headerStannite
Also, instead of using knitr::opts_knit$get..., which will fail with an error when running interactively, you can use if (!knitr::is_latex_output()) { ... }, which works fine interactivelyDalessandro
I recommend using dev.args = list(png = list(type = "cairo")), i.e. apply type = "cairo" to the png device only, without affecting other graphical devices.Interbedded
Oops. I had a typo. I meant png instead of cairo_pdf. I just corrected my comment.Interbedded
Wait, so you could get rid of the conditional test and just set that in the knitr::opts_chunk?? Will have to try that.Stannite
Yes, you can :)Interbedded
OK, has something changed in knitr in the latest version? When I try and recreate this post rmflight.github.io/post/nicer-png-graphics with the latest version of knitr, all the graphics look good ...., the basic one doesn't look horrible anymore.Stannite
I don't recall any changes related to graphical devices in knitr recently. Note that type = "cairo" is actually the default of png() if capabilities('cairo') == TRUE, so I'm actually surprised that this option helped at all...Interbedded
G
1

As an alternative to @rmflight's answer using a piece of code at the top of each file, this can be achieved in the shell wrapper or Makefile that knits Rmarkdown files using the optional parameters to rmarkdown::render():

R -e 'rmarkdown::render("foo.Rmd", "pdf_document", output_file="foo.pdf", runtime = "static", output_options = list(dpi = 300, dev.args = list(type = "cairo")))'
Gourmandise answered 21/10, 2020 at 23:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.