diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 24f218caa5..47cc11a087 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1387,6 +1387,25 @@ int CLI::run(int argc, char **argv) if (downward_check_option) downward_check = downward_check_option->value; + // --export-settings - writes its JSON to stdout, so reject every action or transform that may write there + // too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is + // sliced or exported. + if (std::find(m_actions.begin(), m_actions.end(), "export_settings") != m_actions.end() && m_config.opt_string("export_settings") == "-") { + static const std::set stdout_compatible = { "export_settings", "uptodate", "load_defaultfila", "min_save", + "mtcpp", "mstpp", "no_check", "normative_check", "pipe" }; + for (const std::vector *opt_keys : { &m_actions, &m_transforms }) { + for (const std::string &opt_key : *opt_keys) { + if (stdout_compatible.count(opt_key) == 0) { + std::string flag = opt_key; + std::replace(flag.begin(), flag.end(), '_', '-'); + boost::nowide::cerr << "--export-settings - cannot be combined with --" << flag << std::endl; + record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info); + flush_and_exit(CLI_INVALID_PARAMS); + } + } + } + } + bool start_gui = m_actions.empty() && !downward_check; if (start_gui) { BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl; @@ -5348,7 +5367,7 @@ int CLI::run(int argc, char **argv) //skip this object due to be locked in plate ap.itemid = locked_aps.size(); locked_aps.emplace_back(ap); - boost::nowide::cout <<__FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx; } } } @@ -5937,7 +5956,11 @@ int CLI::run(int argc, char **argv) //FIXME check for mixing the FFF / SLA parameters. // or better save fff_print_config vs. sla_print_config //m_print_config.save(m_config.opt_string("save")); - m_print_config.save_to_json(m_config.opt_string(opt_key), std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION)); + const std::string &settings_file = m_config.opt_string(opt_key); + if (settings_file == "-") + m_print_config.save_to_json(boost::nowide::cout, "project_settings", "project", SoftFever_VERSION, /*replace_invalid_utf8=*/true); + else + m_print_config.save_to_json(settings_file, std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION)); } else if (opt_key == "info") { // --info works on unrepaired model for (Model &model : m_models) { diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 394cfb5b74..52a46dcacf 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -1515,6 +1516,19 @@ std::optional parse_capability_ref(const std::string& value //BBS: add json support void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const +{ + // Serialize first: if that throws (invalid UTF-8), the existing file stays untouched. + std::ostringstream ss; + this->save_to_json(ss, name, from, version); + boost::nowide::ofstream c; + c.open(file, std::ios::out | std::ios::trunc); + c << ss.str(); + c.close(); + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file; +} + +void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const { json j; //record the headers @@ -1561,12 +1575,7 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name, j["plugins"] = unique_refs; } - boost::nowide::ofstream c; - c.open(file, std::ios::out | std::ios::trunc); - c << j.dump(1, '\t') << std::endl; - c.close(); - - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file; + os << j.dump(1, '\t', false, replace_invalid_utf8 ? json::error_handler_t::replace : json::error_handler_t::strict) << std::endl; } void ConfigBase::save(const std::string &file) const diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index ea85cda1e7..6d23ec3770 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2825,6 +2825,9 @@ public: //BBS: add json support void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const; + // Same document, written to a stream. Invalid UTF-8 in a string value throws nlohmann's type_error unless + // replace_invalid_utf8 is set, which writes U+FFFD instead (for callers such as stdout with no handler). + void save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8 = false) const; // Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin // dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json() diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index e8ac749bd3..0b0fe71dc0 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -11916,7 +11916,7 @@ CLIActionsConfigDef::CLIActionsConfigDef() def = this->add("export_settings", coString); def->label = L("Export Settings"); - def->tooltip = L("This exports settings to a file."); + def->tooltip = L("This exports settings to a file. Use - to write them to stdout."); def->cli_params = "settings.json"; def->set_default_value(new ConfigOptionString("output.json")); diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 3813e2df3f..208bbc6cf0 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -15,6 +15,8 @@ #include #include +#include + using namespace Slic3r; SCENARIO("Generic config validation performs as expected.", "[Config]") { @@ -488,6 +490,59 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[ CHECK(reloaded.option("slicing_pipeline_plugin")->values == refs); } +TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") { + DynamicPrintConfig config; + config.set_key_value("layer_height", new ConfigOptionFloat(0.2)); + config.set_key_value("wall_loops", new ConfigOptionInt(3)); + config.set_key_value("filament_type", new ConfigOptionStrings({ "PLA", "PETG" })); + config.set_key_value("machine_start_gcode", new ConfigOptionString("G28\nG1 Z5")); + + ScopedTemporaryFile tmp(".json"); + config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"); + std::string file_contents; + { + boost::nowide::ifstream ifs(tmp.string()); + file_contents.assign(std::istreambuf_iterator(ifs), std::istreambuf_iterator()); + } + // The file format: one tab per nesting level and a trailing newline. + REQUIRE_FALSE(file_contents.empty()); + CHECK(file_contents.rfind("{\n\t\"", 0) == 0); + CHECK(file_contents.back() == '\n'); + + std::ostringstream strict, replaced; + config.save_to_json(strict, "test_preset", "User", "1.0.0.0"); + config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true); + CHECK(strict.str() == file_contents); + CHECK(replaced.str() == file_contents); + CHECK(nlohmann::json::parse(strict.str())["machine_start_gcode"] == "G28\nG1 Z5"); +} + +TEST_CASE("save_to_json replaces invalid UTF-8 in a stream only when asked", "[Config]") { + DynamicPrintConfig config; + config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff")); + + std::ostringstream strict, replaced; + CHECK_THROWS_AS(config.save_to_json(strict, "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error); + REQUIRE_NOTHROW(config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true)); + CHECK(nlohmann::json::parse(replaced.str())["machine_start_gcode"] == "G28 ; \xEF\xBF\xBD"); +} + +TEST_CASE("save_to_json leaves an existing file untouched when the config cannot be serialized", "[Config]") { + DynamicPrintConfig config; + config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff")); + + ScopedTemporaryFile tmp(".json"); + { + boost::nowide::ofstream ofs(tmp.string()); + ofs << "previous"; + } + CHECK_THROWS_AS(config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error); + + boost::nowide::ifstream ifs(tmp.string()); + const std::string contents((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + CHECK(contents == "previous"); +} + TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") { const std::vector refs = { "master_plugin;;header-stamp",