Nothing
# add a border to an image via ImageMagick
add_border = function(input, pixels = 1, color = 'black', output) {
input = normalizePath(input)
if (missing(output))
output = paste0(sans_ext(input), '-output.', file_ext(input))
system2('convert', shQuote(c(
input, '-shave', paste(pixels, pixels, sep = 'x'), '-bordercolor', color,
'-border', pixels, output)
))
optipng(dirname(output))
}
#' Use the Tinify API to compress PNG and JPEG images
#'
#' Compress PNG/JPEG images with \samp{api.tinify.com}, and download the
#' compressed images. These functions require R packages \pkg{curl} and
#' \pkg{jsonlite}. `tinify_dir()` is a wrapper function of `tinify()` to
#' compress images under a directory.
#'
#' You are recommended to set the API key in \file{.Rprofile} or
#' \file{.Renviron}. After that, the only required argument of this function is
#' `input`. If the original images can be overwritten by the compressed images,
#' you may either use `output = identity`, or set the value of the `history`
#' argument in \file{.Rprofile} or \file{.Renviron}.
#' @param input A vector of input paths of images.
#' @param output A vector of output paths or a function that takes `input` and
#' returns a vector of output paths (e.g., `output = `[`identity`] means
#' `output = input`). By default, if the `history` argument is not a provided,
#' `output` is `input` with a suffix `-min` (e.g., when `input = 'foo.png'`,
#' `output = 'foo-min.png'`), otherwise `output` is the same as `input`, which
#' means the original image files will be overwritten.
#' @param quiet Whether to suppress detailed information about the compression,
#' which is of the form \samp{input.png (10 Kb) ==> output.png (5 Kb, 50\%);
#' compression count: 42}. The percentage after `output.png` stands for
#' the compression ratio, and the compression count shows the number of
#' compressions used for the current month.
#' @param force Whether to compress an image again when it appears to have been
#' compressed before. This argument only makes sense when the `history`
#' argument is provided.
#' @param key The Tinify API key. It can be set via either the global option
#' `xfun.tinify.key` or the environment variable `R_XFUN_TINIFY_KEY` (see
#' [env_option()]).
#' @param history Path to a history file to record the MD5 checksum of
#' compressed images. If the checksum of an expected output image exists in
#' this file and `force = FALSE`, the compression will be skipped. This can
#' help you avoid unnecessary API calls.
#' @return The output file paths.
#' @references Tinify API: <https://tinypng.com/developers>.
#' @seealso The \pkg{tinieR} package (<https://github.com/jmablog/tinieR/>) is a
#' more comprehensive implementation of the Tinify API, whereas
#' `xfun::tinify()` has only implemented the feature of shrinking images.
#' @export
#' @examplesIf interactive()
#' f = xfun:::R_logo('jpg$')
#' xfun::tinify(f) # remember to set the API key before trying this
tinify = function(
input, output, quiet = FALSE, force = FALSE,
key = env_option('xfun.tinify.key'),
history = env_option('xfun.tinify.history')
) {
if (!(is.character(key) && length(key) == 1 && key != '')) stop(
"The value of the 'key' argument must be a single non-empty character string."
)
if (length(input) == 0) return(invisible(input))
if (any(i <- !file_exists(input))) stop(
'Input file(s) not found: ', paste(input[i], collapse = ', ')
)
if (missing(output)) {
output = if (is.character(history)) input else {
paste0(sans_ext(input), '-min.', file_ext(input))
}
} else if (is.function(output)) output = output(input)
# avoid optimizing the input image if its md5 checksum exists in history
save_history = function(file) {
if (!is.character(history) || history == '') return()
dir_create(dirname(history))
cat(paste0(tools::md5sum(file), '\n'), file = history, append = TRUE)
}
test_history = function(file) {
is.character(history) && all(file_exists(c(history, file))) &&
(tools::md5sum(file) %in% readLines(history))
}
auth = paste('Authorization: Basic', base64_encode(charToRaw(paste0('api:', key))))
mapply(input, output, FUN = function(i, o) {
if (!force && test_history(o)) {
if (!quiet) message(
'The image ', o, ' has been compressed before. ',
'To compress it again, call tinify() with force = TRUE.'
)
return()
}
if (grepl('[.]png$', i, ignore.case = TRUE))
optipng(files = i, stdout = if (quiet) FALSE else '')
res = curl::curl_upload(i, 'https://api.tinify.com/shrink', httpheader = auth, verbose = FALSE)
cnt = curl::parse_headers_list(res$headers)[['compression-count']]
res = jsonlite::fromJSON(rawToChar(res$content))
if (!is.character(u <- res$output$url)) stop2(
"Failed to shrink '", i, "'", sprintf(': %s (%s)', res$error, res$message)
)
if (!quiet) message(sprintf(
'%s (%s) ==> %s (%s, %.01f%%); compression count: %s',
i, format_bytes(res$input$size), o, format_bytes(res$output$size),
res$output$ratio * 100, if (length(cnt)) cnt else NA
))
# back up the original image and restore it if download failed
if (i == o) {
b = paste0(i, '~')
file.rename(i, b)
on.exit(if (file_exists(o)) file.remove(b) else file.rename(b, i), add = TRUE)
}
curl::curl_download(u, o)
save_history(o)
})
invisible(output)
}
#' @param dir A directory under which all \file{.png}, \file{.jpeg}, and
#' \file{.webp} files are to be compressed.
#' @param ... Arguments passed to [tinify()].
#' @rdname tinify
#' @export
tinify_dir = function(dir = '.', ...) {
tinify(all_files('[.](png|jpe?g|webp)$', dir), ...)
}
#' Shrink images to a maximum width
#'
#' Use [magick::image_resize()] to shrink an
#' image if its width is larger than the value specified by the argument
#' `width`, and optionally call [tinify()] to compress it.
#' @param width The desired maximum width of images.
#' @param dir The directory of images.
#' @param files A vector of image file paths. By default, this is all
#' \file{.png}, \file{.jpeg}, and \file{.webp} images under `dir`.
#' @param tinify Whether to compress images using [tinify()].
#' @export
#' @examples
#' f = xfun:::all_files('[.](png|jpe?g)$', R.home('doc'))
#' file.copy(f, tempdir())
#' f = file.path(tempdir(), basename(f))
#' magick::image_info(magick::image_read(f)) # some widths are larger than 300
#' xfun::shrink_images(300, files = f)
#' magick::image_info(magick::image_read(f)) # all widths <= 300 now
#' file.remove(f)
shrink_images = function(
width = 800, dir = '.', files = all_files('[.](png|jpe?g|webp)$', dir),
tinify = FALSE
) {
for (f in files) {
x = magick::image_read(f)
if (magick::image_info(x)$width <= width) next
x = magick::image_resize(x, sprintf('%dx', width))
magick::image_write(x, f)
}
if (tinify) tinify(files, identity)
}
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.