From baf45561ea28cdf5f67bd258d5dca79348b1ab0b Mon Sep 17 00:00:00 2001 From: Mark Heckmann Date: Thu, 29 Aug 2024 14:00:13 +0200 Subject: [PATCH] add layout_dedupe_ph_labels() for duplicate placholder labels (#589) Building on the code by @Majid-Eismann, I added `layout_dedupe_ph_labels()` to handle duplicate placholder labels. By default, it will only detect duplicate labels, but apply no changes. With `action = "rename"`, it auto-renames duplicate labels and `action = "delete"` deletes duplicates, only keeping their first occurence. If requested, output is printed to the console, informing the user about the changes applied to the placeholder labels. --- DESCRIPTION | 5 +- NAMESPACE | 1 + NEWS.md | 4 + R/layout.R | 80 ------------------ R/ph_location.R | 2 +- R/ppt_ph_dedupe_layout.R | 140 ++++++++++++++++++++++++++++++++ R/pptx_informations.R | 1 + inst/doc_examples/ph_dupes.pptx | Bin 0 -> 22636 bytes man/annotate_base.Rd | 1 + man/layout_dedupe_ph_labels.Rd | 41 ++++++++++ 10 files changed, 192 insertions(+), 83 deletions(-) delete mode 100644 R/layout.R create mode 100644 R/ppt_ph_dedupe_layout.R create mode 100644 inst/doc_examples/ph_dupes.pptx create mode 100644 man/layout_dedupe_ph_labels.Rd diff --git a/DESCRIPTION b/DESCRIPTION index d9aca146..8a2f7e7e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: officer Title: Manipulation of Microsoft Word and PowerPoint Documents -Version: 0.6.7.005 +Version: 0.6.7.006 Authors@R: c( person("David", "Gohel", , "david.gohel@ardata.fr", role = c("aut", "cre")), person("Stefan", "Moog", , "moogs@gmx.de", role = "aut"), @@ -48,7 +48,8 @@ Imports: utils, uuid, xml2 (>= 1.1.0), - zip (>= 2.1.0) + zip (>= 2.1.0), + cli Suggests: devEMF, doconv (>= 0.3.0), diff --git a/NAMESPACE b/NAMESPACE index e6e667e2..272e58df 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -228,6 +228,7 @@ export(headers_replace_img_at_bkm) export(headers_replace_text_at_bkm) export(hyperlink_ftext) export(image_to_base64) +export(layout_dedupe_ph_labels) export(layout_properties) export(layout_summary) export(media_extract) diff --git a/NEWS.md b/NEWS.md index 5a483a2f..f5440dfa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,10 @@ that can not contain ' ' and trigger an error if it contains a ' '. ## Features +- add `layout_dedupe_ph_labels()` to handle duplicate placholder labels (#589). +By default, it will only detect duplicate labels, but apply no changes. With +`action = "rename"`, it auto-renames duplicate labels and `action = "delete"` +deletes duplicates, only keeping their first occurence. - new convenience functions `body_replace_gg_at_bkm()` and `body_replace_plot_at_bkm()` to replace text content enclosed in a bookmark with a ggplot or a base plot. - add `unit` (in, cm, mm) argument in function `page_size()`. diff --git a/R/layout.R b/R/layout.R deleted file mode 100644 index 7e84803b..00000000 --- a/R/layout.R +++ /dev/null @@ -1,80 +0,0 @@ -# rename function -layout_duplicate_rename <- function(x) { - - for (slide_layout in split(x$slideLayouts$get_xfrm_data(),factor(x$slideLayouts$get_xfrm_data()$file))) { - - for(duplicated_layout in unique(slide_layout$ph_label[duplicated(slide_layout$ph_label)])) { - - duplicated_ids <- as.numeric( - slide_layout[ - slide_layout$ph_label %in% slide_layout$ph_label[duplicated(slide_layout$ph_label)], - ]$id - ) - - rename_ids <- - setdiff(duplicated_ids, min(duplicated_ids)) - - layout_file <- - list.files(x$package_dir, paste0(slide_layout$file[1], "$"), recursive = T, full.names = T) - - layout_xml <- - xml2::read_xml(layout_file) - - for (rename_index in 1:length(rename_ids)) { - - layout_delete_nodes <- - xml2::xml_find_all(layout_xml, sprintf("p:cSld/p:spTree/*[p:nvSpPr/p:cNvPr[@id='%s']]", rename_ids[rename_index])) - - xml2::xml_set_attr( - xml2::xml_find_all( - layout_xml, - sprintf("//p:cNvPr[@id='%s']", rename_ids[rename_index]) - ), - "name", paste0(duplicated_layout, " ", rename_index) - ) - - } - - xml2::write_xml(layout_xml, file =layout_file) - - } - - } -} -# delete function -layout_duplicate_delete <- function(x, keep_max_id = TRUE) { - - for (slide_layout in split(x$slideLayouts$get_xfrm_data(),factor(x$slideLayouts$get_xfrm_data()$file))) { - - for(duplicated_layout in unique(slide_layout$ph_label[duplicated(slide_layout$ph_label)])) { - - duplicated_ids <- as.numeric( - slide_layout[ - slide_layout$ph_label %in% slide_layout$ph_label[duplicated(slide_layout$ph_label)], - ]$id - ) - - delete_ids <- - setdiff(duplicated_ids, ifelse(keep_max_id, max(duplicated_ids), min(duplicated_ids))) - - layout_file <- - list.files(x$package_dir, paste0(slide_layout$file[1], "$"), recursive = T, full.names = T) - - layout_xml <- - xml2::read_xml(layout_file) - - for (delete_id in delete_ids) { - - layout_delete_node <- - xml2::xml_find_all(layout_xml, sprintf("p:cSld/p:spTree/*[p:nvSpPr/p:cNvPr[@id='%s']]", delete_id)) - - xml2::xml_remove(layout_delete_node) - - } - - xml2::write_xml(layout_xml, file =layout_file) - - } - - } -} \ No newline at end of file diff --git a/R/ph_location.R b/R/ph_location.R index 452adde7..01fc200b 100644 --- a/R/ph_location.R +++ b/R/ph_location.R @@ -311,7 +311,7 @@ fortify_location.location_label <- function( x, doc, ...){ if( nrow(props) > 1) { stop("Placeholder ", shQuote(x$ph_label), - " in the slide layout is duplicated. It needs to be unique.") + " in the slide layout is duplicated. It needs to be unique. Hint: layout_dedupe_ph_labels() helps handling duplicates.") } props <- props[, c("offx", "offy", "cx", "cy", "ph_label", "ph", "type", "rotation", "fld_id", "fld_type")] diff --git a/R/ppt_ph_dedupe_layout.R b/R/ppt_ph_dedupe_layout.R new file mode 100644 index 00000000..b486cde4 --- /dev/null +++ b/R/ppt_ph_dedupe_layout.R @@ -0,0 +1,140 @@ +#' Detect and handle duplicate placeholder labels +#' +#' PowerPoint does not enforce unique placeholder labels in a layout. +#' Selecting a placeholder via its label using [ph_location_label] will throw +#' an error, if the label is not unique. [layout_dedupe_ph_labels] helps to detect, +#' rename, or delete duplicate placholder labels. +#' +#' @param x An `rpptx` object. +#' @param action Action to perform on duplicate placeholder labels. One of: +#' * `detect` (default) = show info on dupes only, make no changes +#' * `rename` = create unique labels. Labels are renamed by appending a sequential number +#' separated by dot to duplicate labels. For example, `c("title", "title")` becomes `c("title.1", "title.2")`. +#' * `delete` = only keep one of the placeholders with a duplicate label +#' @param print_info Print action information (e.g. renamed placeholders) to console? +#' Default is `FALSE`. Always `TRUE` for action `detect`. +#' @return A `rpptx` object (with modified placeholder labels). +#' @export +#' @examples +#' x <- read_pptx() +#' layout_dedupe_ph_labels(x) +#' +#' file <- system.file("doc_examples", "ph_dupes.pptx", package = "officer") +#' x <- read_pptx(file) +#' layout_dedupe_ph_labels(x) +#' layout_dedupe_ph_labels(x, "rename", print_info = TRUE) +#' +layout_dedupe_ph_labels <- function(x, action = "detect", print_info = FALSE) { + if (!inherits(x, "rpptx")) { + stop("'x' must be an 'rpptx' object", call. = FALSE) + } + action <- match.arg(action, c("detect", "rename", "delete")) + layout_names <- x$slideLayouts$get_metadata()$filename + xfrm_list <- lapply(layout_names, .dedupe_phs_in_layout, x = x, action = action) + x <- reload_slidelayouts(x) # reinit slideLayouts to get processed ph labels [e.g. when calling x$slideLayouts$get_xfrm_data()] + if (print_info | action == "detect") { + .print_dedupe_info(x = x, xfrm_list = xfrm_list, action = action) + } + invisible(x) +} + + +# handle placeholder labels in a single layout +# +# layout_file: layout filename (e.g. "slideLayout1.xml"). +# x: An `rpptx` object +# +# returns: Dataframe with placeholder info. Only needed for .print_dedupe_info() +.dedupe_phs_in_layout <- function(layout_file, x, action = "rename") { + ph_label <- NULL + if (!grepl("\\.xml$", layout_file, ignore.case = TRUE)) { + stop("'layout_file' must be an .xml file", call. = FALSE) + } + action <- match.arg(action, c("detect", "rename", "delete")) + layout <- x$slideLayouts$collection_get(layout_file) + xfrm <- layout$xfrm() + xfrm <- subset(xfrm, duplicated(ph_label) | duplicated(ph_label, fromLast = TRUE)) + if (nrow(xfrm) == 0) { + return() + } + xfrm <- transform(xfrm, ph_label_new = make_strings_unique(ph_label), delete_flag = duplicated(ph_label)) # prepare once for all action types + if (action == "detect") { + return(xfrm) # no further action required + } else if (action == "rename") { + xfrm$delete_flag <- FALSE + } else if (action == "delete") { + xfrm$ph_label_new <- xfrm$ph_label + } + + # rename label or delete ph shape + layout_xml <- layout$get() + for (i in 1L:nrow(xfrm)) { + shape <- xml2::xml_find_first(layout_xml, sprintf("p:cSld/p:spTree/*[p:nvSpPr/p:cNvPr[@id='%s']]", xfrm$id[i])) + if (xfrm$delete_flag[i]) { + xml2::xml_remove(shape) + } else { + xml2::xml_find_first(shape, ".//p:cNvPr") |> xml2::xml_set_attr("name", xfrm$ph_label_new[i]) + } + } + layout$save() # persist changes in slideout xml file + xfrm +} + + +# reload slideLayouts (if layout XML in package_dir has changed) +reload_slidelayouts <- function(x) { + x$slideLayouts$initialize(x$package_dir, + master_metadata = x$masterLayouts$get_metadata(), + master_xfrm = x$masterLayouts$xfrm() + ) + x +} + + +# Create unique string by appending a sepatator and a number +# make_strings_unique(c("A", "B", "B", "C", "A")) +make_strings_unique <- function(x, sep = ".") { + ii <- stats::ave(x, x, FUN = seq_along) + paste0(x, sep, ii) +} + + +# helper mostly for testing +has_ph_dupes <- function(x) { + if (!inherits(x, "rpptx")) { + stop("'x' must be an 'rpptx' object", call. = FALSE) + } + xfrm <- x$slideLayouts$get_xfrm_data() + dupes <- stats::aggregate(ph_label ~ master_name + name, data = xfrm, FUN = function(x) sum(duplicated(x)) > 0) + any(dupes$ph_label) +} + + +# print info on what was done (if print_info = TRUE) +.print_dedupe_info <- function(x, xfrm_list, action) { + .df_1 <- do.call(rbind, xfrm_list) + if (is.null(.df_1)) { + cat("No duplicate placeholder labels detected.") + return(invisible(NULL)) + } + .df_2 <- x$slideLayouts$get_xfrm_data() + .df_2 <- .df_2[, c("master_file", "master_name"), drop = FALSE] |> unique() + df <- merge(.df_1, .df_2, sort = FALSE) + rownames(df) <- NULL + df <- df[, c("master_name", "name", "ph_label", "ph_label_new", "delete_flag"), drop = FALSE] + colnames(df)[2] <- "layout_name" + if (action == "detect") { + cat("Placeholders with duplicate labels:\n") + cat(cli::col_grey("* 'ph_label_new' = new placeholder label for action = 'rename'\n")) + cat(cli::col_grey("* 'delete_flag' = deleted placeholders for action = 'delete'\n")) + } else if (action == "rename") { + df$delete_flag <- NULL + cat("Renamed duplicate placeholder labels:\n") + cat(cli::col_grey("* 'ph_label_new' = new placeholder label\n")) + } else if (action == "delete") { + df <- df[df$delete_flag, , drop = FALSE] + df$ph_label_new <- NULL + cat("Removed placeholders with duplicate labels:\n") + } + print(df) +} diff --git a/R/pptx_informations.R b/R/pptx_informations.R index 31e9fc76..2a386bf4 100644 --- a/R/pptx_informations.R +++ b/R/pptx_informations.R @@ -146,6 +146,7 @@ plot_layout_properties <- function (x, layout = NULL, master = NULL, labels = TR #' \code{ph_location*} calls. The parameters are printed in their corresponding shapes. #' #' Note that if there are duplicated \code{ph_label}, you should not use \code{ph_location_label}. +#' Hint: You can dedupe labels using \code{\link{layout_dedupe_ph_labels}}. #' #' @param path path to the pptx file to use as base document or NULL to use the officer default #' @param output_file filename to store the annotated powerpoint file or NULL to suppress generation diff --git a/inst/doc_examples/ph_dupes.pptx b/inst/doc_examples/ph_dupes.pptx new file mode 100644 index 0000000000000000000000000000000000000000..fbabc28cc9d98c1b7ac57779cccdb67bb5aef790 GIT binary patch literal 22636 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