Files
musrfit/src/musrview.cpp
T
suter_aandClaude Sonnet 5 7a3fa94270 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
2026-09-11 17:42:49 +02:00

531 lines
20 KiB
C++

/***************************************************************************
musrview.cpp
Author: Andreas Suter
e-mail: andreas.suter@psi.ch
***************************************************************************/
/***************************************************************************
* Copyright (C) 2007-2026 by Andreas Suter *
* andreas.suter@psi.ch *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include <TApplication.h>
#include <TROOT.h>
#include <TSAXParser.h>
#include <TSystem.h>
#include <ROOT/RWebWindowsManager.hxx>
#ifdef HAVE_GIT_REV_H
#include "git-revision.h"
#endif
#include "PMusr.h"
#include "PMsgBox.h"
#include "PMsrHandler.h"
#include "PMusrWebCanvas.h"
#include "PRunDataHandler.h"
#include "PRunListCollection.h"
#include "PStartupHandler.h"
//--------------------------------------------------------------------------
/**
* <p>Sends the usage description to the standard output.
*/
void musrview_syntax()
{
std::cout << std::endl << "usage: musrview <msr-file> [Options]";
std::cout << std::endl << " <msr-file>: msr/mlog input file";
std::cout << std::endl << " Options:";
std::cout << std::endl << " --help : display this help and exit.";
std::cout << std::endl << " --version : output version information and exit.";
std::cout << std::endl << " --show-dynamic-path : dumps the dynamic search paths and exit.";
std::cout << std::endl << " -f, --fourier: will directly present the Fourier transform of the <msr-file>.";
std::cout << std::endl << " -a, --avg: will directly present the averaged data/Fourier of the <msr-file>.";
std::cout << std::endl << " -1, --one_to_one: calculate theory points only at data points.";
std::cout << std::endl << " display options (musrview is web based, see the musrfit documentation):";
std::cout << std::endl << " --embedded : show the plots in an embedded browser window, i.e. musrview";
std::cout << std::endl << " behaves like a stand-alone application. This is the default";
std::cout << std::endl << " as long as ROOT provides the Qt6 web display.";
std::cout << std::endl << " --browser : show the plots in the standard web browser of the system.";
std::cout << std::endl << " --browser=<kind> : use a specific display, e.g. chrome, firefox, qt6, server.";
std::cout << std::endl << " 'server' only prints the URL and starts no browser at all,";
std::cout << std::endl << " which is what to use when musrfit runs on a remote machine.";
std::cout << std::endl << " --<graphic-format-extension>: ";
std::cout << std::endl << " will produce a graphics-output-file without opening a window.";
std::cout << std::endl << " the name is based on the <msr-file>, e.g. 3310.msr -> 3310_0.png";
std::cout << std::endl << " supported graphic-format-extension:";
std::cout << std::endl << " eps, pdf, gif, jpg, png, svg, xpm, root";
std::cout << std::endl << " example: musrview 3310.msr --png, will produce a files 3310_X.png";
std::cout << std::endl << " where 'X' stands for the plot number (starting form 0)";
std::cout << std::endl << " --ascii: ";
std::cout << std::endl << " will produce an ascii dump of the data and fit as plotted.";
std::cout << std::endl << " --timeout <timeout>: <timeout> given in seconds after which musrview terminates.";
std::cout << std::endl << " If <timeout> <= 0, no timeout will take place. Default <timeout> is 0.";
std::cout << std::endl << " -s, --show-errMsgBox: if this tag is defined, error message boxes are shown,";
std::cout << std::endl << " rather than only stderr output.";
std::cout << std::endl << std::endl;
}
//--------------------------------------------------------------------------
/**
* <p>Shows an error message in a message box.
*
* \param errMsg the message to be shown
*/
void musrview_error_msg(std::string errMsg)
{
int argc=0;
char **argv=nullptr;
TApplication app("musrviewErrorMsg", &argc, argv);
new PMsgBox(errMsg, gClient->GetRoot(), 600, 200);
app.Run();
}
//--------------------------------------------------------------------------
/**
* <p>Decides which web display musrview should use.
*
* <p>Unless the user asked for something specific, the embedded Qt6 WebEngine
* window is preferred, since it makes musrview appear as the stand-alone
* application it has always been. If ROOT was built without the Qt6 web
* display, the standard browser of the system is used instead.
*
* \param requested what the user asked for, empty if nothing was requested
*
* \return the display string to be handed to ROOT::RWebWindow::Show()
*/
std::string musrview_select_display(const std::string &requested)
{
if (!requested.empty())
return requested;
if (const char *env = gSystem->Getenv("MUSRVIEW_BROWSER"))
if (strlen(env) > 0)
return std::string(env);
// prefer the embedded window if ROOT provides it
gSystem->RedirectOutput("/dev/null", "a");
const Int_t loaded = gSystem->Load("libROOTQt6WebDisplay");
gSystem->RedirectOutput(nullptr);
if (loaded >= 0)
return "qt6";
std::cout << std::endl << ">> musrview **INFO** ROOT has no Qt6 web display, using the standard browser.";
std::cout << std::endl << ">> Rebuild ROOT with -Dqt6web=ON for an embedded window." << std::endl;
return std::string();
}
//--------------------------------------------------------------------------
/**
* <p>The musrview program is used to show muSR fit results in graphical form.
* From it also Fourier transforms, difference between data an theory can be formed.
*
* <p>Since version 1.13 musrview is based on the ROOT7 web graphics: the plots
* are rendered by JSROOT either in an embedded browser window or in the
* standard browser of the system, while the C++ side only holds the data.
*
* <b>return:</b>
* - PMUSR_SUCCESS if everthing went smooth
* - PMUSR_WRONG_STARTUP_SYNTAX if syntax error is encountered
* - line number if an error in the msr-file was encountered which cannot be handled.
*
* \param argc number of input arguments
* \param argv list of input arguments
*/
int main(int argc, char *argv[])
{
int result{PMUSR_SUCCESS};
bool show_syntax{false};
int status;
bool success{true};
std::string fileName{};
bool fourier{false};
bool avg{false};
bool theoAtData{false}; // theory points only at data points
bool graphicsOutput{false};
bool asciiOutput{false};
std::string graphicsExtension{};
std::string display{};
int timeout{0};
bool show_errMsgBox{false};
std::stringstream errMsg;
// add default shared library path /usr/local/lib if not already persent
const char *dsp = gSystem->GetDynamicPath();
if (strstr(dsp, "/usr/local/lib") == nullptr)
gSystem->AddDynamicPath("/usr/local/lib");
// check input arguments
if (argc == 1) {
musrview_syntax();
return PMUSR_SYNTAX_REQUEST;
}
for (int i=1; i<argc; i++) {
if (strstr(argv[i], ".msr") || strstr(argv[i], ".mlog")) {
if (fileName.empty()) {
fileName = argv[i];
} else {
std::cerr << std::endl << "**ERROR** only one file name allowed." << std::endl;
show_syntax = true;
break;
}
} else if (!strcmp(argv[i], "--version")) {
#ifdef HAVE_CONFIG_H
#ifdef HAVE_GIT_REV_H
std::cout << std::endl << "musrview version: " << PACKAGE_VERSION << ", git-branch: " << GIT_BRANCH << ", git-rev: " << GIT_CURRENT_SHA1 << " (" << BUILD_TYPE << "), ROOT version: " << ROOT_VERSION_USED << std::endl << std::endl;
#else
std::cout << std::endl << "musrview version: " << PACKAGE_VERSION << " (" << BUILD_TYPE << "), ROOT version: " << ROOT_VERSION_USED << std::endl << std::endl;
#endif
#else
#ifdef HAVE_GIT_REV_H
std::cout << std::endl << "musrview git-branch: " << GIT_BRANCH << ", git-rev: " << GIT_CURRENT_SHA1 << std::endl << std::endl;
#else
std::cout << std::endl << "musrview version: unknown." << std::endl << std::endl;
#endif
#endif
return PMUSR_SUCCESS;
} else if (!strcmp(argv[i], "--show-dynamic-path")) {
std::cout << std::endl << "musrview: internal dynamic search paths for shared libraries/root dictionaries:";
std::cout << std::endl << " '" << gSystem->GetDynamicPath() << "'" << std::endl << std::endl;
return PMUSR_SUCCESS;
} else if (!strcmp(argv[i], "--help")) {
show_syntax = true;
break;
} else if (!strcmp(argv[i], "-f") || !strcmp(argv[i], "--fourier")) {
fourier = true;
} else if (!strcmp(argv[i], "-a") || !strcmp(argv[i], "--avg")) {
avg = true;
} else if (!strcmp(argv[i], "-1") || !strcmp(argv[i], "--one_to_one")) {
theoAtData = true;
} else if (!strcmp(argv[i], "--embedded")) {
display = "qt6";
} else if (!strcmp(argv[i], "--browser")) {
display = "default";
} else if (!strncmp(argv[i], "--browser=", 10)) {
display = argv[i]+10;
if (display.empty()) {
show_syntax = true;
break;
}
} else if (!strcmp(argv[i], "--eps") || !strcmp(argv[i], "--pdf") || !strcmp(argv[i], "--gif") ||
!strcmp(argv[i], "--jpg") || !strcmp(argv[i], "--png") || !strcmp(argv[i], "--svg") ||
!strcmp(argv[i], "--xpm") || !strcmp(argv[i], "--root")) {
graphicsOutput = true;
graphicsExtension = argv[i]+2;
} else if (!strcmp(argv[i], "--ascii")) {
asciiOutput = true;
} else if (!strcmp(argv[i], "--timeout")) {
if (i+1 < argc) {
TString str(argv[i+1]);
if (str.IsFloat()) {
timeout = str.Atoi();
} else {
show_syntax = true;
break;
}
i++;
} else {
show_syntax = true;
break;
}
} else if (!strcmp(argv[i], "-s") || !strcmp(argv[i], "--show-errMsgBox")) {
show_errMsgBox = true;
} else {
show_syntax = true;
break;
}
}
if (show_syntax) {
musrview_syntax();
return PMUSR_WRONG_STARTUP_SYNTAX;
}
const bool batchMode = graphicsOutput || asciiOutput;
// read startup file
std::string startup_path_name{};
std::unique_ptr<TSAXParser> saxParser = std::make_unique<TSAXParser>();
std::unique_ptr<PStartupHandler> startupHandler = std::make_unique<PStartupHandler>();
if (!startupHandler->StartupFileFound()) {
std::cerr << std::endl << ">> musrview **WARNING** couldn't find " << startupHandler->GetStartupFilePath().Data();
std::cerr << std::endl;
} else {
startup_path_name = startupHandler->GetStartupFilePath().Data();
saxParser->ConnectToHandler("PStartupHandler", startupHandler.get());
// parsing the file via ParseFile() seems to lead to problems in certain
// environments; use parseXmlFile (see PStartupHandler.cpp) instead
status = parseXmlFile(saxParser.get(), startup_path_name.c_str());
if (status) { // error
std::cerr << std::endl << ">> musrview **WARNING** Reading/parsing musrfit_startup.xml failed.";
std::cerr << std::endl << ">> Any graph will appear with random symbols and colors!";
std::cerr << std::endl;
} else {
startupHandler->CheckLists();
}
}
// read msr-file
std::unique_ptr<PMsrHandler> msrHandler = std::make_unique<PMsrHandler>(fileName.c_str());
status = msrHandler->ReadMsrFile();
if (status != PMUSR_SUCCESS) {
errMsg << msrHandler->GetLastErrorMsg();
switch (status) {
case PMUSR_MSR_FILE_NOT_FOUND:
errMsg << "\n";
errMsg << ">> musrview **ERROR** couldn't find '" << fileName << "'\n\n";
break;
case PMUSR_MSR_SYNTAX_ERROR:
errMsg << "\n";
errMsg << ">> musrview **SYNTAX ERROR** in file " << fileName << ", full stop here.\n\n";
break;
default:
errMsg << "\n";
errMsg << ">> musrview **UNKNOWN ERROR** when trying to read the msr-file.\n\n";
break;
}
std::cerr << errMsg.str();
if (show_errMsgBox)
musrview_error_msg(errMsg.str());
return status;
}
// make a plot list vector
PMsrPlotList *msrPlotList = msrHandler->GetMsrPlotList();
PIntVector plotList;
bool runPresent;
for (unsigned int i=0; i<msrPlotList->size(); i++) {
for (unsigned int j=0; j<msrPlotList->at(i).fRuns.size(); j++) {
runPresent = false;
for (unsigned int k=0; k<plotList.size(); k++) {
if (msrPlotList->at(i).fRuns[j] == static_cast<int>(plotList[k])) {
runPresent = true;
break;
}
}
if (!runPresent)
plotList.push_back(static_cast<int>(msrPlotList->at(i).fRuns[j]));
}
}
// read all the necessary runs (raw data)
std::unique_ptr<PRunDataHandler> dataHandler;
if (startupHandler)
dataHandler = std::make_unique<PRunDataHandler>(msrHandler.get(), startupHandler->GetDataPathList());
else
dataHandler = std::make_unique<PRunDataHandler>(msrHandler.get());
dataHandler->ReadData();
success = dataHandler->IsAllDataAvailable();
if (!success) {
errMsg << ">> musrview **ERROR** Couldn't read all data files, will quit ...\n";
std::cerr << errMsg.str();
if (show_errMsgBox)
musrview_error_msg(errMsg.str());
result = PMUSR_DATA_FILE_READ_ERROR;
}
// generate the necessary histogramms for the view
std::unique_ptr<PRunListCollection> runListCollection;
if (result == PMUSR_SUCCESS) {
runListCollection = std::make_unique<PRunListCollection>(msrHandler.get(), dataHandler.get(), theoAtData);
for (unsigned int i=0; i<msrHandler->GetMsrRunList()->size(); i++) {
runPresent = false;
for (unsigned int j=0; j<plotList.size(); j++) {
if (static_cast<unsigned int>(plotList[j]) == i+1) {
runPresent = true;
break;
}
}
if (runPresent) {
success = runListCollection->Add(i, kView);
if (!success) {
errMsg << ">> musrview **ERROR** Couldn't handle run no " << i << " ";
errMsg << (*msrHandler->GetMsrRunList())[i].GetRunName()->Data() << "\n";
std::cerr << errMsg.str();
if (show_errMsgBox)
musrview_error_msg(errMsg.str());
result = PMUSR_MSR_RUN_ERROR;
break;
}
}
}
}
if (result != PMUSR_SUCCESS) {
plotList.clear();
return result;
}
// A ROOT application is needed so that the web display and the graphics
// export find a working environment.
//
// In the interactive case no TCanvas is created at all - the spectra travel
// to JSROOT as a list of drawables - hence ROOT never opens a window of its
// own. Only the graphics export builds a TCanvas, and that runs in batch.
std::vector<char*> args;
args.push_back(argv[0]);
if (batchMode)
args.push_back(const_cast<char*>("-b"));
int cc = static_cast<int>(args.size());
TApplication app("musrview", &cc, args.data());
std::vector<std::unique_ptr<musrview::PMusrWebCanvas>> canvasList;
bool ok = true;
for (unsigned int i=0; i<msrHandler->GetMsrPlotList()->size(); i++) {
auto canvas = std::make_unique<musrview::PMusrWebCanvas>(
static_cast<Int_t>(i), msrHandler->GetMsrTitle()->Data(),
1200, 900,
startupHandler ? startupHandler->GetFourierDefaults() : PMsrFourierStructure{},
startupHandler ? startupHandler->GetMarkerList() : PIntVector{},
startupHandler ? startupHandler->GetColorList() : PIntVector{},
batchMode, fourier, avg, theoAtData);
// ugly but rootcling cannot handle the spirit-parser framework
canvas->SetMsrHandler(msrHandler.get());
canvas->SetRunListCollection(runListCollection.get());
if (!canvas->Build()) {
errMsg << ">> musrview **ERROR** " << canvas->GetLastError() << "\n";
std::cerr << errMsg.str();
if (show_errMsgBox)
musrview_error_msg(errMsg.str());
ok = false;
result = PMUSR_MSR_RUN_ERROR;
break;
}
canvas->SetTimeout(timeout);
canvasList.push_back(std::move(canvas));
}
if (!ok) {
canvasList.clear();
plotList.clear();
return result;
}
if (graphicsOutput) {
for (auto &canvas : canvasList) {
if (!canvas->SaveGraphics(graphicsExtension)) {
std::cerr << std::endl << ">> musrview **ERROR** " << canvas->GetLastError() << std::endl;
result = PMUSR_MSR_RUN_ERROR;
}
}
}
if (asciiOutput) {
// generate the export data file name
TString str(fileName.c_str());
str.Remove(str.Last('.'));
for (size_t i=0; i<canvasList.size(); i++) {
TString name(str);
if (canvasList.size() > 1) {
name += "_";
name += static_cast<Int_t>(i);
}
name += ".dat";
if (!canvasList[i]->ExportData(name.Data())) {
std::cerr << std::endl << ">> musrview **ERROR** " << canvasList[i]->GetLastError() << std::endl;
result = PMUSR_MSR_RUN_ERROR;
}
}
}
if (!batchMode) {
const std::string where = musrview_select_display(display);
// one window per PLOT block, exactly as before
ROOT::RWebWindowsManager::SetSingleConnMode(false);
for (auto &canvas : canvasList) {
if (!canvas->Show(where)) {
std::cerr << std::endl << ">> musrview **ERROR** " << canvas->GetLastError() << std::endl;
result = PMUSR_MSR_RUN_ERROR;
ok = false;
break;
}
}
if (ok) {
// run until every window has been closed, or until the timeout expired
const auto start = std::chrono::steady_clock::now();
while (true) {
gSystem->ProcessEvents();
gSystem->Sleep(20);
bool allDone = true;
for (auto &canvas : canvasList) {
if (!canvas->IsDone()) {
allDone = false;
break;
}
}
if (allDone)
break;
if (timeout > 0) {
const auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now-start).count() >= timeout) {
std::cout << std::endl << ">> musrview: timeout of " << timeout << " s reached, terminating." << std::endl;
break;
}
}
}
}
// End the process here. Tearing down an RWebWindow which drives the
// embedded display blocks, because the Qt event loop ROOT started on our
// behalf is never stopped again. Everything of interest has been written by
// now, so leave immediately rather than hang after the last window closed.
std::cout.flush();
std::cerr.flush();
gSystem->Exit(result, kFALSE);
}
// only reached in batch mode, where no window was ever opened
canvasList.clear();
plotList.clear();
return result;
}