Last active
March 12, 2025 13:46
-
-
Save smach/b1669e05d5ead993bcb62839d4a738de to your computer and use it in GitHub Desktop.
R Shiny app to turn JPGs and PNGs into .ico favicon files. Written mostly by GPT o3-mini-high with help from Claude and Shiny Assistant (and me)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
options(shiny.maxRequestSize = 5 * 1024^2) # Limit uploads to 5 MB | |
library(shiny) | |
library(magick) | |
library(base64enc) | |
library(bslib) | |
# Helper function to sanitize file names | |
safeFileName <- function(filename) { | |
gsub("[^a-zA-Z0-9_.-]", "_", filename) | |
} | |
ui <- page_fluid( | |
theme = bs_theme( | |
version = 5, | |
bootswatch = "flatly", | |
primary = "#2C3E50" | |
), | |
tags$head( | |
tags$style(" | |
.bg-white { | |
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%), | |
linear-gradient(-45deg, #ccc 25%, transparent 25%), | |
linear-gradient(45deg, transparent 75%, #ccc 75%), | |
linear-gradient(-45deg, transparent 75%, #ccc 75%); | |
background-size: 20px 20px; | |
background-position: 0 0, 0 10px, 10px -10px, -10px 0px; | |
background-color: white; | |
} | |
") | |
), | |
# Header card | |
card( | |
full_screen = FALSE, | |
height = "auto", | |
card_header( | |
class = "bg-primary text-white", | |
h1("PNG/JPG to ICO Favicon Converter", class = "mb-0") | |
), | |
p( | |
class = "text-muted mt-2", | |
"Written mostly by ", | |
tags$a( | |
href = "https://openai.com/index/openai-o3-mini/", | |
target = "_blank", | |
"OpenAI o3-mini-high", | |
class = "text-decoration-none" | |
), | |
". Design suggested by ", | |
tags$a( | |
href = "https://gallery.shinyapps.io/assistant", | |
target = "_blank", | |
"Shiny Assistant", | |
class = "text-decoration-none" | |
), | |
". View code on GitHub gist: ", # Note the added space and comma here | |
tags$a( | |
href = "https://gist.github.com/smach/b1669e05d5ead993bcb62839d4a738de", # Replace with your actual gist URL | |
target = "_blank", | |
class = "text-decoration-none", | |
icon("github"), # This adds the GitHub icon | |
"" # Text that appears after the icon | |
) | |
) | |
), | |
layout_columns( | |
col_widths = c(4, 8), | |
gap = "1rem", | |
# Input controls card | |
card( | |
card_header("Settings"), | |
card_body( | |
fileInput("file", "Choose PNG or JPG File", | |
accept = c("image/png", "image/jpeg")), | |
checkboxGroupInput("sizes", "Select favicon sizes (pixels):", | |
choices = list("16x16" = 16, "32x32" = 32, | |
"48x48" = 48, "64x64" = 64), | |
selected = 32), | |
downloadButton("download", "Download Favicon File(s)", | |
class = "btn-primary btn-lg w-100") | |
) | |
), | |
# Preview card | |
card( | |
card_header("Previews"), | |
card_body( | |
uiOutput("img_previews") | |
) | |
) | |
) | |
) | |
server <- function(input, output, session) { | |
# Reactive expression to safely read the uploaded image | |
img_reactive <- reactive({ | |
req(input$file) | |
if (!input$file$type %in% c("image/png", "image/jpeg")) { | |
stop("Uploaded file must be a PNG or JPG image.") | |
} | |
img <- tryCatch({ | |
image_read(input$file$datapath) | |
}, error = function(e) { | |
stop("Could not read image. Please ensure the file is a valid image.") | |
}) | |
img | |
}) | |
# Reactive expression to generate preview images | |
preview_images <- reactive({ | |
req(input$file, input$sizes) | |
sizes <- as.numeric(input$sizes) | |
raw_img <- img_reactive() | |
lapply(sizes, function(sz) { | |
img_scaled <- image_scale(raw_img, paste0(sz, "x", sz)) | |
tmpfile <- tempfile(fileext = ".png") | |
image_write(img_scaled, path = tmpfile, format = "png") | |
uri <- dataURI(file = tmpfile, mime = "image/png") | |
list(size = sz, uri = uri) | |
}) | |
}) | |
# Dynamic UI for previews | |
output$img_previews <- renderUI({ | |
req(preview_images()) | |
images <- preview_images() | |
tagList( | |
div( | |
class = "d-flex flex-wrap gap-4 justify-content-center", | |
lapply(images, function(img_info) { | |
div( | |
class = "text-center preview-container", | |
style = "min-width: 100px;", | |
h4(paste0(img_info$size, " × ", img_info$size)), | |
div( | |
class = "transparency-grid border rounded p-3", | |
style = sprintf("width: %dpx; height: %dpx; display: flex; align-items: center; justify-content: center;", | |
img_info$size + 40, img_info$size + 40), | |
img( | |
src = img_info$uri, | |
width = img_info$size, | |
height = img_info$size, | |
style = "image-rendering: pixelated;" | |
) | |
) | |
) | |
}) | |
) | |
) | |
}) | |
# Download handler | |
output$download <- downloadHandler( | |
filename = function() { | |
req(input$file) | |
base <- safeFileName(tools::file_path_sans_ext(basename(input$file$name))) | |
sizes <- as.numeric(input$sizes) | |
if (length(sizes) == 1) { | |
paste0(base, "_", sizes, "x", sizes, ".ico") | |
} else { | |
paste0(base, "_favicons.zip") | |
} | |
}, | |
content = function(file) { | |
req(input$file, input$sizes) | |
sizes <- as.numeric(input$sizes) | |
raw_img <- img_reactive() | |
if (length(sizes) == 1) { | |
sz <- sizes[1] | |
img_scaled <- image_scale(raw_img, paste0(sz, "x", sz)) | |
image_write(img_scaled, path = file, format = "ico") | |
} else { | |
tmpDir <- tempdir() | |
ico_files <- sapply(sizes, function(sz) { | |
out_file <- file.path(tmpDir, paste0( | |
safeFileName(tools::file_path_sans_ext(basename(input$file$name))), | |
"_", sz, "x", sz, ".ico" | |
)) | |
img_scaled <- image_scale(raw_img, paste0(sz, "x", sz)) | |
image_write(img_scaled, path = out_file, format = "ico") | |
out_file | |
}) | |
oldwd <- getwd() | |
setwd(tmpDir) | |
on.exit(setwd(oldwd), add = TRUE) | |
zip_file <- file.path(tmpDir, "favicons.zip") | |
utils::zip(zipfile = zip_file, files = basename(ico_files)) | |
file.copy(zip_file, file) | |
} | |
} | |
) | |
} | |
shinyApp(ui, server) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment