Skip to content
Permalink
51009f4cff
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
723 lines (606 sloc) 18.7 KB
/*
* RShadowPngGraphicsHandler.cpp
*
* Copyright (C) 2021 by RStudio, PBC
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
#define R_INTERNAL_FUNCTIONS // Rf_warningcall
#include <iostream>
#include <gsl/gsl>
#include <boost/format.hpp>
#include <boost/bind/bind.hpp>
#include <core/system/System.hpp>
#include <core/StringUtils.hpp>
#include <r/RExec.hpp>
#include <r/ROptions.hpp>
#include <r/session/RSessionUtils.hpp>
#include <r/session/RGraphics.hpp>
#undef TRUE
#undef FALSE
#include "RGraphicsHandler.hpp"
#include "RGraphicsUtils.hpp"
#include <Rembedded.h>
using namespace rstudio::core;
using namespace boost::placeholders;
namespace rstudio {
namespace r {
namespace session {
namespace graphics {
namespace device {
void GD_Trace(const std::string&);
} // end namespace device
} // end namespace graphics
} // end namespace session
} // end namespace r
} // end namespace rstudio
#define TRACE_GD_CALL (::rstudio::r::session::graphics::device::GD_Trace(BOOST_CURRENT_FUNCTION))
namespace rstudio {
namespace r {
namespace session {
namespace graphics {
namespace handler {
namespace shadow {
namespace {
class PreserveCurrentDeviceScope
{
public:
PreserveCurrentDeviceScope()
: previousDevice_(nullptr)
{
if (!Rf_NoDevices())
{
previousDevice_ = GEcurrentDevice();
}
}
~PreserveCurrentDeviceScope()
{
try
{
// always restore previous device
if (previousDevice_ != nullptr)
{
int deviceNumber = Rf_ndevNumber(previousDevice_->dev);
Rf_selectDevice(deviceNumber);
}
}
catch (...)
{
}
}
private:
pGEDevDesc previousDevice_;
};
struct ShadowDeviceData
{
ShadowDeviceData() : pShadowPngDevice(nullptr) {}
pDevDesc pShadowPngDevice;
};
void shadowDevOff(DeviceContext* pDC)
{
// check for null pointers
if (pDC == nullptr)
{
LOG_WARNING_MESSAGE("unexpected null device context");
return;
}
if (pDC->pDeviceSpecific == nullptr)
{
LOG_WARNING_MESSAGE("unexpected null device data");
return;
}
// check and see if the device has already been turned off
ShadowDeviceData* pDevData = (ShadowDeviceData*) pDC->pDeviceSpecific;
if (pDevData->pShadowPngDevice == nullptr)
return;
// kill the device if it's still alive
pGEDevDesc geDev = desc2GEDesc(pDevData->pShadowPngDevice);
if (Rf_ndevNumber(pDevData->pShadowPngDevice) > 0)
{
// close the device -- don't log R errors because they can happen
// in the ordinary course of things for invalid graphics staes
Error error = r::exec::executeSafely(boost::bind(GEkillDevice, geDev));
if (error && !r::isCodeExecutionError(error))
LOG_ERROR(error);
}
// set to null
pDevData->pShadowPngDevice = nullptr;
}
Error shadowDevDesc(DeviceContext* pDC, pDevDesc* pDev)
{
ShadowDeviceData* pDevData = (ShadowDeviceData*)pDC->pDeviceSpecific;
// generate on demand
if (pDevData->pShadowPngDevice == nullptr ||
Rf_ndevNumber(pDevData->pShadowPngDevice) == 0)
{
pDevData->pShadowPngDevice = nullptr;
PreserveCurrentDeviceScope preserveCurrentDeviceScope;
// determine width, height, and res
int width = gsl::narrow_cast<int>(pDC->width * pDC->devicePixelRatio);
int height = gsl::narrow_cast<int>(pDC->height * pDC->devicePixelRatio);
int res = gsl::narrow_cast<int>(96.0 * pDC->devicePixelRatio);
// determine the appropriate device
std::string backend = getDefaultBackend();
// validate that the ragg package is available.
// this is mostly a sanity-check against users who might set
// the RStudioGD.backend option without explicitly installing the
// 'ragg' package, or if 'ragg' was uninstalled or otherwise removed
// from the library paths during a session.
if (backend == "ragg")
{
bool installed = false;
Error error = r::exec::RFunction(".rs.isPackageInstalled")
.addParam("ragg")
.call(&installed);
if (error || !installed)
{
if (error)
LOG_ERROR(error);
const char* msg = "package 'ragg' is not available; using default graphics backend instead";
Rf_warningcall(R_NilValue, "%s", msg);
r::options::setOption(kGraphicsOptionBackend, "default");
backend = "default";
}
}
// ensure the directory hosting the plot is available
// (plots are often created within the R session's temporary directory,
// which seems to be opportunisitically deleted in some environments)
//
// https://github.com/rstudio/rstudio/issues/2214
FilePath targetPath = pDC->targetPath;
Error error = targetPath.getParent().ensureDirectory();
if (error)
return error;
if (backend == "ragg")
{
Error error = r::exec::RFunction("ragg:::agg_png")
.addParam("filename", string_utils::utf8ToSystem(targetPath.getAbsolutePath()))
.addParam("width", width)
.addParam("height", height)
.addParam("res", res)
.call();
if (error)
return error;
}
else
{
// create PNG device (completely bail on error)
boost::format fmt("grDevices:::png(\"%1%\", %2%, %3%, res = %4% %5%)");
std::string code = boost::str(fmt %
string_utils::utf8ToSystem(targetPath.getAbsolutePath()) %
width %
height %
res %
r::session::graphics::extraBitmapParams());
Error err = r::exec::executeString(code);
if (err)
return err;
}
// save reference to shadow device
pDevData->pShadowPngDevice = GEcurrentDevice()->dev;
}
// return shadow device
*pDev = pDevData->pShadowPngDevice;
return Success();
}
// this version of the function is called from R graphics primitives
// so can (and should) throw errors in R longjmp style
pDevDesc shadowDevDesc(pDevDesc dev)
{
try
{
DeviceContext* pDC = (DeviceContext*)dev->deviceSpecific;
pDevDesc shadowDev = nullptr;
Error error = shadowDevDesc(pDC, &shadowDev);
if (error)
{
LOG_ERROR(error);
throw r::exec::RErrorException(error.getSummary());
}
return shadowDev;
}
catch(const r::exec::RErrorException& e)
{
r::exec::error("Shadow graphics device error: " +
std::string(e.message()));
}
// keep compiler happy
return nullptr;
}
FilePath tempFile(const std::string& extension)
{
FilePath tempFileDir(string_utils::systemToUtf8(R_TempDir));
FilePath tempFilePath = tempFileDir.completePath(
core::system::generateUuid(false) +
"." + extension);
return tempFilePath;
}
void shadowDevSync(DeviceContext* pDC)
{
// get the rstudio device number
pGEDevDesc rsGEDevDesc = desc2GEDesc(pDC->dev);
int rsDeviceNumber = GEdeviceNumber(rsGEDevDesc);
// copy the rstudio device's display list onto the shadow device
PreserveCurrentDeviceScope preserveCurrentDevice;
pDevDesc dev = nullptr;
Error error = shadowDevDesc(pDC, &dev);
if (error)
{
LOG_ERROR(error);
return;
}
// select the device
int deviceNumber = Rf_ndevNumber(dev);
Rf_selectDevice(deviceNumber);
// copy display list (ignore R errors because they can happen in the normal
// course of things for invalid graphics states). also suppress output
// in scope because R 3.0 seems to sneak out error messages from within
// the invalid name warning in checkValidSymbolId in dotcode.c
{
r::session::utils::SuppressOutputInScope scope;
error = r::exec::RFunction(".rs.GEcopyDisplayList", rsDeviceNumber).call();
if (error && !r::isCodeExecutionError(error))
LOG_ERROR(error);
}
}
} // anonymous namespace
bool initialize(int width, int height, double devicePixelRatio, DeviceContext* pDC)
{
pDC->targetPath = tempFile("png");
pDC->width = width;
pDC->height = height;
pDC->devicePixelRatio = devicePixelRatio;
return true;
}
DeviceContext* allocate(pDevDesc dev)
{
// create device context
DeviceContext* pDC = new DeviceContext(dev);
// create device specific
pDC->pDeviceSpecific = new ShadowDeviceData();
return pDC;
}
void destroy(DeviceContext* pDC)
{
// nix the shadow device
shadowDevOff(pDC);
// delete pointers
ShadowDeviceData* pDevData = (ShadowDeviceData*)pDC->pDeviceSpecific;
delete pDevData;
delete pDC;
}
void setSize(pDevDesc pDev)
{
dev_desc::setSize(pDev);
dev_desc::setSize(shadowDevDesc(pDev));
setDeviceAttributes(pDev);
}
void setDeviceAttributes(pDevDesc pDev)
{
pDevDesc shadowDev = shadowDevDesc(pDev);
if (shadowDev == nullptr)
return;
dev_desc::setDeviceAttributes(pDev, shadowDev);
}
// the shadow device is created during creation of the main RStudio
// interactive graphics device (so we can copy its underlying device
// attributes into the RStudio device) however if we don't turn the
// shadow device off before adding the RStudio device then it shows
// up in the display list AHEAD of the RStudio device. This is a very
// bad state because it leaves the shadow device as the default
// device whenever another device (e.g. png, pdf, x11, etc.) is closed
void onBeforeAddDevice(DeviceContext* pDC)
{
shadowDevOff(pDC);
}
void onAfterAddDevice(DeviceContext* pDC)
{
pDevDesc dev;
Error error = shadowDevDesc(pDC, &dev);
if (error)
LOG_ERROR(error);
}
Error writeToPNG(const FilePath& targetPath, DeviceContext* pDC)
{
// sync the shadow device to ensure we have the full playlist,
shadowDevSync(pDC);
// turn the shadow device off to write the file
shadowDevOff(pDC);
// if the targetPath != the bitmap path then copy it
Error error;
if (targetPath != pDC->targetPath)
{
// the target path would not exist if R failed to write the PNG
// (e.g. because the graphics device was too small for the content)
if (!pDC->targetPath.exists())
{
error = pathNotFoundError(ERROR_LOCATION);
}
else
{
error = pDC->targetPath.copy(targetPath);
Error deleteError = pDC->targetPath.remove();
if (deleteError)
LOG_ERROR(deleteError);
}
}
// regenerate the shadow device
pDevDesc dev = pDC->dev;
int width = pDC->width;
int height = pDC->height;
double devicePixelRatio = pDC->devicePixelRatio;
handler::destroy(pDC);
pDC = handler::allocate(dev);
dev->deviceSpecific = pDC;
// re-create with the correct size
if (!handler::initialize(width, height, devicePixelRatio, pDC))
return systemError(boost::system::errc::not_connected, ERROR_LOCATION);
// now update the device structure
handler::setSize(dev);
// replay the rstudio graphics device context onto the png
shadowDevSync(pDC);
// return status
return error;
}
void circle(double x,
double y,
double r,
const pGEcontext gc,
pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::circle(x, y, r, gc, pngDevDesc);
}
void line(double x1,
double y1,
double x2,
double y2,
const pGEcontext gc,
pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::line(x1, y1, x2, y2, gc, pngDevDesc);
}
void polygon(int n,
double *x,
double *y,
const pGEcontext gc,
pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::polygon(n, x, y, gc, pngDevDesc);
}
void polyline(int n,
double *x,
double *y,
const pGEcontext gc,
pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::polyline(n, x, y, gc, pngDevDesc);
}
void rect(double x0,
double y0,
double x1,
double y1,
const pGEcontext gc,
pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::rect(x0, y0, x1, y1, gc, pngDevDesc);
}
void path(double *x,
double *y,
int npoly,
int *nper,
Rboolean winding,
const pGEcontext gc,
pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return;
dev_desc::path(x, y, npoly, nper, winding, gc, pngDevDesc);
}
void raster(unsigned int *raster,
int w,
int h,
double x,
double y,
double width,
double height,
double rot,
Rboolean interpolate,
const pGEcontext gc,
pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return;
dev_desc::raster(raster,
w,
h,
x,
y,
width,
height,
rot,
interpolate,
gc,
pngDevDesc);
}
SEXP cap(pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return R_NilValue;
return dev_desc::cap(pngDevDesc);
}
void metricInfo(int c,
const pGEcontext gc,
double* ascent,
double* descent,
double* width,
pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::metricInfo(c, gc, ascent, descent, width, pngDevDesc);
}
double strWidth(const char *str, const pGEcontext gc, pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return gsl::narrow_cast<double>(::strlen(str));
return dev_desc::strWidth(str, gc, pngDevDesc);
}
void text(double x,
double y,
const char *str,
double rot,
double hadj,
const pGEcontext gc,
pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::text(x, y, str, rot, hadj, gc, pngDevDesc);
}
void clip(double x0, double x1, double y0, double y1, pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::clip(x0, x1, y0, y1, pngDevDesc);
}
void newPage(const pGEcontext gc, pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::newPage(gc, pngDevDesc);
}
void mode(int mode, pDevDesc dev)
{
pDevDesc pngDevDesc = shadowDevDesc(dev);
if (pngDevDesc == nullptr)
return;
dev_desc::mode(mode, pngDevDesc);
}
void onBeforeExecute(DeviceContext* pDC)
{
// if the shadow device has somehow become the active device
// then switch to the rstudio device. note this can occur if the
// user creates another device such as windows() or postscript() and
// then does a dev.off
pGEDevDesc pCurrentDevice = NoDevices() ? nullptr : GEcurrentDevice();
ShadowDeviceData* pShadowDevData = (ShadowDeviceData*)pDC->pDeviceSpecific;
if (pCurrentDevice != nullptr && pShadowDevData != nullptr)
{
if (pCurrentDevice->dev == pShadowDevData->pShadowPngDevice)
{
// select the rstudio device
int deviceNumber = Rf_ndevNumber(pDC->dev);
Rf_selectDevice(deviceNumber);
}
}
}
SEXP setPattern(SEXP pattern, pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return R_NilValue;
return dev_desc::setPattern(pattern, pngDevDesc);
}
void releasePattern(SEXP ref, pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return;
dev_desc::releasePattern(ref, pngDevDesc);
}
SEXP setClipPath(SEXP path, SEXP ref, pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return R_NilValue;
return dev_desc::setClipPath(path, ref, pngDevDesc);
}
void releaseClipPath(SEXP ref, pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return;
dev_desc::releaseClipPath(ref, pngDevDesc);
}
SEXP setMask(SEXP path, SEXP ref, pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return R_NilValue;
return dev_desc::setMask(path, ref, pngDevDesc);
}
void releaseMask(SEXP ref, pDevDesc dd)
{
pDevDesc pngDevDesc = shadowDevDesc(dd);
if (pngDevDesc == nullptr)
return;
dev_desc::releaseMask(ref, pngDevDesc);
}
} // namespace shadow
void installShadowHandler()
{
handler::allocate = shadow::allocate;
handler::initialize = shadow::initialize;
handler::destroy = shadow::destroy;
handler::setSize = shadow::setSize;
handler::setDeviceAttributes = shadow::setDeviceAttributes;
handler::onBeforeAddDevice = shadow::onBeforeAddDevice;
handler::onAfterAddDevice = shadow::onAfterAddDevice;
handler::writeToPNG = shadow::writeToPNG;
handler::circle = shadow::circle;
handler::line = shadow::line;
handler::polygon = shadow::polygon;
handler::polyline = shadow::polyline;
handler::rect = shadow::rect;
handler::path = shadow::path;
handler::raster = shadow::raster;
handler::cap = shadow::cap;
handler::metricInfo = shadow::metricInfo;
handler::strWidth = shadow::strWidth;
handler::text = shadow::text;
handler::clip = shadow::clip;
handler::newPage = shadow::newPage;
handler::mode = shadow::mode;
handler::onBeforeExecute = shadow::onBeforeExecute;
handler::setPattern = shadow::setPattern;
handler::releasePattern = shadow::releasePattern;
handler::setClipPath = shadow::setClipPath;
handler::releaseClipPath = shadow::releaseClipPath;
handler::setMask = shadow::setMask;
handler::releaseMask = shadow::releaseMask;
}
} // namespace handler
} // namespace graphics
} // namespace session
} // namespace r
} // namespace rstudio