@@ -68,3 +68,330 @@ check_inherits <- function(x,
68
68
call = call
69
69
)
70
70
}
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