1 /*
2  * RGraphicsUtils.cpp
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include "RGraphicsUtils.hpp"
17 
18 #include <boost/format.hpp>
19 
20 #include <shared_core/Error.hpp>
21 
22 #include <core/Algorithm.hpp>
23 #include <core/Log.hpp>
24 
25 #include <r/RExec.hpp>
26 #include <r/ROptions.hpp>
27 #include <r/RUtil.hpp>
28 
29 #include <Rinternals.h>
30 #define R_USE_PROTOTYPES 1
31 #include <R_ext/GraphicsEngine.h>
32 #include <R_ext/GraphicsDevice.h>
33 
34 #include <r/session/RGraphicsConstants.h>
35 
36 #include <r/RErrorCategory.hpp>
37 
38 using namespace rstudio::core;
39 
40 namespace rstudio {
41 namespace r {
42 namespace session {
43 namespace graphics {
44 
45 namespace {
46 
47 int s_compatibleEngineVersion = 13;
48 
49 #ifdef __APPLE__
50 class QuartzStatus : boost::noncopyable
51 {
52 public:
QuartzStatus()53    QuartzStatus() : checked_(false), installed_(false) {}
54 
isInstalled()55    bool isInstalled()
56    {
57       if (!checked_)
58       {
59          checked_ = true;
60 
61          Error error = r::exec::RFunction("capabilities",
62                                           "aqua").call(&installed_);
63          if (error)
64             LOG_ERROR(error);
65       }
66 
67       return installed_;
68    }
69 
70 private:
71    bool checked_;
72    bool installed_;
73 };
74 
hasRequiredGraphicsDevices(std::string * pMessage)75 bool hasRequiredGraphicsDevices(std::string* pMessage)
76 {
77    static QuartzStatus s_quartzStatus;
78    if (!s_quartzStatus.isInstalled())
79    {
80       if (pMessage != nullptr)
81       {
82          *pMessage = "\nWARNING: The version of R you are running against "
83                      "does not support the quartz graphics device (which is "
84                      "required by RStudio for graphics). The Plots tab will "
85                      "be disabled until a version of R that supports quartz "
86                      "is installed.";
87       }
88       return false;
89    }
90    else
91    {
92       return true;
93    }
94 }
95 
96 #else
97 
hasRequiredGraphicsDevices(std::string * pMessage)98 bool hasRequiredGraphicsDevices(std::string* pMessage)
99 {
100    return true;
101 }
102 
103 #endif
104 
105 } // anonymous namespace
106 
getDefaultBackend()107 std::string getDefaultBackend()
108 {
109    return r::options::getOption<std::string>(
110             kGraphicsOptionBackend,
111             "default",
112             false);
113 }
114 
getDefaultAntialiasing()115 std::string getDefaultAntialiasing()
116 {
117    return r::options::getOption<std::string>(
118             kGraphicsOptionAntialias,
119             "default",
120             false);
121 }
122 
setCompatibleEngineVersion(int version)123 void setCompatibleEngineVersion(int version)
124 {
125    s_compatibleEngineVersion = version;
126 }
127 
validateRequirements(std::string * pMessage)128 bool validateRequirements(std::string* pMessage)
129 {
130    // get engineVersion
131    int engineVersion = R_GE_getVersion();
132 
133    // version too old
134    if (engineVersion < 5)
135    {
136       if (pMessage != nullptr)
137       {
138          boost::format fmt(
139             "R graphics engine version %1% is not supported by RStudio. "
140             "The Plots tab will be disabled until a newer version of "
141             "R is installed.");
142          *pMessage = boost::str(fmt % engineVersion);
143       }
144 
145       return false;
146    }
147 
148    // version too new
149    else if (engineVersion > s_compatibleEngineVersion)
150    {
151       if (pMessage != nullptr)
152       {
153          boost::format fmt(
154             "R graphics engine version %1% is not supported by this "
155             "version of RStudio. The Plots tab will be disabled until "
156             "a newer version of RStudio is installed.");
157          *pMessage = boost::str(fmt % engineVersion);
158       }
159 
160       return false;
161    }
162 
163 
164    // check for required devices
165    else
166    {
167       return hasRequiredGraphicsDevices(pMessage);
168    }
169 }
170 
extraBitmapParams()171 std::string extraBitmapParams()
172 {
173    std::vector<std::string> params;
174 
175    std::vector<std::string> supportedBackends;
176    Error error = r::exec::RFunction(".rs.graphics.supportedBackends").call(&supportedBackends);
177    if (error)
178    {
179       LOG_ERROR(error);
180       return "";
181    }
182 
183    std::string backend = getDefaultBackend();
184 
185    // if the requested backend is not supported, silently use the default
186    // (this could happen if a package's configuration was migrated from
187    // one machine to another on a different OS)
188    if (backend != "default")
189    {
190       auto it = std::find(supportedBackends.begin(), supportedBackends.end(), backend);
191       if (it == supportedBackends.end())
192          backend = "default";
193    }
194 
195    // don't use the 'ragg' backend here (these parameters are normally passed
196    // to devices defined by the 'grDevices' package, and it doesn't handle
197    // 'ragg')
198    if (backend != "default" && backend != "ragg")
199    {
200       params.push_back("type = \"" + backend + "\"");
201    }
202 
203    std::string antialias = getDefaultAntialiasing();
204 
205 #ifdef _WIN32
206    // fix up antialias for windows backend
207    if ((backend == "windows" || backend == "default") && antialias == "subpixel")
208       antialias = "cleartype";
209 #endif
210 
211    if (antialias != "default")
212       params.push_back("antialias = \"" + antialias + "\"");
213 
214    if (params.empty())
215       return "";
216 
217    return ", " + core::algorithm::join(params, ", ");
218 }
219 
220 struct RestorePreviousGraphicsDeviceScope::Impl
221 {
Implrstudio::r::session::graphics::RestorePreviousGraphicsDeviceScope::Impl222    Impl() : pPreviousDevice(nullptr) {}
223    pGEDevDesc pPreviousDevice;
224 };
225 
226 
RestorePreviousGraphicsDeviceScope()227 RestorePreviousGraphicsDeviceScope::RestorePreviousGraphicsDeviceScope()
228    : pImpl_(new Impl())
229 {
230    // save ptr to previously selected device (if there is one)
231    pImpl_->pPreviousDevice = Rf_NoDevices() ? nullptr : GEcurrentDevice();
232 }
233 
~RestorePreviousGraphicsDeviceScope()234 RestorePreviousGraphicsDeviceScope::~RestorePreviousGraphicsDeviceScope()
235 {
236    try
237    {
238       // reslect the previously selected device if we had one
239       if (pImpl_->pPreviousDevice != nullptr)
240       {
241          int deviceNumber = Rf_ndevNumber(pImpl_->pPreviousDevice->dev);
242          Rf_selectDevice(deviceNumber);
243       }
244    }
245    catch (...)
246    {
247    }
248 }
249 
reportError(const core::Error & error)250 void reportError(const core::Error& error)
251 {
252    std::string endUserMessage = r::endUserErrorMessage(error);
253    std::string errmsg = "Graphics error: " + endUserMessage;
254    REprintf("%s\n", errmsg.c_str());
255 }
256 
logAndReportError(const Error & error,const ErrorLocation & location)257 void logAndReportError(const Error& error, const ErrorLocation& location)
258 {
259    // log
260    core::log::logError(error, location);
261 
262    // report to user
263    reportError(error);
264 }
265 
266 
267 } // namespace graphics
268 } // namespace session
269 } // namespace r
270 } // namespace rstudio
271 
272