1 /*
2  * SessionPlots.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 "SessionPlots.hpp"
17 
18 #include <boost/format.hpp>
19 #include <boost/iostreams/filter/regex.hpp>
20 
21 #include <shared_core/Error.hpp>
22 #include <core/Log.hpp>
23 #include <core/Exec.hpp>
24 #include <core/Predicate.hpp>
25 #include <shared_core/FilePath.hpp>
26 #include <core/BoostErrors.hpp>
27 #include <core/FileSerializer.hpp>
28 
29 #include <core/system/Environment.hpp>
30 
31 #include <core/text/TemplateFilter.hpp>
32 
33 #include <core/http/Request.hpp>
34 #include <core/http/Response.hpp>
35 
36 #include <r/RSexp.hpp>
37 #include <r/RExec.hpp>
38 #include <r/RRoutines.hpp>
39 #include <r/session/RGraphics.hpp>
40 
41 #include <session/SessionModuleContext.hpp>
42 
43 using namespace rstudio::core;
44 
45 namespace rstudio {
46 namespace session {
47 namespace modules {
48 namespace plots {
49 
50 namespace {
51 
52 #define MAX_FIG_SIZE 3840*2
53 
54 // locations
55 #define kGraphics "/graphics"
56 
nextPlot(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)57 Error nextPlot(const json::JsonRpcRequest& request,
58                json::JsonRpcResponse* pResponse)
59 {
60    r::session::graphics::Display& display = r::session::graphics::display();
61    return display.setActivePlot(display.activePlotIndex() + 1);
62 }
63 
previousPlot(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)64 Error previousPlot(const json::JsonRpcRequest& request,
65                    json::JsonRpcResponse* pResponse)
66 {
67    r::session::graphics::Display& display = r::session::graphics::display();
68    return display.setActivePlot(display.activePlotIndex() - 1);
69 }
70 
71 
removePlot(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)72 Error removePlot(const json::JsonRpcRequest& request,
73                  json::JsonRpcResponse* pResponse)
74 {
75    r::session::graphics::Display& display = r::session::graphics::display();
76 
77    if (display.plotCount() < 1)
78    {
79       return Error(core::json::errc::ParamInvalid, ERROR_LOCATION);
80    }
81    else if (display.plotCount() == 1)
82    {
83       r::session::graphics::display().clear();
84       return Success();
85    }
86    else
87    {
88       int activePlot = display.activePlotIndex();
89       return display.removePlot(activePlot);
90    }
91 }
92 
93 
clearPlots(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)94 Error clearPlots(const json::JsonRpcRequest& request,
95                  json::JsonRpcResponse* pResponse)
96 {
97    r::session::graphics::display().clear();
98    return Success();
99 }
100 
refreshPlot(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)101 Error refreshPlot(const json::JsonRpcRequest& request,
102                   json::JsonRpcResponse* pResponse)
103 {
104    r::session::graphics::display().refresh();
105    return Success();
106 }
107 
boolObject(bool value)108 json::Object boolObject(bool value)
109 {
110    json::Object boolObject;
111    boolObject["value"] = value;
112    return boolObject;
113 }
114 
savePlotAs(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)115 Error savePlotAs(const json::JsonRpcRequest& request,
116                  json::JsonRpcResponse* pResponse)
117 {
118    // get args
119    std::string path, format;
120    int width, height;
121    bool overwrite;
122    Error error = json::readParams(request.params,
123                                   &path,
124                                   &format,
125                                   &width,
126                                   &height,
127                                   &overwrite);
128    if (error)
129       return error;
130 
131    // resolve path
132    FilePath plotPath = module_context::resolveAliasedPath(path);
133 
134    // if it already exists and we aren't ovewriting then return false
135    if (plotPath.exists() && !overwrite)
136    {
137       pResponse->setResult(boolObject(false));
138       return Success();
139    }
140 
141    // save plot
142    using namespace rstudio::r::session::graphics;
143    Display& display = r::session::graphics::display();
144    error = display.savePlotAsImage(plotPath, format, width, height);
145    if (error)
146    {
147        LOG_ERROR(error);
148        return error;
149    }
150 
151 
152    // set success result
153    pResponse->setResult(boolObject(true));
154    return Success();
155 }
156 
157 
savePlotAsPdf(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)158 Error savePlotAsPdf(const json::JsonRpcRequest& request,
159                     json::JsonRpcResponse* pResponse)
160 {
161    // get args
162    std::string path;
163    double width, height;
164    bool useCairoPdf, overwrite;
165    Error error = json::readParams(request.params,
166                                   &path,
167                                   &width,
168                                   &height,
169                                   &useCairoPdf,
170                                   &overwrite);
171    if (error)
172       return error;
173 
174    // resolve path
175    FilePath plotPath = module_context::resolveAliasedPath(path);
176 
177    // if it already exists and we aren't ovewriting then return false
178    if (plotPath.exists() && !overwrite)
179    {
180       pResponse->setResult(boolObject(false));
181       return Success();
182    }
183 
184    // save plot
185    using namespace rstudio::r::session::graphics;
186    Display& display = r::session::graphics::display();
187    error = display.savePlotAsPdf(plotPath, width, height, useCairoPdf);
188    if (error)
189    {
190       LOG_ERROR_MESSAGE(r::endUserErrorMessage(error));
191       return error;
192    }
193 
194    // set success result
195    pResponse->setResult(boolObject(true));
196    return Success();
197 }
198 
copyPlotToClipboardMetafile(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)199 Error copyPlotToClipboardMetafile(const json::JsonRpcRequest& request,
200                                   json::JsonRpcResponse* pResponse)
201 {
202    // get args
203    int width, height;
204    Error error = json::readParams(request.params, &width, &height);
205    if (error)
206       return error;
207 
208 #if _WIN32
209 
210    // create temp file to write to
211    FilePath targetFile = module_context::tempFile("clipboard", "emf");
212 
213    // save as metafile
214    using namespace rstudio::r::session::graphics;
215    Display& display = r::session::graphics::display();
216    error = display.savePlotAsMetafile(targetFile, width, height);
217    if (error)
218       return error;
219 
220    // copy to clipboard
221    error = system::copyMetafileToClipboard(targetFile);
222    if (error)
223       return error;
224 
225    // remove temp file
226    error = targetFile.remove();
227    if (error)
228       LOG_ERROR(error);
229 
230    return Success();
231 
232 #else
233    return systemError(boost::system::errc::not_supported, ERROR_LOCATION);
234 #endif
235 }
236 
copyPlotToCocoaPasteboard(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)237 Error copyPlotToCocoaPasteboard(const json::JsonRpcRequest& request,
238                                 json::JsonRpcResponse* pResponse)
239 {
240    // get args
241    int width, height;
242    Error error = json::readParams(request.params, &width, &height);
243    if (error)
244       return error;
245 
246 #if __APPLE__
247 
248    // create temp file to write to
249    FilePath targetFile = module_context::tempFile("clipboard", "png");
250 
251    // save as png
252    using namespace rstudio::r::session::graphics;
253    Display& display = r::session::graphics::display();
254    error = display.savePlotAsImage(targetFile, "png", width, height);
255    if (error)
256       return error;
257 
258    // copy to pasteboard
259    error = module_context::copyImageToCocoaPasteboard(targetFile);
260    if (error)
261       return error;
262 
263    // remove temp file
264    error = targetFile.remove();
265    if (error)
266       LOG_ERROR(error);
267 
268    return Success();
269 
270 #else
271    return systemError(boost::system::errc::not_supported, ERROR_LOCATION);
272 #endif
273 }
274 
275 
plotsCreateRPubsHtml(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)276 Error plotsCreateRPubsHtml(const json::JsonRpcRequest& request,
277                             json::JsonRpcResponse* pResponse)
278 {
279    // get params
280    std::string title, comment;
281    int width, height;
282    Error error = json::readParams(request.params,
283                                   &title,
284                                   &comment,
285                                   &width,
286                                   &height);
287    if (error)
288       return error;
289 
290    // create a temp directory to work in
291    FilePath tempPath = module_context::tempFile("plots-rpubs", "dir");
292    error = tempPath.ensureDirectory();
293    if (error)
294       return error;
295 
296    // save small plot
297    using namespace rstudio::r::session::graphics;
298    Display& display = r::session::graphics::display();
299    FilePath smallPlotPath = tempPath.completeChildPath("plot-small.png");
300    error = display.savePlotAsImage(smallPlotPath, "png", width, height);
301    if (error)
302    {
303        LOG_ERROR(error);
304        return error;
305    }
306 
307    // save full plot
308    FilePath fullPlotPath = tempPath.completeChildPath("plot-full.png");
309    error = display.savePlotAsImage(fullPlotPath, "png", 1024, 768, 2.0);
310    if (error)
311    {
312        LOG_ERROR(error);
313        return error;
314    }
315 
316    // copy source file to temp dir
317    FilePath sourceFilePath = tempPath.completeChildPath("source.html");
318    FilePath resPath = session::options().rResourcesPath();
319    FilePath plotFilePath = resPath.completePath("plot_publish.html");
320    error = plotFilePath.copy(sourceFilePath);
321 
322    // perform the base64 encode using pandoc
323    FilePath targetFilePath = tempPath.completeChildPath("target.html");
324    error = module_context::createSelfContainedHtml(sourceFilePath,
325                                                    targetFilePath);
326    if (error)
327    {
328       LOG_ERROR(error);
329       return error;
330    }
331 
332    // return target path
333    pResponse->setResult(module_context::createAliasedPath(targetFilePath));
334 
335    // return success
336    return Success();
337 }
338 
339 
340 
341 
342 
uniqueSavePlotStem(const FilePath & directoryPath,std::string * pStem)343 Error uniqueSavePlotStem(const FilePath& directoryPath, std::string* pStem)
344 {
345    return module_context::uniqueSaveStem(directoryPath, "Rplot", pStem);
346 }
347 
getUniqueSavePlotStem(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)348 Error getUniqueSavePlotStem(const json::JsonRpcRequest& request,
349                             json::JsonRpcResponse* pResponse)
350 {
351    // get directory arg and convert to path
352    std::string directory;
353    Error error = json::readParam(request.params, 0, &directory);
354    if (error)
355       return error;
356    FilePath directoryPath = module_context::resolveAliasedPath(directory);
357 
358    // get stem
359    std::string stem;
360    error = uniqueSavePlotStem(directoryPath, &stem);
361    if (error)
362       return error;
363 
364    // set resposne
365    pResponse->setResult(stem);
366    return Success();
367 }
368 
supportsSvg()369 bool supportsSvg()
370 {
371    bool supportsSvg = false;
372    Error error = r::exec::RFunction("capabilities", "cairo").call(&supportsSvg);
373    if (error)
374       LOG_ERROR(error);
375    return supportsSvg;
376 }
377 
getSavePlotContext(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)378 Error getSavePlotContext(const json::JsonRpcRequest& request,
379                          json::JsonRpcResponse* pResponse)
380 {
381    // get directory arg
382    std::string directory;
383    Error error = json::readParam(request.params, 0, &directory);
384    if (error)
385       return error;
386 
387    // context
388    json::Object contextJson;
389 
390    // get supported formats
391    using namespace module_context;
392    using namespace rstudio::r::session::graphics;
393    json::Array formats;
394    formats.push_back(plotExportFormat("PNG", kPngFormat));
395    formats.push_back(plotExportFormat("JPEG", kJpegFormat));
396    formats.push_back(plotExportFormat("TIFF", kTiffFormat));
397    formats.push_back(plotExportFormat("BMP", kBmpFormat));
398 #if _WIN32
399    formats.push_back(plotExportFormat("Metafile", kMetafileFormat));
400 #endif
401    if(supportsSvg())
402       formats.push_back(plotExportFormat("SVG", kSvgFormat));
403    formats.push_back(plotExportFormat("EPS", kPostscriptFormat));
404    contextJson["formats"] = formats;
405 
406    // get directory path -- if it doesn't exist revert to the current
407    // working directory
408    FilePath directoryPath = module_context::resolveAliasedPath(directory);
409    if (!directoryPath.exists())
410       directoryPath = module_context::safeCurrentPath();
411 
412    // reflect directory back to caller
413    contextJson["directory"] = module_context::createFileSystemItem(directoryPath);
414 
415    // get unique stem
416    std::string stem;
417    error = uniqueSavePlotStem(directoryPath, &stem);
418    if (error)
419       return error;
420    contextJson["uniqueFileStem"] = stem;
421 
422    pResponse->setResult(contextJson);
423 
424    return Success();
425 }
426 
427 template <typename T>
extractSizeParams(const http::Request & request,T min,T max,T * pWidth,T * pHeight,http::Response * pResponse)428 bool extractSizeParams(const http::Request& request,
429                        T min,
430                        T max,
431                        T* pWidth,
432                        T* pHeight,
433                        http::Response* pResponse)
434 {
435    // get the width and height parameters
436    if (!request.queryParamValue("width",
437                                 predicate::range(min, max),
438                                 pWidth))
439    {
440       pResponse->setError(http::status::BadRequest, "invalid width");
441       return false;
442    }
443    if (!request.queryParamValue("height",
444                                 predicate::range(min, max),
445                                 pHeight))
446    {
447       pResponse->setError(http::status::BadRequest, "invalid height");
448       return false;
449    }
450 
451    // got two valid params
452    return true;
453 }
454 
setImageFileResponse(const FilePath & imageFilePath,const http::Request & request,http::Response * pResponse)455 void setImageFileResponse(const FilePath& imageFilePath,
456                           const http::Request& request,
457                           http::Response* pResponse)
458 {
459    // set content type
460    pResponse->setContentType(imageFilePath.getMimeContentType());
461 
462    // attempt gzip
463    if (request.acceptsEncoding(http::kGzipEncoding))
464       pResponse->setContentEncoding(http::kGzipEncoding);
465 
466    // set file
467    Error error = pResponse->setBody(imageFilePath);
468    if (error)
469    {
470       if (!core::isPathNotFoundError(error))
471          LOG_ERROR(error);
472       pResponse->setError(http::status::InternalServerError,
473                           error.getMessage());
474    }
475 }
476 
setTemporaryFileResponse(const FilePath & filePath,const http::Request & request,http::Response * pResponse)477 void setTemporaryFileResponse(const FilePath& filePath,
478                               const http::Request& request,
479                               http::Response* pResponse)
480 {
481    // no cache (dynamic content)
482    pResponse->setNoCacheHeaders();
483 
484    // return the file
485    pResponse->setFile(filePath, request);
486 
487    // delete the file
488    Error error = filePath.remove();
489    if (error)
490       LOG_ERROR(error);
491 }
492 
handleZoomRequest(const http::Request & request,http::Response * pResponse)493 void handleZoomRequest(const http::Request& request, http::Response* pResponse)
494 {
495    using namespace rstudio::r::session;
496 
497    // get the width and height parameters
498    int width, height;
499    if (!extractSizeParams(request, 100, MAX_FIG_SIZE, &width, &height, pResponse))
500      return;
501 
502    // fire off the plot zoom size changed event to notify the client
503    // that a new default size should be established
504    json::Object dataJson;
505    dataJson["width"] = width;
506    dataJson["height"] = height;
507    ClientEvent event(client_events::kPlotsZoomSizeChanged, dataJson);
508    module_context::enqueClientEvent(event);
509 
510    // get the scale parameter
511    int scale = request.queryParamValue("scale", 1);
512 
513    // define template
514    std::stringstream templateStream;
515    templateStream <<
516       "<html>"
517          "<head>"
518             "<title>Plot Zoom</title>"
519             "<script type=\"text/javascript\">"
520 
521                "window.onresize = function() {"
522 
523                   "var plotEl = document.getElementById('plot');"
524                   "if (plotEl && (#scale#==1) ) {"
525                      "plotEl.style.width='100%';"
526                      "plotEl.style.height='100%';"
527                   "}"
528 
529                   "if(window.activeTimer)"
530                      "clearTimeout(window.activeTimer);"
531 
532                   "window.activeTimer = setTimeout( function() { "
533 
534                      "window.location.href = "
535                         "\"plot_zoom?width=\" + Math.max(Math.min(document.body.clientWidth, " << MAX_FIG_SIZE << "), 100) "
536                               " + \"&height=\" + Math.max(Math.min(document.body.clientHeight, " << MAX_FIG_SIZE << "), 100) "
537                               " + \"&scale=\" + #scale#;"
538                    "}, 300);"
539                "}"
540             "</script>"
541          "</head>"
542          "<body style=\"margin: 0; overflow: hidden\">"
543             "<img id=\"plot\" width=\"100%\" height=\"100%\" src=\"plot_zoom_png?width=#width#&height=#height#\"/>"
544          "</body>"
545       "</html>";
546 
547    // define variables
548    std::map<std::string,std::string> variables;
549    variables["width"] = safe_convert::numberToString(width);
550    variables["height"] = safe_convert::numberToString(height);
551    variables["scale"] = safe_convert::numberToString(scale);;
552    text::TemplateFilter filter(variables);
553 
554    pResponse->setNoCacheHeaders();
555    pResponse->setBody(templateStream, filter);
556    pResponse->setContentType("text/html");
557 }
558 
handleZoomPngRequest(const http::Request & request,http::Response * pResponse)559 void handleZoomPngRequest(const http::Request& request,
560                           http::Response* pResponse)
561 {
562    using namespace rstudio::r::session;
563 
564    // get the width and height parameters
565    int width, height;
566    if (!extractSizeParams(request, 100, MAX_FIG_SIZE, &width, &height, pResponse))
567      return;
568 
569    // generate the file
570    using namespace rstudio::r::session::graphics;
571    FilePath imagePath = module_context::tempFile("plot", "png");
572    Error saveError = graphics::display().savePlotAsImage(imagePath,
573                                                          kPngFormat,
574                                                          width,
575                                                          height,
576                                                          true);
577    if (saveError)
578    {
579       pResponse->setError(http::status::InternalServerError,
580                           saveError.getMessage());
581       return;
582    }
583 
584    // send it back
585    setImageFileResponse(imagePath, request, pResponse);
586 
587    // delete the temp file
588    Error error = imagePath.remove();
589    if (error)
590       LOG_ERROR(error);
591 }
592 
handlePngRequest(const http::Request & request,http::Response * pResponse)593 void handlePngRequest(const http::Request& request,
594                       http::Response* pResponse)
595 {
596    // get the width and height parameters
597    int width, height;
598    if (!extractSizeParams(request, 100, MAX_FIG_SIZE, &width, &height, pResponse))
599       return;
600 
601    // generate the image
602    using namespace rstudio::r::session;
603    FilePath imagePath = module_context::tempFile("plot", "png");
604    Error error = graphics::display().savePlotAsImage(imagePath,
605                                                       graphics::kPngFormat,
606                                                       width,
607                                                       height);
608    if (error)
609    {
610       pResponse->setError(http::status::InternalServerError,
611                           error.getMessage());
612       return;
613    }
614 
615    // check for attachment flag and set content-disposition
616    bool attachment = request.queryParamValue("attachment") == "1";
617    if (attachment)
618    {
619       pResponse->setHeader("Content-Disposition",
620                            "attachment; filename=rstudio-plot" +
621                            imagePath.getExtension());
622    }
623 
624    // return it
625    setTemporaryFileResponse(imagePath, request, pResponse);
626 }
627 
628 
629 // NOTE: this function assumes it is retreiving the image for the currently
630 // active plot (the assumption is implied by the fact that file not found
631 // on the requested png results in a redirect to the currently active
632 // plot's png). to handle this redirection we should always maintain an
633 // entry point with these semantics. if we wish to have an entry point
634 // for obtaining arbitrary pngs then it should be separate from this.
635 
handleGraphicsRequest(const http::Request & request,http::Response * pResponse)636 void handleGraphicsRequest(const http::Request& request,
637                            http::Response* pResponse)
638 {
639    // extract plot key from request (take everything after the last /)
640    std::string uri = request.uri();
641    std::size_t lastSlashPos = uri.find_last_of('/');
642    if (lastSlashPos == std::string::npos ||
643        lastSlashPos == (uri.length() - 1))
644    {
645       std::string errmsg = "invalid graphics uri: " + uri;
646       LOG_ERROR_MESSAGE(errmsg);
647       pResponse->setNotFoundError(request);
648       return;
649    }
650    std::string filename = uri.substr(lastSlashPos+1);
651 
652    // calculate the path to the png
653    using namespace rstudio::r::session;
654    FilePath imagePath = graphics::display().imagePath(filename);
655 
656    // if it exists then return it
657    if (imagePath.exists())
658    {
659       // strong named - cache permanently (in user's browser only)
660       pResponse->setPrivateCacheForeverHeaders();
661 
662       // set the file
663       setImageFileResponse(imagePath, request, pResponse);
664    }
665    else
666    {
667       // redirect to png for currently active plot
668       if (graphics::display().hasOutput())
669       {
670          // calculate location of current image
671          std::string imageFilename = graphics::display().imageFilename();
672          std::string imageLocation = std::string(kGraphics "/") +
673                                      imageFilename;
674 
675          // redirect to it
676          pResponse->setMovedTemporarily(request, imageLocation);
677       }
678       else
679       {
680          // not found error
681          pResponse->setNotFoundError(request);
682       }
683    }
684 }
685 
686 
enquePlotsChanged(const r::session::graphics::DisplayState & displayState,bool activatePlots,bool showManipulator)687 void enquePlotsChanged(const r::session::graphics::DisplayState& displayState,
688                        bool activatePlots, bool showManipulator)
689 {
690    // build graphics output event
691    json::Object jsonPlotsState;
692    jsonPlotsState["filename"] = displayState.imageFilename;
693    jsonPlotsState["manipulator"] = displayState.manipulatorJson;
694    jsonPlotsState["width"] = displayState.width;
695    jsonPlotsState["height"] = displayState.height;
696    jsonPlotsState["plotIndex"] = displayState.activePlotIndex;
697    jsonPlotsState["plotCount"] = displayState.plotCount;
698    jsonPlotsState["activatePlots"] = activatePlots &&
699                                      (displayState.plotCount > 0);
700    jsonPlotsState["showManipulator"] = showManipulator;
701    ClientEvent plotsStateChangedEvent(client_events::kPlotsStateChanged,
702                                       jsonPlotsState);
703 
704    // fire it
705    module_context::enqueClientEvent(plotsStateChangedEvent);
706 
707 }
708 
renderGraphicsOutput(bool activatePlots,bool showManipulator)709 void renderGraphicsOutput(bool activatePlots, bool showManipulator)
710 {
711    using namespace rstudio::r::session;
712    if (graphics::display().hasOutput())
713    {
714       graphics::display().render(
715          boost::bind(enquePlotsChanged, _1, activatePlots, showManipulator));
716    }
717 }
718 
719 
onClientInit()720 void onClientInit()
721 {
722    // if we have output make sure the client knows about it
723    renderGraphicsOutput(false, false);
724 }
725 
detectChanges(bool activatePlots)726 void detectChanges(bool activatePlots)
727 {
728    // check for changes
729    using namespace rstudio::r::session;
730    if (graphics::display().hasChanges())
731    {
732       graphics::display().render(boost::bind(enquePlotsChanged,
733                                              _1,
734                                              activatePlots,
735                                              false));
736    }
737 }
738 
onDetectChanges(module_context::ChangeSource source)739 void onDetectChanges(module_context::ChangeSource source)
740 {
741    bool activatePlots = source == module_context::ChangeSourceREPL;
742    detectChanges(activatePlots);
743 }
744 
onBackgroundProcessing(bool isIdle)745 void onBackgroundProcessing(bool isIdle)
746 {
747    using namespace rstudio::r::session;
748    if (graphics::display().isActiveDevice() && graphics::display().hasChanges())
749    {
750       // verify that the last change is more than 50ms old. the reason
751       // we check for changes in the background is so we can respect
752       // plot animations (typically implemented using Sys.sleep). however,
753       // we don't want this to spill over inot incrementally rendering all
754       // plots as this will slow down overall plotting performance
755       // considerably.
756       using namespace boost::posix_time;
757       const int kChangeWindowMs = 50;
758       if ((graphics::display().lastChange() + milliseconds(kChangeWindowMs)) <
759            boost::posix_time::microsec_clock::universal_time())
760       {
761          detectChanges(isIdle); // activate plots only when idle
762       }
763    }
764 }
765 
onBeforeExecute()766 void onBeforeExecute()
767 {
768    r::session::graphics::display().onBeforeExecute();
769 }
770 
onShowManipulator()771 void onShowManipulator()
772 {
773    // render changes and show manipulator
774    renderGraphicsOutput(true, true);
775 }
776 
setManipulatorValues(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)777 Error setManipulatorValues(const json::JsonRpcRequest& request,
778                            json::JsonRpcResponse* pResponse)
779 {
780    // read the params
781    json::Object jsObject;
782    Error error = json::readParam(request.params, 0, &jsObject);
783    if (error)
784       return error;
785 
786    // set them
787    using namespace rstudio::r::session;
788    graphics::display().setPlotManipulatorValues(jsObject);
789 
790    // render
791    renderGraphicsOutput(true, false);
792 
793    return Success();
794 }
795 
manipulatorPlotClicked(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)796 Error manipulatorPlotClicked(const json::JsonRpcRequest& request,
797                              json::JsonRpcResponse* pResponse)
798 {
799    // read the params
800    int x, y;
801    Error error = json::readParams(request.params, &x, &y);
802    if (error)
803       return error;
804 
805    // notify the device
806    using namespace rstudio::r::session;
807    graphics::display().manipulatorPlotClicked(x, y);
808 
809    // render
810    renderGraphicsOutput(true, false);
811 
812    return Success();
813 }
814 
rs_emitBeforeNewPlot()815 SEXP rs_emitBeforeNewPlot()
816 {
817    events().onBeforeNewPlot();
818    return R_NilValue;
819 }
820 
rs_emitNewPlot()821 SEXP rs_emitNewPlot()
822 {
823    events().onNewPlot();
824    return R_NilValue;
825 }
826 
rs_emitBeforeNewGridPage()827 SEXP rs_emitBeforeNewGridPage()
828 {
829    events().onBeforeNewGridPage();
830    return R_NilValue;
831 }
832 
rs_savePlotAsImage(SEXP fileSEXP,SEXP formatSEXP,SEXP widthSEXP,SEXP heightSEXP)833 SEXP rs_savePlotAsImage(SEXP fileSEXP,
834                         SEXP formatSEXP,
835                         SEXP widthSEXP,
836                         SEXP heightSEXP)
837 {
838    FilePath filePath(r::sexp::safeAsString(fileSEXP));
839    std::string format = r::sexp::safeAsString(formatSEXP);
840    int width = r::sexp::asInteger(widthSEXP);
841    int height = r::sexp::asInteger(heightSEXP);
842 
843    r::session::graphics::Display& display = r::session::graphics::display();
844    if (display.hasOutput())
845       display.savePlotAsImage(filePath, format, width, height);
846 
847    return R_NilValue;
848 }
849 
850 
851 } // anonymous namespace
852 
haveCairoPdf()853 bool haveCairoPdf()
854 {
855    // make sure there is a real x server running on osx
856 #ifdef __APPLE__
857    std::string display = core::system::getenv("DISPLAY");
858    if (display.empty() || (display == ":0"))
859       return false;
860 #endif
861 
862    SEXP functionSEXP = R_NilValue;
863    r::sexp::Protect rProtect;
864    r::exec::RFunction f(".rs.getPackageFunction", "cairo_pdf", "grDevices");
865    Error error = f.call(&functionSEXP, &rProtect);
866    if (error)
867    {
868       LOG_ERROR(error);
869       return false;
870    }
871 
872    return functionSEXP != R_NilValue;
873 }
874 
events()875 Events& events()
876 {
877    static Events instance;
878    return instance;
879 }
880 
initialize()881 Error initialize()
882 {
883    // subscribe to events
884    using boost::bind;
885    module_context::events().onClientInit.connect(bind(onClientInit));
886    module_context::events().onDetectChanges.connect(bind(onDetectChanges, _1));
887    module_context::events().onBeforeExecute.connect(bind(onBeforeExecute));
888    module_context::events().onBackgroundProcessing.connect(onBackgroundProcessing);
889 
890    RS_REGISTER_CALL_METHOD(rs_emitBeforeNewPlot, 0);
891    RS_REGISTER_CALL_METHOD(rs_emitBeforeNewGridPage, 0);
892    RS_REGISTER_CALL_METHOD(rs_emitNewPlot, 0);
893    RS_REGISTER_CALL_METHOD(rs_savePlotAsImage, 4);
894 
895    // connect to onShowManipulator
896    using namespace rstudio::r::session;
897    graphics::display().onShowManipulator().connect(bind(onShowManipulator));
898 
899    using namespace module_context;
900    ExecBlock initBlock;
901    initBlock.addFunctions()
902       (bind(registerRpcMethod, "next_plot", nextPlot))
903       (bind(registerRpcMethod, "previous_plot", previousPlot))
904       (bind(registerRpcMethod, "remove_plot", removePlot))
905       (bind(registerRpcMethod, "clear_plots", clearPlots))
906       (bind(registerRpcMethod, "refresh_plot", refreshPlot))
907       (bind(registerRpcMethod, "save_plot_as", savePlotAs))
908       (bind(registerRpcMethod, "save_plot_as_pdf", savePlotAsPdf))
909       (bind(registerRpcMethod, "copy_plot_to_clipboard_metafile", copyPlotToClipboardMetafile))
910       (bind(registerRpcMethod, "copy_plot_to_cocoa_pasteboard", copyPlotToCocoaPasteboard))
911       (bind(registerRpcMethod, "plots_create_rpubs_html", plotsCreateRPubsHtml))
912       (bind(registerRpcMethod, "get_unique_save_plot_stem", getUniqueSavePlotStem))
913       (bind(registerRpcMethod, "get_save_plot_context", getSavePlotContext))
914       (bind(registerRpcMethod, "set_manipulator_values", setManipulatorValues))
915       (bind(registerRpcMethod, "manipulator_plot_clicked", manipulatorPlotClicked))
916       (bind(registerUriHandler, kGraphics "/plot_zoom_png", handleZoomPngRequest))
917       (bind(registerUriHandler, kGraphics "/plot_zoom", handleZoomRequest))
918       (bind(registerUriHandler, kGraphics "/plot.png", handlePngRequest))
919       (bind(registerUriHandler, kGraphics, handleGraphicsRequest))
920       (bind(module_context::sourceModuleRFile, "SessionPlots.R"));
921 
922    return initBlock.execute();
923 }
924 
925 } // namespace plots
926 } // namespace modules
927 } // namespace session
928 } // namespace rstudio
929 
930