Skip to content

Commit

Permalink
Add support for Rmd files
Browse files Browse the repository at this point in the history
  • Loading branch information
wch committed Nov 29, 2017
1 parent 1a50c40 commit cf7120e
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 40 deletions.
5 changes: 3 additions & 2 deletions DESCRIPTION
Expand Up @@ -12,7 +12,8 @@ Depends:
R (>= 3.0)
Imports:
magrittr,
jsonlite
jsonlite,
withr
Suggests:
httpuv,
knitr,
Expand All @@ -24,5 +25,5 @@ SystemRequirements: PhantomJS (http://phantomjs.org) for taking screenshots,
ImageMagick (http://www.imagemagick.org) or GraphicsMagick
(http://www.graphicsmagick.org) and OptiPNG (http://optipng.sourceforge.net)
for manipulating images.
RoxygenNote: 6.0.1
RoxygenNote: 6.0.1.9000
URL: https://github.com/wch/webshot/
1 change: 1 addition & 0 deletions NAMESPACE
Expand Up @@ -7,6 +7,7 @@ export("%>%")
export(appshot)
export(install_phantomjs)
export(resize)
export(rmdshot)
export(shrink)
export(webshot)
importFrom(magrittr,"%>%")
35 changes: 7 additions & 28 deletions R/appshot.R
Expand Up @@ -38,40 +38,19 @@ appshot.character <- function(app, file = "webshot.png", ...,
cmd <- "cat(Sys.getpid(), file='%s'); shiny::runApp('%s', port=%d, display.mode='normal')"
cmd <- shQuote(sprintf(cmd, pidfile, app, port))

# Save existing env vars and set new ones
old_unset_vars <- NULL
old_set_vars <- NULL
if (length(envvars) != 0) {
old_vars <- Sys.getenv(names(envvars), unset = NA, names = TRUE)
# Char vector of variables that weren't set
old_unset_vars <- names(old_vars)[is.na(old_vars)]
# Named list of variables that were set
old_set_vars <- as.list(old_vars[!is.na(old_vars)])
withr::local_envvar(envvars)

do.call(Sys.setenv, as.list(envvars))
}

# Run app in background
system2("R", args = c("--slave", "-e", cmd), wait = FALSE)
# Run app in background with envvars
withr::with_envvar(
envvars,
system2("R", args = c("--slave", "-e", cmd), wait = FALSE)
)

on.exit({
# Restore old env vars
if (length(old_set_vars) != 0 )
do.call(Sys.setenv, old_set_vars)
if (length(old_unset_vars) != 0)
Sys.unsetenv(old_unset_vars)

# Kill app on exit
pid <- readLines(pidfile, warn = FALSE)
file.remove(pidfile)
res <- if (is_windows()) {
system2("taskkill", c("/pid", pid, "/f"))
} else {
system2("kill", pid)
}
if (res != 0) {
stop(sprintf("`kill %s` didn't return success code. Value: %d", pid, res))
}
kill_pid(pid)
})

# Wait for app to start
Expand Down
109 changes: 109 additions & 0 deletions R/rmdshot.R
@@ -0,0 +1,109 @@
#' Take a snapshot of an R Markdown document
#'
#' This function can handle both static Rmd documents and Rmd documents with
#' \code{runtime: shiny}.
#'
#' @inheritParams appshot
#' @param doc The path to a Rmd document.
#' @param delay Time to wait before taking screenshot, in seconds. Sometimes a
#' longer delay is needed for all assets to display properly. If NULL (the
#' default), then it will use 0.2 seconds for static Rmd documents, and 3
#' seconds for Rmd documents with runtime:shiny.
#' @param rmd_args A list of additional arguments to pass to either
#' \code{\link[rmarkdown]{render}} (for static Rmd documents) or
#' \code{\link[rmarkdown]{run}} (for Rmd documents with runtime:shiny).
#'
#' @examples
#' if (interactive()) {
#' rmdshot("doc.rmd", "doc.png")
#' }
#'
#' @export
rmdshot <- function(doc, file = "webshot.png", ..., delay = NULL, rmd_args = list(),
port = getOption("shiny.port"), envvars = NULL) {

runtime <- rmarkdown::yaml_front_matter(doc)$runtime

if (is_shiny(runtime)) {
if (is.null(delay)) delay <- 3

rmdshot_shiny(doc, file, ..., delay = delay, rmd_args = rmd_args,
port = port, envvars = envvars)

} else {
if (is.null(delay)) delay <- 0.2

outfile <- tempfile("webshot", fileext = ".html")
render <- rmarkdown::render
do.call("render", c(list(doc, output_file = outfile), rmd_args),
envir = parent.frame())
webshot(outfile, file = file, ...)
}
}


rmdshot_shiny <- function(doc, file, ..., rmd_args, port, envvars) {

pidfile <- normalizePath(tempfile("pid"), winslash = '/', mustWork = FALSE)
on.exit(unlink(pidfile))
port <- available_port(port)
arg_string <- list_to_arg_string(rmd_args)
if (nzchar(arg_string)) {
arg_string <- paste0(", ", arg_string)
}
cmd <- shQuote(sprintf(
"cat(Sys.getpid(), file='%s'); rmarkdown::run('%s', shiny_args=list(port=%d)%s)",
pidfile, doc, port, arg_string
))

# Run app in background with envvars
withr::with_envvar(
envvars,
system2("R", args = c("--slave", "-e", cmd), wait = FALSE)
)

on.exit({
# Kill app on exit
pid <- readLines(pidfile, warn = FALSE)
file.remove(pidfile)
kill_pid(pid)
})

# Wait for app to start
Sys.sleep(0.5)

fileout <- webshot(sprintf("http://127.0.0.1:%d/", port), file = file, ...)

invisible(fileout)
}


# Convert a list of args like list(a=1, b="xyz") to a string like 'a=1, b="xyz"'
list_to_arg_string <- function(x) {

item_to_arg_string <- function(name, val) {
if (is.numeric(val))
as.character(val)
else if (is.character(val))
paste0('"', val, '"')
else
stop("Only know how to handle numbers and strings arguments to rmarkdown::render. ",
"Don't know how to handle argument `", val, "`.")
}

strings <- vapply(seq_along(x), function(n) item_to_arg_string(names(x)[n], x[[n]]), "")

# Convert to a vector like c("a=1", "b=2")
strings <- mapply(names(x), strings,
FUN = function(name, val) paste(name, val, sep ="="),
USE.NAMES = FALSE
)

paste(strings, collapse = ", ")
}


# Borrowed from rmarkdown
is_shiny <- function (runtime) {
!is.null(runtime) && grepl("^shiny", runtime)
}
12 changes: 12 additions & 0 deletions R/utils.R
Expand Up @@ -302,3 +302,15 @@ find_magic = function() {
}
return(convert)
}


kill_pid <- function(pid) {
res <- if (is_windows()) {
system2("taskkill", c("/pid", pid, "/f"))
} else {
system2("kill", pid)
}
if (res != 0) {
stop(sprintf("`kill %s` didn't return success code. Value: %d", pid, res))
}
}
23 changes: 18 additions & 5 deletions README.Rmd
Expand Up @@ -10,17 +10,18 @@ webshot

[![Travis-CI Build Status](https://travis-ci.org/wch/webshot.svg?branch=master)](https://travis-ci.org/wch/webshot) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/wch/webshot?branch=master&svg=true)](https://ci.appveyor.com/project/wch/webshot)

**Webshot** makes it easy to take screenshots of web pages from R. It can also run Shiny applications locally and take screenshots of the app.
**Webshot** makes it easy to take screenshots of web pages from R. It can also:

* Run Shiny applications locally and take screenshots of the application.
* Render R Markdown documents and take screenshots of the document. Webshot can handle both static Rmd documents and interactive ones (those with `runtime: shiny`).

## Installation

It requires an installation of the external program [PhantomJS](http://phantomjs.org/). You may either download PhantomJS from its website, or use the function `webshot::install_phantomjs()` to install it automatically.

Once PhantomJS is installed you can install webshot with:
Webshot can be installed from CRAN. Webshot also requires the external program [PhantomJS](http://phantomjs.org/). You may either download PhantomJS from its website, or use the function `webshot::install_phantomjs()` to install it automatically.

```{r eval=FALSE}
devtools::install_github("wch/webshot")
install.packages("webshot")
webshot::install_phantomjs()
```


Expand Down Expand Up @@ -101,6 +102,18 @@ appdir <- system.file("examples", "01_hello", package="shiny")
appshot(appdir, "01_hello.png")
```


### Screenshots of R Markdown documents

The `rmdshot()` function takes screenshots of R Markdown documents. For static R Markdown documents, it renders them to HTML in a temporary directory (using `rmarkdown::render()`)and then takes a screenshot.

For dynamic R Markdown documents, it runs them using `rmarkdown::run()` in a separate R process and then takes a screenshot. After taking the screenshot, it will kill the R process that is running the document.

```{r eval=FALSE}
rmdshot("document.rmd", "document.png")
```


### Manipulating images

If you have GraphicsMagick (recommended) or ImageMagick installed, you can pass the result to `resize()` to resize the image after taking the screenshot. This can take any valid ImageMagick geometry specifictaion, like `"75%"`, or `"400x"` (for an image 400 pixels wide). However, you may get different (and often better) results by using the `zoom` option: the fonts and graphical elements will render more sharply. However, compared to simply resizing, zooming out may result in slightly different positioning of text and layout elements.
Expand Down
22 changes: 17 additions & 5 deletions README.md
Expand Up @@ -4,17 +4,19 @@ webshot

[![Travis-CI Build Status](https://travis-ci.org/wch/webshot.svg?branch=master)](https://travis-ci.org/wch/webshot) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/wch/webshot?branch=master&svg=true)](https://ci.appveyor.com/project/wch/webshot)

**Webshot** makes it easy to take screenshots of web pages from R. It can also run Shiny applications locally and take screenshots of the app.
**Webshot** makes it easy to take screenshots of web pages from R. It can also:

- Run Shiny applications locally and take screenshots of the application.
- Render R Markdown documents and take screenshots of the document. Webshot can handle both static Rmd documents and interactive ones (those with `runtime: shiny`).

Installation
------------

It requires an installation of the external program [PhantomJS](http://phantomjs.org/). You may either download PhantomJS from its website, or use the function `webshot::install_phantomjs()` to install it automatically.

Once PhantomJS is installed you can install webshot with:
Webshot can be installed from CRAN. Webshot also requires the external program [PhantomJS](http://phantomjs.org/). You may either download PhantomJS from its website, or use the function `webshot::install_phantomjs()` to install it automatically.

``` r
devtools::install_github("wch/webshot")
install.packages("webshot")
webshot::install_phantomjs()
```

Usage
Expand Down Expand Up @@ -93,6 +95,16 @@ appdir <- system.file("examples", "01_hello", package="shiny")
appshot(appdir, "01_hello.png")
```

### Screenshots of R Markdown documents

The `rmdshot()` function takes screenshots of R Markdown documents. For static R Markdown documents, it renders them to HTML in a temporary directory (using `rmarkdown::render()`)and then takes a screenshot.

For dynamic R Markdown documents, it runs them using `rmarkdown::run()` in a separate R process and then takes a screenshot. After taking the screenshot, it will kill the R process that is running the document.

``` r
rmdshot("document.rmd", "document.png")
```

### Manipulating images

If you have GraphicsMagick (recommended) or ImageMagick installed, you can pass the result to `resize()` to resize the image after taking the screenshot. This can take any valid ImageMagick geometry specifictaion, like `"75%"`, or `"400x"` (for an image 400 pixels wide). However, you may get different (and often better) results by using the `zoom` option: the fonts and graphical elements will render more sharply. However, compared to simply resizing, zooming out may result in slightly different positioning of text and layout elements.
Expand Down
45 changes: 45 additions & 0 deletions man/rmdshot.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit cf7120e

Please sign in to comment.