Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 'clippingPathGrob()' and 'alphaMaskGrob()' improvements #78

Merged
merged 1 commit into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: gridpattern
Type: Package
Title: 'grid' Pattern Grobs
Version: 1.2.0-4
Version: 1.2.0-5
Authors@R: c(person("Mike", "FC", role = "aut", comment = "Code/docs adapted from ggpattern"),
person("Trevor L.", "Davis", role=c("aut", "cre"), email="[email protected]",
comment = c(ORCID = "0000-0001-6341-4639")),
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import(grid)
importFrom(glue,glue)
importFrom(grDevices,col2rgb)
importFrom(grDevices,dev.capabilities)
importFrom(grDevices,dev.capture)
importFrom(grDevices,dev.off)
importFrom(grDevices,png)
importFrom(grDevices,rgb)
Expand Down
13 changes: 13 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ New Features
* For completeness there is now a `grid.pattern_none()` corresponding to the previously
supported "none" pattern which draws nothing.

Bug fixes and minor improvements
--------------------------------

* `clippingPathGrob()` can now more efficiently compute a `rasterGrob()` approximation
via `ragg::agg_capture()` and for `png_device` functions that support
the clipping path feature such as`png(type = "cairo")`(#74).
* `alphaMaskGrob()` can now more efficiently compute a `rasterGrob()` approximation
for `png_device` functions that support
the alpha mask feature such as`png(type = "cairo")`(#75).
* `alphaMaskGrob()` and `clippingPathGrob()` now
switch back to the previously open graphics device if
they open and close any new graphics devices.

gridpattern v1.1.1
==================

Expand Down
98 changes: 68 additions & 30 deletions R/alphaMaskGrob.R
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@
#' }
#' @export
alphaMaskGrob <- function(maskee, masker,
use_R4.1_masks = getOption("ggpattern_use_R4.1_masks",
getOption("ggpattern_use_R4.1_features")),
png_device = NULL, res = getOption("ggpattern_res", 72),
name = NULL, gp = gpar(), vp = NULL) {
use_R4.1_masks = getOption("ggpattern_use_R4.1_masks",
getOption("ggpattern_use_R4.1_features")),
png_device = NULL, res = getOption("ggpattern_res", 72),
name = NULL, gp = gpar(), vp = NULL) {
gTree(maskee = maskee, masker = masker,
use_R4.1_masks = use_R4.1_masks,
res = res, png_device = png_device,
name=name, gp=gp, vp=vp, cl="alpha_mask")
name = name, gp = gp, vp = vp, cl = "alpha_mask")
}

# Avoid R CMD check WARNING on R 4.0 which lacks `mask` argument
Expand All @@ -82,52 +82,90 @@ makeContent.alpha_mask <- function(x) {
getRversion() >= '4.1.0' &&
requireNamespace("ragg", quietly = TRUE) &&
packageVersion("ragg") >= '1.2.0') {
grob <- gridpattern_mask_agg_capture(x)
grob <- gridpattern_mask_agg_capture(x$maskee, x$masker, x$res)
} else {
grob <- gridpattern_mask_raster(x)
png_device <- x$png_device %||% default_png_device()
if (device_supports_masks(png_device)) {
grob <- gridpattern_mask_raster_straight(x$maskee, x$masker, x$res, png_device)
} else {
grob <- gridpattern_mask_raster_manual(x$maskee, x$masker, x$res, png_device)
}
}

gl <- gList(grob)
setChildren(x, gl)
}

gridpattern_mask_agg_capture <- function(x) {
height <- x$res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- x$res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)
device_supports_masks <- function(png_device) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))
png_file <- tempfile(fileext = ".png")
on.exit(unlink(png_file), add = TRUE)
png_device(png_file)
value <- guess_has_R4.1_features("masks")
dev.off()
value
}

gridpattern_mask_agg_capture <- function(maskee, masker, res) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))
height <- res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)

f_masked <- ragg::agg_capture(height = height, width = width, res = x$res, bg = "transparent")
grob <- alphaMaskGrob(x$maskee, x$masker, use_R4.1_masks = TRUE)
ragg::agg_capture(height = height, width = width, res = res, bg = "transparent")
grob <- alphaMaskGrob(maskee, masker, use_R4.1_masks = TRUE)
grid.draw(grob)
raster_masked <- f_masked(native = FALSE)
raster_masked <- dev.capture(native = FALSE)
dev.off()
grid::rasterGrob(raster_masked)
}

gridpattern_mask_raster <- function(x) {
height <- x$res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- x$res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)
png_device <- x$png_device
if (is.null(png_device)) {
if (requireNamespace("ragg", quietly = TRUE)) {
png_device <- ragg::agg_png
} else {
stopifnot(capabilities("png"))
png_device <- grDevices::png
}
default_png_device <- function() {
if (requireNamespace("ragg", quietly = TRUE)) {
ragg::agg_png
} else {
stopifnot(capabilities("png"))
grDevices::png
}
}

gridpattern_mask_raster_straight <- function(maskee, masker, res, png_device) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))
height <- res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)

png_masked <- tempfile(fileext = ".png")
on.exit(unlink(png_masked), add = TRUE)
png_device(png_masked, height = height, width = width,
res = res, bg = "transparent")
grob <- alphaMaskGrob(maskee, masker, use_R4.1_masks = TRUE)
grid.draw(grob)
dev.off()

raster_masked <- png::readPNG(png_masked, native = FALSE)
grid::rasterGrob(raster_masked)
}

gridpattern_mask_raster_manual <- function(maskee, masker, res, png_device) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))
height <- res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)

png_maskee <- tempfile(fileext = ".png")
on.exit(unlink(png_maskee))
on.exit(unlink(png_maskee), add = TRUE)
png_device(png_maskee, height = height, width = width,
res = x$res, bg = "transparent")
grid.draw(x$maskee)
res = res, bg = "transparent")
grid.draw(maskee)
dev.off()

png_masker <- tempfile(fileext = ".png")
on.exit(unlink(png_masker))
on.exit(unlink(png_masker), add = TRUE)
png_device(png_masker, height = height, width = width,
res = x$res, bg = "transparent")
grid.draw(x$masker)
res = res, bg = "transparent")
grid.draw(masker)
dev.off()

raster_maskee <- png::readPNG(png_maskee, native = FALSE)
Expand Down
95 changes: 72 additions & 23 deletions R/clippingPathGrob.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
#' If `NULL` try to guess an appropriate choice.
#' Note not all graphic devices support the grid clipping path feature
#' and the grid clipping path feature does not nest.
#' @param png_device \dQuote{png} graphics device to use if `use_R4.1_clipping` is `FALSE`.
#' If `NULL` (default) will use `ragg::agg_png()` if the
#' suggested package `ragg` is available else `grDevices::png()`.
#' @param png_device \dQuote{png} graphics device to save intermediate raster data with if `use_R4.1_clipping` is `FALSE`.
#' If `NULL` and suggested package `ragg` is available
#' and versions are high enough we directly capture clipped raster via [ragg::agg_capture()].
#' Otherwise we will use `png_device`
#' (default [ragg::agg_png()] if available else [grDevices::png()]) and [png::readPNG()]
#' to manually compute a clipped raster.
#' @param res Resolution of desired `rasterGrob` in pixels per inch if `use_R4.1_clipping` is `FALSE`.
#' @return A `grid` grob
#' @inheritParams grid::polygonGrob
Expand Down Expand Up @@ -41,7 +44,7 @@ clippingPathGrob <- function(clippee, clipper,
gTree(clippee = clippee, clipper = clipper,
use_R4.1_clipping = use_R4.1_clipping,
res = res, png_device = png_device,
name=name, gp=gp, vp=vp, cl="clipping_path")
name = name, gp = gp, vp = vp, cl = "clipping_path")
}

#' @export
Expand All @@ -61,40 +64,86 @@ makeContent.clipping_path <- function(x) {
grob <- grobTree(x$clippee,
vp = viewport(clip = x$clipper),
name = "clip")
} else if (is.null(x$png_device) &&
getRversion() >= '4.1.0' &&
requireNamespace("ragg", quietly = TRUE) &&
packageVersion("ragg") >= '1.2.0') {
grob <- gridpattern_clip_agg_capture(x$clippee, x$clipper, x$res)
} else {
grob <- gridpattern_clip_raster(x)
png_device <- x$png_device %||% default_png_device()
if (device_supports_clipping(png_device)) {
grob <- gridpattern_clip_raster_straight(x$clippee, x$clipper, x$res, png_device)
} else {
grob <- gridpattern_clip_raster_manual(x$clippee, x$clipper, x$res, png_device)
}
}

gl <- gList(grob)
setChildren(x, gl)
}

gridpattern_clip_raster <- function(x) {
height <- x$res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- x$res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)
png_device <- x$png_device
if (is.null(png_device)) {
if (requireNamespace("ragg", quietly = TRUE)) {
png_device <- ragg::agg_png
} else {
stopifnot(capabilities("png"))
png_device <- grDevices::png
}
}
device_supports_clipping <- function(png_device) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))
png_file <- tempfile(fileext = ".png")
on.exit(unlink(png_file), add = TRUE)
png_device(png_file)
value <- guess_has_R4.1_features("clippingPaths")
dev.off()
value
}

gridpattern_clip_agg_capture <- function(clippee, clipper, res) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))
height <- res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)

ragg::agg_capture(height = height, width = width, res = res, bg = "transparent")
grob <- clippingPathGrob(clippee, clipper, use_R4.1_clipping = TRUE)
grid.draw(grob)
raster_clipped <- dev.capture(native = FALSE)
dev.off()
grid::rasterGrob(raster_clipped)
}

gridpattern_clip_raster_straight <- function(clippee, clipper, res, png_device) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))
height <- res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)

png_clipped <- tempfile(fileext = ".png")
on.exit(unlink(png_clipped), add = TRUE)
png_device(png_clipped, height = height, width = width,
res = res, bg = "transparent")
grob <- clippingPathGrob(clippee, clipper, use_R4.1_clipping = TRUE)
grid.draw(grob)
dev.off()

raster_clipped <- png::readPNG(png_clipped, native = FALSE)
grid::rasterGrob(raster_clipped)
}

gridpattern_clip_raster_manual <- function(clippee, clipper, res, png_device) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))
height <- res * convertHeight(unit(1, "npc"), "in", valueOnly = TRUE)
width <- res * convertWidth(unit(1, "npc"), "in", valueOnly = TRUE)

png_clippee <- tempfile(fileext = ".png")
on.exit(unlink(png_clippee))
on.exit(unlink(png_clippee), add = TRUE)
png_device(png_clippee, height = height, width = width,
res = x$res, bg = "transparent")
grid.draw(x$clippee)
res = res, bg = "transparent")
grid.draw(clippee)
dev.off()

png_clipper <- tempfile(fileext = ".png")
on.exit(unlink(png_clipper))
on.exit(unlink(png_clipper), add = TRUE)
png_device(png_clipper, height = height, width = width,
res = x$res, bg = "transparent")
res = res, bg = "transparent")
pushViewport(viewport(gp = gpar(lwd = 0, col = NA, fill = "black")))
grid.draw(x$clipper)
grid.draw(clipper)
popViewport()
dev.off()

Expand Down
2 changes: 1 addition & 1 deletion R/gridpattern-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
#' If `FALSE` do a `rasterGrob` approximation of the masked pattern.
#' If `NULL` try to guess an appropriate choice.}
#' \item{ggpattern_use_R4.1_patterns}{If `TRUE` use the grid pattern feature introduced in R v4.1.0.
#' Currently unused by this package.}
#' Currently only used by a couple of examples.}
#' }
#' Note to use the R v4.1.0 features one needs R be (at least) version 4.1 and not all graphic devices
#' support any/all these features. See \url{https://www.stat.auckland.ac.nz/~paul/Reports/GraphicsEngine/definitions/definitions.html} for more information on these features.
Expand Down
6 changes: 5 additions & 1 deletion R/utils-polygon_df.R
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,17 @@ get_poly_lengths <- function(sf_object) {
#'
#' @noRd
convert_polygon_df_to_alpha_channel <- function(polygon_df, width, height) {
current_dev <- grDevices::dev.cur()
if (current_dev > 1) on.exit(grDevices::dev.set(current_dev))

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Convert the polygon to an actual grob, coloured 'black'
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
gp <- gpar(fill = 'black')
boundary_grob <- convert_polygon_df_to_polygon_grob(polygon_df, gp=gp)

# Note `ragg::agg_capture()`'s non-"native" format is a matrix of color strings
# while `png::readPNG()`'s non-"native" format is an array of numeric values
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Save the grob as an image of the given size
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -167,7 +171,7 @@ convert_polygon_df_to_alpha_channel <- function(polygon_df, width, height) {
# Load the file and convert o a numeric matrix with values 0/1 depending
# on whether the pixel is white or black.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
alpha_channel <- png::readPNG(png_file)
alpha_channel <- png::readPNG(png_file, native = FALSE)
alpha_channel <- alpha_channel[,,1] < 0.5
storage.mode(alpha_channel) <- 'numeric'

Expand Down
2 changes: 1 addition & 1 deletion R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ img_read_memoised <- img_read

#' @import grid
#' @importFrom glue glue
#' @importFrom grDevices col2rgb dev.capabilities dev.off png rgb
#' @importFrom grDevices col2rgb dev.capture dev.capabilities dev.off png rgb
#' @importFrom rlang %||% abort inform warn
#' @importFrom utils hasName head packageVersion tail
NULL
9 changes: 6 additions & 3 deletions man/clippingPathGrob.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/gridpattern-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added tests/figs/array/alphaMaskGrob_manual.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/figs/array/clipGrob_cairo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/figs/array/clipGrob_feature.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added tests/figs/array/clipGrob_ragg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading