#include "SL1.hpp" #include "GCode/ThumbnailData.hpp" #include "libslic3r/Time.hpp" #include #include #include "libslic3r/Zipper.hpp" #include "libslic3r/SLAPrint.hpp" #include #include "libslic3r/Exception.hpp" #include "libslic3r/SlicesToTriangleMesh.hpp" #include "libslic3r/MarchingSquares.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/SLA/RasterBase.hpp" #include "libslic3r/miniz_extension.hpp" #include "libslic3r/PNGReadWrite.hpp" #include #include #include 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 { namespace { struct PNGBuffer { std::vector buf; std::string fname; }; struct ArchiveData { boost::property_tree::ptree profile, config; std::vector images; }; static const constexpr char *CONFIG_FNAME = "config.ini"; static const constexpr char *PROFILE_FNAME = "prusaslicer.ini"; boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry, MZ_Archive & zip) { std::string buf(size_t(entry.m_uncomp_size), '\0'); if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, buf.data(), buf.size(), 0)) throw Slic3r::FileIOError(zip.get_errorstr()); boost::property_tree::ptree tree; std::stringstream ss(buf); boost::property_tree::read_ini(ss, tree); return tree; } PNGBuffer read_png(const mz_zip_archive_file_stat &entry, MZ_Archive & zip, const std::string & name) { std::vector buf(entry.m_uncomp_size); if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename, buf.data(), buf.size(), 0)) throw Slic3r::FileIOError(zip.get_errorstr()); return {std::move(buf), (name.empty() ? entry.m_filename : name)}; } ArchiveData extract_sla_archive(const std::string &zipfname, const std::string &exclude) { ArchiveData arch; // Little RAII struct Arch: public MZ_Archive { Arch(const std::string &fname) { if (!open_zip_reader(&arch, fname)) throw Slic3r::FileIOError(get_errorstr()); } ~Arch() { close_zip_reader(&arch); } } zip (zipfname); mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch); for (mz_uint i = 0; i < num_entries; ++i) { mz_zip_archive_file_stat entry; if (mz_zip_reader_file_stat(&zip.arch, i, &entry)) { std::string name = entry.m_filename; boost::algorithm::to_lower(name); if (boost::algorithm::contains(name, exclude)) continue; if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip); if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip); if (boost::filesystem::path(name).extension().string() == ".png") { auto it = std::lower_bound( arch.images.begin(), arch.images.end(), PNGBuffer{{}, name}, [](const PNGBuffer &r1, const PNGBuffer &r2) { return std::less()(r1.fname, r2.fname); }); arch.images.insert(it, read_png(entry, zip, name)); } } } return arch; } ExPolygons rings_to_expolygons(const std::vector &rings, double px_w, double px_h) { ExPolygons polys; polys.reserve(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); } // reverse the raster transformations return union_ex(polys); } template 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(); } } } struct RasterParams { sla::RasterBase::Trafo trafo; // Raster transformations coord_t width, height; // scaled raster dimensions (not resolution) double px_h, px_w; // pixel dimesions marchsq::Coord win; // marching squares window size }; 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 Slic3r::FileIOError("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; } struct SliceParams { double layerh = 0., initial_layerh = 0.; }; SliceParams get_slice_params(const DynamicPrintConfig &cfg) { auto *opt_layerh = cfg.option("layer_height"); auto *opt_init_layerh = cfg.option("initial_layer_height"); if (!opt_layerh || !opt_init_layerh) throw Slic3r::FileIOError("Invalid SL1 / SL1S file"); return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()}; } std::vector extract_slices_from_sla_archive( ArchiveData & arch, const RasterParams & rstp, std::function progr) { auto jobdir = arch.config.get("jobDir"); for (auto &c : jobdir) c = std::tolower(c); std::vector slices(arch.images.size()); struct Status { double incr, val, prev; bool stop = false; tbb::spin_mutex mutex; } st {100. / slices.size(), 0., 0.}; tbb::parallel_for(size_t(0), arch.images.size(), [&arch, &slices, &st, &rstp, 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.images[i].buf.data(), arch.images[i].buf.size()}; if (!png::decode_png(rb, img)) return; auto rings = marchsq::execute(img, 128, rstp.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); }); if (st.stop) slices = {}; return slices; } } // namespace ConfigSubstitutions import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out) { ArchiveData arch = extract_sla_archive(zipfname, "png"); return out.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); } ConfigSubstitutions import_sla_archive( const std::string & zipfname, Vec2i windowsize, TriangleMesh & out, DynamicPrintConfig & profile, std::function progr) { // Ensure minimum window size for marching squares windowsize.x() = std::max(2, windowsize.x()); windowsize.y() = std::max(2, windowsize.y()); ArchiveData arch = extract_sla_archive(zipfname, "thumbnail"); ConfigSubstitutions config_substitutions = profile.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable); RasterParams rstp = get_raster_params(profile); rstp.win = {windowsize.y(), windowsize.x()}; SliceParams slicp = get_slice_params(profile); std::vector slices = extract_slices_from_sla_archive(arch, rstp, progr); if (!slices.empty()) out = slices_to_triangle_mesh(slices, 0, slicp.layerh, slicp.initial_layerh); return config_substitutions; } 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; } 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) { 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"); 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); 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 &cfg = print.full_print_config(); for (const std::string &key : cfg.keys()) if (! is_banned(key) && ! cfg.option(key)->is_nil()) m[key] = cfg.opt_serialize(key); } } // namespace uqptr SL1Archive::create_raster() const { sla::RasterBase::Resolution res; sla::RasterBase::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::RasterBase::Resolution{pw, ph}; pxdim = sla::RasterBase::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{}; } void SL1Archive::export_print(Zipper& zipper, const SLAPrint &print, 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); 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()); } } catch(std::exception& e) { BOOST_LOG_TRIVIAL(error) << e.what(); // Rethrow the exception throw; } } } // namespace Slic3r