diff --git a/NEWS.md b/NEWS.md index 6aed010..5ebe5ce 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # polish 0.3.1 * surface gt as_word argument autonum in polish_content_word.gt_tbl +* Add new feature to polishing figures (including ggplots) to allow choosing to stretch or scaling images to fit placeholders. * add outputting html chunks to word documents # polish 3.0.0 diff --git a/R/ggplot2.R b/R/ggplot2.R index 724fa16..d465ee0 100644 --- a/R/ggplot2.R +++ b/R/ggplot2.R @@ -7,19 +7,69 @@ polish_content_word.ggplot <- function(x, inline = FALSE, ..., height = 5, width polish_content_word(png, inline = inline, ..., height = height, width = width, units = "in", error_call = error_call) } -#' @param width,height,units Plot size in `units` ("in", "cm", "mm", or "px"). -#' defaults to 5 inches tall and 6 inches wide. +#' @param width,height defaults to match the height and width of the placeholder +#' provided. When that cannot be derived, it assumes the plot should be 5 +#' inches tall and 6 inches wide. Users may set the width and height, but they +#' most both be set. #' @param res Plot resolution. Also accepts a string input: "retina" (320), #' "print" (300), or "screen" (72). Applies only to raster output types. +#' @param device what graphic device to use when saving the plot. Can be one of +#' "png","svg", or "jpeg". +#' +#' @param scale Multiplicative scaling factor. #' @rdname polish_content_pptx #' @export -polish_content_pptx.ggplot <- function(x, ph = "", pptx, ..., height = 5, width = 6, units = "in", res = 300, error_call = current_env()){ - png <- polish_ggplot(x, height = height, width = width, units = units, dpi = res) - polish_content_pptx(png, ph = ph, pptx = pptx, ..., height = height, width = width, units = "in", error_call = error_call) +polish_content_pptx.ggplot <- function(x, ph = "", pptx, ..., height = NULL, width = NULL, res = 300, device = c("png","svg","jpeg"), scale = 1, error_call = current_env()){ + + ## get width and height of placeholder if not set + if(is.null(height) & is.null(width)){ + + # wrap ph in a node + sp_ph <- sp_shell(ph) + + if (length(xml_find_all(sp_ph, ".//a:xfrm")) == 1) { + ## gives dimensions of the placehlder in inches + dims <- dim_extract(sp_ph, ".//a:ext", c("cx", "cy")) + height <- dims$cy + width <- dims$cx + image_fit <- "stretch" + }else{ + height <- 5 + width <- 6 + image_fit <- "scale" + } + }else{ + + invalid_dims <- is.null(height) | is.null(width) + if(!is.null(width) & !invalid_dims){ + invalid_dims <- !is.numeric(width) | (length(width) > 1) | isTRUE(width <= 0) + } + if(!is.null(height) & !invalid_dims){ + invalid_dims <- !is.numeric(height) | (length(height) > 1) | isTRUE(height <= 0) + } + + if(invalid_dims){ + + width_value <- width_value %||% "NULL" + height_value <- height %||% "NULL" + + cli_abort(c( + "{.arg height} and {.arg width} must both be set if defining the plot size, and be positive numeric values.", + i="You've supplied for {.arg height} a {.cls {class(height)}} vector with value {.val {height_value}}.", + i="You've supplied for {.arg width} a {.cls {class(width)}} vector with value {.val {width_value}}." + ), call = error_call) + } + + image_fit <- "scale" + } + + png <- polish_ggplot(x, device = device, height = height, width = width, units = "in", dpi = res, scale = scale) + polish_content_pptx(png, ph = ph, pptx = pptx, ..., height = height, width = width, units = "in", image_fit = image_fit, error_call = error_call) } -polish_ggplot <- function(x, ...) { - tmp_png <- tempfile(fileext = ".png") - ggsave(x, filename = tmp_png, ...) - as_file(tmp_png) +polish_ggplot <- function(x, device = c("png","svg","jpeg"), ...) { + device <- match.arg(device) + tmp_img <- tempfile(fileext = paste0(".", device)) + ggsave(x, filename = tmp_img, device = device, ...) + as_file(tmp_img) } diff --git a/R/pptx.R b/R/pptx.R index 5d7fc2f..436ab85 100644 --- a/R/pptx.R +++ b/R/pptx.R @@ -98,7 +98,6 @@ polish_content_pptx.data.frame <- function(x, ph = '', pptx, ..., table_s #' - "none" (default) puts contents on new lines and indents lists accordingly. #' - "unordered" makes the content bulleted list with indents. #' - "ordered" makes a numbered list with intents. -#' #' @rdname polish_content_pptx #' @export polish_content_pptx.list <- function(x, ph = '', pptx, ..., list_type = c("none","unordered","ordered"), error_call = current_env()){ @@ -130,39 +129,51 @@ polish_content_pptx.list <- function(x, ph = '', pptx, ..., list_type = c # file_ ------------------------------------------------------------------- -#' @param guess_size see [officer::external_img] +#' @param units see [officer::external_img] +#' @param image_fit Should the image be distorted to match the dimensions of the placeholder, or scaled up/down and keep dimension ratio (scale). Default is "stretch". +#' @param scale Multiplicative scaling factor to use when saving the plot. See [ggplot2::ggsave]. +#' #' @rdname polish_content_pptx #' @export -polish_content_pptx.file_png <- function(x, ph = '', pptx, ..., height = 5, width = 6, guess_size = FALSE, units = "in", error_call = current_env()) { +polish_content_pptx.file_png <- function(x, ph = '', pptx, ..., height = NULL, width = NULL, units = "in", image_fit = c("stretch","scale"), error_call = current_env()) { check_dots_empty(call = error_call) + image_fit <- match.arg(image_fit) + + ## get image size, either pre-defined, or guess size + if(is.null(height) & is.null(width)){ + ext_img <- officer::external_img(x, guess_size = TRUE) + }else{ + ext_img <- officer::external_img(x, width = width, height = height, guess_size = FALSE) + } + + image_definition <- list(dims = attr(ext_img, "dims"), offset = list(top = 0, left = 0)) + # wrap ph in a node sp_ph <- sp_shell(ph) - ext_img <- officer::external_img(x, height = height, width = width, unit = units, guess_size = guess_size) - + ## If placeholder has dims, get them if (length(xml_find_all(sp_ph, ".//a:xfrm")) == 1) { - offsets <- dim_extract(sp_ph, ".//a:off", c("x", "y")) - top <- offsets$y - left <- offsets$x - - dims <- dim_extract(sp_ph, ".//a:ext", c("cx", "cy")) - height <- dims$cy - width <- dims$cx - } else { - height <- attr(ext_img, "dims")$height - width <- attr(ext_img, "dims")$width - top <- 0 - left <- 0 + ph_offsets <- dim_extract(sp_ph, ".//a:off", c("x", "y")) + names(ph_offsets) <- c("left","top") + ph_dims <- dim_extract(sp_ph, ".//a:ext", c("cx", "cy")) + names(ph_dims) <- c("width","height") + + if(image_fit == "scale"){ + image_definition <- image_fit_scale(image_definition = image_definition, ph_offsets = ph_offsets, ph_dims = ph_dims) + }else{ + image_definition <- image_fit_stretch(image_definition = image_definition, ph_offsets = ph_offsets, ph_dims = ph_dims) + } + } # make the node xml <- as_xml_pptx(officer::to_pml( x = ext_img, - top = top, - left = left, - height = height, - width = width, + top = image_definition$offset$top, + left = image_definition$offset$left, + height = image_definition$dims$height, + width = image_definition$dims$width, add_ns = TRUE, ln = officer::sp_line(lwd = 0, linecmpd = "sng", lineend = "rnd"), ph = as.character(xml_find_all(sp_ph, "//p:ph")) @@ -201,7 +212,7 @@ polish_content_pptx.file_rtf <- function(x, ph = '', pptx, ..., error_cal check_dots_empty(call = error_call) png_path <- convert_rtf_file_to_png(x) - polish_content_pptx(as_file(png_path), ph = ph, guess_size = TRUE, error_call = error_call) + polish_content_pptx(as_file(png_path), ph = ph, image_fit = "scale", error_call = error_call) } #' @export @@ -210,8 +221,9 @@ polish_content_pptx.file_html <- function(x, ph = '', pptx, ..., error_ca polish_content_pptx( as_file(convert_html_file_to_png(x)), - ph = ph, guess_size = TRUE, error_call = error_call + ph = ph, image_fit = "scale", error_call = error_call ) + } #' @export @@ -220,7 +232,7 @@ polish_content_pptx.file_pdf <- function(x, ph = '', pptx, ..., error_cal polish_content_pptx( as_file(convert_pdf_file_to_png(x)), - ph = ph, guess_size = TRUE, error_call = error_call + ph = ph, image_fit = "scale", error_call = error_call ) } @@ -268,6 +280,44 @@ dim_extract <- function(xml, xpath, attrs = c("x, y")) { out } +image_fit_scale <- function(image_definition, ph_offsets, ph_dims){ + + ## get image ratio + ratio <- image_definition$dims$height/image_definition$dims$width + + ##determine if scaling image for width or for height + ### get potential new width/heights + ratioed_height <- ratio * ph_dims$width + ratioed_width <- ph_dims$height/ratio + + if(ratioed_height <= ph_dims$height){ + img_height <- ratioed_height + img_width <- ph_dims$width + img_top <- ph_offsets$top+((ph_dims$height-img_height)/2) + img_left <- ph_offsets$left + }else{ + img_height <- ph_dims$height + img_width <- ratioed_width + img_top <- ph_offsets$top + img_left <- ph_offsets$left+((ph_dims$width-img_width)/2) + } + + list( + dims = list(height = img_height, width = img_width), + offset = list(top = img_top, left = img_left) + ) + +} + +image_fit_stretch <- function(image_definition, ph_offsets, ph_dims){ + + list( + dims = ph_dims, + offset = ph_offsets + ) + +} + polish_pptx_strings <- function(txt, escape = TRUE, collapse = FALSE, font_color = NULL, font_style = NULL, font_size = NULL, font_typeface = NULL, error_call = caller_env()) { if (escape){ txt <- htmlEscape(txt) diff --git a/man/polish_content_pptx.Rd b/man/polish_content_pptx.Rd index d31bfc8..2e48187 100644 --- a/man/polish_content_pptx.Rd +++ b/man/polish_content_pptx.Rd @@ -25,10 +25,11 @@ ph = "", pptx, ..., - height = 5, - width = 6, - units = "in", + height = NULL, + width = NULL, res = 300, + device = c("png", "svg", "jpeg"), + scale = 1, error_call = current_env() ) @@ -62,10 +63,10 @@ polish_content_pptx(x, ph = "", pptx, ..., error_call = current_env()) ph = "", pptx, ..., - height = 5, - width = 6, - guess_size = FALSE, + height = NULL, + width = NULL, units = "in", + image_fit = c("stretch", "scale"), error_call = current_env() ) } @@ -92,12 +93,19 @@ running function, e.g. \code{caller_env()}. The function will be mentioned in error messages as the source of the error. See the \code{call} argument of \code{\link[rlang:abort]{abort()}} for more information.} -\item{width, height, units}{Plot size in \code{units} ("in", "cm", "mm", or "px"). -defaults to 5 inches tall and 6 inches wide.} +\item{width, height}{defaults to match the height and width of the placeholder +provided. When that cannot be derived, it assumes the plot should be 5 +inches tall and 6 inches wide. Users may set the width and height, but they +most both be set.} \item{res}{Plot resolution. Also accepts a string input: "retina" (320), "print" (300), or "screen" (72). Applies only to raster output types.} +\item{device}{what graphic device to use when saving the plot. Can be one of +"png","svg", or "jpeg".} + +\item{scale}{Multiplicative scaling factor to use when saving the plot. See \link[ggplot2:ggsave]{ggplot2::ggsave}.} + \item{escape}{Should \code{x} be escaped with \code{\link[htmltools:htmlEscape]{htmltools::htmlEscape()}}} \item{collapse}{If \code{TRUE} a \verb{} element is added between the \verb{} nodes} @@ -118,7 +126,9 @@ defaults to 5 inches tall and 6 inches wide.} \item "ordered" makes a numbered list with intents. }} -\item{guess_size}{see \link[officer:external_img]{officer::external_img}} +\item{units}{see \link[officer:external_img]{officer::external_img}} + +\item{image_fit}{Should the image be distorted to match the dimensions of the placeholder, or scaled up/down and keep dimension ratio (scale). Default is "stretch".} } \description{ Polish content for Powerpoint diff --git a/tests/testthat/_snaps/pptx_ggplot.md b/tests/testthat/_snaps/pptx_ggplot.md new file mode 100644 index 0000000..5604482 --- /dev/null +++ b/tests/testthat/_snaps/pptx_ggplot.md @@ -0,0 +1,40 @@ +# polish_content_word().ggplot - partially or incorrectly defined height and width, + + Code + polish_content_pptx(p, ph = ph_dims_unset, height = NA, width = 12) + Condition + Error in `polish_content_pptx.ggplot()`: + ! object 'width_value' not found + +--- + + Code + polish_content_pptx(p, ph = ph_dims_unset, height = NULL, width = 12) + Condition + Error in `polish_content_pptx.ggplot()`: + ! object 'width_value' not found + +--- + + Code + polish_content_pptx(p, ph = ph_dims_unset, height = -10, width = 12) + Condition + Error in `polish_content_pptx.ggplot()`: + ! object 'width_value' not found + +--- + + Code + polish_content_pptx(p, ph = ph_dims_unset, height = c(10, 11), width = 12) + Condition + Error in `polish_content_pptx.ggplot()`: + ! object 'width_value' not found + +--- + + Code + polish_content_pptx(p, ph = ph_dims_unset, height = "invalid", width = 12) + Condition + Error in `polish_content_pptx.ggplot()`: + ! object 'width_value' not found + diff --git a/tests/testthat/test-pptx_file_png.R b/tests/testthat/test-pptx_file_png.R index d9543be..1166f16 100644 --- a/tests/testthat/test-pptx_file_png.R +++ b/tests/testthat/test-pptx_file_png.R @@ -3,8 +3,13 @@ test_that("polish_content_pptx.file_png() only creates one cNvPr", { ph <- "" + ## create png file p <- ggplot(mtcars, aes(mpg, cyl)) + geom_point() - xml <- polish_content_pptx(p, ph = ph) + temp_png <- tempfile(fileext = ".png") + ggsave(plot = p, filename = temp_png, width = 6, height = 5, units = "in", dpi = 300) + img_file <- as_file(temp_png) + + xml <- polish_content_pptx(img_file, ph = ph) node_cNvPr <- xml2::xml_find_all(xml, ".//p:cNvPr") expect_equal(length(node_cNvPr), 1) @@ -17,3 +22,53 @@ test_that("polish_content_pptx.file_png() only creates one cNvPr", { expect_equal(xml_attr(xml_find_all(xml, ".//a:ext"), "cx"), "7071360") expect_equal(xml_attr(xml_find_all(xml, ".//a:ext"), "cy"), "5829300") }) + + +test_that("polish_content_pptx.file_png() image_fit", { + + suppressWarnings(withr::local_package("ggplot2")) + + ph <- "" + + ## create png file + p <- ggplot(mtcars, aes(mpg, cyl)) + geom_point() + temp_wide_png <- tempfile(fileext = ".png") + ggsave(plot = p, filename = temp_wide_png, width = 6, height = 2, units = "in", dpi = 300) + img_file_wide <- as_file(temp_wide_png) + + temp_tall_png <- tempfile(fileext = ".png") + ggsave(plot = p, filename = temp_tall_png, width = 2, height = 6, units = "in", dpi = 300) + img_file_tall <- as_file(temp_tall_png) + + ## scale image, keeping ratio + xml_scale_wide <- polish_content_pptx(img_file_wide, ph = ph, image_fit = "scale") + xml_scale_tall <- polish_content_pptx(img_file_tall, ph = ph, image_fit = "scale") + + ## wide, keep x offset and width, move y offset and height + expect_equal(xml_attr(xml_find_all(xml_scale_wide, ".//a:off"), "x"), "487680") + expect_equal(xml_attr(xml_find_all(xml_scale_wide, ".//a:off"), "y"), "2216150") + expect_equal(xml_attr(xml_find_all(xml_scale_wide, ".//a:ext"), "cx"), "7071360") + expect_equal(xml_attr(xml_find_all(xml_scale_wide, ".//a:ext"), "cy"), "2357120") + + ## tall, keep y offset and height, move x offset and width + expect_equal(xml_attr(xml_find_all(xml_scale_tall, ".//a:off"), "x"), "3051810") + expect_equal(xml_attr(xml_find_all(xml_scale_tall, ".//a:off"), "y"), "480060") + expect_equal(xml_attr(xml_find_all(xml_scale_tall, ".//a:ext"), "cx"), "1943100") + expect_equal(xml_attr(xml_find_all(xml_scale_tall, ".//a:ext"), "cy"), "5829300") + + ## stretch image to ph + + xml_stretch_wide <- polish_content_pptx(img_file_wide, ph = ph, image_fit = "stretch") + xml_stretch_tall <- polish_content_pptx(img_file_tall, ph = ph, image_fit = "stretch") + + expect_equal(xml_attr(xml_find_all(xml_stretch_wide, ".//a:off"), "x"), "487680") + expect_equal(xml_attr(xml_find_all(xml_stretch_wide, ".//a:off"), "y"), "480060") + expect_equal(xml_attr(xml_find_all(xml_stretch_wide, ".//a:ext"), "cx"), "7071360") + expect_equal(xml_attr(xml_find_all(xml_stretch_wide, ".//a:ext"), "cy"), "5829300") + + expect_equal(xml_attr(xml_find_all(xml_stretch_tall, ".//a:off"), "x"), "487680") + expect_equal(xml_attr(xml_find_all(xml_stretch_tall, ".//a:off"), "y"), "480060") + expect_equal(xml_attr(xml_find_all(xml_stretch_tall, ".//a:ext"), "cx"), "7071360") + expect_equal(xml_attr(xml_find_all(xml_stretch_tall, ".//a:ext"), "cy"), "5829300") + +}) diff --git a/tests/testthat/test-pptx_ggplot.R b/tests/testthat/test-pptx_ggplot.R new file mode 100644 index 0000000..9a7cd80 --- /dev/null +++ b/tests/testthat/test-pptx_ggplot.R @@ -0,0 +1,91 @@ +## defined placeholders +ph_dims_unset <- "" +ph_dims_set <- "" + +test_that("polish_content_word().ggplot - undefined height and width", { + suppressWarnings(withr::local_package("ggplot2")) + + p <- ggplot(mtcars, aes(mpg, cyl)) + geom_point() + + ## provided ph with out set dims/location + xml_dims_unset <- polish_content_pptx(p, ph = ph_dims_unset) + + expect_equal(xml_attr(xml_find_all(xml_dims_unset, ".//a:off"), "x"), "0") + expect_equal(xml_attr(xml_find_all(xml_dims_unset, ".//a:off"), "y"), "0") + expect_equal(xml_attr(xml_find_all(xml_dims_unset, ".//a:ext"), "cx"), "5486400") + expect_equal(xml_attr(xml_find_all(xml_dims_unset, ".//a:ext"), "cy"), "4572000") + + ## provided ph with set dims/location + xml_dims_set <- polish_content_pptx(p, ph = ph_dims_set) + + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:off"), "x"), "487680") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:off"), "y"), "480060") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:ext"), "cx"), "7071360") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:ext"), "cy"), "5829300") + +}) + +test_that("polish_content_word().ggplot - defined height and width,", { + + suppressWarnings(withr::local_package("ggplot2")) + + p <- ggplot(mtcars, aes(mpg, cyl)) + geom_point() + + ## provided ph with out set dims/location + xml_dims_unset <- polish_content_pptx(p, ph = ph_dims_unset, height = 10, width = 12) + + expect_equal(xml_attr(xml_find_all(xml_dims_unset, ".//a:off"), "x"), "0") + expect_equal(xml_attr(xml_find_all(xml_dims_unset, ".//a:off"), "y"), "0") + expect_equal(xml_attr(xml_find_all(xml_dims_unset, ".//a:ext"), "cx"), "10972800") + expect_equal(xml_attr(xml_find_all(xml_dims_unset, ".//a:ext"), "cy"), "9144000") + + ## provided ph with set dims/location, image dims smaller than ph, image ratio of .83333 + xml_dims_set <- polish_content_pptx(p, ph = ph_dims_set, height = 2, width = 2.4) + + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:off"), "x"), "525780") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:off"), "y"), "480060") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:ext"), "cx"), "6995160") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:ext"), "cy"), "5829300") + + ## provided ph with set dims/location, image dims larger than ph, image ratio of .83333 + xml_dims_set <- polish_content_pptx(p, ph = ph_dims_set, height = 10, width = 12) + + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:off"), "x"), "525780") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:off"), "y"), "480060") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:ext"), "cx"), "6995160") + expect_equal(xml_attr(xml_find_all(xml_dims_set, ".//a:ext"), "cy"), "5829300") + +}) + +test_that("polish_content_word().ggplot - partially or incorrectly defined height and width,", { + + suppressWarnings(withr::local_package("ggplot2")) + + p <- ggplot(mtcars, aes(mpg, cyl)) + geom_point() + + expect_snapshot( + polish_content_pptx(p, ph = ph_dims_unset, height = NA, width = 12), + error = TRUE + ) + + expect_snapshot( + polish_content_pptx(p, ph = ph_dims_unset, height = NULL, width = 12), + error = TRUE + ) + + expect_snapshot( + polish_content_pptx(p, ph = ph_dims_unset, height = -10, width = 12), + error = TRUE + ) + + expect_snapshot( + polish_content_pptx(p, ph = ph_dims_unset, height = c(10,11), width = 12), + error = TRUE + ) + + expect_snapshot( + polish_content_pptx(p, ph = ph_dims_unset, height = "invalid", width = 12), + error = TRUE + ) + +})