///|/ Copyright (c) Prusa Research 2020 - 2023 Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv ///|/ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ #include "SL1.hpp" #include #include #include #include "libslic3r/Time.hpp" #include "libslic3r/Zipper.hpp" #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Exception.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/miniz_extension.hpp" #include "libslic3r/LocalesUtils.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Utils/JsonUtils.hpp" #include "SLAArchiveReader.hpp" #include "SLAArchiveFormatRegistry.hpp" #include "ZipperArchiveImport.hpp" #include "libslic3r/MarchingSquares.hpp" #include "libslic3r/PNGReadWrite.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Execution/ExecutionTBB.hpp" #include "libslic3r/SLA/RasterBase.hpp" #include #include #include #include namespace Slic3r { using ConfMap = std::map; namespace { std::string to_ini(const ConfMap &m) { std::string ret; for (auto ¶m : m) ret += param.first + " = " + param.second + "\n"; return ret; } static std::string get_key(const std::string& opt_key) { static const std::set ms_opts = { "delay_before_exposure" , "delay_after_exposure" , "tilt_down_offset_delay" , "tilt_up_offset_delay" , "tilt_down_delay" , "tilt_up_delay" }; static const std::set nm_opts = { "tower_hop_height" }; static const std::set speed_opts = { "tower_speed" , "tilt_down_initial_speed" , "tilt_down_finish_speed" , "tilt_up_initial_speed" , "tilt_up_finish_speed" }; if (ms_opts.find(opt_key) != ms_opts.end()) return opt_key + "_ms"; if (nm_opts.find(opt_key) != nm_opts.end()) return opt_key + "_nm"; if (speed_opts.find(opt_key) != speed_opts.end()) return boost::replace_all_copy(opt_key, "_speed", "_profile"); return opt_key; } namespace pt = boost::property_tree; std::string to_json(const SLAPrint& print, const ConfMap &m) { auto& cfg = print.full_print_config(); pt::ptree below_node; pt::ptree above_node; const t_config_enum_names& tilt_enum_names = ConfigOptionEnum< TiltSpeeds>::get_enum_names(); const t_config_enum_names& tower_enum_names = ConfigOptionEnum::get_enum_names(); for (const std::string& opt_key : tilt_options()) { const ConfigOption* opt = cfg.option(opt_key); assert(opt != nullptr); switch (opt->type()) { case coFloats: { auto values = static_cast(opt); // those options have to be exported in ms instead of s below_node.put(get_key(opt_key), int(1000 * values->get_at(0))); above_node.put(get_key(opt_key), int(1000 * values->get_at(1))); } break; case coInts: { auto values = static_cast(opt); int koef = opt_key == "tower_hop_height" ? 1000000 : 1; below_node.put(get_key(opt_key), koef * values->get_at(0)); above_node.put(get_key(opt_key), koef * values->get_at(1)); } break; case coBools: { auto values = static_cast(opt); below_node.put(get_key(opt_key), values->get_at(0)); above_node.put(get_key(opt_key), values->get_at(1)); } break; case coEnums: { const t_config_enum_names& enum_names = opt_key == "tower_speed" ? tower_enum_names : tilt_enum_names; auto values = static_cast*>(opt); below_node.put(get_key(opt_key), enum_names[values->get_at(0)]); above_node.put(get_key(opt_key), enum_names[values->get_at(1)]); } break; case coNone: default: break; } } pt::ptree profile_node; profile_node.put("area_fill", cfg.option("area_fill")->serialize()); profile_node.add_child("below_area_fill", below_node); profile_node.add_child("above_area_fill", above_node); pt::ptree root; // params from config.ini for (auto& param : m) root.put(param.first, param.second ); root.put("version", "1"); root.add_child("exposure_profile", profile_node); // Boost confirms its implementation has no 100% conformance to JSON standard. // In the boost libraries, boost will always serialize each value as string and parse all values to a string equivalent. // so, post-prosess output return write_json_with_post_process(root); } std::string get_cfg_value(const DynamicPrintConfig &cfg, const std::string &key) { std::string ret; if (cfg.has(key)) { auto opt = cfg.option(key); if (opt) ret = opt->serialize(); } return ret; } void fill_iniconf(ConfMap &m, const SLAPrint &print) { CNumericLocalesSetter locales_setter; // for to_string auto &cfg = print.full_print_config(); m["layerHeight"] = get_cfg_value(cfg, "layer_height"); m["expTime"] = get_cfg_value(cfg, "exposure_time"); m["expTimeFirst"] = get_cfg_value(cfg, "initial_exposure_time"); const std::string mps = get_cfg_value(cfg, "material_print_speed"); m["expUserProfile"] = mps == "slow" ? "1" : mps == "fast" ? "0" : "2"; m["materialName"] = get_cfg_value(cfg, "sla_material_settings_id"); m["printerModel"] = get_cfg_value(cfg, "printer_model"); m["printerVariant"] = get_cfg_value(cfg, "printer_variant"); m["printerProfile"] = get_cfg_value(cfg, "printer_settings_id"); m["printProfile"] = get_cfg_value(cfg, "sla_print_settings_id"); m["fileCreationTimestamp"] = Utils::utc_timestamp(); m["prusaSlicerVersion"] = SLIC3R_BUILD_ID; SLAPrintStatistics stats = print.print_statistics(); // Set statistics values to the printer double used_material = (stats.objects_used_material + stats.support_used_material) / 1000; int num_fade = print.default_object_config().faded_layers.getInt(); num_fade = num_fade >= 0 ? num_fade : 0; m["usedMaterial"] = std::to_string(used_material); m["numFade"] = std::to_string(num_fade); m["numSlow"] = std::to_string(stats.slow_layers_count); m["numFast"] = std::to_string(stats.fast_layers_count); m["printTime"] = std::to_string(stats.estimated_print_time); bool hollow_en = false; auto it = print.objects().begin(); while (!hollow_en && it != print.objects().end()) hollow_en = (*it++)->config().hollowing_enable; m["hollow"] = hollow_en ? "1" : "0"; m["action"] = "print"; } void fill_slicerconf(ConfMap &m, const SLAPrint &print) { using namespace std::literals::string_view_literals; // Sorted list of config keys, which shall not be stored into the ini. static constexpr auto banned_keys = { "compatible_printers"sv, "compatible_prints"sv, //FIXME The print host keys should not be exported to full_print_config anymore. The following keys may likely be removed. "print_host"sv, "printhost_apikey"sv, "printhost_cafile"sv }; assert(std::is_sorted(banned_keys.begin(), banned_keys.end())); auto is_banned = [](const std::string &key) { return std::binary_search(banned_keys.begin(), banned_keys.end(), key); }; auto is_tilt_param = [](const std::string& key) -> bool { const auto& keys = tilt_options(); return std::find(keys.begin(), keys.end(), key) != keys.end(); }; auto &cfg = print.full_print_config(); for (const std::string &key : cfg.keys()) if (! is_banned(key) && !is_tilt_param(key) && ! cfg.option(key)->is_nil()) m[key] = cfg.opt_serialize(key); } } // namespace std::unique_ptr SL1Archive::create_raster() const { sla::Resolution res; sla::PixelDim pxdim; std::array mirror; double w = m_cfg.display_width.getFloat(); double h = m_cfg.display_height.getFloat(); auto pw = size_t(m_cfg.display_pixels_x.getInt()); auto ph = size_t(m_cfg.display_pixels_y.getInt()); mirror[X] = m_cfg.display_mirror_x.getBool(); mirror[Y] = m_cfg.display_mirror_y.getBool(); auto ro = m_cfg.display_orientation.getInt(); sla::RasterBase::Orientation orientation = ro == sla::RasterBase::roPortrait ? sla::RasterBase::roPortrait : sla::RasterBase::roLandscape; if (orientation == sla::RasterBase::roPortrait) { std::swap(w, h); std::swap(pw, ph); } res = sla::Resolution{pw, ph}; pxdim = sla::PixelDim{w / pw, h / ph}; sla::RasterBase::Trafo tr{orientation, mirror}; double gamma = m_cfg.gamma_correction.getFloat(); return sla::create_raster_grayscale_aa(res, pxdim, gamma, tr); } sla::RasterEncoder SL1Archive::get_encoder() const { return sla::PNGRasterEncoder{}; } static void write_thumbnail(Zipper &zipper, const ThumbnailData &data) { size_t png_size = 0; void *png_data = tdefl_write_image_to_png_file_in_memory_ex( (const void *) data.pixels.data(), data.width, data.height, 4, &png_size, MZ_DEFAULT_LEVEL, 1); if (png_data != nullptr) { zipper.add_entry("thumbnail/thumbnail" + std::to_string(data.width) + "x" + std::to_string(data.height) + ".png", static_cast(png_data), png_size); mz_free(png_data); } } void SL1Archive::export_print(Zipper &zipper, const SLAPrint &print, const ThumbnailsList &thumbnails, const std::string &prjname) { std::string project = prjname.empty() ? boost::filesystem::path(zipper.get_filename()).stem().string() : prjname; ConfMap iniconf, slicerconf; fill_iniconf(iniconf, print); iniconf["jobDir"] = project; fill_slicerconf(slicerconf, print); try { zipper.add_entry("config.ini"); zipper << to_ini(iniconf); zipper.add_entry("prusaslicer.ini"); zipper << to_ini(slicerconf); zipper.add_entry("config.json"); zipper << to_json(print, iniconf); size_t i = 0; for (const sla::EncodedRaster &rst : m_layers) { std::string imgname = project + string_printf("%.5d", i++) + "." + rst.extension(); zipper.add_entry(imgname.c_str(), rst.data(), rst.size()); } for (const ThumbnailData& data : thumbnails) if (data.is_valid()) write_thumbnail(zipper, data); zipper.finalize(); } catch(std::exception& e) { BOOST_LOG_TRIVIAL(error) << e.what(); // Rethrow the exception throw; } } void SL1Archive::export_print(const std::string fname, const SLAPrint &print, const ThumbnailsList &thumbnails, const std::string &prjname) { Zipper zipper{fname, Zipper::FAST_COMPRESSION}; export_print(zipper, print, thumbnails, prjname); } } // namespace Slic3r // ///////////////////////////////////////////////////////////////////////////// // Reader implementation // ///////////////////////////////////////////////////////////////////////////// namespace marchsq { template<> struct _RasterTraits { using Rst = Slic3r::png::ImageGreyscale; // The type of pixel cell in the raster using ValueType = uint8_t; // Value at a given position static uint8_t get(const Rst &rst, size_t row, size_t col) { return rst.get(row, col); } // Number of rows and cols of the raster static size_t rows(const Rst &rst) { return rst.rows; } static size_t cols(const Rst &rst) { return rst.cols; } }; } // namespace marchsq namespace Slic3r { template static void foreach_vertex(ExPolygon &poly, Fn &&fn) { for (auto &p : poly.contour.points) fn(p); for (auto &h : poly.holes) for (auto &p : h.points) fn(p); } void invert_raster_trafo(ExPolygons & expolys, const sla::RasterBase::Trafo &trafo, coord_t width, coord_t height) { if (trafo.flipXY) std::swap(height, width); for (auto &expoly : expolys) { if (trafo.mirror_y) foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); }); if (trafo.mirror_x) foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); }); expoly.translate(-trafo.center_x, -trafo.center_y); if (trafo.flipXY) foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); }); if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) { expoly.contour.reverse(); for (auto &h : expoly.holes) h.reverse(); } } } RasterParams get_raster_params(const DynamicPrintConfig &cfg) { auto *opt_disp_cols = cfg.option("display_pixels_x"); auto *opt_disp_rows = cfg.option("display_pixels_y"); auto *opt_disp_w = cfg.option("display_width"); auto *opt_disp_h = cfg.option("display_height"); auto *opt_mirror_x = cfg.option("display_mirror_x"); auto *opt_mirror_y = cfg.option("display_mirror_y"); auto *opt_orient = cfg.option>("display_orientation"); if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h || !opt_mirror_x || !opt_mirror_y || !opt_orient) throw MissingProfileError("Invalid SL1 / SL1S file"); RasterParams rstp; rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1); rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1); rstp.trafo = sla::RasterBase::Trafo{opt_orient->value == sladoLandscape ? sla::RasterBase::roLandscape : sla::RasterBase::roPortrait, {opt_mirror_x->value, opt_mirror_y->value}}; rstp.height = scaled(opt_disp_h->value); rstp.width = scaled(opt_disp_w->value); return rstp; } namespace { ExPolygons rings_to_expolygons(const std::vector &rings, double px_w, double px_h) { auto polys = reserve_vector(rings.size()); for (const marchsq::Ring &ring : rings) { Polygon poly; Points &pts = poly.points; pts.reserve(ring.size()); for (const marchsq::Coord &crd : ring) pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h)); polys.emplace_back(poly); } // TODO: Is a union necessary? return union_ex(polys); } std::vector extract_slices_from_sla_archive( ZipperArchive &arch, const RasterParams &rstp, const marchsq::Coord &win, std::function progr) { std::vector slices(arch.entries.size()); struct Status { double incr, val, prev; bool stop = false; execution::SpinningMutex mutex = {}; } st{100. / slices.size(), 0., 0.}; execution::for_each( ex_tbb, size_t(0), arch.entries.size(), [&arch, &slices, &st, &rstp, &win, progr](size_t i) { // Status indication guarded with the spinlock { std::lock_guard lck(st.mutex); if (st.stop) return; st.val += st.incr; double curr = std::round(st.val); if (curr > st.prev) { st.prev = curr; st.stop = !progr(int(curr)); } } png::ImageGreyscale img; png::ReadBuf rb{arch.entries[i].buf.data(), arch.entries[i].buf.size()}; if (!png::decode_png(rb, img)) return; constexpr uint8_t isoval = 128; auto rings = marchsq::execute(img, isoval, win); ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h); // Invert the raster transformations indicated in the profile metadata invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height); slices[i] = std::move(expolys); }, execution::max_concurrency(ex_tbb)); if (st.stop) slices = {}; return slices; } } // namespace ConfigSubstitutions SL1Reader::read(std::vector &slices, DynamicPrintConfig &profile_out) { Vec2i windowsize; switch(m_quality) { case SLAImportQuality::Fast: windowsize = {8, 8}; break; case SLAImportQuality::Balanced: windowsize = {4, 4}; break; default: case SLAImportQuality::Accurate: windowsize = {2, 2}; break; }; // Ensure minimum window size for marching squares windowsize.x() = std::max(2, windowsize.x()); windowsize.y() = std::max(2, windowsize.y()); std::vector includes = { "ini", "png"}; std::vector excludes = { "thumbnail" }; ZipperArchive arch = read_zipper_archive(m_fname, includes, excludes); auto [profile_use, config_substitutions] = extract_profile(arch, profile_out); RasterParams rstp = get_raster_params(profile_use); marchsq::Coord win = {windowsize.y(), windowsize.x()}; slices = extract_slices_from_sla_archive(arch, rstp, win, m_progr); return std::move(config_substitutions); } ConfigSubstitutions SL1Reader::read(DynamicPrintConfig &out) { ZipperArchive arch = read_zipper_archive(m_fname, {"ini"}, {"png", "thumbnail"}); return out.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); } } // namespace Slic3r