svglite 2.0.0

https://www.tidyverse.org/blog/2021/02/svglite-2-0-0

This is a companion discussion topic for the original entry at https://www.tidyverse.org/blog/2021/02/svglite-2-0-0


We’re extremely happy to announce the release of svglite 2.0.0. svglite is a graphic device that is capable of creating SVG files from R graphics. SVG is a vector graphic format which means that it encodes the instructions for recreating a graphic in a scale-independent way. This is in contrast with raster graphics, such as PNG (as can be produced with the graphic devices in ragg), which encode actual pixel values and will get pixelated as you zoom in.

You can install the latest release of svglite from CRAN with:

install.packages("svglite")

Much time has passed since svglite had a major release and this blog post will go into detail with all the major changes. You can see a full list of changes in the release notes.

Motivation for svglite

As part of this release, we have sharpened our motivation for creating and maintaining svglite given the pre-existing svg() device provided by R. All of the changes that are part of this release somehow plays into these motivations and are thus grouped by them below.

Editability

The main difference between the output of svg() and svglite() is that the former encodes text as polygons, whereas the later simply encodes it as text and relies on the svg renderer (usually a browser or a vector drawing program) to figure out the look of the text. There are upsides and downsides to this:

Pros

  • Encoding text as text means that you can open up the resulting SVG in a vector drawing program (e.g. Inkscape or Illustrator) and do further modifications on the text, etc. While R graphics is capable of producing amazing visualizations by itself, it is not uncommon that some amount of post-processing is required, and maximum editability of the file is a huge boon there.

  • Encoding text as text means that the resulting file is more accessible. Screen readers will be able to understand and read the text and the text is selectable and copyable.

  • File size will be dramatically smaller (more on that [below]).

Cons

  • Encoding text as polygons will mean that the file does not require the used fonts to be present on the system it is viewed on.

  • Encoding text as text means that the final look depend on the implementation of the svg renderer as well as the fonts available on the system where it is rendered. This might sound more ominous than it really is. The vast majority of interactive data visualizations on the web are now based on D3.js which often renders to SVG and it all seems to behave. Still, this is something to be mindful of, and a reason to use svg() if exactness of the rendered text is of prime importance.

With this release we further improve upon the editability of the output on key points:

  1. The horizontal alignment of the text is now encoded in the CSS, rather than being pre-computed and offsetted. This is only possible for left, center, and right alignment as these are the only recognized alignment types in SVG, so setting it to something different will still give you the old offsetting behavior. This change means that you can more easily modify center and right aligned text in post processing without messing up the alignment of the text block.

  2. Fixing of the text width is now optional. Since the text is not rendered before the SVG is opened the final width of text is unknown while it is being generated. The text width is used in key places by the R graphics engine, partly for alignment offsetting (which we don’t need anymore), but also by e.g. ggplot2’s geom_label() to figure out the size of the background box. svglite solves this by making a good guess at the width and using the textLength attribute of the tag to force the renderer to make the text fit into that space. While this works, it also means that if you modify the text in post processing the text will remain at the same width no matter what you do. Since there’s no single right way to handle this, we have made the fixing of text width optional using the fix_text_size argument. It defaults to TRUE which is the old behavior, but if you know you are going to edit some aspect of the text in post processing you can set it to FALSE.

Speed

When first created, svglite was much faster than svg(), but at some point this regressed quite considerably so the previous release of svglite was rather slow. This release not only brings svglite back to its past glory, but improves further upon it: this release is ~13x faster than the previous and is ~3x faster than using svg() (based on admittedly simple benchmark in the readme).

library(tidyverse)
library(svglite)

x <- runif(1e3)
y <- runif(1e3)
tmp1 <- tempfile()
tmp2 <- tempfile()

svglite_test <- function() {
svglite(tmp1)
plot(x, y)
dev.off()
}
svg_test <- function() {
svg(tmp2, onefile = TRUE)
plot(x, y)
dev.off()
}

bench::mark(
"svglite()" = svglite_test(),
"svg()" = svg_test(),
min_iterations = 250
) %>%
plot(type = "ridge") +
labs(x = NULL, y = NULL)

The additional speed is mainly achieved by no longer ensuring that the file is valid svg while it is being rendered. If you need this, you can get the old behavior (and speed) back by setting always_valid = TRUE when opening the device. You should however consider using svgstring() instead of svglite() if you need to continuously access the svg while it is being rendered.

File size

svglite has always produced smaller files than the svg() device because of the difference in how text is encoded, but this has been improved on further with this release, mainly by being smarter about how to handle clipping regions in the plot. Another file-size-related feature in the new release is built-in support for SVGZ (gzipped SVG), simply by providing a file name with an .svgz extension:

p <- ggplot(mtcars) + 
  geom_point(aes(x = disp, y = mpg, color = gear))

svglite_file <- tempfile()
svglite_file_gz <- tempfile(fileext = ".svgz")
svg_file <- tempfile()

svglite(svglite_file)
plot(p)
invisible(dev.off())

svglite(svglite_file_gz)
plot(p)
invisible(dev.off())

svg(svg_file)
plot(p)
invisible(dev.off())

sizes <- data.frame(
device = c("svglite()", "svglite()\n", "svg()"),
size = fs::file_size(c(svglite_file, svglite_file_gz, svg_file))
)

ggplot(sizes) +
geom_col(aes(x = size / 1024, y = device)) +
labs(x = "Size (Kb)", y = NULL)

Font support

You may remember a few days ago when we announced some major improvements to ragg and its rendering of text. What about svglite? For the most part svglite has delegated its rendering of text to the svg renderer so by and large it has always supported advanced text features such as right-to-left text, font fallback, and color fonts. Some changes have spilled over though, as svglite is now also based on systemfonts and uses that for doing font lookup and text width calculations. This change means that svglite now have direct access to all your installed fonts along with whatever you have registered with register_font() and register_variant(). If you have registered any OpenType features or alternative weights with a font you use, these will now be encoded into the CSS of the text block:

library(systemfonts)
register_variant(
  name = "Montserrat Extreme", 
  family = "Montserrat", 
  weight = "semibold",
  features = font_feature(ligatures = "discretionary", letters = "stylistic")
)

p <- ggplot() +
geom_text(
aes(x = 0, y = 1, label = "This text should definitely differ"),
family = "Montserrat",
size = 6
) +
geom_text(
aes(x = 0, y = 0, label = "This text should definitely differ"),
family = "Montserrat Extreme",
size = 6
) +
theme_void() +
expand_limits(y = c(-1, 2))

svg <- svgstring(fix_text_size = FALSE, standalone = FALSE)
plot(p)
invisible(dev.off())

svg()

<svg class='svglite' width='720.00pt' height='576.00pt' viewBox='0 0 720.00 576.00'>

  <style type='text/css'><![CDATA[
    .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle {
      fill: none;
      stroke: #000000;
      stroke-linecap: round;
      stroke-linejoin: round;
      stroke-miterlimit: 10.00;
    }
  ]]>

<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/>

  <clipPath id='cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA='>
    <rect x='0.00' y='0.00' width='720.00' height='576.00' />
  

<g clip-path='url(#cpMC4wMHw3MjAuMDB8MC4wMHw1NzYuMDA=)'>
<text x='360.00' y='206.70' text-anchor='middle' style='font-size: 17.07px; font-family: Montserrat;'>This text should definitely differ
<text x='360.00' y='381.25' text-anchor='middle' style='font-size: 17.07px; font-weight: 600; font-family: Montserrat; font-feature-settings: "dlig" 1,"salt" 1;'>This text should definitely differ


If you compare the two tags in the end, you can see that the last one has the equivalent of semibold (600) encoded as the weight, along with the registered font feature settings (dlig is the tag for discretionary ligatures and salt is the tag for stylistic alternatives).

The switch to using systemfonts means that two old arguments to svglite: system_fonts and user_fonts have become superseded by the font registration in systemfonts. While they work for now they will likely become deprecated in the future as we streamline the text handling among our graphics devices.

The last addition to font handling in svglite is the support for embedding web font imports in the output. As we discussed in the editability section, the output from svglite will only render correctly if the used fonts are available. This is not a given when using non-standard fonts, but CSS provides a safety net by allowing the viewer to download fonts on the fly if required. You can specify a web font in two ways:

  1. As a URL (e.g. from Google Fonts) which will be used in an @import directive.

  2. Using font_face() which allows you to manually set the location of different font files and local backup fonts along with a identification name and potential styles to use. This will all be collected into a @font-face directive.

We can use this with the example above to make sure that the SVG renders correctly even on computers that doesn’t have the Montserrat font installed.

svg <- svgstring(
  width = 6, height = 2,
  web_fonts = list(
    "https://brick.freetls.fastly.net/Montserrat:400,600"
  ),
  fix_text_size = FALSE, 
  standalone = FALSE
)
plot(p)
invisible(dev.off())

svg()



This text should definitely differ This text should definitely differ

I’m using Brick as a web font host since they include all OpenType features in their served font (which we need here), but the mechanism is the same for using e.g. Google Font.

All the rest

These are just some of the many new features in this release. Depending on your temperament these other features may also get you excited:

  • Support for generating multiple SVG files in one go using the standard file naming pattern (e.g. "Rplot%03d.svg").

  • Direct encoding of raster data instead of rendering it through Cairo (this avoids downscaling any raster data).

  • Addition of a top-level .svglite class and possibility of adding a custom top-level id.

  • Addition of scaling argument in line with the ragg devices.

Acknowledgments

This release comprises the work of 15 different contributors. Thank you all for your thoughtful comments, patience, and hard work!

@barrel0luck, @CameronNemo, @cpsievert, @hadley, @jeroen, @jimhester, @jplecavalier, @kurpav00, @kuzmenkov111, @nx10, @paessens, @pmur002, @sqjin, @svenyos, and @thomasp85.