简体   繁体   中英

How do I set the device-copy parameters when initializing a graphics device?

I'm working on a graphics device in a R package, and need to set some graphical parameters for text / labels on the devices as they are initialized or reset.

This feature is described in R Internals:

The three copies of the GPar structure are used to store the current parameters (accessed via gpptr), the 'device copy' (accessed via dpptr) and space for a saved copy of the 'device copy' parameters. The current parameters are, clearly, those currently in use and are copied from the 'device copy' whenever plot.new() is called (whether or not that advances to the next 'page'). The saved copy keeps the state when the device was last completely cleared (eg when plot.new() was called with par(new=TRUE)), and is used to replay the display list.

How do I, from a package, actually access and initialize the "device copy"?

All I've been able to find is a comment containing an older, copy-pasted comment in GraphicsDevice.h :

  * 2. I found this comment in the doc for dev_Open -- looks nasty
  *    Any known instances of such a thing happening?  Should be
  *    replaced by a function to query the device for preferred gpars
  *    settings? (to be called when the device is initialised)
          *
          * NOTE that it is perfectly acceptable for this
          * function to set generic graphics parameters too
          * (i.e., override the generic parameter settings
          * which GInit sets up) all at the author's own risk
          * of course :)

I don't know if I fully understand what you're trying to do, but I think you may find this guide to be useful. Some key excerpts:

To create a running graphics device with our own functions, we call the graphicsDevice() function. While there are several methods for this, essentially we give it a list of named functions that specify the implementation of some or all of the 21 graphical primitive operations. We might give this as a list or as an instance of RDevDescMethods or of a sub-class that we define for a particular type of device. So we focus on writing these functions.

Then:

Each of the methods is passed an object of class DevDescPtr. This is also the type of the value returned by the top-level function graphicsDevice(). This is a reference the C-level data structure that represents the graphics device. We can use this to query the settings of the graphics device.

Some of these fields in the device are used when initializing the device rather than within the functions (eg those whose names are prefixed with "start"). Other fields are structural information about the rendering of different aspects of the device. For example, we can find the dimensions of the drawing area, The DevDescPtr class is essentially an opaque data type in R (containing an external pointer to the C-level data structure) and is intended to be used as if it were an R-level list. We can use the $ operator to access individual fields and we can find the names of these fields with names().

and finally:

Under some rare circumstances, it is convenient to convert the reference to an R object. We can do this by coercing it to the corresponding R class named DevDesc (ie with the "Ptr" remove), ie as(dev, "DevDesc"). This copies each of the fields in the C-level structure to the corresponding slot in the R class.

For example, the circle method of the device has this signature:

circle ( numeric, numeric, numeric, R_GE_gcontextPtr, DevDescPtr )

R_GE_gcontextPtr is:

...another reference to an instance of a C-level data type. This is the information about the "current" settings of the device. This gives us information about the current pen/foreground color, the background color, the setting for the gamma level, the line width, style, join, the character point size and expansion/magnification, and the font information. The available fields are

names(new("R_GE_gcontextPtr"))
 [1] "col"        "fill"       "gamma"      "lwd"        "lty"       
 [6] "lend"       "ljoin"      "lmitre"     "cex"        "ps"        
[11] "lineheight" "fontface"   "fontfamily"

Caveat

I will present a solution here that predominantly uses C++ code. To make it more reproducible so that it can be run from within an R console I have done this using Rcpp::cppFunction . However, this is clearly not the method you would use while building a package. The resultant functions work by accessing raw pointers to R graphics devices that the user must specify, and if you call them using a non-existent device number your R session will crash.


Solution

The three copies of the GPar structure that these comments are describing are kept together in another structure called baseSystemState , which is defined here .

Each graphics device has a pointer to a baseSystemState , and we can access the graphics device using C or C++ code if we include the header file include/R_ext/GraphicsEngine.h in our own code.

However, there's a snag. Although we can get a pointer to the baseSystemState struct, our code has no idea what this actually is, since the definition of baseSystemState and GPar are not part of the public API.

So in order to read the baseSystemState and the GPar s it contains, we have to redefine these structures in our own code (as Dirk suggested in his comment). Some of the members of GPar are also of types or enums that need to be defined first.

We can take these definitions, compactify them into a single string and use them as includes in a Rcpp::cppFunction call. Here is a wrapper function that does that, and therefore allows you to write C++ functions that have access to the existing graphic devices' parameters:

cppFunction_graphics <- function(s) 
{
  include <-  paste0("#include \"", R.home("include/R_ext/GraphicsEngine.h\""))
  Rcpp::cppFunction(s, includes = c(include, 
  "typedef enum {DEVICE= 0, NDC= 1, INCHES = 13, 
  NIC = 6, OMA1= 2, OMA2= 3, OMA3 = 4,OMA4= 5,NFC = 7, NPC= 16,USER= 12, MAR1 = 8,
  MAR2= 9, MAR3= 10,MAR4= 11, LINES = 14, CHARS =15 } GUnit; typedef struct {
  double ax; double bx; double ay; double by;} GTrans; typedef struct {int state; 
  Rboolean valid; double adj; Rboolean ann; rcolor bg; char bty; double cex; 
  double lheight; rcolor col; double crt; double din[2]; int  err; rcolor fg; 
  char family[201]; int font; double gamma; int lab[3]; int las; int lty; 
  double lwd; R_GE_lineend lend; R_GE_linejoin ljoin; double lmitre; double mgp[3];
  double mkh; int pch; double ps; int smo; double srt; double tck; double tcl;
  double xaxp[3]; char xaxs; char xaxt; Rboolean xlog; int xpd; int oldxpd;
  double yaxp[3]; char yaxs; char yaxt; Rboolean ylog; double cexbase;
  double cexmain; double cexlab; double cexsub; double cexaxis; int fontmain; 
  int fontlab; int fontsub; int fontaxis; rcolor colmain; rcolor collab; 
  rcolor colsub; rcolor colaxis; Rboolean layout; int numrows; int numcols; 
  int currentFigure; int lastFigure; double heights[200]; double widths[200]; 
  int cmHeights[200]; int cmWidths[200]; unsigned short order[10007]; int rspct;
  unsigned char respect[10007]; int mfind; double fig[4]; double fin[2];
  GUnit fUnits; double plt[4]; double pin[2]; GUnit pUnits; Rboolean defaultFigure;
  Rboolean defaultPlot; double mar[4]; double mai[4]; GUnit mUnits; double mex; 
  double oma[4]; double omi[4]; double omd[4]; GUnit oUnits; char pty; 
  double usr[4]; double logusr[4]; Rboolean new_one; int devmode; 
  double xNDCPerChar; double yNDCPerChar; double xNDCPerLine; double yNDCPerLine; 
  double xNDCPerInch; double yNDCPerInch; GTrans fig2dev; GTrans inner2dev; 
  GTrans ndc2dev; GTrans win2fig; double scale;} GPar; typedef struct {GPar dp; 
  GPar gp; GPar dpSaved; Rboolean baseDevice;} baseSystemState;"),
  env = parent.frame(2))
}

So now we can write a function that will extract or write to the graphics parameters of our choice from the device's starting parameters. Here, we will get our function to return a list of various colour parameters, but you can return whatever parameters you like from GPar , most of which are self-explanatory in the GPar struct definition

cppFunction_graphics("

Rcpp::List get_default_GPar(int devnum)
{
  pGEDevDesc dd = GEgetDevice(devnum);
  baseSystemState *bss = (baseSystemState*) dd->gesd[0]->systemSpecific;
  GPar GP = bss->dp;
  auto get_colour = [](rcolor rcol){
    return Rcpp::NumericVector::create(
       Rcpp::Named(\"red\") = rcol & 0xff,
       Rcpp::Named(\"green\") = (rcol >> 8) & 0xff,
       Rcpp::Named(\"blue\") = (rcol >> 16) & 0xff);
    };
  return Rcpp::List::create(Rcpp::Named(\"fg\") = get_colour(GP.fg),
    Rcpp::Named(\"bg\") = get_colour(GP.bg),
    Rcpp::Named(\"col\") = get_colour(GP.col),
    Rcpp::Named(\"colmain\") = get_colour(GP.colmain),
    Rcpp::Named(\"collab\") = get_colour(GP.collab),
    Rcpp::Named(\"colaxis\") = get_colour(GP.colaxis));
}

")

So now in R I can ensure I have a device operating by doing:

plot(1:10)

在此处输入图像描述

And to access the current device's default graphics parameters I can do:

get_default_GPar(dev.cur() - 1)
#> $fg
#>   red green  blue 
#>     0     0     0 
#> 
#> $bg
#>   red green  blue 
#>   255   255   255 
#> 
#> $col
#>   red green  blue 
#>     0     0     0 
#> 
#> $colmain
#>   red green  blue 
#>     0     0     0 
#> 
#> $collab
#>   red green  blue 
#>     0     0     0 
#> 
#> $colaxis
#>   red green  blue 
#>     0     0     0 

Which gives me the correct values for the default device parameters.

Now I can also write to the default device parameters if I define another function. Suppose I want to be able to change the default colour of the device's labels:

cppFunction_graphics("

void set_col(int dn, int red, int green, int blue, int alpha)
{
  int new_col = red | (green << 8) | (blue << 16) | (alpha << 24);
  pGEDevDesc dd = GEgetDevice(dn);
  baseSystemState *bss = (baseSystemState*) dd->gesd[0]->systemSpecific;
  bss->dp.collab = new_col;
}

")

Now I have a function in R that can overwrite the default label colours of the device. Let's make the default labels red:

set_col(dev.cur() - 1, 255, 0, 0, 255)

So now when I make a new plot on the same device, the labels will automatically be red:

plot(1:10)

在此处输入图像描述

So, as desired, you can change the device's gpars without interfering directly with par .

As for accessing the saved GPars and current GPars, this is just a case of changing the line GPar GP = bss->dp; to GPar GP = bss->gp or GPar GP = bss->dpSaved

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM