Nothing
#' @title Render a Title on an Image
#'
#' @description Adds a title with optional styling and a title bar to an image.
#' The image can be previewed or saved to a file. Supports both the `grid`-based
#' method and (deprecated) `magick` package for rendering the title.
#'
#' @param image 3-layer RGB/4-layer RGBA array, `rayimg` class, or filename of an image.
#' @param title_text Default `""`. Text string to be added as the title to the image.
#' @param title_offset Default `c(15,15)`. Numeric vector specifying the horizontal
#' and vertical offset of the title text, relative to its anchor position.
#' @param title_lineheight Default `1`. Multiplier for the lineheight.
#' @param title_color Default `"black"`. String specifying the color of the title text.
#' @param title_size Default `30`. Numeric value specifying the font size of the title text.
#' @param title_font Default `"Arial"`. String specifying the font family for the title text.
#' Common options include `"sans"`, `"mono"`, `"serif"`, `"Times"`, `"Helvetica"`, etc.
#' @param title_style Default `"plain"`. String specifying the font style, such as
#' `"plain"`, `"italic"`, or `"bold"`.
#' @param title_bar_color Default `NULL`. Color of the optional title bar. If `NULL`, no bar is added.
#' @param title_bar_alpha Default `0.5`. Transparency level of the title bar. A value
#' between `0` (fully transparent) and `1` (fully opaque).
#' @param title_bar_width Default `NULL`. Numeric value for the height of the title bar in pixels.
#' If `NULL`, it is automatically calculated based on the text size and line breaks.
#' @param title_position Default `"northwest"`. String specifying the position of the title text.
#' Only used when `use_magick = TRUE`. Common options include `"northwest"`, `"center"`, `"south"`, etc.
#' @param title_just Default `"left"`. Horizontal alignment of the title text: `"left"`,
#' `"center"`, or `"right"`.
#' @param use_magick Default `FALSE`. Boolean indicating whether to use the `magick` package for
#' rendering titles. This option will be deprecated in future versions.
#' @param filename Default `NULL`. String specifying the file path to save the resulting image.
#' If `NULL` and `preview = FALSE`, the function returns the processed RGB array.
#' @param preview Default `FALSE`. Boolean indicating whether to display the image after processing.
#' If `TRUE`, the image is displayed but not saved or returned.
#'
#' @return A 3-layer RGB array of the processed image if `filename = NULL` and `preview = FALSE`.
#' Otherwise, writes the image to the specified file or displays it if `preview = TRUE`.
#'
#' @note The `use_magick` parameter and all functionality tied to the `magick` package are
#' planned for deprecation. It is recommended to use the `grid`-based method for
#' future compatibility.
#'
#' @import grDevices grid
#' @export
#'
#'@examplesIf interactive() || identical(Sys.getenv("IN_PKGDOWN"), "true")
#'#Plot the dragon
#'render_title(dragon, preview = TRUE, title_text = "Dragon", title_size=20)
#'#That's hard to see--let's add a title bar:
#'render_title(dragon, preview = TRUE, title_text = "Dragon", title_size=20,
#' title_bar_color="white")
#'#Change the width of the bar:
#'render_title(dragon, preview = TRUE, title_text = "Dragon", title_size=20,
#' title_bar_color="white", title_offset = c(8,8))
#'#The width of the bar will also automatically adjust for newlines:
#'render_title(dragon, preview = TRUE, title_text = "Dragon\n(Blue)", title_size=20,
#' title_bar_color="white")
#'#Change the color and title color:
#'render_title(dragon, preview = TRUE, title_text = "Dragon", title_size=20,
#' title_bar_color="red", title_color = "white")
#'#Change the transparency:
#'render_title(dragon, preview = TRUE, title_text = "Dragon",
#' title_size=20, title_bar_alpha = 0.8,
#' title_bar_color="red", title_color = "white")
#' #Read directly from a file
#' temp_image = tempfile(fileext = ".png")
#' ray_write_image(dragon, temp_image)
#' render_title(temp_image, preview = TRUE, title_text = "Dragon",
#' title_size=20, title_bar_alpha = 0.8,
#' title_bar_color="red", title_color = "white")
render_title = function(
image,
title_text = "",
title_size = 30,
title_offset = rep(title_size / 2, 2),
title_lineheight = 1,
title_color = "black",
title_font = "Arial",
title_style = "plain",
title_bar_color = NA,
title_bar_alpha = 0.5,
title_bar_width = NULL,
title_position = NA,
title_just = "left",
use_magick = FALSE,
filename = NULL,
preview = FALSE
) {
#Always output RGBA array, need to ensure alpha is preserved
image = ray_read_image(
image,
convert_to_array = TRUE,
reset_camera_settings = TRUE
)
temp = tempfile(fileext = ".png")
ray_write_image(
image = ray_read_image(array(0, dim = dim(image))),
filename = temp
)
if (use_magick) {
if (title_style == "plain") {
title_style = "normal"
}
if (is.na(title_bar_color)) {
title_bar_color = NULL
}
if (is.na(title_position)) {
title_position = "northwest"
}
dimensions = dim(image)
if (!("magick" %in% rownames(utils::installed.packages()))) {
stop("`magick` package required for adding title")
}
if (!is.null(title_bar_color)) {
title_bar_color = col2rgb(title_bar_color) / 255
title_bar = array(0, c(dimensions[1], dimensions[2], 4))
if (is.null(title_bar_width)) {
#Detect newlines and adjust title bar width
char_vector = unlist(strsplit(title_text, ""))
newline_indices = which(char_vector == "\n")
newline_indices = newline_indices[
newline_indices != length(char_vector)
]
newlines = length(newline_indices)
title_bar_width = 2 * title_offset[2] + title_size * (1 + newlines)
}
if (title_bar_width > dimensions[1]) {
message(paste0(
c(
"Input title_bar_width (",
title_bar_width,
") greater than image height (",
dimensions[1],
"), reducing size."
),
collapse = ""
))
title_bar_width = dimensions[1]
}
if (title_position %in% c("northwest", "north", "northeast")) {
title_bar[1:title_bar_width, , 1] = title_bar_color[1]
title_bar[1:title_bar_width, , 2] = title_bar_color[2]
title_bar[1:title_bar_width, , 3] = title_bar_color[3]
title_bar[1:title_bar_width, , 4] = title_bar_alpha
} else if (title_position %in% c("southwest", "south", "southeast")) {
title_bar[
(nrow(title_bar) - title_bar_width):nrow(title_bar),
,
1
] = title_bar_color[1]
title_bar[
(nrow(title_bar) - title_bar_width):nrow(title_bar),
,
2
] = title_bar_color[2]
title_bar[
(nrow(title_bar) - title_bar_width):nrow(title_bar),
,
3
] = title_bar_color[3]
title_bar[
(nrow(title_bar) - title_bar_width):nrow(title_bar),
,
4
] = title_bar_alpha
} else {
title_bar[
(nrow(title_bar) / 2 - title_bar_width / 2):(nrow(title_bar) /
2 +
title_bar_width / 2),
,
1
] = title_bar_color[1]
title_bar[
(nrow(title_bar) / 2 - title_bar_width / 2):(nrow(title_bar) /
2 +
title_bar_width / 2),
,
2
] = title_bar_color[2]
title_bar[
(nrow(title_bar) / 2 - title_bar_width / 2):(nrow(title_bar) /
2 +
title_bar_width / 2),
,
3
] = title_bar_color[3]
title_bar[
(nrow(title_bar) / 2 - title_bar_width / 2):(nrow(title_bar) /
2 +
title_bar_width / 2),
,
4
] = title_bar_alpha
}
title_bar_temp = paste0(tempfile(), ".png")
png::writePNG(title_bar, title_bar_temp)
magick::image_read(temp) |>
magick::image_composite(magick::image_read(title_bar_temp), ) |>
magick::image_write(path = temp, format = "png")
}
magick::image_read(temp) |>
magick::image_annotate(
title_text,
location = paste0("+", title_offset[1], "+", title_offset[2]),
size = title_size,
color = title_color,
style = title_style,
font = title_font,
gravity = title_position
) |>
magick::image_write(path = temp, format = "png")
new_image = render_image_overlay(image, temp)
} else {
draw_title_card = function(
image,
title,
padding_x = 10,
padding_y = 10,
gp_text = grid::gpar(col = "white", fontsize = title_size),
bg_color = "black",
bg_alpha = 0.65,
title_just = c("left", "top")
) {
close_device_safely = function(dev_id) {
try(grid::upViewport(0), silent = TRUE)
devs = grDevices::dev.list()
if (!is.null(devs) && dev_id %in% devs) {
grDevices::dev.off(dev_id)
}
}
png_device = linear_png_device()
png_device(
temp,
width = ncol(image),
height = nrow(image),
pointsize = 12,
family = title_font,
units = "px",
res = 72
)
dev_id = grDevices::dev.cur()
pushed = 0L
on.exit(
{
if (pushed > 0L) {
try(grid::popViewport(pushed), silent = TRUE)
}
close_device_safely(dev_id)
},
add = TRUE
)
grid::grid.newpage()
prepared = prepare_native_raster(image)
nr = prepared$native_raster
image_dim = prepared$display_dim
image_width = image_dim[2]
image_height = image_dim[1]
image_vp = grid::viewport(
name = "image",
xscale = c(0, image_width),
yscale = c(image_height, 0),
gp = gp_text
)
grid::pushViewport(image_vp)
pushed = pushed + 1L
image_grob = grid::rasterGrob(
nr,
interpolate = FALSE,
width = grid::unit(1, "npc"),
height = grid::unit(1, "npc")
)
grid::grid.draw(image_grob)
title_hjust = title_just[1]
if (!title_hjust %in% c("left", "center", "right")) {
stop(
'Invalid title_just value for horizontal alignment. Must be "left", "center", or "right".'
)
}
# Available width for text depends on horizontal justification.
# For left/right aligned text, `padding_x` is only applied on one side.
available_width = if (identical(title_hjust, "center")) {
image_width - padding_x * 2
} else {
image_width - padding_x
}
available_width = max(0, available_width)
# Wrap text into lines based on available width
wrap_text = function(text, available_width) {
# Split text into segments by newline characters
segments = unlist(strsplit(text, "\n"))
all_lines = character(0)
for (segment in segments) {
words = strsplit(segment, "\\s+")[[1]]
if (length(words) == 0) {
all_lines = c(all_lines, "") # Add an empty line for consecutive newlines
next
}
lines = character(0)
current_line = words[1]
for (word in words[-1]) {
test_line = paste(current_line, word)
test_width = grid::convertWidth(
grid::stringWidth(test_line),
"native",
valueOnly = TRUE
)
if (test_width > available_width) {
lines = c(lines, current_line)
current_line = word
} else {
current_line = test_line
}
}
lines = c(lines, current_line)
all_lines = c(all_lines, lines)
}
return(all_lines)
}
# Wrap the title text
lines = wrap_text(title, available_width)
lines_with_newlines = paste0(lines, collapse = "\n")
text_size = systemfonts::shape_string(
lines_with_newlines,
size = title_size,
vjust = 0,
res = 72,
lineheight = title_lineheight,
family = title_font
)[["metrics"]]
# Position adjustments
if (title_hjust == "left") {
x = padding_x
just = c("left", "top")
} else if (title_hjust == "center") {
x = image_width / 2
just = c("center", "top")
} else if (title_hjust == "right") {
x = image_width - padding_x
just = c("right", "top")
}
height_with_padding = text_size$height -
text_size$top_bearing -
text_size$bottom_bearing -
text_size$top_border +
padding_y * 2
# Draw background rectangle
grid::grid.rect(
x = 0,
y = 0,
width = image_width,
height = height_with_padding,
just = c("left", "bottom"),
default.units = "native",
gp = grid::gpar(
fill = grDevices::adjustcolor(bg_color, alpha.f = bg_alpha),
col = NA
)
)
grid::grid.text(
label = lines_with_newlines,
x = x,
y = padding_y,
default.units = "native",
just = just,
gp = grid::gpar(
col = title_color,
fontsize = title_size,
lineheight = title_lineheight,
cex = 1,
fontfamily = title_font
)
)
grid::popViewport(pushed)
pushed = 0L
}
if (!is.na(title_position)) {
warning("Title position is ignored when not using {magick}")
}
if (is.null(title_bar_color)) {
title_bar_color = NA
}
if (is.null(title_bar_alpha)) {
title_bar_alpha = NA
}
draw_title_card(
image,
title = title_text,
padding_x = title_offset[1],
padding_y = title_offset[2],
gp_text = grid::gpar(
col = title_color,
fontsize = title_size,
fontfamily = title_font,
lineheight = title_lineheight,
fontface = title_style,
fill = NA
),
bg_color = title_bar_color,
bg_alpha = title_bar_alpha,
title_just = title_just
)
new_image = ray_read_image(temp)
}
handle_image_output(new_image, filename = filename, preview = preview)
}
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.