Introducing the `minipdf` package for writing raw PDFs

minipdf

minipdf is a package for creating simple, single-page PDF documents written in pure R.

This package is very, very far from supporting the full PDF spec (which is over 750 pages!), but it supports enough to draw simple figures - for example the package logo was created with the package itself.

Supported features

  • Documents with a single page only
  • Only one font per document (set during initialization)
  • Objects: Text, lines, polylines, polygons, rectangles and circles
  • Attributes: Fill colour (including alpha), stroke colour (including alpha), linewidth, linetype
  • Clipping regions

Currently there is no support for annotations or any sort of text layout.

What’s in the box

  • PDFDocument
    • an R6 class for building a PDF document
    • object methods: add_line(), add_rect(), add_polyline(), add_polygon(), add_text(), add_circle(),
    • other methods: set_clip_rect(), write_pdf()
  • A suite of functions that wrap the R6 class for easier use with a pipe.

Installation

You can install the development version from GitHub with:

# install.packages("devtools")
devtools::install_github("coolbutuseless/minipdf")

Future possibilities

  • Better text handling (multiple fonts per document)
  • Annotations

Shoulders of Giants

  • R6 authors for putting together such an intuitive OO system for R
  • Raphael Sonabend for writing the R62S3 package - this package automatically creates wrappers for R6 methods so that they be called as regular functions. I’ve re-implemented a cut-down version of R62S3::R62Fun() as part of minipdf

Creating a simple PDF with the R6 interface

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Initialise the document
# Draw a pretty geometric pattern
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pdfdoc <- minipdf::PDFDocument$new(width = 400, height = 400)

for (i in seq(0, 400, 20)) {
  pdfdoc$add_line(i    ,     0,   400,     i, stroke = '#00000020')
  pdfdoc$add_line(400  ,     i, 400-i,   400, stroke = '#00000020')
  pdfdoc$add_line(400-i,   400,     0, 400-i, stroke = '#00000020')
  pdfdoc$add_line(0    , 400-i,     i,     0, stroke = '#00000020')
}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Draw a circle in the centre with some text around
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pdfdoc$add_circle(200, 200, 50, fill = "#12345680", stroke = NULL)

ncolours <- 40
pal <- rainbow(ncolours)
for (i in seq(ncolours)) {
  pdfdoc$add_text("                        #Rstats", x = 200, y = 200, 
                  angle = (1 - i)/ncolours * 360, fill = pal[i])
}

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Write the PDF to file
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pdfdoc$write_pdf("/img/minipdf/example1.pdf")

Creating a simple PDF with the pipe-compatible functions

For those more used to working with pipes, there are pipe-friendly versions of most methods. These all take the R6 PDFDocument class as their first argument.

pdfdoc <- create_pdfdoc(width = 400, height = 400, font = 'Helvetica-Bold')

pal <- viridisLite::viridis(80)
i   <- 0
for (x in seq(20, 400, 40)) {
  for (y in seq(100, 400, 40)) {
    i <- i + 1
    pdfdoc <- pdfdoc %>% 
      add_circle(x, y, r = 20, fill = pal[i])
  }
}

pdfdoc <- pdfdoc %>%
  add_text("Pipe-friendly!", x = 10, y = 20, size = 65, stroke = 'grey50', fill = 'lightblue', text_mode = 2) %>%
  add_rect(0, 0, 400, 400, fill = NULL, stroke = 'grey70')

pdfdoc %>% write_pdf("/img/minipdf/example-pipe.pdf")

Creating the logo for this package

  • Design inspiration: the whole decade of the 1970s.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create the hexagonal clipping path
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
spacing <- 200
w <- sqrt(3) * spacing
h <- 2       * spacing

qh <- h/4
hw <- w/2

path <- glue("
{hw} {     0} m
{ 0} {    qh} l
{ 0} {3 * qh} l
{hw} {4 * qh} l
{ w} {3 * qh} l
{ w} {    qh} l
{hw} {     0} l
")


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Setup circles
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
o1 <- 80
o2 <- 60
o3 <- 40
o4 <- 30

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create the PDF document with hexagonal clipping path
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pdfdoc <- PDFDocument$new(width = w, height = h, font = 'Courier-Bold')
pdfdoc$add_clip_stream(clip_path = path)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Solid Background colour
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pdfdoc$add_rect(0, 0, w, h, fill = "#A84128", stroke = NULL)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Circles. Lots of circles
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pdfdoc$add_circle(x = hw + o1, y = 2*qh + o1, r = o1    , fill = '#f8b918', stroke = NULL)
pdfdoc$add_circle(x = hw + o1, y = 2*qh - o1, r = o1    , fill = '#f8b918', stroke = NULL)
pdfdoc$add_circle(x = hw - o1, y = 2*qh + o1, r = o1    , fill = '#f8b918', stroke = NULL)
pdfdoc$add_circle(x = hw - o1, y = 2*qh - o1, r = o1    , fill = '#f8b918', stroke = NULL)

pdfdoc$add_circle(x = hw + o2, y = 2*qh + o2, r = o2-8.5, fill = '#ee9421', stroke = NULL)
pdfdoc$add_circle(x = hw + o2, y = 2*qh - o2, r = o2-8.5, fill = '#ee9421', stroke = NULL)
pdfdoc$add_circle(x = hw - o2, y = 2*qh + o2, r = o2-8.5, fill = '#ee9421', stroke = NULL)
pdfdoc$add_circle(x = hw - o2, y = 2*qh - o2, r = o2-8.5, fill = '#ee9421', stroke = NULL)

pdfdoc$add_circle(x = hw + o3, y = 2*qh + o3, r = o3-17 , fill = '#e75920', stroke = NULL)
pdfdoc$add_circle(x = hw + o3, y = 2*qh - o3, r = o3-17 , fill = '#e75920', stroke = NULL)
pdfdoc$add_circle(x = hw - o3, y = 2*qh + o3, r = o3-17 , fill = '#e75920', stroke = NULL)
pdfdoc$add_circle(x = hw - o3, y = 2*qh - o3, r = o3-17 , fill = '#e75920', stroke = NULL)

pdfdoc$add_circle(x = hw + o4, y = 2*qh + o4, r = 10    , fill = '#Af2a33', stroke = NULL)
pdfdoc$add_circle(x = hw + o4, y = 2*qh - o4, r = 10    , fill = '#Af2a33', stroke = NULL)
pdfdoc$add_circle(x = hw - o4, y = 2*qh + o4, r = 10    , fill = '#Af2a33', stroke = NULL)
pdfdoc$add_circle(x = hw - o4, y = 2*qh - o4, r = 10    , fill = '#Af2a33', stroke = NULL)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Label
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pdfdoc$add_text("minipdf", x = hw+5, y = 15, fill = '#ffffff', size = 44, angle = 30)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Write the PDF to file
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pdfdoc$write_pdf("/img/minipdf/logo.pdf")