library(shiny)
library(shinyvalidate)
ui <- fluidPage(
div(id = "form",
radioButtons("type", "Profile image type", choices = c(
"Take selfie" = "selfie",
"Upload image file" = "upload",
"Gravatar" = "gravatar"
)),
conditionalPanel("input.type === 'selfie'",
camera_input("selfie", "Take a selfie")
),
conditionalPanel("input.type === 'upload'",
fileInput("upload", NULL, accept = c("image/jpeg", "image/png")),
uiOutput("upload_preview", container = tags$p)
),
conditionalPanel("input.type === 'gravatar'",
textInput("email", "Email address"),
uiOutput("gravatar_preview", container = tags$p)
),
actionButton("submit", "Submit", class = "btn-primary")
)
)
server <- function(input, output, session) {
iv <- InputValidator$new()
selfie_iv <- InputValidator$new()
selfie_iv$add_rule("selfie",
sv_required("Click the 'Take photo' button before submitting"))
selfie_iv$condition(~ input$type == "selfie")
iv$add_validator(selfie_iv)
upload_iv <- InputValidator$new()
upload_iv$add_rule("upload", sv_required("Please choose a file"))
upload_iv$add_rule("upload", function(upload) {
if (!tools::file_ext(upload$name) %in% c("jpg", "jpeg", "png")) {
"A JPEG or PNG file is required"
}
})
upload_iv$condition(~ input$type == "upload")
iv$add_validator(upload_iv)
gravatar_iv <- InputValidator$new()
gravatar_iv$add_rule("email", sv_required())
gravatar_iv$add_rule("email", sv_email())
gravatar_iv$condition(~ input$type == "gravatar")
iv$add_validator(gravatar_iv)
output$upload_preview <- renderUI({
req(input$upload)
req(nrow(input$upload) == 1)
req(tools::file_ext(input$upload$name) %in% c("jpg", "jpeg", "png"))
uri <- base64enc::dataURI(file = input$upload$datapath,
mime = input$upload$type)
tags$img(src = uri, style = htmltools::css(max_width = "300px", max_height = "300px"))
})
output$gravatar_preview <- renderUI({
req(gravatar_iv$is_valid())
email <- gsub("^\\s*(.*?)\\s*$", "\\1", input$email)
email <- tolower(email)
hash <- digest::digest(email, algo = "md5", serialize = FALSE)
url <- paste0("https://www.gravatar.com/avatar/", hash)
tags$img(src = url, alt = "Gravatar image")
})
observeEvent(input$submit, {
if (iv$is_valid()) {
removeUI("#form")
insertUI(".container-fluid", "beforeEnd", "Thank you for your submission!")
# input$selfie contains raw png data, suitable for png::readPNG()
} else {
iv$enable() # Start showing validation feedback
}
})
}
shinyApp(ui, server)
if (window.Shiny) {
var cameraInputBinding = new Shiny.InputBinding();
$.extend(cameraInputBinding, {
find: function(scope) {
return $(scope).find(".shiny-camera-input");
},
initialize: function(el) {
const $el = $(el);
new ShinyCameraInput(
el,
$el.children("video")[0],
$el.children("canvas")[0],
$el.find(">output>img")[0],
$el.children("button.take")[0],
$el.children("button.retake")[0]
);
},
getType: function() {
return "camera-datauri";
},
getValue: function(el) {
if (el.classList.contains("shot")) {
const img = el.querySelector("output img");
if (img) {
return img.src;
}
}
return null;
},
setValue: function(el, value) {
},
subscribe: function(el, callback) {
$(el).on("change.cameraInputBinding", function(e) {
callback();
});
},
unsubscribe: function(el) {
$(el).off(".cameraInputBinding");
},
// The following two methods, setInvalid and clearInvalid, will be called
// whenever this input fails or passes (respectively) validation.
setInvalid: function(el, data) {
el.classList.add("invalid");
el.querySelector(".feedback-message").innerText = data.message;
},
clearInvalid: function(el) {
el.classList.remove("invalid");
el.querySelector(".feedback-message").innerText = "";
}
});
Shiny.inputBindings.register(cameraInputBinding);
}
class ShinyCameraInput {
constructor(container, video, canvas, photo, startbutton, restartbutton) {
this.stream = null;
this.streaming = false;
this.container = container;
this.video = video;
this.canvas = canvas;
this.photo = photo;
this.startbutton = startbutton;
this.restartbutton = restartbutton;
this.width = this.container.getBoundingClientRect().width;
this.height = 0; // This will be computed based on the input stream
this.video.addEventListener('canplay', ev => {
this.height = this.video.videoHeight / (this.video.videoWidth/this.width);
this.container.style.width = this.width + "px";
this.container.style.height = this.height + "px";
this.container.classList.add("ready");
this.video.setAttribute('width', this.width);
this.video.setAttribute('height', this.height);
this.canvas.setAttribute('width', this.width);
this.canvas.setAttribute('height', this.height);
}, false);
this.startbutton.addEventListener('click', ev => {
this.takepicture();
ev.preventDefault();
}, false);
this.restartbutton.addEventListener('click', ev => {
this.clearphoto();
ev.preventDefault();
}, false);
this.clearphoto();
}
start() {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(stream => {
this.stream = stream;
this.video.srcObject = stream;
this.video.play();
})
.catch(function(err) {
console.log("An error occurred: " + err);
});
}
clearphoto() {
var context = this.canvas.getContext('2d');
context.fillStyle = "#AAA";
context.fillRect(0, 0, this.canvas.width, this.canvas.height);
var data = this.canvas.toDataURL('image/png');
this.photo.setAttribute('src', data);
this.container.classList.remove("shot");
this.container.classList.remove("ready");
$(this.container).trigger("change");
this.start();
}
takepicture() {
var context = this.canvas.getContext('2d');
if (this.width && this.height) {
this.canvas.width = this.width;
this.canvas.height = this.height;
context.drawImage(this.video, 0, 0, this.width, this.height);
var data = this.canvas.toDataURL('image/png');
this.photo.setAttribute('src', data);
this.container.classList.add("shot");
$(this.container).trigger("change");
this.stream.getTracks().forEach(track => {
if (track.readyState === "live") {
track.stop();
}
});
} else {
this.clearphoto();
}
}
}