R/render_title.R

Defines functions render_title

Documented in render_title

#' @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)
}

Try the rayimage package in your browser

Any scripts or data that you put into this service are public.

rayimage documentation built on June 12, 2026, 5:06 p.m.