1 #include "SL1.hpp"
2 #include "GCode/ThumbnailData.hpp"
3 #include "libslic3r/Time.hpp"
4
5 #include <boost/log/trivial.hpp>
6 #include <boost/filesystem.hpp>
7
8 #include "libslic3r/Zipper.hpp"
9 #include "libslic3r/SLAPrint.hpp"
10
11 #include <sstream>
12
13 #include "libslic3r/Exception.hpp"
14 #include "libslic3r/SlicesToTriangleMesh.hpp"
15 #include "libslic3r/MarchingSquares.hpp"
16 #include "libslic3r/ClipperUtils.hpp"
17 #include "libslic3r/MTUtils.hpp"
18 #include "libslic3r/PrintConfig.hpp"
19 #include "libslic3r/SLA/RasterBase.hpp"
20 #include "libslic3r/miniz_extension.hpp"
21 #include "libslic3r/PNGReadWrite.hpp"
22
23 #include <boost/property_tree/ini_parser.hpp>
24 #include <boost/filesystem/path.hpp>
25 #include <boost/algorithm/string.hpp>
26
27 namespace marchsq {
28
29 template<> struct _RasterTraits<Slic3r::png::ImageGreyscale> {
30 using Rst = Slic3r::png::ImageGreyscale;
31
32 // The type of pixel cell in the raster
33 using ValueType = uint8_t;
34
35 // Value at a given position
getmarchsq::_RasterTraits36 static uint8_t get(const Rst &rst, size_t row, size_t col)
37 {
38 return rst.get(row, col);
39 }
40
41 // Number of rows and cols of the raster
rowsmarchsq::_RasterTraits42 static size_t rows(const Rst &rst) { return rst.rows; }
colsmarchsq::_RasterTraits43 static size_t cols(const Rst &rst) { return rst.cols; }
44 };
45
46 } // namespace marchsq
47
48 namespace Slic3r {
49
50 namespace {
51
52 struct PNGBuffer { std::vector<uint8_t> buf; std::string fname; };
53 struct ArchiveData {
54 boost::property_tree::ptree profile, config;
55 std::vector<PNGBuffer> images;
56 };
57
58 static const constexpr char *CONFIG_FNAME = "config.ini";
59 static const constexpr char *PROFILE_FNAME = "prusaslicer.ini";
60
read_ini(const mz_zip_archive_file_stat & entry,MZ_Archive & zip)61 boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry,
62 MZ_Archive & zip)
63 {
64 std::string buf(size_t(entry.m_uncomp_size), '\0');
65
66 if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename,
67 buf.data(), buf.size(), 0))
68 throw Slic3r::FileIOError(zip.get_errorstr());
69
70 boost::property_tree::ptree tree;
71 std::stringstream ss(buf);
72 boost::property_tree::read_ini(ss, tree);
73 return tree;
74 }
75
read_png(const mz_zip_archive_file_stat & entry,MZ_Archive & zip,const std::string & name)76 PNGBuffer read_png(const mz_zip_archive_file_stat &entry,
77 MZ_Archive & zip,
78 const std::string & name)
79 {
80 std::vector<uint8_t> buf(entry.m_uncomp_size);
81
82 if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename,
83 buf.data(), buf.size(), 0))
84 throw Slic3r::FileIOError(zip.get_errorstr());
85
86 return {std::move(buf), (name.empty() ? entry.m_filename : name)};
87 }
88
extract_sla_archive(const std::string & zipfname,const std::string & exclude)89 ArchiveData extract_sla_archive(const std::string &zipfname,
90 const std::string &exclude)
91 {
92 ArchiveData arch;
93
94 // Little RAII
95 struct Arch: public MZ_Archive {
96 Arch(const std::string &fname) {
97 if (!open_zip_reader(&arch, fname))
98 throw Slic3r::FileIOError(get_errorstr());
99 }
100
101 ~Arch() { close_zip_reader(&arch); }
102 } zip (zipfname);
103
104 mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch);
105
106 for (mz_uint i = 0; i < num_entries; ++i)
107 {
108 mz_zip_archive_file_stat entry;
109
110 if (mz_zip_reader_file_stat(&zip.arch, i, &entry))
111 {
112 std::string name = entry.m_filename;
113 boost::algorithm::to_lower(name);
114
115 if (boost::algorithm::contains(name, exclude)) continue;
116
117 if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip);
118 if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip);
119
120 if (boost::filesystem::path(name).extension().string() == ".png") {
121 auto it = std::lower_bound(
122 arch.images.begin(), arch.images.end(), PNGBuffer{{}, name},
123 [](const PNGBuffer &r1, const PNGBuffer &r2) {
124 return std::less<std::string>()(r1.fname, r2.fname);
125 });
126
127 arch.images.insert(it, read_png(entry, zip, name));
128 }
129 }
130 }
131
132 return arch;
133 }
134
rings_to_expolygons(const std::vector<marchsq::Ring> & rings,double px_w,double px_h)135 ExPolygons rings_to_expolygons(const std::vector<marchsq::Ring> &rings,
136 double px_w, double px_h)
137 {
138 ExPolygons polys; polys.reserve(rings.size());
139
140 for (const marchsq::Ring &ring : rings) {
141 Polygon poly; Points &pts = poly.points;
142 pts.reserve(ring.size());
143
144 for (const marchsq::Coord &crd : ring)
145 pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h));
146
147 polys.emplace_back(poly);
148 }
149
150 // reverse the raster transformations
151 return union_ex(polys);
152 }
153
foreach_vertex(ExPolygon & poly,Fn && fn)154 template<class Fn> void foreach_vertex(ExPolygon &poly, Fn &&fn)
155 {
156 for (auto &p : poly.contour.points) fn(p);
157 for (auto &h : poly.holes)
158 for (auto &p : h.points) fn(p);
159 }
160
invert_raster_trafo(ExPolygons & expolys,const sla::RasterBase::Trafo & trafo,coord_t width,coord_t height)161 void invert_raster_trafo(ExPolygons & expolys,
162 const sla::RasterBase::Trafo &trafo,
163 coord_t width,
164 coord_t height)
165 {
166 if (trafo.flipXY) std::swap(height, width);
167
168 for (auto &expoly : expolys) {
169 if (trafo.mirror_y)
170 foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); });
171
172 if (trafo.mirror_x)
173 foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); });
174
175 expoly.translate(-trafo.center_x, -trafo.center_y);
176
177 if (trafo.flipXY)
178 foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); });
179
180 if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) {
181 expoly.contour.reverse();
182 for (auto &h : expoly.holes) h.reverse();
183 }
184 }
185 }
186
187 struct RasterParams {
188 sla::RasterBase::Trafo trafo; // Raster transformations
189 coord_t width, height; // scaled raster dimensions (not resolution)
190 double px_h, px_w; // pixel dimesions
191 marchsq::Coord win; // marching squares window size
192 };
193
get_raster_params(const DynamicPrintConfig & cfg)194 RasterParams get_raster_params(const DynamicPrintConfig &cfg)
195 {
196 auto *opt_disp_cols = cfg.option<ConfigOptionInt>("display_pixels_x");
197 auto *opt_disp_rows = cfg.option<ConfigOptionInt>("display_pixels_y");
198 auto *opt_disp_w = cfg.option<ConfigOptionFloat>("display_width");
199 auto *opt_disp_h = cfg.option<ConfigOptionFloat>("display_height");
200 auto *opt_mirror_x = cfg.option<ConfigOptionBool>("display_mirror_x");
201 auto *opt_mirror_y = cfg.option<ConfigOptionBool>("display_mirror_y");
202 auto *opt_orient = cfg.option<ConfigOptionEnum<SLADisplayOrientation>>("display_orientation");
203
204 if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h ||
205 !opt_mirror_x || !opt_mirror_y || !opt_orient)
206 throw Slic3r::FileIOError("Invalid SL1 / SL1S file");
207
208 RasterParams rstp;
209
210 rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1);
211 rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1);
212
213 rstp.trafo = sla::RasterBase::Trafo{opt_orient->value == sladoLandscape ?
214 sla::RasterBase::roLandscape :
215 sla::RasterBase::roPortrait,
216 {opt_mirror_x->value, opt_mirror_y->value}};
217
218 rstp.height = scaled(opt_disp_h->value);
219 rstp.width = scaled(opt_disp_w->value);
220
221 return rstp;
222 }
223
224 struct SliceParams { double layerh = 0., initial_layerh = 0.; };
225
get_slice_params(const DynamicPrintConfig & cfg)226 SliceParams get_slice_params(const DynamicPrintConfig &cfg)
227 {
228 auto *opt_layerh = cfg.option<ConfigOptionFloat>("layer_height");
229 auto *opt_init_layerh = cfg.option<ConfigOptionFloat>("initial_layer_height");
230
231 if (!opt_layerh || !opt_init_layerh)
232 throw Slic3r::FileIOError("Invalid SL1 / SL1S file");
233
234 return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()};
235 }
236
extract_slices_from_sla_archive(ArchiveData & arch,const RasterParams & rstp,std::function<bool (int)> progr)237 std::vector<ExPolygons> extract_slices_from_sla_archive(
238 ArchiveData & arch,
239 const RasterParams & rstp,
240 std::function<bool(int)> progr)
241 {
242 auto jobdir = arch.config.get<std::string>("jobDir");
243 for (auto &c : jobdir) c = std::tolower(c);
244
245 std::vector<ExPolygons> slices(arch.images.size());
246
247 struct Status
248 {
249 double incr, val, prev;
250 bool stop = false;
251 tbb::spin_mutex mutex;
252 } st {100. / slices.size(), 0., 0.};
253
254 tbb::parallel_for(size_t(0), arch.images.size(),
255 [&arch, &slices, &st, &rstp, progr](size_t i) {
256 // Status indication guarded with the spinlock
257 {
258 std::lock_guard<tbb::spin_mutex> lck(st.mutex);
259 if (st.stop) return;
260
261 st.val += st.incr;
262 double curr = std::round(st.val);
263 if (curr > st.prev) {
264 st.prev = curr;
265 st.stop = !progr(int(curr));
266 }
267 }
268
269 png::ImageGreyscale img;
270 png::ReadBuf rb{arch.images[i].buf.data(), arch.images[i].buf.size()};
271 if (!png::decode_png(rb, img)) return;
272
273 auto rings = marchsq::execute(img, 128, rstp.win);
274 ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h);
275
276 // Invert the raster transformations indicated in
277 // the profile metadata
278 invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height);
279
280 slices[i] = std::move(expolys);
281 });
282
283 if (st.stop) slices = {};
284
285 return slices;
286 }
287
288 } // namespace
289
import_sla_archive(const std::string & zipfname,DynamicPrintConfig & out)290 ConfigSubstitutions import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out)
291 {
292 ArchiveData arch = extract_sla_archive(zipfname, "png");
293 return out.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable);
294 }
295
import_sla_archive(const std::string & zipfname,Vec2i windowsize,TriangleMesh & out,DynamicPrintConfig & profile,std::function<bool (int)> progr)296 ConfigSubstitutions import_sla_archive(
297 const std::string & zipfname,
298 Vec2i windowsize,
299 TriangleMesh & out,
300 DynamicPrintConfig & profile,
301 std::function<bool(int)> progr)
302 {
303 // Ensure minimum window size for marching squares
304 windowsize.x() = std::max(2, windowsize.x());
305 windowsize.y() = std::max(2, windowsize.y());
306
307 ArchiveData arch = extract_sla_archive(zipfname, "thumbnail");
308 ConfigSubstitutions config_substitutions = profile.load(arch.profile, ForwardCompatibilitySubstitutionRule::Enable);
309
310 RasterParams rstp = get_raster_params(profile);
311 rstp.win = {windowsize.y(), windowsize.x()};
312
313 SliceParams slicp = get_slice_params(profile);
314
315 std::vector<ExPolygons> slices =
316 extract_slices_from_sla_archive(arch, rstp, progr);
317
318 if (!slices.empty())
319 out = slices_to_triangle_mesh(slices, 0, slicp.layerh, slicp.initial_layerh);
320
321 return config_substitutions;
322 }
323
324 using ConfMap = std::map<std::string, std::string>;
325
326 namespace {
327
to_ini(const ConfMap & m)328 std::string to_ini(const ConfMap &m)
329 {
330 std::string ret;
331 for (auto ¶m : m) ret += param.first + " = " + param.second + "\n";
332
333 return ret;
334 }
335
get_cfg_value(const DynamicPrintConfig & cfg,const std::string & key)336 std::string get_cfg_value(const DynamicPrintConfig &cfg, const std::string &key)
337 {
338 std::string ret;
339
340 if (cfg.has(key)) {
341 auto opt = cfg.option(key);
342 if (opt) ret = opt->serialize();
343 }
344
345 return ret;
346 }
347
fill_iniconf(ConfMap & m,const SLAPrint & print)348 void fill_iniconf(ConfMap &m, const SLAPrint &print)
349 {
350 auto &cfg = print.full_print_config();
351 m["layerHeight"] = get_cfg_value(cfg, "layer_height");
352 m["expTime"] = get_cfg_value(cfg, "exposure_time");
353 m["expTimeFirst"] = get_cfg_value(cfg, "initial_exposure_time");
354 m["materialName"] = get_cfg_value(cfg, "sla_material_settings_id");
355 m["printerModel"] = get_cfg_value(cfg, "printer_model");
356 m["printerVariant"] = get_cfg_value(cfg, "printer_variant");
357 m["printerProfile"] = get_cfg_value(cfg, "printer_settings_id");
358 m["printProfile"] = get_cfg_value(cfg, "sla_print_settings_id");
359 m["fileCreationTimestamp"] = Utils::utc_timestamp();
360 m["prusaSlicerVersion"] = SLIC3R_BUILD_ID;
361
362 SLAPrintStatistics stats = print.print_statistics();
363 // Set statistics values to the printer
364
365 double used_material = (stats.objects_used_material +
366 stats.support_used_material) / 1000;
367
368 int num_fade = print.default_object_config().faded_layers.getInt();
369 num_fade = num_fade >= 0 ? num_fade : 0;
370
371 m["usedMaterial"] = std::to_string(used_material);
372 m["numFade"] = std::to_string(num_fade);
373 m["numSlow"] = std::to_string(stats.slow_layers_count);
374 m["numFast"] = std::to_string(stats.fast_layers_count);
375 m["printTime"] = std::to_string(stats.estimated_print_time);
376
377 m["action"] = "print";
378 }
379
fill_slicerconf(ConfMap & m,const SLAPrint & print)380 void fill_slicerconf(ConfMap &m, const SLAPrint &print)
381 {
382 using namespace std::literals::string_view_literals;
383
384 // Sorted list of config keys, which shall not be stored into the ini.
385 static constexpr auto banned_keys = {
386 "compatible_printers"sv,
387 "compatible_prints"sv,
388 //FIXME The print host keys should not be exported to full_print_config anymore. The following keys may likely be removed.
389 "print_host"sv,
390 "printhost_apikey"sv,
391 "printhost_cafile"sv
392 };
393
394 assert(std::is_sorted(banned_keys.begin(), banned_keys.end()));
395 auto is_banned = [](const std::string &key) {
396 return std::binary_search(banned_keys.begin(), banned_keys.end(), key);
397 };
398
399 auto &cfg = print.full_print_config();
400 for (const std::string &key : cfg.keys())
401 if (! is_banned(key) && ! cfg.option(key)->is_nil())
402 m[key] = cfg.opt_serialize(key);
403
404 }
405
406 } // namespace
407
create_raster() const408 uqptr<sla::RasterBase> SL1Archive::create_raster() const
409 {
410 sla::RasterBase::Resolution res;
411 sla::RasterBase::PixelDim pxdim;
412 std::array<bool, 2> mirror;
413
414 double w = m_cfg.display_width.getFloat();
415 double h = m_cfg.display_height.getFloat();
416 auto pw = size_t(m_cfg.display_pixels_x.getInt());
417 auto ph = size_t(m_cfg.display_pixels_y.getInt());
418
419 mirror[X] = m_cfg.display_mirror_x.getBool();
420 mirror[Y] = m_cfg.display_mirror_y.getBool();
421
422 auto ro = m_cfg.display_orientation.getInt();
423 sla::RasterBase::Orientation orientation =
424 ro == sla::RasterBase::roPortrait ? sla::RasterBase::roPortrait :
425 sla::RasterBase::roLandscape;
426
427 if (orientation == sla::RasterBase::roPortrait) {
428 std::swap(w, h);
429 std::swap(pw, ph);
430 }
431
432 res = sla::RasterBase::Resolution{pw, ph};
433 pxdim = sla::RasterBase::PixelDim{w / pw, h / ph};
434 sla::RasterBase::Trafo tr{orientation, mirror};
435
436 double gamma = m_cfg.gamma_correction.getFloat();
437
438 return sla::create_raster_grayscale_aa(res, pxdim, gamma, tr);
439 }
440
get_encoder() const441 sla::RasterEncoder SL1Archive::get_encoder() const
442 {
443 return sla::PNGRasterEncoder{};
444 }
445
export_print(Zipper & zipper,const SLAPrint & print,const std::string & prjname)446 void SL1Archive::export_print(Zipper& zipper,
447 const SLAPrint &print,
448 const std::string &prjname)
449 {
450 std::string project =
451 prjname.empty() ?
452 boost::filesystem::path(zipper.get_filename()).stem().string() :
453 prjname;
454
455 ConfMap iniconf, slicerconf;
456 fill_iniconf(iniconf, print);
457
458 iniconf["jobDir"] = project;
459
460 fill_slicerconf(slicerconf, print);
461
462 try {
463 zipper.add_entry("config.ini");
464 zipper << to_ini(iniconf);
465 zipper.add_entry("prusaslicer.ini");
466 zipper << to_ini(slicerconf);
467
468 size_t i = 0;
469 for (const sla::EncodedRaster &rst : m_layers) {
470
471 std::string imgname = project + string_printf("%.5d", i++) + "." +
472 rst.extension();
473
474 zipper.add_entry(imgname.c_str(), rst.data(), rst.size());
475 }
476 } catch(std::exception& e) {
477 BOOST_LOG_TRIVIAL(error) << e.what();
478 // Rethrow the exception
479 throw;
480 }
481 }
482
483 } // namespace Slic3r
484