SVG In, SVG Out

by Paul Murrell http://orcid.org/0000-0002-3224-8858

Version 2: cat(format(Sys.Date(), "%A %d %B %Y"))

Version 1: original publication
Version 2: added date

opts_chunk$set(comment=" ", tidy=FALSE, dpi=96) options(width=100) ## For wonky desktop set up options(bitmapType="cairo") library(grid)

Creative Commons License
This document by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.


This report discusses how to work with complex SVG images in R. We look at importing an external SVG image into R with the 'grImport2' package, integrating the imported image with other R graphics, such as plots, and exporting the result to an external SVG image with the 'gridSVG' package. We discuss some of the complications that can arise with this workflow and show that Version 0.2-0 of the 'grImport2' package helps to deal with those complications.

Introduction

The 'grImport2' package () can be used to import SVG images into R. For example, the following code imports the SVG logo and draws it in R graphics. The steps involved are: convert the SVG image to a Cairo-based SVG image (one way to do this is with the 'rsvg' package; ); read the Cairo-based SVG into R with readPicture(); and draw the image in R with grid.picture().

library(grImport2) library(rsvg) rsvg_svg("svg-logo-v.svg", "svg-logo-v-cairo.svg") SVGlogo <- readPicture("svg-logo-v-cairo.svg") grid.picture(SVGlogo)

The value of being able to import external SVG images like this is that you can then integrate them with other R graphics, such as plots. For example, the following code places the SVG logo in the top-right corner of a 'lattice' plot (). This technique can be used, for example, to add a company or institution logo to a plot.

library(lattice) xyplot(mpg ~ disp, mtcars, panel=function(...) { panel.xyplot(...) grid.picture(SVGlogo, x=1, y=1, just=c("right", "top"), width=unit(2, "cm"), height=unit(2, "cm")) })

Producing SVG output from R is useful because SVG is a vector format, so it produces a nicer (smoother) image at any size. The plot above is a PNG image and we can see, for example, jagged edges on the letters in the SVG logo. The plot below is an SVG version, produced using the svg() device, and the result is much smoother.

xyplot(mpg ~ disp, mtcars, panel=function(...) { panel.xyplot(...) grid.picture(SVGlogo, x=1, y=1, just=c("right", "top"), width=unit(2, "cm"), height=unit(2, "cm")) })

In summary, it is useful to be able to import SVG images into R because we may want to include images that have been created outside of R as part of an R plot. It is also useful to be able to generate SVG output from R because that produces the best visual result in web pages.

Complex SVG

External SVG images may contain more sophisticated graphical features. For example, the R logo, shown below, consists of two paths, each of which is filled with a (subtle) colour gradient.

The R logo (SVG format)

The 'grImport2' package will happily import images with more sophisticated features, but drawing the image in R is a problem because R graphics does not support some of these features.

For example, the following code imports the R logo (SVG format) and draws it within R, but because R graphics does not support colour gradient fills, drawing the R logo produces no output at all!

rsvg_svg("Rlogo.svg", "Rlogo-cairo.svg") Rlogo <- readPicture("Rlogo-cairo.svg") grid.picture(Rlogo) grid.rect()

The following code demonstrates that we have imported the R logo correctly. In this code, we override the colour gradient fills in the original image and just draw the outlines of the two paths that we have imported from the logo.

grid.picture(Rlogo, gpFUN=function(gp) { gpar(col="black") })

The 'gridSVG' package () can help us here. This package can export ('grid') R graphics in SVG format including sophisticated graphics features. The following code draws the R logo again, in SVG format, but this time it uses 'gridSVG' to do the drawing. The steps involved are: open a 'gridSVG' graphics device, with gridsvg(); and supply ext="gridSVG" to grid.picture().

library(gridSVG) gridsvg("Rlogo-gridSVG.svg", width=3, height=3, res=96) grid.picture(Rlogo, ext="gridSVG") dev.off()

The R logo (SVG format) drawn by R

The following code demonstrates the full value of these tools (at least conceptually): an external SVG image, including sophisticated features, has been imported into R, integrated with an R plot, and exported in SVG format, complete with sophisticated features.

gridsvg("Rplot-gridSVG.svg", width=5, height=5, res=96) xyplot(mpg ~ disp, mtcars, panel=function(...) { panel.xyplot(...) grid.picture(Rlogo, x=1, y=1, just=c("right", "top"), width=unit(2, "cm"), height=unit(1.55, "cm"), ext="gridSVG") }) dev.off()

R plot with R logo in top-right corner

Context-sensitive SVG

This section side-tracks into a discussion of some of the details about sophisticated SVG graphics features. We might not choose to make use of these details deliberately when generating our own SVG images, but they become important when we import an external SVG image because we have no control over whether the person who created the external image has made use of these details.

A complication that can arise when generating SVG from R is the export of "context-sensitive" graphics features, such as an SVG mask. A mask is a shape that is used to affect the transparency of another shape; wherever the mask is white, the other shape is opaque, wherever the mask is black, the other shape is transparent and (what makes masks different from clipping paths), wherever the mask is grey, the other shape is translucent.

The following code and images demonstrate how a mask works. We will work with a mask that consists of three vertical bars side by side, with one filled black, one filled grey, and one filled white.

maskShape <- rectGrob(x=0:2/3, width=1/3, just="left", gp=gpar(col=NA, fill=c("black", "grey", "white"))) grid.draw(maskShape)

The shape that we are going to apply this mask to is a red circle, with the name "c".

shape <- circleGrob(name="c", r=.4, gp=gpar(col=NA, fill=hcl(0, 60, 60))) grid.draw(shape)

The following code generates an SVG image consisting of a blue background, with the red circle drawn on top, and the mask applied to the red circle. The steps involved are: "register" the mask and associate it with a label; draw the shape that we want to mask; and apply the mask to the shape (referring to the shape by its name and the mask by its label). The result is that the left slice of the circle becomes fully transparent, the middle slice of the circle becomes translucent (the result is a mix of the red circle and the blue background), and the right slice of the circle is fully opaque.

gridsvg("mask.svg", width=3, height=3, res=96) grid.rect(gp=gpar(col=NA, fill=hcl(240, 60, 60))) registerMask("image-slice", mask(maskShape)) grid.draw(shape) grid.mask("c", label="image-slice") dev.off()

A red circle on a blue background with the mask applied

The next examples demonstrate the idea of "context sensitivity". First of all, we will modify the code to draw the circle (and apply the mask) within a 'grid' viewport that only occupies the right half of the image. The result is that most of the circle is opaque because the mask that we are applying was registered relative to the whole image - the mask occupies all of the image while the circle only occupies the right half of the image.

gridsvg("mask-page.svg", width=3, height=3, res=96) grid.rect(gp=gpar(col=NA, fill=hcl(240, 60, 60))) pushViewport(viewport(x=.5, width=.5, just="left")) grid.draw(shape) grid.mask("c", label="image-slice") dev.off()

A red circle in the right half of a blue background 
                with the mask relative to the whole page

In the following code, we register the mask within the viewport as well as drawing the circle within the viewport. The result now is a smaller version of the original masked circle because the mask that we are applying was registered relative to the viewport - both the mask and the circle only occupy the right half of the image.

gridsvg("mask-vp.svg", width=3, height=3, res=96) grid.rect(gp=gpar(col=NA, fill=hcl(240, 60, 60))) pushViewport(viewport(x=.5, width=.5, just="left")) registerMask("vp-slice", mask(maskShape)) grid.draw(shape) grid.mask("c", label="vp-slice") dev.off()

A red circle in the right half of a blue background 
                with the mask relative to the right half

While this level of control is interesting and powerful, it may not be something we choose to make use of deliberately. However, when we import an external SVG image with 'grImport2' whether we end up with an image that makes use of these "context-sensitive" features, like masks, is out of our control.

Exporting imported context-sensitive SVG

In this section, we bring together the import of SVG images with sophisticated and context-sensitive graphics features and the export of those SVG images (to SVG).

The following image is a diagram that was drawn in Adobe Illustrator (thanks to Artem Sokolov). The original image was PDF, but we cannot directly import PDF images, so the image has been converted to a Cairo-based SVG version using the pdf2svg tool (). The goal is to import this image into R and combine it with an R plot.

SVG version of diagram

An important feature of this SVG image is that that light blue fill is achieved by way of an SVG mask (and an SVG filter) applied to an opaque dark blue fill. This is an example of an image that contains sophisticated context-sensitive features over which we have no control. The presence of the mask (and filter) becomes apparent when we import the image to R and attempt to render it with R graphics. R graphics supports translucent colours, but it does not support masks (or filters), so we only get the dark blue fill from the SVG image when we draw it in R.

test1 <- readPicture("test1.svg") grid.picture(test1)

The following code demonstrates that we can fix the problem by exporting the imported image to SVG using 'gridSVG', which does support masks (and filters). The delayContent argument is significant, but it will be explained later.

gridsvg("test1-gridSVG.svg", width=3, height=3, res=96) grid.picture(test1, ext="gridSVG", delayContent=FALSE) dev.off()

The diagram imported and rendered in R

The following code draws the imported image again (in SVG format, using 'gridSVG'), but this time the imported image is combined with a 'ggplot2' plot () using the 'cowplot' package (). We have to call pictureGrob() rather than grid.picture() because we need a 'grid' grob to pass to plot_grid(). The result is not good - part of the imported image has disappeared!

library(cowplot) gridsvg("test1-gridSVG-right.svg", width=6, height=3, res=96) grid.rect(gp=gpar(col=NA, fill="grey80")) ggplot <- qplot(disp, mpg, data=mtcars) test1grob <- pictureGrob(test1, ext="gridSVG", delayContent=FALSE) plot_grid(ggplot, test1grob) dev.off()

The diagram drawn alongside a plot, 
              but registered on the whole page

This is a manifestation of context-sensitive SVG features (when exporting to SVG with 'gridSVG'); it is just a more complicated example of the circle drawn on half the page masked by a mask that is relative to the whole page from the previous section. The problem is that the mask (and filter) within the imported image is being registered when we call pictureGrob(), which is relative to the whole page, but the imported image is being drawn by 'cowplot' in only half of the page.

The solution is to make sure that the masks (and filters) in the imported image are registered in the correct viewport; just like when the circle and the mask were both relative to half the page in the previous section. The way that we do that is by specifying delayContent=TRUE to pictureGrob(); this means that registration only happens when the imported image is drawn (not when we call pictureGrob()), which means that registration happens in the correct viewport. The code below demonstrates this and the resulting image is now correct.

gridsvg("test1-gridSVG-right-delay.svg", width=6, height=3, res=96) grid.rect(gp=gpar(col=NA, fill="grey80")) ggplot <- qplot(disp, mpg, data=mtcars) test1grob <- pictureGrob(test1, ext="gridSVG", delayContent=TRUE) plot_grid(ggplot, test1grob) dev.off()

The diagram drawn alongside a plot, 
              with the diagram registered in the viewport it is drawn within

In the latest version of 'grImport2' (version 0.2-0), delayContent=TRUE is the default when ext="gridSVG", so the right thing should happen automatically, without us having to specify the delayContent argument explicitly. The following code will also produce the correct result.

gridsvg("test1-gridSVG-right-delay.svg", width=6, height=3, res=96) grid.rect(gp=gpar(col=NA, fill="grey80")) ggplot <- qplot(disp, mpg, data=mtcars) test1grob <- pictureGrob(test1, ext="gridSVG") plot_grid(ggplot, test1grob) dev.off()

Summary

SVG images can contain sophisticated graphics features. The 'grImport2' package allows us to import SVG images that contain sophisticated features into R. The 'gridSVG' package allows us to export SVG images that contain sophisticated features. Version 0.2-0 of the 'grImport2' package makes sure that when we import an SVG image, combine the imported image with other R graphics, and then export an SVG image, the imported image is exported correctly.

Technical requirements

The examples and discussion in this document relate to grImport2_0.2-0, which is available from R-Forge, and gridSVG_1.7-1, which is available from CRAN.

This report was generated within a Docker container (see Resources section below).

Resources

How to cite this document

Murrell, P. (2019). "SVG In, SVG Out" Technical Report 2019-02, Department of Statistics, The University of Auckland. Version 2. [ bib | DOI | http ]

References


Creative Commons License
This document by Paul Murrell is licensed under a Creative Commons Attribution 4.0 International License.