musrview: add "Save graphics" to the interactive (root7/web) viewer
root6 musrview could save the plot as png/pdf/svg/...; the ROOT7 web rewrite dropped that. Add a "Save graphics" toolbar entry: the server renders the classic layout (same code path as --png/--pdf/... batch export) into the current working directory and hands it to the browser as a download, so the browser's own save dialog/download folder decides where it lands -- a native OS folder picker isn't reachable from here, since the --embedded window is a Qt6 QWebEngineView owned by ROOT itself, which auto-accepts downloads with no dialog. Also fixes rasterized batch export (png/gif/jpg) coming out blank when triggered from an already-running interactive session: TCanvas decides at *construction* time, from gROOT->IsBatch(), whether to build a real GUI canvas or a headless one, so canvas.SetBatch(kTRUE) after the fact was too late. Vector formats (pdf/eps/svg) don't need pixel readback and were unaffected, which is why this went unnoticed. Fixed by toggling gROOT's batch flag for the export canvas's lifetime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cuZPrSNaKjG8xHPVNwV16
This commit is contained in:
1 parent
4302b40d9e
commit
7a3fa94270
4 files changed
+176
-40
No files matched your search
+132
-37
@@ -253,6 +253,8 @@ void PMusrWebCanvas::Close()
|
||||
void PMusrWebCanvas::SetMsrHandler(PMsrHandler *msrHandler)
|
||||
{
|
||||
fModel->SetMsrHandler(msrHandler);
|
||||
if (msrHandler)
|
||||
fMsrFileName = msrHandler->GetFileName().Data();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -870,14 +872,13 @@ void PMusrWebCanvas::ProcessMessage(unsigned connid, const std::string &msg)
|
||||
}
|
||||
|
||||
if (msg.rfind("save:", 0) == 0) {
|
||||
const std::string arg = msg.substr(5);
|
||||
const size_t sep = arg.find(':');
|
||||
if (sep == std::string::npos)
|
||||
return;
|
||||
const std::string fileName = arg.substr(0, sep);
|
||||
const std::string format = arg.substr(sep+1);
|
||||
if (SaveGraphics(fileName, format))
|
||||
fWindow->Send(connid, "INFO:graphics written (" + JsonEscape(format) + ")");
|
||||
// save:<format> - the file is written to the current working directory
|
||||
// and handed to the browser as a download, so its own save dialog (or
|
||||
// download folder) decides where it ends up
|
||||
const std::string format = msg.substr(5);
|
||||
std::string relUrl;
|
||||
if (SaveGraphicsForDownload(format, relUrl))
|
||||
fWindow->Send(connid, "DOWNLOAD:" + JsonEscape(relUrl));
|
||||
else
|
||||
fWindow->Send(connid, "ERROR:" + JsonEscape(fLastError));
|
||||
return;
|
||||
@@ -926,6 +927,10 @@ Bool_t PMusrWebCanvas::Show(const std::string &where)
|
||||
fWindow->SetDefaultPage("file:" + webDir + "/musrview.html");
|
||||
fWindow->SetGeometry(fWidth, fHeight);
|
||||
fWindow->SetConnLimit(1);
|
||||
// lets the client download files written into the current working
|
||||
// directory from "currentdir/<name>", used by "Save graphics" (see
|
||||
// SaveGraphicsForDownload())
|
||||
fWindow->SetUseCurrentDir(true);
|
||||
|
||||
fWindow->SetDataCallBack([this](unsigned connid, const std::string &msg) {
|
||||
ProcessMessage(connid, msg);
|
||||
@@ -948,29 +953,20 @@ Bool_t PMusrWebCanvas::Show(const std::string &where)
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// SaveGraphics (public)
|
||||
// DefaultGraphicsBaseName (private)
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* <p>Writes the classic musrview layout (title, spectra, fit parameters,
|
||||
* theory, run info) into a graphics file.
|
||||
* <p>Derives the base name (no extension) under which this plot block's
|
||||
* graphics are saved by default, mirroring the --png/--pdf/... batch export
|
||||
* naming: <msr-base>_<plotNo>[_F]. Used both by the batch export and as the
|
||||
* suggested name offered to the interactive "Save graphics" dialog.
|
||||
*
|
||||
* <p>This path deliberately does not involve the browser: batch operation must
|
||||
* not depend on a browser installation, and the output stays reproducible.
|
||||
*
|
||||
* \param msrFileName name of the msr-file; the output name is derived from it
|
||||
* \param graphicsFormat extension such as "png", "pdf", "eps", ...
|
||||
*
|
||||
* \return true on success
|
||||
* \return the base name, or empty if the msr-file name carries neither a
|
||||
* ".msr" nor a ".mlog" extension to replace
|
||||
*/
|
||||
Bool_t PMusrWebCanvas::SaveGraphics(const std::string &msrFileName, const std::string &graphicsFormat)
|
||||
std::string PMusrWebCanvas::DefaultGraphicsBaseName() const
|
||||
{
|
||||
if (!fValid) {
|
||||
fLastError = "canvas is not valid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// derive the output file name: <base>_<plotNo>[_F].<ext>
|
||||
TString str(msrFileName.c_str());
|
||||
TString str(fMsrFileName.c_str());
|
||||
Int_t idx = -1;
|
||||
Int_t size = 0;
|
||||
if (str.Contains(".msr")) {
|
||||
@@ -980,11 +976,8 @@ Bool_t PMusrWebCanvas::SaveGraphics(const std::string &msrFileName, const std::s
|
||||
idx = str.Index(".mlog");
|
||||
size = 5;
|
||||
}
|
||||
if (idx == -1) {
|
||||
fLastError = "file name '" + msrFileName + "' is invalid.";
|
||||
std::cerr << std::endl << ">> PMusrWebCanvas::SaveGraphics(): **ERROR** " << fLastError << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (idx == -1)
|
||||
return std::string();
|
||||
|
||||
Char_t ext[64];
|
||||
if (fMode.IsFourier())
|
||||
@@ -992,10 +985,95 @@ Bool_t PMusrWebCanvas::SaveGraphics(const std::string &msrFileName, const std::s
|
||||
else
|
||||
snprintf(ext, sizeof(ext), "_%d", fPlotNumber);
|
||||
str.Replace(idx, size, ext, strlen(ext));
|
||||
idx += strlen(ext);
|
||||
size = strlen(ext);
|
||||
snprintf(ext, sizeof(ext), ".%s", graphicsFormat.c_str());
|
||||
str.Replace(idx, size, ext, strlen(ext));
|
||||
|
||||
return str.Data();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// SaveGraphics (public)
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* <p>Writes the classic musrview layout into a graphics file whose name is
|
||||
* derived from the msr-file name. Used by the --png/--pdf/... batch export.
|
||||
*
|
||||
* \param graphicsFormat extension such as "png", "pdf", "eps", ...
|
||||
*
|
||||
* \return true on success
|
||||
*/
|
||||
Bool_t PMusrWebCanvas::SaveGraphics(const std::string &graphicsFormat)
|
||||
{
|
||||
const std::string base = DefaultGraphicsBaseName();
|
||||
if (base.empty()) {
|
||||
fLastError = "msr-file name '" + fMsrFileName + "' is invalid.";
|
||||
std::cerr << std::endl << ">> PMusrWebCanvas::SaveGraphics(): **ERROR** " << fLastError << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return RenderGraphics(base + "." + graphicsFormat);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// SaveGraphicsForDownload (public)
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* <p>Writes the classic musrview layout into the process' current working
|
||||
* directory and reports the path under which the web window's "currentdir/"
|
||||
* location (see Show()) makes it downloadable.
|
||||
*
|
||||
* <p>Used by the interactive "Save graphics" toolbar entry: rather than
|
||||
* musrview picking a location on the (possibly remote) machine it runs on,
|
||||
* the file is handed to the browser as a normal download, so the browser's
|
||||
* own save dialog (or download folder) decides where it ends up.
|
||||
*
|
||||
* \param graphicsFormat extension such as "png", "pdf", "eps", ...
|
||||
* \param relUrl the URL (relative to the web window) the file can be
|
||||
* downloaded from, on success
|
||||
*
|
||||
* \return true on success
|
||||
*/
|
||||
Bool_t PMusrWebCanvas::SaveGraphicsForDownload(const std::string &graphicsFormat, std::string &relUrl)
|
||||
{
|
||||
const std::string base = DefaultGraphicsBaseName();
|
||||
if (base.empty()) {
|
||||
fLastError = "msr-file name '" + fMsrFileName + "' is invalid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ignore any directory the msr-file name may carry: the download is always
|
||||
// served out of the current working directory (see Show()/currentdir/)
|
||||
const std::string fileName = std::string(gSystem->BaseName(base.c_str())) + "." + graphicsFormat;
|
||||
|
||||
if (!RenderGraphics(fileName))
|
||||
return false;
|
||||
|
||||
relUrl = "currentdir/" + fileName;
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// RenderGraphics (private)
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* <p>Builds the classic musrview layout (title, spectra, fit parameters,
|
||||
* theory, run info) and writes it into the given graphics file.
|
||||
*
|
||||
* <p>This path deliberately does not involve the browser: the file is written
|
||||
* directly by ROOT from a batch TCanvas, so it works whether or not a browser
|
||||
* is even installed, and interactive Save produces bit-for-bit the same
|
||||
* output as the --png/--pdf/... batch export.
|
||||
*
|
||||
* \param fileName the file to write; its extension selects the format
|
||||
*
|
||||
* \return true on success
|
||||
*/
|
||||
Bool_t PMusrWebCanvas::RenderGraphics(const std::string &fileName)
|
||||
{
|
||||
if (!fValid) {
|
||||
fLastError = "canvas is not valid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
TString str(fileName.c_str());
|
||||
|
||||
// Build the classic layout. Its size is deliberately not the size of the
|
||||
// musrview window: the exported graphics have always been 800x600 and users
|
||||
@@ -1003,9 +1081,22 @@ Bool_t PMusrWebCanvas::SaveGraphics(const std::string &msrFileName, const std::s
|
||||
const Int_t exportWidth = 800;
|
||||
const Int_t exportHeight = 600;
|
||||
|
||||
// TCanvas decides at *construction* time, from gROOT->IsBatch(), whether to
|
||||
// build a real (mapped) GUI canvas or a headless one; canvas.SetBatch(kTRUE)
|
||||
// afterwards is too late to change that choice. In the interactive session
|
||||
// gROOT is not in batch mode, so without this the export canvas would be a
|
||||
// real, briefly-mapped GUI canvas, and rasterized output (png, jpg, gif) --
|
||||
// which is read back from that window's pixmap -- comes out blank; vector
|
||||
// formats (pdf, eps, svg) are unaffected since they replay recorded
|
||||
// primitives directly. Flipping gROOT's batch flag for the lifetime of this
|
||||
// function is safe here because this canvas is the only one that ever
|
||||
// exists in the process: the interactive display never builds a TCanvas of
|
||||
// its own (see the class comment above).
|
||||
const Bool_t wasBatch = gROOT->IsBatch();
|
||||
gROOT->SetBatch(kTRUE);
|
||||
|
||||
std::string name = "musrviewBatch" + std::to_string(fPlotNumber);
|
||||
TCanvas canvas(name.c_str(), fTitle.c_str(), exportWidth, exportHeight);
|
||||
canvas.SetBatch(kTRUE);
|
||||
|
||||
const Double_t yInfo = 0.1;
|
||||
const Double_t yTitle = 0.95;
|
||||
@@ -1060,8 +1151,10 @@ Bool_t PMusrWebCanvas::SaveGraphics(const std::string &msrFileName, const std::s
|
||||
}
|
||||
infoPad.Draw();
|
||||
|
||||
if (!FillPlotPad(&dataTheoryPad))
|
||||
if (!FillPlotPad(&dataTheoryPad)) {
|
||||
gROOT->SetBatch(wasBatch);
|
||||
return false;
|
||||
}
|
||||
|
||||
dataTheoryPad.Update();
|
||||
canvas.cd();
|
||||
@@ -1074,6 +1167,8 @@ Bool_t PMusrWebCanvas::SaveGraphics(const std::string &msrFileName, const std::s
|
||||
dataTheoryPad.GetListOfPrimitives()->Clear();
|
||||
canvas.GetListOfPrimitives()->Clear();
|
||||
|
||||
gROOT->SetBatch(wasBatch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,8 +121,16 @@ class PMusrWebCanvas
|
||||
/// Called when the user quits; used to terminate the application.
|
||||
void SetDoneCallback(std::function<void(Int_t)> cb) { fDoneCallback = std::move(cb); }
|
||||
|
||||
/// Writes the classic canvas layout into a graphics file.
|
||||
Bool_t SaveGraphics(const std::string &msrFileName, const std::string &graphicsFormat);
|
||||
/// Writes the classic canvas layout into a graphics file named after the
|
||||
/// msr-file (batch export, e.g. --png).
|
||||
Bool_t SaveGraphics(const std::string &graphicsFormat);
|
||||
/// Writes the classic canvas layout into the current working directory and
|
||||
/// returns the URL (relative to the web window) it can be downloaded from;
|
||||
/// used by the interactive "Save graphics" toolbar entry.
|
||||
Bool_t SaveGraphicsForDownload(const std::string &graphicsFormat, std::string &relUrl);
|
||||
/// The name/location that graphics files are derived from by default,
|
||||
/// without extension.
|
||||
std::string DefaultGraphicsBaseName() const;
|
||||
/// Writes the currently shown data as ascii.
|
||||
Bool_t ExportData(const std::string &fileName);
|
||||
|
||||
@@ -143,6 +151,7 @@ class PMusrWebCanvas
|
||||
Bool_t fDone{false}; ///< true once the user quit
|
||||
Int_t fTimeout{0}; ///< timeout in seconds
|
||||
std::string fLastError{}; ///< last error message
|
||||
std::string fMsrFileName{}; ///< msr-file name, used to derive graphics/export file names
|
||||
|
||||
std::unique_ptr<PMusrViewModel> fModel; ///< the data layer
|
||||
PMusrViewMode fMode{}; ///< what is currently shown
|
||||
@@ -170,6 +179,8 @@ class PMusrWebCanvas
|
||||
Bool_t CollectDrawables(std::vector<std::pair<TObject*, TString>> &out);
|
||||
/// Draws the spectra of the current mode into the given pad (graphics export).
|
||||
Bool_t FillPlotPad(TVirtualPad *pad);
|
||||
/// Builds the classic layout and writes it into the given graphics file.
|
||||
Bool_t RenderGraphics(const std::string &fileName);
|
||||
/// Serializes the objects of the current view for JSROOT.
|
||||
Bool_t BuildPlotJson(TString &json);
|
||||
|
||||
|
||||
+1
-1
@@ -446,7 +446,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
if (graphicsOutput) {
|
||||
for (auto &canvas : canvasList) {
|
||||
if (!canvas->SaveGraphics(fileName, graphicsExtension)) {
|
||||
if (!canvas->SaveGraphics(graphicsExtension)) {
|
||||
std::cerr << std::endl << ">> musrview **ERROR** " << canvas->GetLastError() << std::endl;
|
||||
result = PMUSR_MSR_RUN_ERROR;
|
||||
}
|
||||
|
||||
@@ -257,6 +257,20 @@
|
||||
|
||||
<div class="sep"></div>
|
||||
|
||||
<div class="toggle-group">
|
||||
<button id="btn-save" title="save the current view as a graphics file; downloads it through the browser">Save graphics</button>
|
||||
<select id="sel-save-format" title="graphics format">
|
||||
<option value="png">png</option>
|
||||
<option value="pdf">pdf</option>
|
||||
<option value="svg">svg</option>
|
||||
<option value="eps">eps</option>
|
||||
<option value="gif">gif</option>
|
||||
<option value="jpg">jpg</option>
|
||||
<option value="xpm">xpm</option>
|
||||
<option value="root">root</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="btn-export" title="export the shown data as ascii">Export data</button>
|
||||
<button id="btn-quit" title="close musrview (key: q)">Quit</button>
|
||||
</div>
|
||||
@@ -320,6 +334,18 @@ function cmd(name) {
|
||||
send('cmd:' + name);
|
||||
}
|
||||
|
||||
/** Triggers a browser download of a file the server just wrote, from its URL
|
||||
* relative to this web window (see PMusrWebCanvas::SaveGraphicsForDownload). */
|
||||
function downloadFile(relUrl) {
|
||||
if (!handle) return;
|
||||
const a = document.createElement('a');
|
||||
a.href = handle.getHRef(relUrl);
|
||||
a.download = relUrl.slice(relUrl.lastIndexOf('/') + 1);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
/** True while the user is typing somewhere, e.g. into the text/latex dialog
|
||||
* which JSROOT opens from the canvas menu. This is what keeps a typed 'd' or
|
||||
* 'f' from being executed as the difference/Fourier command. */
|
||||
@@ -647,6 +673,8 @@ $('btn-export').onclick = () => {
|
||||
if (name) send('export:' + name);
|
||||
};
|
||||
|
||||
$('btn-save').onclick = () => send('save:' + $('sel-save-format').value);
|
||||
|
||||
$('pan-title').onclick = () => togglePanel('title');
|
||||
$('pan-params').onclick = () => togglePanel('params');
|
||||
$('pan-theory').onclick = () => togglePanel('theory');
|
||||
@@ -683,6 +711,8 @@ const receiver = {
|
||||
setStatus(msg.slice(5));
|
||||
} else if (msg.startsWith('ERROR:')) {
|
||||
setStatus(msg.slice(6), true);
|
||||
} else if (msg.startsWith('DOWNLOAD:')) {
|
||||
downloadFile(msg.slice(9));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user