Importing vector images into R

The grImport package can be used to import vector images into R so that you can edit and/or combine it other plots. In this post, I will go through the grImport workflow and finally show how vector images can be incorporated with other graphical objects.

The grImport workflow goes like this:

  1. We first convert a PostScript file into a R-specific XML format (RGML) file
  2. Next we read and store the newly created RGML file into R as a Picture object
  3. We can edit the Picture object as we please and finally draw the object

To get started, first install the grImport package if you haven’t already and load the library.

if(!"grImport" %in% installed.packages()){
  install.packages("grImport")
}

library(grImport)

I created a simple vector image using Adobe Illustrator that I will use as the example file. First, we will use the function PostScriptTrace to create a RGML file; this will create a new file with an .xml extension by default (shapes.eps.xml).

# you can specify an outfilename if you don't like the default naming scheme
PostScriptTrace("shapes.eps")

Let’s check out the RGML file.

head -17 shapes.eps.xml

<?xml version='1.0' encoding='ISO-8859-1' ?>

<picture version='3' xmlns:rgml='http://r-project.org/RGML' source='shapes.eps' date='2019-02-23 00:18:40' creator='R (3.5.2)' >

<path type='fill' id='1'>
	<context>
		<rgb r='0.308629' g='0.555627' b='1.0'/>
		<style lwd='10.0' lty='' lineend='0' linemitre='10.0' linejoin='0'/>
	</context>

	<move y='57.9805' x='289.77'/>
	<line y='57.9805' x='5.0'/>
	<line y='548.047' x='5.0'/>
	<line y='548.047' x='289.77'/>
	<line y='57.9805' x='289.77'/>
	<line y='57.9805' x='289.77'/>
</path>

The first 3 lines contain the file metadata and is followed by a path element; this describes the first shape in the file by specifying lines between a set of points. The first path (id = 1) has a fill type and this defines a shape that will be filled with the colour specified by the rgb element inside the context element.

Let’s take a look at the second path element in the RGML file.

cat shapes.eps.xml | sed -n '19,31p'

<path type='stroke' id='2'>
	<context>
		<rgb r='0.0' g='0.0' b='0.0'/>
		<style lwd='10.0' lty='' lineend='0' linemitre='10.0' linejoin='0'/>
	</context>

	<move y='57.9805' x='289.77'/>
	<line y='57.9805' x='5.0'/>
	<line y='548.047' x='5.0'/>
	<line y='548.047' x='289.77'/>
	<line y='57.9805' x='289.77'/>
	<line y='57.9805' x='289.77'/>
</path>

This path element is almost identical to the first path element but it has the type stroke; this describes the outline of the shape.

Finally the end of the RGML file provides a summary of the number of paths in the file as well as the minimum and maximum x,y coordinates.

tail shapes.eps.xml

	<line y='231.777' x='826.984'/>
	<line y='213.039' x='829.844'/>
	<line y='198.566' x='830.82'/>
	<line y='193.742' x='830.82'/>
	<line y='193.742' x='830.82'/>
</path>

<summary count='6' ymax='914.629' ymin='5.0' xmax='830.82' xmin='5.0'/>

</picture>

We will use the readPicture function to load the RGML file into R; this creates a Picture object that has two slots: one containing all the paths and the other containing the summary.

my_shape <- readPicture("shapes.eps.xml")

class(my_shape)
[1] "Picture"
attr(,"package")
[1] "grImport"

slotNames(my_shape)
[1] "paths"   "summary"

The paths slot contains a list of PictureFill and PictureStroke objects, which correspond to the two types of path elements we saw in the RGML file.

sapply(my_shape@paths, class)
           path            path            path            path            path            path 
  "PictureFill" "PictureStroke"   "PictureFill" "PictureStroke"   "PictureFill" "PictureStroke"

We can take a look inside one path (and see that it corresponds to the first path element in the RGML file).

my_shape@paths[[1]]
An object of class "PictureFill"
Slot "rule":
[1] "nonzero"

Slot "x":
  move   line   line   line   line   line 
289.77   5.00   5.00 289.77 289.77 289.77 

Slot "y":
    move     line     line     line     line     line 
 57.9805  57.9805 548.0470 548.0470  57.9805  57.9805 

Slot "rgb":
[1] "#4F8EFF"

Slot "lty":
numeric(0)

Slot "lwd":
[1] 10

Slot "lineend":
[1] 1

Slot "linejoin":
[1] 1

Slot "linemitre":
[1] 10

We can use the grid.picture function to draw our vector image.

grid.picture(my_shape)

We can use the use.gc argument to adjust graphical parameters using a gpar object; see ?gpar for list of aesthetics.

grid.picture(my_shape,
             use.gc = FALSE,
             gp = gpar(fill = "#54af44",
                       col = "red",
                       lty = 3,
                       lwd = 5))

We can also draw a subset of the paths; below I draw the fills and strokes separately.

my_fill <- sapply(my_shape@paths, function(x){
  ifelse(class(x) == "PictureFill", TRUE, FALSE)
})

grid.picture(my_shape[my_fill])

my_stroke <- sapply(my_shape@paths, function(x){
  ifelse(class(x) == "PictureStroke", TRUE, FALSE)
})

grid.picture(my_shape[my_stroke])

I will need to cover a little background before I demonstrate how we can combine our imported vector image with other graphical objects. The grid package is the underlying system used to draw the Picture objects in the grImport package. The grid package has become a core package in R (i.e. comes shipped with R) and is a separate system from base R graphics. Graphical objects, called grobs, are printed to a graphics device when you print a grid graphics plot. You can create and edit various grobs (such as circles, lines, polygons, etc.) using grid graphics functions.

library(grid)
my_circle <- circleGrob(x = 0.5,
                        y = 0.5,
                        r = 0.5,
                        gp = gpar(col = "black", lty = 4))

class(my_circle)
[1] "circle" "grob"   "gDesc"

grid.draw(my_circle)

In order to combine our Picture objects with other grobs, we need to convert our Picture into a grob (using the grobify function) and use the gridExtra package for writing multiple grobs to a grahpics device.

class(my_shape)
[1] "Picture"
attr(,"package")
[1] "grImport"

# pictureGrob runs the generic grobify function
my_shape_grob <- pictureGrob(my_shape)

class(my_shape_grob)
[1] "picture" "gTree"   "grob"    "gDesc"

I downloaded an additional SVG file for the combined plot; this file was then opened in Adobe Illustrator and saved as an EPS file. The grid package also underlies the ggplot2 plotting system, so it makes it easy to combine ggplot2 plots. We will use the grid.arrange function from the gridExtra package to create a plot with multiple grobs plotted on it.

library(gridExtra)
library(ggplot2)

# create RGML file
PostScriptTrace("Ennedi_tiger.eps")

# load RGML file
my_tiger <- readPicture("Ennedi_tiger.eps.xml")

p1 <- pictureGrob(my_tiger)
p2 <- pictureGrob(my_shape)
p3 <- my_circle
p4 <- ggplot(iris, aes(x = Petal.Width, y = Petal.Length, colour = Species)) +
  geom_point(size = 0.5) +
  theme(legend.position = "none")

grid.arrange(p1, p2, p3, p4, ncol=2)

# you can use ggsave from the ggplot2 package to save the plot
# blah <- grid.arrange(p1, p2, p3, p4, ncol=2)
# ggsave(filename = "grid_arrange.png", plot = blah, width = 8, height = 6)

We can use the cowplot package to add labels.

cowplot::plot_grid(p1, p2, p3, p4, labels = letters[1:4])

Finally, if you want to learn more about the grid package, check out Mastering Software Development in R. The chapter elaborates on grobs and introduces Viewports, which is a system for drawing in smaller spaces of a larger plot.

Print Friendly, PDF & Email



Creative Commons License
This work is licensed under a Creative Commons
Attribution 4.0 International License
.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.