1 /*
2  * RGraphicsPlot.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 "RGraphicsPlot.hpp"
17 
18 #include <iostream>
19 
20 #include <boost/format.hpp>
21 
22 #include <shared_core/Error.hpp>
23 #include <core/Log.hpp>
24 
25 #include <core/system/System.hpp>
26 #include <core/StringUtils.hpp>
27 
28 #include <r/RExec.hpp>
29 #include <r/session/RGraphics.hpp>
30 
31 using namespace rstudio::core;
32 
33 namespace rstudio {
34 namespace r {
35 namespace session {
36 namespace graphics {
37 
Plot(const GraphicsDeviceFunctions & graphicsDevice,const FilePath & baseDirPath,SEXP manipulatorSEXP)38 Plot::Plot(const GraphicsDeviceFunctions& graphicsDevice,
39            const FilePath& baseDirPath,
40            SEXP manipulatorSEXP)
41    : graphicsDevice_(graphicsDevice),
42      baseDirPath_(baseDirPath),
43      needsUpdate_(false),
44      manipulator_(manipulatorSEXP)
45 {
46 }
47 
Plot(const GraphicsDeviceFunctions & graphicsDevice,const FilePath & baseDirPath,const std::string & storageUuid,const DisplaySize & renderedSize)48 Plot::Plot(const GraphicsDeviceFunctions& graphicsDevice,
49            const FilePath& baseDirPath,
50            const std::string& storageUuid,
51            const DisplaySize& renderedSize)
52    : graphicsDevice_(graphicsDevice),
53      baseDirPath_(baseDirPath),
54      storageUuid_(storageUuid),
55      renderedSize_(renderedSize),
56      needsUpdate_(false),
57      manipulator_()
58 {
59    // invalidate if the image file doesn't exist (allows the server
60    // to migrate between different image backends e.g. png, jpeg, etc)
61    if (!imageFilePath(storageUuid_).exists())
62       invalidate();
63 }
64 
storageUuid() const65 std::string Plot::storageUuid() const
66 {
67    return storageUuid_;
68 }
69 
hasValidStorage() const70 bool Plot::hasValidStorage() const
71 {
72    return hasStorage() && snapshotFilePath().exists();
73 }
74 
invalidate()75 void Plot::invalidate()
76 {
77    needsUpdate_ = true;
78 }
79 
hasManipulator() const80 bool Plot::hasManipulator() const
81 {
82    // check is a bit complicated because defer loading the manipulator
83    // until it is asked for
84 
85    return !manipulator_.empty() || hasManipulatorFile();
86 }
87 
manipulatorSEXP() const88 SEXP Plot::manipulatorSEXP() const
89 {
90    if (hasManipulator())
91    {
92       loadManipulatorIfNecessary();
93       return manipulator_.sexp();
94    }
95    else
96    {
97       return R_NilValue;
98    }
99 
100    return manipulator_.sexp();
101 }
102 
manipulatorAsJson(json::Value * pValue) const103 void Plot::manipulatorAsJson(json::Value* pValue) const
104 {
105    if (hasManipulator())
106    {
107       loadManipulatorIfNecessary();
108       manipulator_.asJson(pValue);
109    }
110    else
111    {
112       *pValue = json::Value();
113    }
114 }
115 
saveManipulator() const116 void Plot::saveManipulator() const
117 {
118    if (hasManipulator() && !storageUuid_.empty())
119       saveManipulator(storageUuid_);
120 }
121 
renderFromDisplay()122 Error Plot::renderFromDisplay()
123 {
124    // we can use our cached representation if we don't need an update and our
125    // rendered size is the same as the current graphics device size
126    if ( !needsUpdate_ &&
127         (renderedSize() == graphicsDevice_.displaySize()) )
128    {
129       return Success();
130    }
131 
132    // generate a new storage uuid
133    std::string storageUuid = core::system::generateUuid();
134 
135    // generate snapshot and image files
136    FilePath snapshotPath = snapshotFilePath(storageUuid);
137    FilePath imagePath = imageFilePath(storageUuid);
138    Error error = graphicsDevice_.saveSnapshot(snapshotPath, imagePath);
139    if (error)
140       return Error(errc::PlotRenderingError, error, ERROR_LOCATION);
141 
142    // save rendered size
143    renderedSize_ = graphicsDevice_.displaySize();
144 
145    // save manipulator (if any)
146    saveManipulator(storageUuid);
147 
148    // delete existing files (if any)
149    Error removeError = removeFiles();
150 
151    // update state
152    storageUuid_ = storageUuid;
153    needsUpdate_ = false;
154 
155    // return error status
156    return removeError;
157 }
158 
renderFromDisplaySnapshot(SEXP snapshot)159 Error Plot::renderFromDisplaySnapshot(SEXP snapshot)
160 {
161    // if our baseDirPath_ no longer exists it means that someone
162    // has closed the graphics device underneath it, in this case
163    // just silently return success
164    if (!baseDirPath_.exists())
165       return Success();
166 
167    // generate a new storage uuid
168    std::string storageUuid = core::system::generateUuid();
169 
170    // generate snapshot file
171    FilePath snapshotFile = snapshotFilePath(storageUuid);
172    Error error = r::exec::RFunction(".rs.saveGraphicsSnapshot",
173                                     snapshot,
174                                     string_utils::utf8ToSystem(snapshotFile.getAbsolutePath())).call();
175    if (error)
176       return error;
177 
178    //
179    // we can't generate an image file at this point in the processing
180    // because the GraphicsDevice has already moved on to the next page. this is
181    // OK though because we simply set needsUpdate_ = true below and the next
182    // time renderFromDisplay is called it will be rendered
183    //
184 
185    // save rendered size
186    renderedSize_ = graphicsDevice_.displaySize();
187 
188    // save manipulator (if any)
189    saveManipulator(storageUuid);
190 
191    // delete existing files (if any)
192    Error removeError = removeFiles();
193 
194    // update state
195    storageUuid_ = storageUuid;
196    needsUpdate_ = true;
197 
198    // return error status
199    return removeError;
200 }
201 
202 
imageFilename() const203 std::string Plot::imageFilename() const
204 {
205    return imageFilePath(storageUuid()).getFilename();
206 }
207 
renderToDisplay()208 Error Plot::renderToDisplay()
209 {
210    Error error = graphicsDevice_.restoreSnapshot(snapshotFilePath());
211    if (error)
212    {
213       Error graphicsError(errc::PlotRenderingError, error, ERROR_LOCATION);
214       DisplaySize deviceSize = graphicsDevice_.displaySize();
215       graphicsError.addProperty("device-width", deviceSize.width);
216       graphicsError.addProperty("device-height", deviceSize.height);
217       return graphicsError;
218    }
219    else
220       return Success();
221 }
222 
removeFiles()223 Error Plot::removeFiles()
224 {
225    // bail if we don't have any storage
226    if (storageUuid_.empty())
227       return Success();
228 
229    Error snapshotError = snapshotFilePath(storageUuid_).removeIfExists();
230    Error imageError = imageFilePath(storageUuid_).removeIfExists();
231    Error manipulatorError = manipulatorFilePath(storageUuid_).removeIfExists();
232 
233    if (snapshotError)
234       return Error(errc::PlotFileError, snapshotError, ERROR_LOCATION);
235    else if (imageError)
236       return Error(errc::PlotFileError, imageError, ERROR_LOCATION);
237    else if (manipulatorError)
238       return Error(errc::PlotFileError, manipulatorError, ERROR_LOCATION);
239    else
240       return Success();
241 }
242 
purgeInMemoryResources()243 void Plot::purgeInMemoryResources()
244 {
245    manipulator_.clear();
246 }
247 
hasStorage() const248 bool Plot::hasStorage() const
249 {
250    return !storageUuid_.empty();
251 }
252 
snapshotFilePath() const253 FilePath Plot::snapshotFilePath() const
254 {
255    return snapshotFilePath(storageUuid());
256 }
257 
258 
snapshotFilePath(const std::string & storageUuid) const259 FilePath Plot::snapshotFilePath(const std::string& storageUuid) const
260 {
261    return baseDirPath_.completePath(storageUuid + ".snapshot");
262 }
263 
imageFilePath(const std::string & storageUuid) const264 FilePath Plot::imageFilePath(const std::string& storageUuid) const
265 {
266    std::string extension = graphicsDevice_.imageFileExtension();
267    return baseDirPath_.completePath(storageUuid + "." + extension);
268 }
269 
hasManipulatorFile() const270 bool Plot::hasManipulatorFile() const
271 {
272    return hasStorage() && manipulatorFilePath(storageUuid()).exists();
273 }
274 
manipulatorFilePath(const std::string & storageUuid) const275 FilePath Plot::manipulatorFilePath(const std::string& storageUuid) const
276 {
277    return baseDirPath_.completePath(storageUuid + ".manip");
278 }
279 
loadManipulatorIfNecessary() const280 void Plot::loadManipulatorIfNecessary() const
281 {
282    if (manipulator_.empty() && hasManipulatorFile())
283    {
284       FilePath manipPath = manipulatorFilePath(storageUuid());
285       Error error = manipulator_.load(manipPath);
286       if (error)
287          LOG_ERROR(error);
288    }
289 }
290 
saveManipulator(const std::string & storageUuid) const291 void Plot::saveManipulator(const std::string& storageUuid) const
292 {
293    loadManipulatorIfNecessary();
294    if (!manipulator_.empty())
295    {
296       Error error = manipulator_.save(manipulatorFilePath(storageUuid));
297       if (error)
298          LOG_ERROR(error);
299    }
300 }
301 
302 
303 } // namespace graphics
304 } // namespace session
305 } // namespace r
306 } // namespace rstudio
307 
308 
309 
310