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
+ )
+
+})