library(minisvg)
library(purrr)

Manipulating/Editing an Existing SVG

Editing SVG can be tricky because they are nested structures.

{minisvg} contains some tools for interogating/manipulating existing SVG, but the current focus in {minisvg} is really creating SVG documents in R.

Overview of the structure of a {minisvg} document

A {minisvg} SVG document is just a tree of R6 objects, where each object represents a single SVG tag (e.g. <rect> or <pattern>).

Each object has a list of children ($childen) and a list of attributes ($attrib).

There is also an alternate representation of the child elements of a node in the member $child. E.g. All the children of the current element which are <rect> tags are stored in the list $child$rect

Example SVG for this vignette

svg_text <- '
<svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <g>
    <circle fill="lightgrey" cx="50%" cy="50%" r = "45%" id="bgcircle" stroke="black" stroke-width="4" />
  </g>
  <rect fill="red"    x="40%" y="10%" width="20%" height="20%" />
  <rect fill="yellow" x="40%" y="40%" width="20%" height="20%" />
  <g>
    <rect fill="green"  x="40%" y="70%" width="20%" height="20%" id="gogogo" stroke="black" stroke-width="4" />
  </g>
</svg>'

doc_orig <- minisvg::parse_svg_doc(svg_text)

Show SVG text (click to open)

   <?xml version="1.0" encoding="UTF-8"?>
   <svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <g>
       <circle fill="lightgrey" cx="50%" cy="50%" r="45%" id="bgcircle" stroke="black" stroke-width="4" />
     </g>
     <rect fill="red" x="40%" y="10%" width="20%" height="20%" />
     <rect fill="yellow" x="40%" y="40%" width="20%" height="20%" />
     <g>
       <rect fill="green" x="40%" y="70%" width="20%" height="20%" id="gogogo" stroke="black" stroke-width="4" />
     </g>
   </svg>


Selecting and editing an element by its index in $children

If the structure of an SVG document is known, the element may be selected by its index in the $children list of all child nodes.

In this example, the second child of the root document is selected (i.e. the red square) and its fill attribute is updated.

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Extract the first rectangle and change its fill attribute
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
doc         <- doc_orig$copy()
first_child <- doc$children[[2]]

first_child$attribs$fill <- 'pink'

Show SVG text (click to open)

   <?xml version="1.0" encoding="UTF-8"?>
   <svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <g>
       <circle fill="lightgrey" cx="50%" cy="50%" r="45%" id="bgcircle" stroke="black" stroke-width="4" />
     </g>
     <rect fill="pink" x="40%" y="10%" width="20%" height="20%" />
     <rect fill="yellow" x="40%" y="40%" width="20%" height="20%" />
     <g>
       <rect fill="green" x="40%" y="70%" width="20%" height="20%" id="gogogo" stroke="black" stroke-width="4" />
     </g>
   </svg>


Selecting and editing all direct child elements by type

Each SVG element node keeps both a $children list of nodes (which is arranged by insertion order), and a $child list which is grouped by tag names.

All direct child elements with a particular tag name, e.g. circle, may be accessed using $child[['circle']].

In the following example, the two direct child rectangles have their fill attribute updated. Note that the small green rectangle is not updated as it is not a direct child of the root document.

doc   <- doc_orig$copy()
rects <- doc$child$rect

purrr::walk(rects, ~.x$update(fill = 'blue'))

Show SVG text (click to open)

   <?xml version="1.0" encoding="UTF-8"?>
   <svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <g>
       <circle fill="lightgrey" cx="50%" cy="50%" r="45%" id="bgcircle" stroke="black" stroke-width="4" />
     </g>
     <rect fill="blue" x="40%" y="10%" width="20%" height="20%" />
     <rect fill="blue" x="40%" y="40%" width="20%" height="20%" />
     <g>
       <rect fill="green" x="40%" y="70%" width="20%" height="20%" id="gogogo" stroke="black" stroke-width="4" />
     </g>
   </svg>


Selecting and editing a nested child element by type

In the following example, the green rectangle is updated. It is accessed as the first child rectangle of the second child g tag.

doc        <- doc_orig$copy()
small_rect <- doc$child$g[[2]]$child$rect[[1]]

small_rect$update(fill = 'lightblue')

Show SVG text (click to open)

   <?xml version="1.0" encoding="UTF-8"?>
   <svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <g>
       <circle fill="lightgrey" cx="50%" cy="50%" r="45%" id="bgcircle" stroke="black" stroke-width="4" />
     </g>
     <rect fill="red" x="40%" y="10%" width="20%" height="20%" />
     <rect fill="yellow" x="40%" y="40%" width="20%" height="20%" />
     <g>
       <rect fill="lightblue" x="40%" y="70%" width="20%" height="20%" id="gogogo" stroke="black" stroke-width="4" />
     </g>
   </svg>


Finding any nested child element by attribute

In the following example, all elements with with green or light grey fill are found and updated

doc      <- doc_orig$copy()
rg_elems <- doc$find(attribs = list(fill=c('lightgrey', 'green')))

purrr::walk(rg_elems, ~.x$update(fill = 'blue'))

Show SVG text (click to open)

   <?xml version="1.0" encoding="UTF-8"?>
   <svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <g>
       <circle fill="blue" cx="50%" cy="50%" r="45%" id="bgcircle" stroke="black" stroke-width="4" />
     </g>
     <rect fill="red" x="40%" y="10%" width="20%" height="20%" />
     <rect fill="yellow" x="40%" y="40%" width="20%" height="20%" />
     <g>
       <rect fill="blue" x="40%" y="70%" width="20%" height="20%" id="gogogo" stroke="black" stroke-width="4" />
     </g>
   </svg>


Finding any nested child element by attribute and tag name.

In the following example, all rectangles with a black outline (stroke) are found and updated

doc        <- doc_orig$copy()
rect_elems <- doc$find(tag = 'rect', attribs = list(stroke = 'black'))

purrr::walk(rect_elems, ~.x$update(fill = 'black', stroke = 'green'))

Show SVG text (click to open)

   <?xml version="1.0" encoding="UTF-8"?>
   <svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
     <g>
       <circle fill="lightgrey" cx="50%" cy="50%" r="45%" id="bgcircle" stroke="black" stroke-width="4" />
     </g>
     <rect fill="red" x="40%" y="10%" width="20%" height="20%" />
     <rect fill="yellow" x="40%" y="40%" width="20%" height="20%" />
     <g>
       <rect fill="black" x="40%" y="70%" width="20%" height="20%" id="gogogo" stroke="green" stroke-width="4" />
     </g>
   </svg>