diff --git a/application/F3DStarter.cxx b/application/F3DStarter.cxx index 9401df191d..98c627da14 100644 --- a/application/F3DStarter.cxx +++ b/application/F3DStarter.cxx @@ -15,6 +15,7 @@ #include #include #include +#include #include namespace fs = std::filesystem; @@ -102,28 +103,28 @@ class F3DStarter::F3DInternals return false; } - static void SetVerboseLevel(const std::string& level) + static void SetVerboseLevel(const std::string& level, bool forceStdErr) { // A switch/case over verbose level if (level == "quiet") { - f3d::log::setVerboseLevel(f3d::log::VerboseLevel::QUIET); + f3d::log::setVerboseLevel(f3d::log::VerboseLevel::QUIET, forceStdErr); } else if (level == "error") { - f3d::log::setVerboseLevel(f3d::log::VerboseLevel::ERROR); + f3d::log::setVerboseLevel(f3d::log::VerboseLevel::ERROR, forceStdErr); } else if (level == "warning") { - f3d::log::setVerboseLevel(f3d::log::VerboseLevel::WARN); + f3d::log::setVerboseLevel(f3d::log::VerboseLevel::WARN, forceStdErr); } else if (level == "info") { - f3d::log::setVerboseLevel(f3d::log::VerboseLevel::INFO); + f3d::log::setVerboseLevel(f3d::log::VerboseLevel::INFO, forceStdErr); } else if (level == "debug") { - f3d::log::setVerboseLevel(f3d::log::VerboseLevel::DEBUG); + f3d::log::setVerboseLevel(f3d::log::VerboseLevel::DEBUG, forceStdErr); } else { @@ -164,8 +165,16 @@ int F3DStarter::Start(int argc, char** argv) this->Internals->Parser.GetOptions( this->Internals->AppOptions, this->Internals->DynamicOptions, files); + const bool renderToStdout = this->Internals->AppOptions.Output == "-"; + // Set verbosity level early from command line - F3DInternals::SetVerboseLevel(this->Internals->AppOptions.VerboseLevel); + F3DInternals::SetVerboseLevel(this->Internals->AppOptions.VerboseLevel, renderToStdout); + + if (renderToStdout) + { + f3d::log::info("Output image will be saved to stdout, all log types including debug and info " + "levels are redirected to stderr"); + } f3d::log::debug("========== Initializing =========="); @@ -184,7 +193,7 @@ int F3DStarter::Start(int argc, char** argv) this->Internals->AppOptions, this->Internals->DynamicOptions, files); // Set verbosity level again if it was defined in the configuration file global block - F3DInternals::SetVerboseLevel(this->Internals->AppOptions.VerboseLevel); + F3DInternals::SetVerboseLevel(this->Internals->AppOptions.VerboseLevel, renderToStdout); } #if __APPLE__ @@ -426,8 +435,17 @@ int F3DStarter::Start(int argc, char** argv) } f3d::image img = window.renderToImage(this->Internals->AppOptions.NoBackground); - img.save(this->Internals->AppOptions.Output); - f3d::log::debug("Output image saved to ", this->Internals->AppOptions.Output); + if (renderToStdout) + { + const auto buffer = img.saveBuffer(); + std::copy(buffer.begin(), buffer.end(), std::ostreambuf_iterator(std::cout)); + f3d::log::debug("Output image saved to stdout"); + } + else + { + img.save(this->Internals->AppOptions.Output); + f3d::log::debug("Output image saved to ", this->Internals->AppOptions.Output); + } if (this->Internals->FilesList.size() > 1) { diff --git a/application/testing/CMakeLists.txt b/application/testing/CMakeLists.txt index d44f826820..6b8e0f0d2c 100644 --- a/application/testing/CMakeLists.txt +++ b/application/testing/CMakeLists.txt @@ -198,6 +198,8 @@ f3d_test(NAME TestUpDirectionNoSign DATA suzanne.ply ARGS --up=X DEFAULT_LIGHTS) f3d_test(NAME TestTextureMatCap DATA suzanne.ply ARGS --texture-matcap=${F3D_SOURCE_DIR}/testing/data/skin.png DEFAULT_LIGHTS) f3d_test(NAME TestTextureMatCapWithTCoords DATA WaterBottle.glb ARGS --geometry-only --texture-matcap=${F3D_SOURCE_DIR}/testing/data/skin.png DEFAULT_LIGHTS) f3d_test(NAME TestConfigOrder DATA suzanne.ply ARGS CONFIG ${F3D_SOURCE_DIR}/testing/data/config-order.json DEFAULT_LIGHTS) +f3d_test(NAME TestOutputStream DATA suzanne.ply ARGS --verbose=quiet --output=- REGEXP "^.PNG" NO_BASELINE NO_OUTPUT) +f3d_test(NAME TestOutputStreamInfo DATA suzanne.ply ARGS --verbose=info --output=- REGEXP "redirected to stderr" NO_BASELINE NO_OUTPUT) # Needs SSBO: https://gitlab.kitware.com/vtk/vtk/-/merge_requests/10675 if(VTK_VERSION VERSION_GREATER_EQUAL 9.3.20231108) diff --git a/doc/user/OPTIONS.md b/doc/user/OPTIONS.md index 75b7fbd9c4..a557960cc4 100644 --- a/doc/user/OPTIONS.md +++ b/doc/user/OPTIONS.md @@ -6,7 +6,7 @@ F3D behavior can be fully controlled from the command line using the following o Options|Default|Description ------|------|------ -\-\-output=\||Instead of showing a render view and render into it, *render directly into a png file*. When used with \-\-ref option, only outputs on failure. +\-\-output=\||Instead of showing a render view and render into it, *render directly into a png file*. When used with \-\-ref option, only outputs on failure. If `-` is specified instead of a filename, the PNG file is streamed to the stdout. \-\-no-background||Use with \-\-output to output a png file with a transparent background. -h, \-\-help||Print *help* and exit. Ignore `--verbose`. \-\-version||Show *version* information and exit. Ignore `--verbose`. diff --git a/library/public/log.h b/library/public/log.h index 9641844c5a..f3c4f8af39 100644 --- a/library/public/log.h +++ b/library/public/log.h @@ -101,8 +101,10 @@ class F3D_EXPORT log /** * Set the verbose level. + * By default, only warnings and errors are written to stderr, debug and info are written to + * stdout. If forceStdErr is true, all messages including debug and info are written to stderr. */ - static void setVerboseLevel(VerboseLevel level); + static void setVerboseLevel(VerboseLevel level, bool forceStdErr = false); /** * Wait for user if applicable (eg: win32 output window). diff --git a/library/src/log.cxx b/library/src/log.cxx index f5838b03ba..763ca59a5f 100644 --- a/library/src/log.cxx +++ b/library/src/log.cxx @@ -69,10 +69,20 @@ void log::setUseColoring(bool use) } //---------------------------------------------------------------------------- -void log::setVerboseLevel(log::VerboseLevel level) +void log::setVerboseLevel(log::VerboseLevel level, bool forceStdErr) { detail::init::initialize(); - F3DLog::SetQuiet(level == log::VerboseLevel::QUIET); + + if (level == log::VerboseLevel::QUIET) + { + F3DLog::SetStandardStream(F3DLog::StandardStream::None); + } + else + { + F3DLog::SetStandardStream( + forceStdErr ? F3DLog::StandardStream::AlwaysStdErr : F3DLog::StandardStream::Default); + } + switch (level) { case (log::VerboseLevel::DEBUG): diff --git a/vtkext/private/module/F3DLog.cxx b/vtkext/private/module/F3DLog.cxx index 90f8ffcf44..cf1f07c5e5 100644 --- a/vtkext/private/module/F3DLog.cxx +++ b/vtkext/private/module/F3DLog.cxx @@ -55,10 +55,23 @@ void F3DLog::SetUseColoring(bool use) } //---------------------------------------------------------------------------- -void F3DLog::SetQuiet(bool quiet) +void F3DLog::SetStandardStream(StandardStream mode) { vtkOutputWindow* win = vtkOutputWindow::GetInstance(); - win->SetDisplayMode(quiet ? vtkOutputWindow::NEVER : vtkOutputWindow::ALWAYS); + + switch (mode) + { + case StandardStream::None: + win->SetDisplayMode(vtkOutputWindow::NEVER); + break; + case StandardStream::AlwaysStdErr: + win->SetDisplayMode(vtkOutputWindow::ALWAYS_STDERR); + break; + case StandardStream::Default: + default: + win->SetDisplayMode(vtkOutputWindow::ALWAYS); + break; + } } //---------------------------------------------------------------------------- diff --git a/vtkext/private/module/F3DLog.h b/vtkext/private/module/F3DLog.h index 1977aa2a9d..3663dc3573 100644 --- a/vtkext/private/module/F3DLog.h +++ b/vtkext/private/module/F3DLog.h @@ -23,6 +23,13 @@ enum class Severity : unsigned char Error }; +enum class StandardStream : unsigned char +{ + Default = 0, + None, + AlwaysStdErr +}; + /** * Set this global variable to control the verbose level * that actually display something in Print @@ -41,10 +48,13 @@ void Print(Severity sev, const std::string& msg); void SetUseColoring(bool use); /** - * Set if any log should be shown or not. - * Override the verbosity level completely. + * Determine how standard stream should be used. + * If mode is None, then no message is written at all (including errors). + * If mode is AlwaysStdErr, then all messages are written to stderr. + * If mode is Default, then only warnings and errors are written to stderr. Debug and info messages + * are written to stdout. */ -void SetQuiet(bool quiet); +void SetStandardStream(StandardStream mode); /** * If output window is a vtkF3DWin32OutputWindow, diff --git a/vtkext/private/module/Testing/TestF3DLog.cxx b/vtkext/private/module/Testing/TestF3DLog.cxx index d8e654c1df..efb21b468b 100644 --- a/vtkext/private/module/Testing/TestF3DLog.cxx +++ b/vtkext/private/module/Testing/TestF3DLog.cxx @@ -27,12 +27,12 @@ int TestF3DLog(int argc, char* argv[]) F3DLog::Print(F3DLog::Severity::Error, "Test Error\n"); F3DLog::VerboseLevel = F3DLog::Severity::Info; - F3DLog::SetQuiet(true); // Next print calls should print nothing + F3DLog::SetStandardStream(F3DLog::StandardStream::None); // Next print calls should print nothing F3DLog::Print(F3DLog::Severity::Debug, "Test Debug Quiet "); F3DLog::Print(F3DLog::Severity::Info, "Test Info Quiet "); F3DLog::Print(F3DLog::Severity::Warning, "Test Warning Quiet "); F3DLog::Print(F3DLog::Severity::Error, "Test Error Quiet\n"); - F3DLog::SetQuiet(false); + F3DLog::SetStandardStream(F3DLog::StandardStream::Default); // Without the object factory created, this is expected to have no effect F3DLog::SetUseColoring(true);