4

Is it possible to get R to write a plot in bitmap format (e.g. PNG) to standard output? If so, how?

Specifically I would like to run Rscript myscript.R | other_prog_that_reads_a_png_from_stdin. I realise it's possible to create a temporary file and use that, but it's inconvenient as there will potentially be many copies of this pipeline running at the same time, necessitating schemes for choosing unique filenames and removing them afterwards.

I have so far tried setting outf <- file("stdout") and then running either bitmap(file=outf, ...) or png(filename=outf, ...), but both complain ('file' must be a non-empty character string and invalid 'filename' argument, respectively), which is in line with the official documentation for these functions.

Since I was able to persuade R's read.table() function to read from standard input, I'm hoping there's a way. I wasn't able to find anything relevant here on SO by searching for [r] stdout plot, or any of the variations with stdout replaced by "standard output" (with or without double quotes), and/or plot replaced by png.

Thanks!

j_random_hacker
  • 50,331
  • 10
  • 105
  • 169

2 Answers2

5

Unfortunately the {grDevices} (and, by implication, {ggplot2}) seems to fundamentally not support this.

The obvious approach to work around this is: let a graphics device write to a temporary file, and then read that temporary file back into the R session and write it to stdout.

But this fails because, on the one hand, the data cannot be read into a string: character strings in R do not support embedded null characters (if you try you’ll get an error such as “nul character not allowed”). On the other hand, readBin and writeBin fail because writeBin categorically refuses to write to any device that’s hooked up to stdout, which is in text mode (ignoring the fact that, on POSIX system, the two are identical).

This can only be circumvented in incredibly hacky ways, e.g. by opening a binary pipe to a command such as cat:

dev_stdout = function (underlying_device = png, ...) {
    filename = tempfile()
    underlying_device(filename, ...)
    filename
}

dev_stdout_off = function (filename) {
    dev.off()
    on.exit(unlink(filename))
    fake_stdout = pipe('cat', 'wb')
    on.exit(close(fake_stdout), add = TRUE)
    writeBin(readBin(filename, 'raw', file.info(filename)$size), fake_stdout)
}

To use it:

tmp_dev = dev_stdout()
contour(volcano)
dev_stdout_off(tmp_dev)

On systems where /dev/stdout exists (which are most but not all POSIX systems), the dev_stdout_off function can be simplified slightly by removing the command redirection:

dev_stdout_off = function (filename) {
    dev.off()
    on.exit(unlink(filename))
    fake_stdout = file('/dev/stdout', 'wb')
    on.exit(close(fake_stdout), add = TRUE)
    writeBin(readBin(filename, 'raw', file.info(filename)$size), fake_stdout)
}
Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • Thanks, this is good to know. Since it seems some kind of hack is necessary, I will probably just go with the just-create-a-temporary-file hack, as it's a hack that I've hacked before ;) – j_random_hacker Jan 22 '19 at 15:45
1

This might not be a complete answer, but it's the best I've got: can you open a connection using the stdout() command? I know that png() will change the output device to a file connection, but that's not what you want, so it might work to simply substitute png by stdout. I don't know enough about standard outputs to test this theory, however.

The help page suggests that this connection might be text-only. In that case, a solution might be to generate a random string to use as a filename, and pass the name of the file through stdout so that the next step in your pipeline knows where to find your file.

A. Stam
  • 2,148
  • 14
  • 29
  • No, you unfortunately cannot create devices without specifying a *filename*. – Konrad Rudolph Jan 22 '19 at 13:51
  • Can you use /dev/stdout as the filename? – Peter Cock Jan 22 '19 at 14:38
  • 2
    @peterjc Indeed you can (but not directly, you need to go the detour via my answer, otherwise you’ll get “QuartzBitmap_Output - unable to open file '/dev/stdout'” …)! However, [that device file does not exist on all POSIX platforms](https://unix.stackexchange.com/q/338667/3651). – Konrad Rudolph Jan 22 '19 at 14:40