Open logstar opened 2 years ago
I did an example over stackoverflow
https://stackoverflow.com/questions/65098103/plumber-r-render-a-svg-file/65101289#65101289
library(plumber)
device_size <- function() {
h_ <- 7
w_ <- 7
list(
h = function() h_,
w = function() w_,
set_h = function(h) if (!is.null(h)) {h_ <<- as.numeric(h)},
set_w = function(w) if (!is.null(w)) {w_ <<- as.numeric(w)}
)
}
output_size <- device_size()
serializer_dynamic_svg <- function(..., type = "image/svg+xml") {
serializer_device(
type = type,
dev_on = function(filename) {
grDevices::svg(filename,
width = output_size$w(),
height = output_size$h())
}
)
}
register_serializer("svg", serializer_dynamic_svg)
#* @filter dynamic_size
function(req) {
if (req$PATH_INFO == "/plot") {
output_size$set_w(req$args$width)
output_size$set_h(req$args$height)
}
plumber::forward()
}
Will revisit
This is a great solution given the tools available, @meztez !
@logstar Dynamic sizes is a difficult thing to implement as plumber
has to assume that once the endpoint has started running, images are being created. @meztez has skirted around not being able to use the route definition by using a filter
to set size information that is later used in the image serialization. This solution still allows for the device to be opened before the route execution begins and for the device to be closed once the route function ends.
However, is dyn_param_png_serializer properly implemented?
I believe so!
Minor adjustments:
on.exit
No need for using a file connection to read the binary file. Just use the temp filename as is.
dyn_param_png_serializer <- function(plot_func, width, height, res) {
tmpfn <- base::tempfile()
base::on.exit({
if (file.exists(tmpfn)) {
unlink(tmpfn)
}
}, add = TRUE)
grDevices::png(tmpfn, width = width, height = height, res = res)
device_id <- grDevices::dev.cur()
on.exit({
grDevices::dev.off(device_id)
}, add = TRUE)
plot_func()
img <- base::readBin(tmpfn, "raw", n = base::file.info(tmpfn)$size)
return(img)
}
As plumber::serializer_device apparently also supports async execution, will dyn_param_png_serializer cause async errors? No. (yay!) Once
dyn_param_png_serializer()
begins its execution, it will not stop. So async code is not allowed to run as the main R session is not free.Will dyn_param_png_serializer likely be supported by future plumber versions?
I would like to make this experience better. I don't know if it will be this function directly or through something similar to @meztez 's filter
solution.
why the passed my_plot_func can be called directly within dyn_param_png_serializer? Is there any pointers on the scope of variables in the context of plumber endpoint function definition, registration, runtime, etc?
Check out https://adv-r.hadley.nz/functions.html?q=lexi#lexical-scoping . The whole book is a great resource for learning the nitty gritty details about R!
Here is a good example from https://prl.ccs.neu.edu/blog/2019/09/10/scoping-in-r/:
x <- 1
f <- function() {
x
}
g <- function() {
x <- 2
f()
}
g() # What does this return?
I did an example over stackoverflow
https://stackoverflow.com/questions/65098103/plumber-r-render-a-svg-file/65101289#65101289
library(plumber) device_size <- function() { h_ <- 7 w_ <- 7 list( h = function() h_, w = function() w_, set_h = function(h) if (!is.null(h)) {h_ <<- as.numeric(h)}, set_w = function(w) if (!is.null(w)) {w_ <<- as.numeric(w)} ) } output_size <- device_size() serializer_dynamic_svg <- function(..., type = "image/svg+xml") { serializer_device( type = type, dev_on = function(filename) { grDevices::svg(filename, width = output_size$w(), height = output_size$h()) } ) } register_serializer("svg", serializer_dynamic_svg) #* @filter dynamic_size function(req) { if (req$PATH_INFO == "/plot") { output_size$set_w(req$args$width) output_size$set_h(req$args$height) } plumber::forward() }
Will revisit
Hi @meztez ,
Thanks for the tips to overwrite the 'png'. I just have one question for the req$args
. how can I set values of the width
and height
in the request. I'm new to the plumber. I try to add them in the endpoint but failed.
here is my code:
#* @parser json
#* @parser multi
#* @serializer png list(width = 600, height = 800)
#* @param width
#* @param height
#* @post /plot
function(req, res, width, height){
df <- data.frame(
gp = factor(rep(letters[1:3], each = 10)),
y = rnorm(30)
)
ds <- do.call(rbind, lapply(split(df, df$gp), function(d) {
data.frame(mean = mean(d$y), sd = sd(d$y), gp = d$gp)
}))
p <- ggplot2::ggplot(df, ggplot2::aes(gp, y)) +
ggplot2::geom_point() +
ggplot2::geom_point(data = ds, ggplot2::aes(y = mean), colour = 'red', size = 3)
print(p)
}
r <- httr::POST(
"host://0.0.0.0/plot",
httr::accept_json(),
body = list(
width = jsonlite::toJSON(list(width = 600)) ,
height = jsonlite::toJSON(list(height = 800))
),
httr::write_disk("test.png"), overwrite = TRUE)
)
Will dyn_param_png_serializer likely be supported by future plumber versions?
I'd love to see this (or the filter version) inside the library. Anything I (or others) can assist with?
In addition to the ideas above, I'm wondering about whether to make the content-type itself dynamic - switching between jpeg, png and svg on client demand
@slodge
This provides a way forward without adding new serializer methods...
req
and res
should be added to dev_on()
and dev_off()
dev_on()
should accept req
and res
.
dev_off()
as is unless we see a need to pass in req
/res
dev_on()
.
if (length(formals(dev_on)) > 1) dev_on(filename, req = req, res = res) else dev_on(filename)
length(formals(dev_on)) > 1
validate that there is req
, res
, and ...
.
...
is for future parameter expansionendpoint_serializer()
so that it is O(1)
given many route callsAny serializer methods that call serializer_device()
should be upgraded to handle both the plumber doc args and the appropriate dynamic args.
Ex: (untested)
serializer_png <- function(..., type = "image/png") {
doc_args <- list(...)
serializer_device(
type = type,
dev_on = function(filename, ..., req, res) {
# Overwrite args that can be dynamic (Possibly more args exist)
doc_args$width <- req$args$width %||% doc_args$width
doc_args$height <- req$args$height %||% doc_args$height
# Open device
rlang::exec(grDevices::png, filename, !!!doc_args)
}
)
}
Breaking changes
news entry that parameters like width
and height
are now dynamic and will effect the image size. To get around this, please change your parameter name.Thank you!
Start of a PR is in.... there is lots of testing and documentation needed if anyone else wants to contribute 👍
Going to need to get the other PRs #889 #891 #892 out of the way before this one can be merged - as there will be merge conflicts around the endpoint, parameter and open api code...
Small nudge: could do with RStudio making decisions on these PRs. Delays will cause extra work ... my memory is fading... making changes gets harder every day :)
PR Redone with personal email... and might have to do it again yet... I have too many email addresses ...
-- https://www.rplumber.io/articles/rendering-output.html#customizing-image-serializers viewed on 10/18/2021
According to the above documentation, a
png
serializer with dynamic image sizes can be implemented with the currentplumber
framework. This issue is a feature request for creating a friendly and stable interface for users to directly usepng
serializer with dynamic image sizes.I attempted to implement a
png
serializer with dynamic image sizes,dyn_param_png_serializer
, which is listed below. However, isdyn_param_png_serializer
properly implemented? Asplumber::serializer_device
apparently also supports async execution, willdyn_param_png_serializer
cause async errors? Willdyn_param_png_serializer
likely be supported by futureplumber
versions?Additionally, expressions inside
dyn_param_png_serializer
cannot directly access variables in/plot
endpoint function, e.g.image_width
andmyData
, but why the passedmy_plot_func
can be called directly withindyn_param_png_serializer
? Is there any pointers on the scope of variables in the context ofplumber
endpoint function definition, registration, runtime, etc?This example is adapted from https://www.rplumber.io/articles/introduction.html and https://github.com/rstudio/plumber/blob/06e46f3ff5119e5f1cb8af29ef49aecb3cbb932a/R/serializer.R#L473-L535.