Nothing
#' @name pdfcombiner
#' @title Launches PDF Combiner
#'
#' @description
#' Launches the PDF Combiner \link[shiny]{shiny} App. Instructions on usage are
#' included on the left hand side of the page. By default it uses the bootstrap theme,
#' allowing minimization of the sidebar. Set argument \code{boostrap_theme = FALSE} to turn this
#' off.
#'
#' @param max_file_size Max upload file size in MB, change if needed
#' @param bootstrap_theme When TRUE, uses bslib bootstrap theme to allow minimizing sidebar
#' @param sidebar_width Only applicable when bootstrap theme is used, in pixels
#' @param defaultwm_fontsize Default Watermark font size
#' @param defaultwm_col Default Watermark color
#' @param defaultwm_alpha Default Watermark alpha
#' @param defaultwm_rot Default Watermark rotation angle
#' @param defaultwm_fontface Default Watermark fontface (one of "plain", "italic", "bold", "bold.italic")
#' @param defaultwm_height Default Watermark height in inches (US letter size = 11, A4 = 11.69)
#' @param defaultwm_width Default Watermark width in inches (US letter size = 8.5, A4 = 8.27)
#' @param image_dpi Dots per inch for use when converting to images
#' @param compact_level One of "none" (not used), "printer" (300dpi), "ebook" (150dpi), "screen" (72dpi), only applicable if Ghostscript is installed
#' @param linearize Default FALSE. When TRUE, optimizes PDF for web-viewing (loads first page quickly)
#' @param verbose Prints out working messages in console
#'
#' @details
#' The user is highly recommended to also install the \link[staplr]{staplr} package as it supports bookmarks, however
#' it uses Java so you may need to install Java separately if your system does not currently have it.
#' In addition, the \link[magick]{magick} package is recommended for supporting uploading of image files.
#'
#' @examples
#' if (interactive()) {
#' pdfcombiner(bootstrap_theme = FALSE) # Do not use bootstrap theme (if `shiny` version is < 1.7.4)
#' }
#' @note
#' All PDF files are stored locally in a per-session temporary directory, given by the \code{tempdir()} function.
#' @seealso
#' \url{https://github.com/stevechoy/pdfcombiner}, \link[pdftools]{pdftools}, \link[qpdf]{qpdf}
#'
#'
#' @returns a Shiny App
#' @export
pdfcombiner <- function(max_file_size = 500,
bootstrap_theme = TRUE,
sidebar_width = 700,
defaultwm_fontsize = 50,
defaultwm_col = "gray80",
defaultwm_alpha = 0.6,
defaultwm_rot = 45,
defaultwm_fontface = "bold",
defaultwm_height = 11,
defaultwm_width = 8.5,
image_dpi = 300,
compact_level = "ebook",
linearize = FALSE,
verbose = TRUE) {
# Sanity checks for arguments that should be a non-negative numeric
is_non_negative_numeric(max_file_size, "max_file_size")
is_non_negative_numeric(sidebar_width, "sidebar_width")
is_non_negative_numeric(defaultwm_fontsize, "defaultwm_fontsize")
is_non_negative_numeric(defaultwm_alpha, "defaultwm_alpha")
is_non_negative_numeric(defaultwm_rot, "defaultwm_rot")
is_non_negative_numeric(defaultwm_height, "defaultwm_height")
is_non_negative_numeric(defaultwm_width, "defaultwm_width")
is_non_negative_numeric(image_dpi, "image_dpi")
if (!is.logical(bootstrap_theme)) {
stop("bootstrap_theme argument must be a logical (TRUE or FALSE).")
}
# Check if the input is valid; if not, default to "bold"
if (!(defaultwm_fontface %in% c("plain", "bold", "italic", "bold.italic"))) {
stop("Default_fontface must be one of 'plain', 'bold', 'italic', 'bold.italic'.")
}
options(shiny.maxRequestSize = max_file_size * 1024^2)
# Check if shinythemes is installed, optional
if (requireNamespace("shinythemes", quietly = TRUE)) {
requireNamespace("shinythemes")
app_theme <- shinythemes::shinytheme("flatly")
} else {
app_theme <- NULL # Default to no theme if shinythemes is not installed
}
# Check if staplr is installed, recommended for handling bookmarks
if (requireNamespace("staplr", quietly = TRUE)) {
requireNamespace("rJava") # If running this line fails, that means you need to install Java separately
requireNamespace("staplr")
}
# UI
ui <- if (requireNamespace("bslib", quietly = TRUE) && bootstrap_theme && packageVersion("shiny") >= "1.7.4") { # Loads a bootstrap UI if bslib is installed
requireNamespace("bslib")
bslib::page_sidebar(
theme = bslib::bs_theme(version = 5, # Use Bootstrap 5
preset = "flatly", # cerulean
font_scale = 1),
sidebar = bslib::sidebar(
width = sidebar_width,
title = tags$span("PDF Combiner", style = "font-size: 20px; font-weight: bold;"), # Title text styling
p("1. Upload PDF or Image file(s). All files will be combined automatically by default."),
p("2. Use the file selector to choose which files to include, and click the 'Update / Combine PDF' button (delete unwanted files, order matters)."),
p("3. Verify changes on the right and download the updated PDF."),
p(HTML("<strong>Note:</strong> You can also apply any optional features below, <em>before</em> downloading the updated PDF.")),
bslib::card(class ="shadow border-primary",
bslib::card_header("Input Files"),
if (requireNamespace("magick", quietly = TRUE)) {
fileInput("pdf_files", paste0("Upload PDF or Image File(s): [Max ", max_file_size, " MB]"), multiple = TRUE,
accept = magick_formats)
} else {
fileInput("pdf_files", paste0("Upload PDF File(s): [Max ", max_file_size, " MB]"), multiple = TRUE, accept = ".pdf") # Allow multiple file uploads
},
# Selector for choosing PDFs to combine
selectInput("selected_pdfs", "Select Files to Combine (in this order - select / delete files below):", choices = NULL, multiple = TRUE),
fluidRow(
column(
width = 9,
textInput("save_as_name", label = "(Optional) File Name to Save as...", value = "", placeholder = "Default if not provided: 'updated_pdf_YYYY-MM-DD.pdf'")
),
column(
width = 3,
div(style = "height: 38px;"), # Empty div to add space
checkboxInput("compress",
tagList(
HTML(" Compress"),
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 0px; font-size: 16px;",
title = "Smart lossless compression, will always return the smallest file (details will be shown on bottom right)."
)
),
value = TRUE
)
) # end of column
), # end of fluidRow
# Combine button and compress checkbox
fluidRow(
column(
width = 4,
actionButton("combine_btn", "Update / Combine PDF", style = "width: 100%; margin-top: 0px;")
),
column(
width = 5,
uiOutput("download_ui")
),
column(
width = 3,
div(style = "height: 5px;"), # Empty div to add space
checkboxInput("compact",
tagList(
HTML(" Compact"),
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 0px; font-size: 16px;",
title = "**LOSSY** compression, i.e. degrades raster image quality (will first compress, then compact)."
)
),
value = FALSE
)
) # end of column
) # end of fluidRow for Combine button
), # end of card
bslib::card(class ="shadow",
bslib::card_header("Page Editor"),
# Custom text with inline question mark tooltip
tags$div(
style = "display: flex; align-items: center;", # Align label and input inline
tags$label(
"Enter (Current) Page Numbers to Remove / Select: ",
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 5px; font-size: 20px;",
title = "Note: Bookmarks will not be retained when pages are removed or selected."
)
)
),
# Page removal input
textInput("remove_pages", label = NULL, value = "", placeholder = page_placeholder_text), # Input without default label
# Buttons side by side with 20px gap
div(
style = "display: flex; align-items: center; gap: 20px;",
actionButton("remove_pages_btn",
label = tagList(icon("xmark", class = "fa-lg"), "Remove Pages")), # Remove Pages button with xmark icon
actionButton("select_pages_btn",
label = tagList(icon("check", class = "fa-lg"), "Select Pages")), # Select Pages button with check icon
actionButton("reset_btn",
label = tagList(icon("sync-alt", class = "fa-lg"), "Reset")) # Reset button with refresh icon
),
#tags$hr(style = "border: 2px solid #ccc;"), # Grey divider line
# Custom text with inline question mark tooltip
tags$div(
style = "display: flex; align-items: center;", # Align label and input inline
tags$label(
"Enter (Current) Page Numbers to Rotate: ",
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 5px; font-size: 20px;",
title = "Note: Bookmarks will not be retained when pages are rotated."
)
)
),
# Page rotate input
textInput("rotate_pages", label = NULL, value = "", placeholder = page_placeholder_text),
# Buttons side by side with 20px gap
div(
style = "display: flex; align-items: center; gap: 20px;",
actionButton("rotate_pages_btn",
label = tagList(icon("rotate-left", class = "fa-lg"), "Rotate Pages (90\u00B0 counterclockwise)")), # Remove Pages button with xmark icon
actionButton("reset_btn_rot",
label = tagList(icon("sync-alt", class = "fa-lg"), "Reset"))#, # Reset button with refresh icon
),
# Insert Watermark with inline question mark tooltip
tags$div(
style = "display: flex; align-items: center;", # Align label and input inline
tags$label(
"(Optional) Watermark Stamp: ",
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 5px; font-size: 20px;",
title = "Applies a see-through text across all pages of the PDF when downloaded."
)
)
),
# Watermark input and settings
fluidRow(
column(9, textInput("watermark_text", label = NULL, placeholder = "e.g. 'For Internal Use Only'")),
column(1, actionButton("customize_watermark", label = NULL, icon = icon("gear"), class = "btn-secondary")),
column(2, actionButton("render_preview2", label = NULL, icon = icon("play"), class = "btn-primary")),
),
), # end of card
bslib::card(class = "shadow",
bslib::card_header("Remove Password Protection"),
# Custom text with inline question mark tooltip
tags$div(
style = "display: flex; align-items: center;", # Align label and input inline
tags$label(
paste0("Upload Password-protected PDF File: [Max ", max_file_size, " MB]"),
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 5px; font-size: 20px;",
title = "Note: Bookmarks will not be retained when PDFs are unlocked."
)
)
),
fileInput("locked_pdf", label = NULL, multiple = FALSE, accept = ".pdf"),
textInput("password", label = "Password:", value = "", placeholder = "Enter password here"),
div(
style = "display: flex; align-items: center; gap: 20px;", # Flexbox layout with 20px gap
actionButton("unlock_btn",
label = tagList(icon("lock-open", class = "fa-lg"), "Unlock PDF")), # Unlock button
uiOutput("download_unlocked_ui") # UI for downloading unlocked file
)
),
bslib::card(class = "bg-light shadow",
bslib::card_header("PDF Conversion"),
# Dropdown for selecting output format
selectInput(
"convert_format",
"(Experimental) Convert Updated PDF to:",
choices = c("Word (.docx)", "Excel (.xlsx)", "PowerPoint (.pptx)", "Images (.png as a zip file)"),
selected = "Word (.docx)"
),
div(
style = "display: flex; align-items: center; gap: 20px;", # Flexbox layout with 20px gap
actionButton("convert_btn", "Convert PDF"), # Conversion button
uiOutput("download_conversion_ui") # UI for downloading converted file
)
),
tags$p("Author: Steve Choy (v1.9.8)",
a(href = "https://github.com/stevechoy/pdfcombiner", "(GitHub Repo)", target = "_blank"),
style = "font-size: 0.9em; color: #555; text-align: left;")
), # end of sidebar
htmlOutput("pdfviewer") # Embedded PDF viewer
) # end of page_sidebar
} else { # if not using Bootstrap theme, then use regular sidebar
fluidPage(
theme = app_theme,
tags$head(tags$title("PDF Combiner")),
sidebarLayout(
sidebarPanel(
width = 5,
tags$span("PDF Combiner", style = "font-size: 20px; font-weight: bold;"), # Title text styling
br(),br(),
p("1. Upload PDF or Image file(s). All files will be combined automatically by default."),
p("2. Use the file selector to choose which files to include, and click the 'Update / Combine PDF' button (delete unwanted files, order matters)."),
p("3. Verify changes on the right and download the updated PDF."),
p(HTML("<strong>Note:</strong> You can also apply any optional features below, <em>before</em> downloading the updated PDF.")),
br(),
if (requireNamespace("magick", quietly = TRUE)) {
fileInput("pdf_files", paste0("Upload PDF or Image File(s): [Max ", max_file_size, " MB]"), multiple = TRUE,
accept = magick_formats)
} else {
fileInput("pdf_files", paste0("Upload PDF File(s): [Max ", max_file_size, " MB]"), multiple = TRUE, accept = ".pdf") # Allow multiple file uploads
},
# Selector for choosing PDFs to combine
selectInput("selected_pdfs", "Select Files to Combine (in this order - select / delete files below):", choices = NULL, multiple = TRUE),
fluidRow(
column(
width = 9,
textInput("save_as_name", label = "(Optional) File Name to Save as...", value = "", placeholder = "Default if not provided: 'updated_pdf_YYYY-MM-DD.pdf'")
),
column(
width = 3,
div(style = "height: 38px;"), # Empty div to add space
checkboxInput("compress",
tagList(
HTML(" Compress"),
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 0px; font-size: 16px;",
title = "Smart lossless compression, will always return the smallest file (details will be shown on bottom right)."
)
),
value = TRUE
)
) # end of column
), # end of fluidRow
# Combine button and compress checkbox
fluidRow(
column(
width = 4,
actionButton("combine_btn", "Update / Combine PDF", style = "width: 100%; margin-top: 0px;")
),
column(
width = 5,
uiOutput("download_ui")
),
column(
width = 3,
div(style = "height: 5px;"), # Empty div to add space
checkboxInput("compact",
tagList(
HTML(" Compact"),
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 0px; font-size: 16px;",
title = "**LOSSY** compression, i.e. degrades raster image quality (will first compress, then compact)."
)
),
value = FALSE
)
) # end of column
), # end of fluidRow for Combine button
tags$hr(style = "border: 2px solid #ccc;"), # Grey divider line
# Custom text with inline question mark tooltip
tags$div(
style = "display: flex; align-items: center;", # Align label and input inline
tags$label(
"Enter (Current) Page Numbers to Remove / Select: ",
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 5px; font-size: 20px;",
title = "Note: Bookmarks will not be retained when pages are removed or selected."
)
)
),
# Page removal input
textInput("remove_pages", label = NULL, value = "", placeholder = page_placeholder_text), # Input without default label
# Buttons side by side with 20px gap
div(
style = "display: flex; align-items: center; gap: 20px;",
actionButton("remove_pages_btn",
label = tagList(icon("xmark", class = "fa-lg"), "Remove Pages")), # Remove Pages button with xmark icon
actionButton("select_pages_btn",
label = tagList(icon("check", class = "fa-lg"), "Select Pages")), # Select Pages button with check icon
actionButton("reset_btn",
label = tagList(icon("sync-alt", class = "fa-lg"), "Reset")) # Reset button with refresh icon
),
tags$hr(style = "border: 2px solid #ccc;"), # Grey divider line
# Custom text with inline question mark tooltip
tags$div(
style = "display: flex; align-items: center;", # Align label and input inline
tags$label(
"Enter (Current) Page Numbers to Rotate: ",
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 5px; font-size: 20px;",
title = "Note: Bookmarks will not be retained when pages are rotated."
)
)
),
# Page rotate input
textInput("rotate_pages", label = NULL, value = "", placeholder = page_placeholder_text),
# Buttons side by side with 20px gap
div(
style = "display: flex; align-items: center; gap: 20px;",
actionButton("rotate_pages_btn",
label = tagList(icon("rotate-left", class = "fa-lg"), "Rotate Pages (90\u00B0 counterclockwise)")), # Remove Pages button with xmark icon
actionButton("reset_btn_rot",
label = tagList(icon("sync-alt", class = "fa-lg"), "Reset"))#, # Reset button with refresh icon
),
tags$hr(style = "border: 2px solid #ccc;"), # Grey divider line
# Insert Watermark with inline question mark tooltip
tags$div(
style = "display: flex; align-items: center;", # Align label and input inline
tags$label(
"(Optional) Watermark Stamp: ",
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 5px; font-size: 20px;",
title = "Applies a see-through text across all pages of the PDF when downloaded."
)
)
),
# Watermark input and settings
fluidRow(
column(9, textInput("watermark_text", label = NULL, placeholder = "e.g. 'For Internal Use Only'")),
column(1, actionButton("customize_watermark", label = NULL, icon = icon("gear"), class = "btn-secondary")),
column(2, actionButton("render_preview2", label = NULL, icon = icon("play"), class = "btn-primary")),
),
tags$hr(style = "border: 2px solid #ccc;"), # Grey divider line
tags$div(
style = "display: flex; align-items: center;", # Align label and input inline
tags$label(
paste0("Upload Password-protected PDF File: [Max ", max_file_size, " MB]"),
tags$span(
"?",
style = "color: blue; cursor: help; font-weight: bold; margin-left: 5px; font-size: 20px;",
title = "Note: Bookmarks will not be retained when PDFs are unlocked."
)
)
),
fileInput("locked_pdf", label = NULL, multiple = FALSE, accept = ".pdf"),
textInput("password", label = "Password:", value = "", placeholder = "Enter password here"),
div(
style = "display: flex; align-items: center; gap: 20px;", # Flexbox layout with 20px gap
actionButton("unlock_btn",
label = tagList(icon("lock-open", class = "fa-lg"), "Unlock PDF")), # Unlock button
uiOutput("download_unlocked_ui") # UI for downloading unlocked file
),
tags$hr(style = "border: 2px solid #ccc;"), # Grey divider line
# Dropdown for selecting output format
selectInput(
"convert_format",
"(Experimental) Convert Updated PDF to:",
choices = c("Word (.docx)", "Excel (.xlsx)", "PowerPoint (.pptx)", "Images (.png as a zip file)"),
selected = "Word (.docx)"
),
div(
style = "display: flex; align-items: center; gap: 20px;", # Flexbox layout with 20px gap
actionButton("convert_btn", "Convert PDF"), # Conversion button
uiOutput("download_conversion_ui") # UI for downloading converted file
),
br(),
tags$p("Author: Steve Choy (v1.9.8)",
a(href = "https://github.com/stevechoy/PDF_Combiner", "(GitHub Repo)", target = "_blank"),
style = "font-size: 0.9em; color: #555; text-align: left;")
), # end of sidebarPanel
mainPanel(
width = 7,
htmlOutput("pdfviewer") # Embedded PDF viewer
)
) # end of sidebarLayout
) # end of ui regular
} # end of ui conditional check
# Server
server <- function(input, output, session) {
# Reactive values to store uploaded PDFs and their original names
uploaded_pdfs <- reactiveVal(list()) # List to store uploaded PDFs
combined_pdf <- reactiveVal(NULL) # Stores the combined PDF path
original_pdf <- reactiveVal(NULL) # Stores the original combined PDF path
temp_dir <- tempdir() # Temporary directory for storing combined PDFs
shiny::addResourcePath("pdfs", temp_dir) # Serve files from the temp directory
#rotation_angle <- reactiveVal(0) # Reactive value to store the current rotation angle for staplr, not currently used
unlocked_pdf <- reactiveVal(NULL) # Stores unlocked PDF path (singular file)
combined_pdf_nowm <- reactiveVal(NULL) # Stores combined_pdf without watermarks for final download
original_file_sizes <- reactiveVal(NULL) # Stores sum of original files disk spaces
# Default watermark settings
default_settings <- list(
fontsize = defaultwm_fontsize,
col = defaultwm_col,
alpha = defaultwm_alpha,
rot = defaultwm_rot,
fontface = defaultwm_fontface,
height = defaultwm_height,
width = defaultwm_width
)
# Reactive values to store watermark settings
watermark_settings <- reactiveValues(
fontsize = default_settings$fontsize,
col = default_settings$col,
alpha = default_settings$alpha,
rot = default_settings$rot,
fontface = default_settings$fontface,
height = default_settings$height,
width = default_settings$width
)
# Helper function to combine PDFs
combine_pdfs <- function(pdf_paths, output_path) {
original_file_sizes(sum_disk_space(pdf_paths))
if(verbose) {
message("Sum of File Sizes (KB):", original_file_sizes()) # Debugging
}
# If there's only 1 PDF file, don't do anything to it, just copy it to temp_dir
if(length(pdf_paths) == 1) {
file.copy(unlist(pdf_paths), output_path)
if(verbose) {
message("Single File: copying from ", unlist(pdf_paths), " to ", output_path)# Debugging
}
} else {
# Interesting that when PDFs are read, they are already somehow compressed
if(package_check("staplr", bookmarks = TRUE)) {
staplr::staple_pdf(input_files = unlist(pdf_paths), output_filepath = output_path)
} else {
pdf_combine(unlist(pdf_paths), output = output_path)
}
}
combined_pdf(output_path)
combined_pdf_nowm(output_path)
original_pdf(output_path) # Save the original combined PDF
if(verbose) {
message("Combined PDF Path:", output_path) # Debugging: Print combined PDF path
}
}
# Observe file upload
observeEvent(input$pdf_files, {
shiny::req(input$pdf_files)
# Add the new PDFs to the list of uploaded PDFs
current_pdfs <- uploaded_pdfs()
for (i in seq_along(input$pdf_files$name)) {
file_name <- input$pdf_files$name[i]
file_path <- input$pdf_files$datapath[i]
file_extension <- tools::file_ext(file_name) # Get the file extension (e.g., "png", "pdf")
if (requireNamespace("magick", quietly = TRUE)) {
# Check if the file extension is in magick_formats
if (paste0(".", file_extension) %in% magick_formats) {
if (file_extension == "pdf") { # If PDF, store the file as is
# Check if the file is encrypted
pdf_is_locked <- (pdf_info(file_path)$locked | pdf_info(file_path)$encrypted)
if (pdf_is_locked) {
showNotification(paste0(file_name, " could not be read. It may not be a valid PDF, or it is password-protected. If so, please first unlock it down below."), type = "error", duration = 12)
return()
} else {
current_pdfs[[file_name]] <- file_path
}
} else {
img <- magick::image_read(file_path) # Read the image
new_path <- file.path(temp_dir, paste0("converted_image_", i, "_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf"))
magick::image_write(img, path = new_path, format = "pdf") # Write the image as a PDF
current_pdfs[[file_name]] <- new_path
#file.remove(file_path) # Delete original image file
}
}
} else {
# Get PDF metadata
pdf_is_locked <- (pdf_info(file_path)$locked | pdf_info(file_path)$encrypted)
if(pdf_is_locked) {
showNotification(paste0(file_name, " could not be read. It may not be a valid PDF, or it is password-protected. If so, please first unlock it down below."), type = "error", duration = 12)
return()
} else {
current_pdfs[[file_name]] <- file_path
}
}
}
uploaded_pdfs(current_pdfs) # List of uploaded pdfs
# Update the selector choices
updateSelectInput(session, "selected_pdfs", choices = names(current_pdfs), selected = names(current_pdfs))
# Automatically combine all uploaded PDFs
combine_pdfs(current_pdfs, file.path(temp_dir, paste0("combined_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf")))
# Debugging: Print uploaded PDFs
if(verbose) {
message("Uploaded PDFs:", paste(names(current_pdfs), collapse = ", "))
}
})
# Combine selected PDFs
observeEvent(input$combine_btn, {
shiny::req(input$selected_pdfs)
# Get the selected PDFs
selected_paths <- uploaded_pdfs()[input$selected_pdfs]
# Combine the selected PDFs
combine_pdfs(selected_paths, file.path(temp_dir, paste0("combined_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf")))
showNotification("Selected PDFs combined successfully!", type = "message")
})
# Render PDF Viewer
output$pdfviewer <- renderUI({
shiny::req(combined_pdf())
# JavaScript to dynamically adjust iframe height
tags$div(
style = "position: relative;",
tags$iframe(
id = "pdfIframe",
src = paste0("pdfs/", basename(combined_pdf())), # Serve the file via the resource path
width = "100%",
style = "border: none; height: 100%;"
),
tags$script(HTML("
function resizeIframe() {
var iframe = document.getElementById('pdfIframe');
if (iframe) {
// Calculate available height dynamically
var windowHeight = window.innerHeight;
var iframeTop = iframe.getBoundingClientRect().top;
var availableHeight = windowHeight - iframeTop - 0; // Add some padding if required
iframe.style.height = availableHeight + 'px';
}
}
// Call resizeIframe on window resize and load
window.addEventListener('resize', resizeIframe);
window.addEventListener('load', resizeIframe);
resizeIframe(); // Initial call
"))
)
})
# Helper function to process pages
process_pdf_pages <- function(action, input_pages, input_pdf_path) {
shiny::req(input_pdf_path, input_pages)
# Parse the page numbers
pages <- parse_pages_to_remove(input_pages)
# Debugging: Print pages
if(verbose) {
message("Pages to ", action, ":", paste(pages, collapse = ", "))
}
# Read the combined PDF
total_pages <- pdf_info(input_pdf_path)$pages
# Validate page numbers
if (any(pages < 1 | pages > total_pages)) {
showNotification("Invalid page numbers. Please enter valid page numbers within the range of the PDF.", type = "error")
return(NULL)
}
# Determine pages to keep based on the action
pages_to_keep <- switch(
action,
"Remove" = setdiff(seq_len(total_pages), pages),
"Select" = intersect(seq_len(total_pages), pages),
stop("Invalid action specified")
)
# Check if there are any pages left to keep
if (length(pages_to_keep) == 0) {
showNotification(paste("Cannot remove all pages from the PDF. At least one page must remain."), type = "error")
return(NULL)
}
# Create a new PDF with the remaining pages
updated_pdf_path <- file.path(temp_dir, paste0("updated_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf"))
#if(package_check("staplr", silent = TRUE)) {
# staplr::select_pages(selpages = pages_to_keep, input_filepath = pdf_path, output_filepath = updated_pdf_path)
#} else {
pdf_subset(input_pdf_path, pages = pages_to_keep, output = updated_pdf_path)
#}
# Update reactive variables
combined_pdf(updated_pdf_path)
combined_pdf_nowm(updated_pdf_path)
# Debugging: Print updated PDF path
if(verbose) {
message("Updated PDF Path:", updated_pdf_path)
}
showNotification(
switch(
action,
"Remove" = paste0("Pages Removed Successfully!"),
"Select" = paste0("Pages Selected Successfully!")
),
type = "message")
}
# Remove Pages
observeEvent(input$remove_pages_btn, {
process_pdf_pages("Remove", input$remove_pages, combined_pdf())
})
# Select Pages
observeEvent(input$select_pages_btn, {
process_pdf_pages("Select", input$remove_pages, combined_pdf())
})
# Rotate Pages
observeEvent(input$rotate_pages_btn, {
shiny::req(combined_pdf(), input$rotate_pages)
# Parse the page numbers to remove
pages_to_rotate <- parse_pages_to_remove(input$rotate_pages)
# Debugging: Print pages to remove
if(verbose) {
message("Pages to Rotate:", paste(pages_to_rotate, collapse = ", "))
}
# Read the combined PDF
pdf_path <- combined_pdf()
total_pages <- pdf_info(pdf_path)$pages
# Validate page numbers
if (any(pages_to_rotate < 1 | pages_to_rotate > total_pages)) {
showNotification("Invalid page numbers. Please enter valid page numbers within the range of the PDF.", type = "error")
return()
}
# Check if there are any pages left to rotate
if (length(pages_to_rotate) == 0) {
return()
}
# Create a new PDF with the remaining pages
updated_pdf_path <- file.path(temp_dir, paste0("updated_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf"))
# if(package_check("staplr", silent = TRUE)) {
# new_angle <- (rotation_angle() - 90) %% 360 # # Decrement the rotation angle by 90 degrees (counterclockwise), Cycle back to 270 after -90
# if (new_angle < 0) new_angle <- new_angle + 360 # Handle negative values
# rotation_angle(new_angle)
# # Debugging: Print Rotation angle
# message("Rotation angle:", rotation_angle())
# staplr::rotate_pages(rotatepages = pages_to_rotate, page_rotation = rotation_angle(), input_filepath = pdf_path, output_filepath = updated_pdf_path)
# } else {
#requireNamespace("qpdf") # "qpdf" is an Import from pdftools
qpdf::pdf_rotate_pages(pdf_path, pages = pages_to_rotate, angle = 270, relative = TRUE, output = updated_pdf_path)
#}
combined_pdf(updated_pdf_path) # Update the combined PDF
combined_pdf_nowm(updated_pdf_path) # Update the combined PDF without watermarks
# Debugging: Print updated PDF path
if(verbose) {
message("Updated PDF Path:", updated_pdf_path)
}
showNotification("Pages rotated successfully!", type = "message")
})
# Reset Button
observeEvent(input$reset_btn, {
shiny::req(original_pdf()) # Ensure there is an original combined PDF
combined_pdf(original_pdf()) # Restore the original combined PDF
combined_pdf_nowm(original_pdf()) # Restore the original combined PDF no watermarks
updateTextInput(session, "remove_pages", value = "") # Reset page removal input
showNotification("Page reset successful! Restored to the current list of PDFs.", type = "message")
})
# Reset Button from page rotation section
observeEvent(input$reset_btn_rot, {
shiny::req(original_pdf()) # Ensure there is an original combined PDF
combined_pdf(original_pdf()) # Restore the original combined PDF
combined_pdf_nowm(original_pdf()) # Restore the original combined PDF no watermarks
updateTextInput(session, "rotate_pages", value = "") # Reset page removal input
showNotification("Page reset successful! Restored to the current list of PDFs.", type = "message")
})
# Show a modal dialog for customizing watermark settings
observeEvent(input$customize_watermark, {
showModal(
modalDialog(
title = "Customize Watermark Settings",
easyClose = TRUE, fade = TRUE,
fluidRow(
# First column
column(6,
textInput("col", HTML('Font Color <a href="https://r-charts.com/colors/" target="_blank">(character or Hex)</a>'), value = watermark_settings$col),
numericInput("fontsize", "Font Size", value = watermark_settings$fontsize, min = 1),
selectInput("fontface", "Font Face", choices = c("plain", "bold", "italic", "bold.italic"), selected = "bold", multiple = FALSE),
numericInput("rot", "Rotation Angle", value = watermark_settings$rot, min = 0, max = 360),
),
# Second column
column(6,
sliderInput("alpha", "Transparency (Alpha)", min = 0, max = 1, value = watermark_settings$alpha),
numericInput("height", "Overlay Height (inches)", value = watermark_settings$height, min = 1),
numericInput("width", "Overlay Width (inches)", value = watermark_settings$width, min = 1)
)
),
footer = tagList(
actionButton("reset_wm", "Reset", class = "btn-warning"),
actionButton("render_preview", "Render Preview", class = "btn-info"),
actionButton("apply", "Apply Settings", class = "btn-success"),
modalButton("OK")
)
)
)
})
# Reset settings to default values
observeEvent(input$reset_wm, {
# Reset the reactive values
watermark_settings$fontsize <- default_settings$fontsize
watermark_settings$col <- default_settings$col
watermark_settings$alpha <- default_settings$alpha
watermark_settings$rot <- default_settings$rot
watermark_settings$fontface <- default_settings$fontface
watermark_settings$height <- default_settings$height
watermark_settings$width <- default_settings$width
# Update the modal input fields
updateNumericInput(session, "fontsize", value = default_settings$fontsize)
updateTextInput(session, "col", value = default_settings$col)
updateSliderInput(session, "alpha", value = default_settings$alpha)
updateNumericInput(session, "rot", value = default_settings$rot)
updateSelectInput(session, "fontface", selected = default_settings$fontface)
updateNumericInput(session, "height", value = default_settings$height)
updateNumericInput(session, "width", value = default_settings$width)
})
# Apply new settings to update the watermark_settings reactive
observeEvent(input$apply, {
# Update the reactive values
watermark_settings$fontsize <- input$fontsize
watermark_settings$col <- input$col
watermark_settings$alpha <- input$alpha
watermark_settings$rot <- input$rot
watermark_settings$fontface <- input$fontface
watermark_settings$height <- input$height
watermark_settings$width <- input$width
# Update the modal input fields
updateNumericInput(session, "fontsize", value = input$fontsize)
updateTextInput(session, "col", value = input$col)
updateSliderInput(session, "alpha", value = input$alpha)
updateNumericInput(session, "rot", value = input$rot)
updateSelectInput(session, "fontface", selected = input$fontface)
updateNumericInput(session, "height", value = input$height)
updateNumericInput(session, "width", value = input$width)
})
# Render preview
observeEvent(input$render_preview, {
shiny::req(original_pdf()) # Ensure there is an original combined PDF
shiny::req(combined_pdf())
shiny::req(input$watermark_text)
# Update the reactive values
watermark_settings$fontsize <- input$fontsize
watermark_settings$col <- input$col
watermark_settings$alpha <- input$alpha
watermark_settings$rot <- input$rot
watermark_settings$fontface <- input$fontface
watermark_settings$height <- input$height
watermark_settings$width <- input$width
# Update the modal input fields
updateNumericInput(session, "fontsize", value = input$fontsize)
updateTextInput(session, "col", value = input$col)
updateSliderInput(session, "alpha", value = input$alpha)
updateNumericInput(session, "rot", value = input$rot)
updateSelectInput(session, "fontface", selected = input$fontface)
updateNumericInput(session, "height", value = input$height)
updateNumericInput(session, "width", value = input$width)
tmp_pdf <- watermark_stamp(input_pdf = combined_pdf_nowm(),
output_pdf = file.path(temp_dir, paste0("preview_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf")),
watermark_text = input$watermark_text,
watermark_fontsize = watermark_settings$fontsize,
watermark_col = watermark_settings$col,
fallback_col = defaultwm_col,
watermark_alpha = watermark_settings$alpha,
watermark_rot = watermark_settings$rot,
watermark_fontface = watermark_settings$fontface,
watermark_height = watermark_settings$height,
watermark_width = watermark_settings$width)
combined_pdf(tmp_pdf)
})
# Render preview from main UI
observeEvent(input$render_preview2, {
shiny::req(original_pdf()) # Ensure there is an original combined PDF
shiny::req(combined_pdf())
#shiny::req(input$watermark_text) # Want to show users if no text is applied too
tmp_pdf <- watermark_stamp(input_pdf = combined_pdf_nowm(),
output_pdf = file.path(temp_dir, paste0("preview_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf")),
watermark_text = input$watermark_text,
watermark_fontsize = watermark_settings$fontsize,
watermark_col = watermark_settings$col,
fallback_col = defaultwm_col,
watermark_alpha = watermark_settings$alpha,
watermark_rot = watermark_settings$rot,
watermark_fontface = watermark_settings$fontface,
watermark_height = watermark_settings$height,
watermark_width = watermark_settings$width)
combined_pdf(tmp_pdf)
})
# Always set compress = TRUE when compact = TRUE
observeEvent(input$compact, {
if (input$compact) {
if(Sys.which("gs") == "") {
showNotification("Please first install Ghostscript to use the Compact option.", type = "error")
updateCheckboxInput(session, "compact", value = FALSE)
}
updateCheckboxInput(session, "compress", value = TRUE)
}
})
# Download UI
output$download_ui <- renderUI({
shiny::req(combined_pdf())
downloadButton("download", "Download Updated PDF", class = "btn-primary")
})
output$download <- downloadHandler(
# Dynamically set the file name
filename = function() {
# Use the user-provided name, or default to "updated_pdf_YYYY-MM-DD.pdf" if empty
if (input$save_as_name == "" | is.null(input$save_as_name)) {
paste0("updated_pdf_", Sys.Date(), ".pdf")
} else {
paste0(sanitize_filename(input$save_as_name), ".pdf") # Append ".pdf" to the user-provided name
}
},
content = function(file) {
if(input$watermark_text == "" | is.null(input$watermark_text)) {
pdf_to_save <- combined_pdf_nowm()
} else {
pdf_to_save <- watermark_stamp(input_pdf = combined_pdf_nowm(), # using the no wm version for applying final wm
output_pdf = file.path(temp_dir, paste0("stamped_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf")),
watermark_text = input$watermark_text,
watermark_fontsize = watermark_settings$fontsize,
watermark_col = watermark_settings$col,
fallback_col = defaultwm_col,
watermark_alpha = watermark_settings$alpha,
watermark_rot = watermark_settings$rot,
watermark_fontface = watermark_settings$fontface,
watermark_height = watermark_settings$height,
watermark_width = watermark_settings$width)
showNotification(paste0("Watermark (", input$watermark_text, ") applied!"), type = "message")
}
if(input$compress & !input$compact) {
compressed_path <- file.path(temp_dir, paste0("compressed_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf"))
showNotification("Compressing PDF File...", type = "message")
pdf_compress(input = pdf_to_save, output = compressed_path, linearize = linearize)
# Compare file sizes
#original_size <- file.info(pdf_to_save)$size / 1024 # Convert bytes to KB
original_size <- original_file_sizes()
compressed_size <- file.info(compressed_path)$size / 1024 # Convert bytes to KB
space_saved <- original_size - compressed_size
percentage_saved <- space_saved / original_size * 100
if(verbose) {
message("Original size(s):", original_size, "KB\n")
message("Compressed size:", compressed_size, "KB\n")
message("Space saved:", space_saved, "KB\n")
message("Percentage saved:", round(percentage_saved, 2), "%\n")
}
showNotification(paste0("Original size(s): ", round(original_size), " KB, ",
"Compressed size: ", round(compressed_size), " KB (",
round(percentage_saved, 2), "% reduction)"), type = "message", duration = 15)
if(space_saved > 0) {
file.copy(compressed_path, file)
} else {
showNotification(paste0("Compression resulted in a larger file. Saving uncompressed version instead..."), type = "warning", duration = 10)
file.copy(pdf_to_save, file)
}
} else if (input$compact) {
compact_path <- file.path(temp_dir, paste0("compact_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf"))
# First compress anyway
showNotification("Compressing PDF File prior to Compacting...", type = "message")
pdf_compress(input = pdf_to_save, output = compact_path, linearize = linearize)
showNotification("Compacting lossy PDF File (could take very long, please wait patiently for download file to trigger)...", type = "message", duration = 20)
# Note that we're overwriting the same path of the compressed PDF!
tools::compactPDF(paths = compact_path, gs_quality = compact_level) # one of "none", "printer" (300 dpi), "ebook" (150 dpi), "screen" (72 dpi)
original_size <- original_file_sizes()
compacted_size <- file.info(compact_path)$size / 1024 # Convert bytes to KB
space_saved <- original_size - compacted_size
percentage_saved <- space_saved / original_size * 100
if(verbose) {
message("Original size(s):", original_size, "KB\n")
message("Compacted size:", compacted_size, "KB\n")
message("Space saved:", space_saved, "KB\n")
message("Percentage saved:", round(percentage_saved, 2), "%\n")
}
showNotification(paste0("Original size(s): ", round(original_size), " KB, ",
"Compacted size: ", round(compacted_size), " KB (",
round(percentage_saved, 2), "% reduction)"), type = "message", duration = 15)
if(space_saved > 0) {
file.copy(compact_path, file)
} else {
showNotification(paste0("Compact had no effect (no raster images?). Saving uncompressed version instead..."), type = "warning", duration = 10)
file.copy(pdf_to_save, file)
}
} else {
file.copy(pdf_to_save, file)
}
}
)
observeEvent(input$convert_btn, {
shiny::req(combined_pdf())
format <- input$convert_format
converted_file <- NULL
# Progress bar for conversion
withProgress(message = paste("Converting PDF to", format, "..."), value = 0, {
incProgress(0.2, detail = "Preparing conversion...")
# Perform conversion based on selected format
if (format == "Word (.docx)" && package_check("officer")) {
converted_file <- file.path(temp_dir, "converted.docx")
convert_to_word(combined_pdf(), converted_file)
showNotification(paste("PDF converted to", format, "successfully!"), type = "message")
} else if (format == "Excel (.xlsx)" && package_check("openxlsx")) {
converted_file <- file.path(temp_dir, "converted.xlsx")
convert_to_excel(combined_pdf(), converted_file)
showNotification(paste("PDF converted to", format, "successfully!"), type = "message")
} else if (format == "PowerPoint (.pptx)" && package_check("officer")) {
converted_file <- file.path(temp_dir, "converted.pptx")
convert_to_powerpoint(combined_pdf(), converted_file)
showNotification(paste("PDF converted to", format, "successfully!"), type = "message")
} else if (format == "Images (.png as a zip file)" && package_check("magick")) {
converted_file <- convert_to_images(combined_pdf(), temp_dir, dpi = image_dpi)
showNotification(paste("PDF converted to", format, "successfully!"), type = "message")
}
setProgress(0.9, detail = "Finalizing conversion...")
output$download_conversion_ui <- renderUI({
shiny::req(converted_file)
downloadButton("download_conversion", paste0("Download ", format), class = "btn-primary") # requires a new variable here
})
output$download_conversion <- downloadHandler(
filename = function() {
basename(converted_file)
},
content = function(file) {
file.copy(converted_file, file)
}
)
}) # end of progress bar
}) # end of convert_btn
# Observe password-file upload
observeEvent(input$unlock_btn, {
shiny::req(input$locked_pdf)
shiny::req(input$password)
# Add the new PDFs to the list of uploaded locked PDFs
current_locked_pdf <- list()
for (i in seq_along(input$locked_pdf$name)) {
current_locked_pdf[[input$locked_pdf$name[i]]] <- input$locked_pdf$datapath[i]
}
unlocked_file_path <- file.path(temp_dir, paste0("unlocked_", format(Sys.time(), "%Y%m%d%H%M%S"), ".pdf"))
# Remove the password and save as a regular PDF
tryCatch({
pdftools::pdf_combine(input = unlist(current_locked_pdf),
output = unlocked_file_path,
password = input$password)
}, error = function(e) { # Fails gracefully and alert the user if password is incorrect
showNotification(paste("PDF unlock failed. Wrong password?"), type = "error")
unlocked_pdf(NULL)
})
# Check if the output file was created
if (file.exists(unlocked_file_path)) {
showNotification(paste("PDF unlocked successfully!"), type = "message")
unlocked_pdf(unlocked_file_path)
}
})
# Download UI for unlocked PDF
output$download_unlocked_ui <- renderUI({
shiny::req(unlocked_pdf())
downloadButton("download_unlocked", "Download Unlocked PDF", class = "btn-primary")
})
output$download_unlocked <- downloadHandler(
filename = function() paste0(sub("\\.pdf$", "", input$locked_pdf$name), "_unlocked.pdf"),
content = function(file) file.copy(unlocked_pdf(), file)
)
} # end of server
shinyApp(ui = ui, server = server, options = list(launch.browser = TRUE))
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.