@@ -68,3 +68,330 @@ check_inherits <- function(x,
6868 call = call
6969 )
7070}
71+
72+ # ' Check graphics device capabilities
73+ # '
74+ # ' This function makes an attempt to estimate whether the graphics device is
75+ # ' able to render newer graphics features.
76+ # '
77+ # ' @param feature A string naming a graphics device feature. One of:
78+ # ' `"clippingPaths"`, `"alpha_masks"`, `"lumi_masks"`, `"compositing"`,
79+ # ' `"blending"`, `"transformations"`, `"gradients"`, `"patterns"`, `"paths"`
80+ # ' or `"glyphs"`. See the 'Features' section below for an explanation
81+ # ' of these terms.
82+ # ' @param action A string for what action to take. One of:
83+ # ' * `"test"` returns `TRUE` or `FALSE` indicating support of the feature.
84+ # ' * `"warn"` also returns a logical, but throws an informative warning when
85+ # ' `FALSE`.
86+ # ' * `"abort"` throws an error when the device is estimated to not support
87+ # ' the feature.
88+ # ' @param op A string for a specific operation to test for when `feature` is
89+ # ' either `"blending"` or `"compositing"`. If `NULL` (default), support for
90+ # ' all known blending or compositing operations is queried.
91+ # ' @param maybe A logical of length 1 determining what the return value should
92+ # ' be in case the device capabilities cannot be assessed.
93+ # ' @param call The execution environment of a currently running function, e.g.
94+ # ' [`caller_env()`][rlang::caller_env()]. The function will be mentioned in
95+ # ' warnings and error messages as the source of the warning or error. See
96+ # ' the `call` argument of [`abort()`][rlang::abort()] for more information.
97+ # '
98+ # ' @details
99+ # ' The procedure for testing is as follows:
100+ # '
101+ # ' * First, the \R version is checked against the version wherein a feature was
102+ # ' introduced.
103+ # ' * Next, the [dev.capabilities()][grDevices::dev.capabilities()] function is
104+ # ' queried for support of the feature.
105+ # ' * If that check is ambiguous, the \pkg{svglite} and \pkg{ragg} devices are
106+ # ' checked for known support.
107+ # ' * Lastly, if there is no answer yet, it is checked whether the device is one
108+ # ' of the 'known' devices that supports a feature.
109+ # '
110+ # ' @section Features:
111+ # ' \describe{
112+ # ' \item{`"clippingPaths"`}{While most devices support rectangular clipping
113+ # ' regions, this feature is about the support for clipping to arbitrary paths.
114+ # ' It can be used to only display a part of a drawing.}
115+ # ' \item{`"alpha_masks"`}{Like clipping regions and paths, alpha masks can also
116+ # ' be used to only display a part of a drawing. In particular a
117+ # ' semi-transparent mask can be used to display a drawing in the opaque parts
118+ # ' of the mask and hide a drawing in transparent part of a mask.}
119+ # ' \item{`"lumi_masks`}{Similar to alpha masks, but using the mask's luminance
120+ # ' (greyscale value) to determine what is drawn. Light values are opaque and
121+ # ' dark values are transparent.}
122+ # ' \item{`"compositing"`}{Compositing allows one to control how to drawings
123+ # ' are drawn in relation to one another. By default, one drawing is drawn
124+ # ' 'over' the previous one, but other operators are possible, like 'clear',
125+ # ' 'in' and 'out'.}
126+ # ' \item{`"blending"`}{When placing one drawing atop of another, the blend
127+ # ' mode determines how the colours of the drawings relate to one another.}
128+ # ' \item{`"transformations"`}{Performing an affine transformation on a group
129+ # ' can be used to translate, rotate, scale, shear and flip the drawing.}
130+ # ' \item{`"gradients"`}{Gradients can be used to show a transition between
131+ # ' two or more colours as a fill in a drawing. The checks expects both linear
132+ # ' and radial gradients to be supported.}
133+ # ' \item{`"patterns"`}{Patterns can be used to display a repeated, tiled
134+ # ' drawing as a fill in another drawing.}
135+ # ' \item{`"paths"`}{Contrary to 'paths' as polyline or polygon drawings,
136+ # ' `"paths"` refers to the ability to fill and stroke collections of
137+ # ' drawings.}
138+ # ' \item{`"glyphs"`}{Refers to the advanced typesetting feature for
139+ # ' controlling the appearance of individual glyphs.}
140+ # ' }
141+ # '
142+ # ' @section Limitations:
143+ # '
144+ # ' * On Windows machines, bitmap devices such as `png()` or `jpeg()` default
145+ # ' to `type = "windows"`. At the time of writing, these don't support any
146+ # ' new features, in contrast to `type = "cairo"`, which does. Prior to \R
147+ # ' version 4.2.0, the capabilities cannot be resolved and the value of the
148+ # ' `maybe` argument is returned.
149+ # ' * With the exception of the \pkg{ragg} and \pkg{svglite} devices, if the
150+ # ' device doesn't report their capabilities via
151+ # ' [dev.capabilities()][grDevices::dev.capabilities()], or the \R version is
152+ # ' below 4.2.0, the `maybe` value is returned.
153+ # ' * Even though patterns and gradients where introduced in \R 4.1.0, they
154+ # ' are considered unsupported because providing vectorised patterns and
155+ # ' gradients was only introduced later in \R 4.2.0.
156+ # ' * When using the RStudio graphics device, the back end is assumed to be the
157+ # ' next device on the list. This assumption is typically met by default,
158+ # ' unless the device list is purposefully rearranged.
159+ # '
160+ # ' @return `TRUE` when the feature is thought to be supported and `FALSE`
161+ # ' otherwise.
162+ # ' @export
163+ # ' @keywords internal
164+ # '
165+ # ' @examples
166+ # ' # Typically you'd run `check_device()` inside a function that might produce
167+ # ' # advanced graphics.
168+ # ' # The check is designed for use in control flow statements in the test mode
169+ # ' if (check_device("patterns", action = "test")) {
170+ # ' print("Yay")
171+ # ' } else {
172+ # ' print("Nay")
173+ # ' }
174+ # '
175+ # ' # Automatically throw a warning when unavailable
176+ # ' if (check_device("compositing", action = "warn")) {
177+ # ' print("Yay")
178+ # ' } else {
179+ # ' print("Nay")
180+ # ' }
181+ # '
182+ # ' # Possibly throw an error
183+ # ' try(check_device("glyphs", action = "abort"))
184+ check_device = function (feature , action = " warn" , op = NULL , maybe = FALSE ,
185+ call = caller_env()) {
186+
187+ check_bool(maybe , allow_na = TRUE )
188+
189+ action <- arg_match0(action , c(" test" , " warn" , " abort" ))
190+ action_fun <- switch (
191+ action ,
192+ warn = cli :: cli_warn ,
193+ abort = cli :: cli_abort ,
194+ function (... ) invisible ()
195+ )
196+
197+ feature <- arg_match0(
198+ feature ,
199+ c(" clippingPaths" , " alpha_masks" , " lumi_masks" , " compositing" , " blending" ,
200+ " transformations" , " glyphs" , " patterns" , " gradients" , " paths" ,
201+ " .test_feature" )
202+ )
203+ # Formatting prettier feature names
204+ feat_name <- switch (
205+ feature ,
206+ clippingPaths = " clipping paths" ,
207+ patterns = " tiled patterns" ,
208+ blending = " blend modes" ,
209+ gradients = " colour gradients" ,
210+ glyphs = " typeset glyphs" ,
211+ paths = " stroking and filling paths" ,
212+ transformations = " affine transformations" ,
213+ alpha_masks = " alpha masks" ,
214+ lumi_masks = " luminance masks" ,
215+ feature
216+ )
217+
218+ # Perform version check
219+ version <- getRversion()
220+ capable <- switch (
221+ feature ,
222+ glyphs = version > = " 4.3.0" ,
223+ paths = , transformations = , compositing = ,
224+ patterns = , lumi_masks = , blending = ,
225+ gradients = version > = " 4.2.0" ,
226+ alpha_masks = ,
227+ clippingPaths = version > = " 4.1.0" ,
228+ TRUE
229+ )
230+ if (isFALSE(capable )) {
231+ action_fun(" R {version} does not support {.emph {feature}}." ,
232+ call = call )
233+ return (FALSE )
234+ }
235+
236+ # Grab device for checking
237+ dev_cur <- grDevices :: dev.cur()
238+ dev_name <- names(dev_cur )
239+
240+ if (dev_name == " RStudioGD" ) {
241+ # RStudio opens RStudioGD as the active graphics device, but the back-end
242+ # appears to be the *next* device. Temporarily set the next device as the
243+ # device to check capabilities.
244+ dev_old <- dev_cur
245+ on.exit(grDevices :: dev.set(dev_old ), add = TRUE )
246+ dev_cur <- grDevices :: dev.set(grDevices :: dev.next())
247+ dev_name <- names(dev_cur )
248+ }
249+
250+ # For blending/compositing, maybe test a specific operation
251+ if (! is.null(op ) && feature %in% c(" blending" , " compositing" )) {
252+ op <- arg_match0(op , c(.blend_ops , .compo_ops ))
253+ .blend_ops <- .compo_ops <- op
254+ feat_name <- paste0(" '" , gsub(" \\ ." , " " , op ), " ' " , feat_name )
255+ }
256+
257+ # The dev.capabilities() approach may work from R 4.2.0 onwards
258+ if (version > = " 4.2.0" ) {
259+ capa <- grDevices :: dev.capabilities()
260+
261+ # Test if device explicitly states that it is capable of this feature
262+ capable <- switch (
263+ feature ,
264+ clippingPaths = isTRUE(capa $ clippingPaths ),
265+ gradients = all(c(" LinearGradient" , " RadialGradient" ) %in% capa $ patterns ),
266+ alpha_masks = " alpha" %in% capa $ masks ,
267+ lumi_masks = " luminance" %in% capa $ masks ,
268+ patterns = " TilingPattern" %in% capa $ patterns ,
269+ compositing = all(.compo_ops %in% capa $ compositing ),
270+ blending = all(.blend_ops %in% capa $ compositing ),
271+ transformations = isTRUE(capa $ transformations ),
272+ paths = isTRUE(capa $ paths ),
273+ glyphs = isTRUE(capa $ glyphs ),
274+ NA
275+ )
276+ if (isTRUE(capable )) {
277+ return (TRUE )
278+ }
279+
280+ # Test if device explicitly denies that it is capable of this feature
281+ incapable <- switch (
282+ feature ,
283+ clippingPaths = isFALSE(capa $ clippingPaths ),
284+ gradients = ! all(is.na(capa $ patterns )) &&
285+ ! all(c(" LinearGradient" , " RadialGradient" ) %in% capa $ patterns ),
286+ alpha_masks = ! is.na(capa $ masks ) && ! (" alpha" %in% capa $ masks ),
287+ lumi_masks = ! is.na(capa $ masks ) && ! (" luminance" %in% capa $ masks ),
288+ patterns = ! is.na(capa $ patterns ) && ! (" TilingPattern" %in% capa $ patterns ),
289+ compositing = ! all(is.na(capa $ compositing )) &&
290+ ! all(.compo_ops %in% capa $ compositing ),
291+ blending = ! all(is.na(capa $ compositing )) &&
292+ ! all(.blend_ops %in% capa $ compositing ),
293+ transformations = isFALSE(capa $ transformations ),
294+ paths = isFALSE(capa $ paths ),
295+ glyphs = isFALSE(capa $ glyphs ),
296+ NA
297+ )
298+
299+ if (isTRUE(incapable )) {
300+ action_fun(
301+ " The {.field {dev_name}} device does not support {.emph {feat_name}}." ,
302+ call = call
303+ )
304+ return (FALSE )
305+ }
306+ }
307+
308+ # Test {ragg}'s capabilities
309+ if (dev_name %in% c(" agg_jpeg" , " agg_ppm" , " agg_png" , " agg_tiff" )) {
310+ # We return ragg's version number if not installed, so we can suggest to
311+ # install it.
312+ capable <- switch (
313+ feature ,
314+ clippingPaths = , alpha_masks = , gradients = ,
315+ patterns = if (is_installed(" ragg" , version = " 1.2.0" )) TRUE else " 1.2.0" ,
316+ FALSE
317+ )
318+ if (isTRUE(capable )) {
319+ return (TRUE )
320+ }
321+ if (is.character(capable ) && action != " test" ) {
322+ check_installed(
323+ " ragg" , version = capable ,
324+ reason = paste0(" for graphics support of " , feat_name , " ." )
325+ )
326+ }
327+ action_fun(paste0(
328+ " The {.pkg ragg} package's {.field {dev_name}} device does not support " ,
329+ " {.emph {feat_name}}."
330+ ), call = call )
331+ return (FALSE )
332+ }
333+
334+ # The vdiffr version of the SVG device is known to not support any newer
335+ # features
336+ if (dev_name == " devSVG_vdiffr" ) {
337+ action_fun(
338+ " The {.pkg vdiffr} package's device does not support {.emph {feat_name}}." ,
339+ call = call
340+ )
341+ return (FALSE )
342+ }
343+
344+ # The same logic applies to {svglite} but is tested separately in case
345+ # {ragg} and {svglite} diverge at some point.
346+ if (dev_name == " devSVG" ) {
347+ # We'll return a version number if not installed so we can suggest it
348+ capable <- switch (
349+ feature ,
350+ clippingPaths = , gradients = , alpha_masks = ,
351+ patterns = if (is_installed(" svglite" , version = " 2.1.0" )) TRUE else " 2.1.0" ,
352+ FALSE
353+ )
354+
355+ if (isTRUE(capable )) {
356+ return (TRUE )
357+ }
358+ if (is.character(capable ) && action != " test" ) {
359+ check_installed(
360+ " svglite" , version = capable ,
361+ reason = paste0(" for graphics support of " , feat_name , " ." )
362+ )
363+ }
364+ action_fun(paste0(
365+ " The {.pkg {pkg}} package's {.field {dev_name}} device does not " ,
366+ " support {.emph {feat_name}}." ), call = call
367+ )
368+ return (FALSE )
369+ }
370+
371+ # Last resort: list of known support prior to R 4.2.0
372+ supported <- c(" pdf" , " cairo_pdf" , " cairo_ps" , " svg" )
373+ if (feature == " compositing" ) {
374+ supported <- setdiff(supported , " pdf" )
375+ }
376+ if (.Platform $ OS.type == " unix" ) {
377+ # These devices *can* be supported on Windows, but would have to have
378+ # type = "cairo", which we can't check.
379+ supported <- c(supported , " bmp" , " jpeg" , " png" , " tiff" )
380+ }
381+ if (isTRUE(dev_name %in% supported )) {
382+ return (TRUE )
383+ }
384+ action_fun(
385+ " Unable to check the capabilities of the {.field {dev_name}} device." ,
386+ call = call
387+ )
388+ return (maybe )
389+ }
390+
391+ .compo_ops <- c(" clear" , " source" , " over" , " in" , " out" , " atop" , " dest" ,
392+ " dest.over" , " dest.in" , " dest.out" , " dest.atop" , " xor" , " add" ,
393+ " saturate" )
394+
395+ .blend_ops <- c(" multiply" , " screen" , " overlay" , " darken" , " lighten" ,
396+ " color.dodge" , " color.burn" , " hard.light" , " soft.light" ,
397+ " difference" , " exclusion" )
0 commit comments