diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 0de0b4e517..2c4a22fc5d 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -162,6 +162,8 @@ set(SLIC3R_SOURCES GCode/WipeTower.hpp GCode/GCodeProcessor.cpp GCode/GCodeProcessor.hpp + GCode/GCodeBinarizer.cpp + GCode/GCodeBinarizer.hpp GCode/AvoidCrossingPerimeters.cpp GCode/AvoidCrossingPerimeters.hpp GCode.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 1f306c83c6..6f148cc695 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -19,7 +19,10 @@ #include "ClipperUtils.hpp" #include "libslic3r.h" #include "LocalesUtils.hpp" -#include "libslic3r/format.hpp" +#include "format.hpp" +#if ENABLE_BINARIZED_GCODE +#include "libslic3r_version.h" +#endif // ENABLE_BINARIZED_GCODE #include #include @@ -837,6 +840,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.initialize(path_tmp); m_processor.set_print(print); +#if ENABLE_BINARIZED_GCODE + m_processor.get_binary_data().reset(); +#endif // ENABLE_BINARIZED_GCODE GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor); if (! file.is_open()) throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); @@ -969,35 +975,47 @@ namespace DoExport { } // Fill in print_statistics and return formatted string containing filament statistics to be inserted into G-code comment section. +#if ENABLE_BINARIZED_GCODE static std::string update_print_stats_and_format_filament_stats( const bool has_wipe_tower, - const WipeTowerData &wipe_tower_data, + const WipeTowerData &wipe_tower_data, + const FullPrintConfig &config, + const std::vector &extruders, + unsigned int initial_extruder_id, + PrintStatistics &print_statistics, + bool export_binary_data, + BinaryGCode::BinaryData &binary_data) +#else + static std::string update_print_stats_and_format_filament_stats( + const bool has_wipe_tower, + const WipeTowerData &wipe_tower_data, const FullPrintConfig &config, - const std::vector &extruders, + const std::vector &extruders, unsigned int initial_extruder_id, PrintStatistics &print_statistics) +#endif // ENABLE_BINARIZED_GCODE { - std::string filament_stats_string_out; + std::string filament_stats_string_out; - print_statistics.clear(); + print_statistics.clear(); print_statistics.total_toolchanges = std::max(0, wipe_tower_data.number_of_toolchanges); print_statistics.initial_extruder_id = initial_extruder_id; std::vector filament_types; - if (! extruders.empty()) { + if (! extruders.empty()) { std::pair out_filament_used_mm ("; filament used [mm] = ", 0); std::pair out_filament_used_cm3("; filament used [cm3] = ", 0); std::pair out_filament_used_g ("; filament used [g] = ", 0); std::pair out_filament_cost ("; filament cost = ", 0); for (const Extruder &extruder : extruders) { - print_statistics.printing_extruders.emplace_back(extruder.id()); - filament_types.emplace_back(config.filament_type.get_at(extruder.id())); + print_statistics.printing_extruders.emplace_back(extruder.id()); + filament_types.emplace_back(config.filament_type.get_at(extruder.id())); double used_filament = extruder.used_filament() + (has_wipe_tower ? wipe_tower_data.used_filament[extruder.id()] : 0.f); double extruded_volume = extruder.extruded_volume() + (has_wipe_tower ? wipe_tower_data.used_filament[extruder.id()] * 2.4052f : 0.f); // assumes 1.75mm filament diameter double filament_weight = extruded_volume * extruder.filament_density() * 0.001; double filament_cost = filament_weight * extruder.filament_cost() * 0.001; - auto append = [&extruder](std::pair &dst, const char *tmpl, double value) { - assert(is_decimal_separator_point()); + auto append = [&extruder](std::pair &dst, const char *tmpl, double value) { + assert(is_decimal_separator_point()); while (dst.second < extruder.id()) { // Fill in the non-printing extruders with zeros. dst.first += (dst.second > 0) ? ", 0" : "0"; @@ -1006,18 +1024,47 @@ namespace DoExport { if (dst.second > 0) dst.first += ", "; char buf[64]; - sprintf(buf, tmpl, value); + sprintf(buf, tmpl, value); dst.first += buf; ++ dst.second; - }; - append(out_filament_used_mm, "%.2lf", used_filament); - append(out_filament_used_cm3, "%.2lf", extruded_volume * 0.001); - if (filament_weight > 0.) { + }; +#if ENABLE_BINARIZED_GCODE + if (export_binary_data) { + char buf[128]; + sprintf(buf, "%.2lf", used_filament); + binary_data.print_metadata.raw_data.push_back({ "filament used [mm]", std::string(buf) }); + sprintf(buf, "%.2lf", extruded_volume * 0.001); + binary_data.print_metadata.raw_data.push_back({ "filament used [cm3]", std::string(buf) }); + } + else { +#endif // ENABLE_BINARIZED_GCODE + append(out_filament_used_mm, "%.2lf", used_filament); + append(out_filament_used_cm3, "%.2lf", extruded_volume * 0.001); +#if ENABLE_BINARIZED_GCODE + } +#endif // ENABLE_BINARIZED_GCODE + if (filament_weight > 0.) { print_statistics.total_weight = print_statistics.total_weight + filament_weight; - append(out_filament_used_g, "%.2lf", filament_weight); - if (filament_cost > 0.) { +#if ENABLE_BINARIZED_GCODE + if (export_binary_data) { + char buf[128]; + sprintf(buf, "%.2lf", filament_weight); + binary_data.print_metadata.raw_data.push_back({ "filament used [g]", std::string(buf) }); + } + else +#endif // ENABLE_BINARIZED_GCODE + append(out_filament_used_g, "%.2lf", filament_weight); + if (filament_cost > 0.) { print_statistics.total_cost = print_statistics.total_cost + filament_cost; - append(out_filament_cost, "%.2lf", filament_cost); +#if ENABLE_BINARIZED_GCODE + if (export_binary_data) { + char buf[128]; + sprintf(buf, "%.2lf", filament_cost); + binary_data.print_metadata.raw_data.push_back({ "filament cost", std::string(buf) }); + } + else +#endif // ENABLE_BINARIZED_GCODE + append(out_filament_cost, "%.2lf", filament_cost); } } print_statistics.total_used_filament += used_filament; @@ -1026,18 +1073,18 @@ namespace DoExport { print_statistics.total_wipe_tower_cost += has_wipe_tower ? (extruded_volume - extruder.extruded_volume())* extruder.filament_density() * 0.001 * extruder.filament_cost() * 0.001 : 0.; } filament_stats_string_out += out_filament_used_mm.first; - filament_stats_string_out += "\n" + out_filament_used_cm3.first; - if (out_filament_used_g.second) - filament_stats_string_out += "\n" + out_filament_used_g.first; - if (out_filament_cost.second) - filament_stats_string_out += "\n" + out_filament_cost.first; - print_statistics.initial_filament_type = config.filament_type.get_at(initial_extruder_id); - std::sort(filament_types.begin(), filament_types.end()); - print_statistics.printing_filament_types = filament_types.front(); - for (size_t i = 1; i < filament_types.size(); ++ i) { - print_statistics.printing_filament_types += ","; - print_statistics.printing_filament_types += filament_types[i]; - } + filament_stats_string_out += "\n" + out_filament_used_cm3.first; + if (out_filament_used_g.second) + filament_stats_string_out += "\n" + out_filament_used_g.first; + if (out_filament_cost.second) + filament_stats_string_out += "\n" + out_filament_cost.first; + print_statistics.initial_filament_type = config.filament_type.get_at(initial_extruder_id); + std::sort(filament_types.begin(), filament_types.end()); + print_statistics.printing_filament_types = filament_types.front(); + for (size_t i = 1; i < filament_types.size(); ++ i) { + print_statistics.printing_filament_types += ","; + print_statistics.printing_filament_types += filament_types[i]; + } } return filament_stats_string_out; } @@ -1082,8 +1129,42 @@ std::vector sort_object_instances_by_model_order(const Pri void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb) { +#if ENABLE_BINARIZED_GCODE + const bool export_to_binary_gcode = print.full_print_config().option("gcode_binary")->value; + // if exporting gcode in binary format: + // we generate here the data to be passed to the post-processor, who is responsible to export them to file + // 1) generate the thumbnails + // 2) collect the config data + if (export_to_binary_gcode) { + BinaryGCode::BinaryData& binary_data = m_processor.get_binary_data(); + + // Unit tests or command line slicing may not define "thumbnails" or "thumbnails_format". + // If "thumbnails_format" is not defined, export to PNG. + if (const auto [thumbnails, thumbnails_format] = std::make_pair( + print.full_print_config().option("thumbnails"), + print.full_print_config().option>("thumbnails_format")); + thumbnails) { + GCodeThumbnails::generate_binary_thumbnails( + thumbnail_cb, binary_data.thumbnails, thumbnails->values, thumbnails_format ? thumbnails_format->value : GCodeThumbnailsFormat::PNG, + [&print]() { print.throw_if_canceled(); }); + } + + // file data + binary_data.file_metadata.encoding_type = (uint16_t)BinaryGCode::EMetadataEncodingType::INI; + binary_data.file_metadata.raw_data.emplace_back("Producer", std::string(SLIC3R_APP_NAME) + " " + std::string(SLIC3R_VERSION)); + // add here other key/value pairs + + // config data + binary_data.slicer_metadata.encoding_type = (uint16_t)BinaryGCode::EMetadataEncodingType::INI; + encode_full_config(print, binary_data.slicer_metadata.raw_data); + } + // modifies m_silent_time_estimator_enabled DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); +#else + // modifies m_silent_time_estimator_enabled + DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); +#endif // ENABLE_BINARIZED_GCODE if (! print.config().gcode_substitutions.values.empty()) { m_find_replace = make_unique(print.config()); @@ -1138,16 +1219,23 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Write information on the generator. file.write_format("; %s\n\n", Slic3r::header_slic3r_generated().c_str()); - // Unit tests or command line slicing may not define "thumbnails" or "thumbnails_format". - // If "thumbnails_format" is not defined, export to PNG. - if (const auto [thumbnails, thumbnails_format] = std::make_pair( - print.full_print_config().option("thumbnails"), - print.full_print_config().option>("thumbnails_format")); - thumbnails) - GCodeThumbnails::export_thumbnails_to_file( - thumbnail_cb, thumbnails->values, thumbnails_format ? thumbnails_format->value : GCodeThumbnailsFormat::PNG, - [&file](const char* sz) { file.write(sz); }, - [&print]() { print.throw_if_canceled(); }); +#if ENABLE_BINARIZED_GCODE + // if exporting gcode in ascii format, generate the thumbnails here + if (!export_to_binary_gcode) { +#endif // ENABLE_BINARIZED_GCODE + // Unit tests or command line slicing may not define "thumbnails" or "thumbnails_format". + // If "thumbnails_format" is not defined, export to PNG. + if (const auto [thumbnails, thumbnails_format] = std::make_pair( + print.full_print_config().option("thumbnails"), + print.full_print_config().option>("thumbnails_format")); + thumbnails) + GCodeThumbnails::export_thumbnails_to_file( + thumbnail_cb, thumbnails->values, thumbnails_format ? thumbnails_format->value : GCodeThumbnailsFormat::PNG, + [&file](const char* sz) { file.write(sz); }, + [&print]() { print.throw_if_canceled(); }); +#if ENABLE_BINARIZED_GCODE + } +#endif // ENABLE_BINARIZED_GCODE // Write notes (content of the Print Settings tab -> Notes) { @@ -1169,20 +1257,26 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato const double layer_height = first_object->config().layer_height.value; assert(! print.config().first_layer_height.percent); const double first_layer_height = print.config().first_layer_height.value; - for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) { - const PrintRegion ®ion = print.get_print_region(region_id); - file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width()); - file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width()); - file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width()); - file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width()); - file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width()); - if (print.has_support_material()) - file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width()); - if (print.config().first_layer_extrusion_width.value > 0) - file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, first_layer_height, true).width()); - file.write_format("\n"); +#if ENABLE_BINARIZED_GCODE + if (!export_to_binary_gcode) { +#endif // ENABLE_BINARIZED_GCODE + for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) { + const PrintRegion ®ion = print.get_print_region(region_id); + file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width()); + file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width()); + file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width()); + file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width()); + file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width()); + if (print.has_support_material()) + file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width()); + if (print.config().first_layer_extrusion_width.value > 0) + file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, first_layer_height, true).width()); + file.write_format("\n"); + } + print.throw_if_canceled(); +#if ENABLE_BINARIZED_GCODE } - print.throw_if_canceled(); +#endif // ENABLE_BINARIZED_GCODE // adds tags for time estimators if (print.config().remaining_times.value) @@ -1487,31 +1581,66 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato print.throw_if_canceled(); // Get filament stats. +#if ENABLE_BINARIZED_GCODE file.write(DoExport::update_print_stats_and_format_filament_stats( - // Const inputs + // Const inputs has_wipe_tower, print.wipe_tower_data(), this->config(), m_writer.extruders(), initial_extruder_id, // Modifies - print.m_print_statistics)); - file.write("\n"); - file.write_format("; total filament used [g] = %.2lf\n", print.m_print_statistics.total_weight); - file.write_format("; total filament cost = %.2lf\n", print.m_print_statistics.total_cost); - if (print.m_print_statistics.total_toolchanges > 0) - file.write_format("; total toolchanges = %i\n", print.m_print_statistics.total_toolchanges); - file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); + print.m_print_statistics, + export_to_binary_gcode, + m_processor.get_binary_data() + )); - // Append full config, delimited by two 'phony' configuration keys prusaslicer_config = begin and prusaslicer_config = end. - // The delimiters are structured as configuration key / value pairs to be parsable by older versions of PrusaSlicer G-code viewer. - { - file.write("\n; prusaslicer_config = begin\n"); - std::string full_config; - append_full_config(print, full_config); - if (!full_config.empty()) - file.write(full_config); - file.write("; prusaslicer_config = end\n"); + if (export_to_binary_gcode) { + BinaryGCode::BinaryData& binary_data = m_processor.get_binary_data(); + char buf[128]; + sprintf(buf, "%.2lf", print.m_print_statistics.total_weight); + binary_data.print_metadata.raw_data.push_back({ "total filament used [g]", std::string(buf) }); + sprintf(buf, "%.2lf", print.m_print_statistics.total_cost); + binary_data.print_metadata.raw_data.push_back({ "total filament cost", std::string(buf) }); + if (print.m_print_statistics.total_toolchanges > 0) + binary_data.print_metadata.raw_data.push_back({ "total toolchanges", std::to_string(print.m_print_statistics.total_toolchanges) }); } + else { +#else + file.write(DoExport::update_print_stats_and_format_filament_stats( + // Const inputs + has_wipe_tower, print.wipe_tower_data(), + this->config(), + m_writer.extruders(), + initial_extruder_id, + // Modifies + print.m_print_statistics)); +#endif // ENABLE_BINARIZED_GCODE +#if ENABLE_BINARIZED_GCODE + // if exporting gcode in ascii format, statistics export is done here +#endif // ENABLE_BINARIZED_GCODE + file.write("\n"); + file.write_format("; total filament used [g] = %.2lf\n", print.m_print_statistics.total_weight); + file.write_format("; total filament cost = %.2lf\n", print.m_print_statistics.total_cost); + if (print.m_print_statistics.total_toolchanges > 0) + file.write_format("; total toolchanges = %i\n", print.m_print_statistics.total_toolchanges); + file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); + +#if ENABLE_BINARIZED_GCODE + // if exporting gcode in ascii format, config export is done here +#endif // ENABLE_BINARIZED_GCODE + // Append full config, delimited by two 'phony' configuration keys prusaslicer_config = begin and prusaslicer_config = end. + // The delimiters are structured as configuration key / value pairs to be parsable by older versions of PrusaSlicer G-code viewer. + { + file.write("\n; prusaslicer_config = begin\n"); + std::string full_config; + append_full_config(print, full_config); + if (!full_config.empty()) + file.write(full_config); + file.write("; prusaslicer_config = end\n"); + } +#if ENABLE_BINARIZED_GCODE + } +#endif // ENABLE_BINARIZED_GCODE print.throw_if_canceled(); } @@ -2571,6 +2700,13 @@ void GCode::apply_print_config(const PrintConfig &print_config) void GCode::append_full_config(const Print &print, std::string &str) { +#if ENABLE_BINARIZED_GCODE + std::vector> config; + encode_full_config(print, config); + for (const auto& [key, value] : config) { + str += "; " + key + " = " + value + "\n"; + } +#else const DynamicPrintConfig &cfg = print.full_print_config(); // Sorted list of config keys, which shall not be stored into the G-code. Initializer list. static constexpr auto banned_keys = { @@ -2588,8 +2724,35 @@ void GCode::append_full_config(const Print &print, std::string &str) for (const std::string &key : cfg.keys()) if (! is_banned(key) && ! cfg.option(key)->is_nil()) str += "; " + key + " = " + cfg.opt_serialize(key) + "\n"; +#endif // ENABLE_BINARIZED_GCODE } +#if ENABLE_BINARIZED_GCODE +void GCode::encode_full_config(const Print& print, std::vector>& config) +{ + const DynamicPrintConfig& cfg = print.full_print_config(); + // Sorted list of config keys, which shall not be stored into the G-code. Initializer list. + 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); + }; + config.reserve(config.size() + cfg.keys().size()); + for (const std::string& key : cfg.keys()) { + if (!is_banned(key) && !cfg.option(key)->is_nil()) + config.emplace_back(key, cfg.opt_serialize(key)); + } + config.shrink_to_fit(); +} +#endif // ENABLE_BINARIZED_GCODE + void GCode::set_extruders(const std::vector &extruder_ids) { m_writer.set_extruders(extruder_ids); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 346ececba7..7358c15058 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -21,6 +21,9 @@ #include "GCode/GCodeProcessor.hpp" #include "EdgeGrid.hpp" #include "GCode/ThumbnailData.hpp" +#if ENABLE_BINARIZED_GCODE +#include "GCode/GCodeBinarizer.hpp" +#endif // ENABLE_BINARIZED_GCODE #include #include @@ -187,6 +190,10 @@ public: // append full config to the given string static void append_full_config(const Print& print, std::string& str); +#if ENABLE_BINARIZED_GCODE + // translate full config into a list of items + static void encode_full_config(const Print& print, std::vector>& config); +#endif // ENABLE_BINARIZED_GCODE // Object and support extrusions of the same PrintObject at the same print_z. // public, so that it could be accessed by free helper functions from GCode.cpp diff --git a/src/libslic3r/GCode/GCodeBinarizer.cpp b/src/libslic3r/GCode/GCodeBinarizer.cpp new file mode 100644 index 0000000000..9a205f5327 --- /dev/null +++ b/src/libslic3r/GCode/GCodeBinarizer.cpp @@ -0,0 +1,909 @@ +#include "GCodeBinarizer.hpp" + +#if ENABLE_BINARIZED_GCODE_DEBUG +#define NOMINMAX +#include +#include +#endif // ENABLE_BINARIZED_GCODE_DEBUG + +#include +#include + +namespace BinaryGCode { + +static size_t g_checksum_max_cache_size = 65536; + +std::string translate_result(BinaryGCode::EResult result) +{ + switch (result) + { + case BinaryGCode::EResult::Success: { return "Success"; } + case BinaryGCode::EResult::ReadError: { return "Read error"; } + case BinaryGCode::EResult::WriteError: { return "Write error"; } + case BinaryGCode::EResult::InvalidMagicNumber: { return "Invalid magic number"; } + case BinaryGCode::EResult::InvalidVersionNumber: { return "Invalid version number"; } + case BinaryGCode::EResult::InvalidChecksumType: { return "Invalid checksum type"; } + case BinaryGCode::EResult::InvalidBlockType: { return "Invalid block type"; } + case BinaryGCode::EResult::InvalidCompressionType: { return "Invalid compression type"; } + case BinaryGCode::EResult::InvalidMetadataEncodingType: { return "Invalid metadata encoding type"; } + case BinaryGCode::EResult::DataCompressionError: { return "Data compression error"; } + case BinaryGCode::EResult::DataUncompressionError: { return "Data uncompression error"; } + case BinaryGCode::EResult::MetadataEncodingError: { return "Data encoding error"; } + case BinaryGCode::EResult::MetadataDecodingError: { return "Data decoding error"; } + case BinaryGCode::EResult::BlockNotFound: { return "Block not found"; } + case BinaryGCode::EResult::InvalidChecksum: { return "Invalid checksum"; } + case BinaryGCode::EResult::InvalidThumbnailFormat: { return "Invalid thumbnail format"; } + case BinaryGCode::EResult::InvalidThumbnailWidth: { return "Invalid thumbnail width"; } + case BinaryGCode::EResult::InvalidThumbnailHeight: { return "Invalid thumbnail height"; } + case BinaryGCode::EResult::InvalidThumbnailDataSize: { return "Invalid thumbnail data size"; } + } + return std::string(); +} + +size_t get_checksum_max_cache_size() { return g_checksum_max_cache_size; } +void set_checksum_max_cache_size(size_t size) { g_checksum_max_cache_size = size; } + +static uint16_t checksum_types_count() { return 1 + (uint16_t)EChecksumType::CRC32; } +static uint16_t block_types_count() { return 1 + (uint16_t)EBlockType::Thumbnail; } +static uint16_t compression_types_count() { return 1 + (uint16_t)ECompressionType::None; } +static uint16_t thumbnail_formats_count() { return 1 + (uint16_t)EThumbnailFormat::QOI; } +static uint16_t metadata_encoding_types_count() { return 1 + (uint16_t)EMetadataEncodingType::INI; } + +static bool write_to_file(FILE& file, const void* data, size_t data_size) +{ + fwrite(data, 1, data_size, &file); + return !ferror(&file); +} + +static bool read_from_file(FILE& file, void* data, size_t data_size) +{ + fread(data, 1, data_size, &file); + return !ferror(&file); +} + +static bool encode_metadata(const std::vector>& data_in, std::vector& data_out, + EMetadataEncodingType encoding_type) +{ + for (const auto& [key, value] : data_in) { + switch (encoding_type) + { + case EMetadataEncodingType::INI: + { + data_out.insert(data_out.end(), key.begin(), key.end()); + data_out.emplace_back('='); + data_out.insert(data_out.end(), value.begin(), value.end()); + data_out.emplace_back('\n'); + break; + } + } + } + + return true; +} + +static bool decode_metadata(const std::vector& data_in, std::vector>& data_out, + EMetadataEncodingType encoding_type) +{ + switch (encoding_type) + { + case EMetadataEncodingType::INI: + { + auto start_it = data_in.begin(); + auto end_it = data_in.begin(); + while (end_it != data_in.end()) { + while (end_it != data_in.end() && *end_it != '\n') { + ++end_it; + } + const std::string item(start_it, end_it); + const size_t pos = item.find_first_of('='); + if (pos != std::string::npos) { + data_out.emplace_back(std::make_pair(item.substr(0, pos), item.substr(pos + 1))); + start_it = ++end_it; + } + } + break; + } + } + + return true; +} + +static bool compress(const std::vector& data_in, std::vector& data_out, ECompressionType compression_type) +{ + return true; +} + +static bool uncompress(const std::vector& data_in, std::vector& data_out, ECompressionType compression_type) +{ + return true; +} + +static uint32_t crc32_sw(const uint8_t* buffer, uint32_t length, uint32_t crc) +{ + uint32_t value = crc ^ 0xFFFFFFFF; + while (length--) { + value ^= (uint32_t)*buffer++; + for (int bit = 0; bit < 8; bit++) { + if (value & 1) + value = (value >> 1) ^ 0xEDB88320; + else + value >>= 1; + } + } + value ^= 0xFFFFFFFF; + return value; +} + +std::vector encode(const void* data, size_t data_size) +{ + std::vector ret(data_size); + memcpy(ret.data(), data, data_size); + return ret; +} + +Checksum::Checksum(EChecksumType type) +: m_type(type) +{ + if (m_type != EChecksumType::None) + m_checksum = std::vector(checksum_size(m_type), '\0'); +} + +EChecksumType Checksum::get_type() const +{ + return m_type; +} + +void Checksum::append(const std::vector& data) +{ + size_t remaining_data_size = std::distance(data.begin(), data.end()); + auto it_begin = data.begin(); + while (remaining_data_size + m_cache.size() > g_checksum_max_cache_size) { + update(); + if (remaining_data_size > g_checksum_max_cache_size) { + m_cache.insert(m_cache.end(), it_begin, it_begin + g_checksum_max_cache_size); + it_begin += g_checksum_max_cache_size; + remaining_data_size -= g_checksum_max_cache_size; + } + } + + m_cache.insert(m_cache.end(), it_begin, data.end()); +} + +bool Checksum::matches(Checksum& other) +{ + update(); + other.update(); + return m_checksum == other.m_checksum; +} + +EResult Checksum::write(FILE& file) +{ + if (m_type != EChecksumType::None) { + update(); + if (!write_to_file(file, (const void*)m_checksum.data(), m_checksum.size())) + return EResult::WriteError; + } + return EResult::Success; +} + +EResult Checksum::read(FILE& file) +{ + if (m_type != EChecksumType::None) { + if (!read_from_file(file, (void*)m_checksum.data(), m_checksum.size())) + return EResult::ReadError; + } + return EResult::Success; +} + +void Checksum::update() +{ + if (m_cache.empty()) + return; + + switch (m_type) + { + case EChecksumType::None: + { + break; + } + case EChecksumType::CRC32: + { + const uint32_t old_crc = *(uint32_t*)m_checksum.data(); + const uint32_t new_crc = crc32_sw(m_cache.data(), (uint32_t)m_cache.size(), old_crc); + *(uint32_t*)m_checksum.data() = new_crc; + break; + } + } + + m_cache.clear(); +} + +EResult FileHeader::write(FILE& file) const +{ + if (magic != *(uint32_t*)(MAGIC.data())) + return EResult::InvalidMagicNumber; + if (checksum_type >= checksum_types_count()) + return EResult::InvalidChecksumType; + + if (!write_to_file(file, (const void*)&magic, sizeof(magic))) + return EResult::WriteError; + if (!write_to_file(file, (const void*)&version, sizeof(version))) + return EResult::WriteError; + if (!write_to_file(file, (const void*)&checksum_type, sizeof(checksum_type))) + return EResult::WriteError; + + return EResult::Success; +} + +EResult FileHeader::read(FILE& file, const uint32_t* const max_version) +{ + if (!read_from_file(file, (void*)&magic, sizeof(magic))) + return EResult::ReadError; + if (magic != *(uint32_t*)(MAGIC.data())) + return EResult::InvalidMagicNumber; + + if (!read_from_file(file, (void*)&version, sizeof(version))) + return EResult::ReadError; + if (max_version != nullptr && version > *max_version) + return EResult::InvalidVersionNumber; + + if (!read_from_file(file, (void*)&checksum_type, sizeof(checksum_type))) + return EResult::ReadError; + if (checksum_type >= checksum_types_count()) + return EResult::InvalidChecksumType; + + return EResult::Success; +} + +void BlockHeader::update_checksum(Checksum& checksum) const +{ + checksum.append(encode((const void*)&type, sizeof(type))); + checksum.append(encode((const void*)&compression, sizeof(compression))); + checksum.append(encode((const void*)&uncompressed_size, sizeof(uncompressed_size))); + if (compression != (uint16_t)ECompressionType::None) + checksum.append(encode((const void*)&compressed_size, sizeof(compressed_size))); +} + +EResult BlockHeader::write(FILE& file) const +{ + if (!write_to_file(file, (const void*)&type, sizeof(type))) + return EResult::WriteError; + if (!write_to_file(file, (const void*)&compression, sizeof(compression))) + return EResult::WriteError; + if (!write_to_file(file, (const void*)&uncompressed_size, sizeof(uncompressed_size))) + return EResult::WriteError; + if (compression != (uint16_t)ECompressionType::None) { + if (!write_to_file(file, (const void*)&compressed_size, sizeof(compressed_size))) + return EResult::WriteError; + } + return EResult::Success; +} + +EResult BlockHeader::read(FILE& file) +{ + if (!read_from_file(file, (void*)&type, sizeof(type))) + return EResult::ReadError; + if (type >= block_types_count()) + return EResult::InvalidBlockType; + + if (!read_from_file(file, (void*)&compression, sizeof(compression))) + return EResult::ReadError; + if (compression >= compression_types_count()) + return EResult::InvalidCompressionType; + + if (!read_from_file(file, (void*)&uncompressed_size, sizeof(uncompressed_size))) + return EResult::ReadError; + if (compression != (uint16_t)ECompressionType::None) { + if (!read_from_file(file, (void*)&compressed_size, sizeof(compressed_size))) + return EResult::ReadError; + } + + return EResult::Success; +} + +EResult BaseMetadataBlock::write(FILE& file, EBlockType block_type, ECompressionType compression_type, Checksum& checksum) const +{ + if (encoding_type > metadata_encoding_types_count()) + return EResult::InvalidMetadataEncodingType; + + BlockHeader block_header = { (uint16_t)block_type, (uint16_t)compression_type, (uint32_t)0 }; + std::vector out_data; + if (!raw_data.empty()) { + // process payload encoding + std::vector uncompressed_data; + if (!encode_metadata(raw_data, uncompressed_data, (EMetadataEncodingType)encoding_type)) + return EResult::MetadataEncodingError; + // process payload compression + block_header.uncompressed_size = (uint32_t)uncompressed_data.size(); + std::vector compressed_data; + if (compression_type != ECompressionType::None) { + if (!compress(uncompressed_data, compressed_data, compression_type)) + return EResult::DataCompressionError; + block_header.compressed_size = (uint32_t)compressed_data.size(); + } + out_data.swap((compression_type == ECompressionType::None) ? uncompressed_data : compressed_data); + } + + // write block header + EResult res = block_header.write(file); + if (res != EResult::Success) + // propagate error + return res; + + // write block payload + if (!write_to_file(file, (const void*)&encoding_type, sizeof(encoding_type))) + return EResult::WriteError; + if (!out_data.empty()) { + if (!write_to_file(file, (const void*)out_data.data(), out_data.size())) + return EResult::WriteError; + } + + if (checksum.get_type() != EChecksumType::None) { + // update checksum with block header + block_header.update_checksum(checksum); + // update checksum with block payload + checksum.append(encode((const void*)&encoding_type, sizeof(encoding_type))); + if (!out_data.empty()) + checksum.append(out_data); + } + + return EResult::Success; +} + +EResult BaseMetadataBlock::read_data(FILE& file, const BlockHeader& block_header) +{ + const ECompressionType compression_type = (ECompressionType)block_header.compression; + + if (!read_from_file(file, (void*)&encoding_type, sizeof(encoding_type))) + return EResult::ReadError; + if (encoding_type > metadata_encoding_types_count()) + // Found invalid metadata encoding type + return EResult::InvalidMetadataEncodingType; + + std::vector data; + const size_t data_size = (compression_type == ECompressionType::None) ? block_header.uncompressed_size : block_header.compressed_size; + if (data_size > 0) { + data.resize(data_size); + if (!read_from_file(file, (void*)data.data(), data_size)) + return EResult::ReadError; + } + + std::vector uncompressed_data; + if (compression_type != ECompressionType::None) { + if (!uncompress(data, uncompressed_data, compression_type)) + return EResult::DataUncompressionError; + } + + if (!decode_metadata((compression_type == ECompressionType::None) ? data : uncompressed_data, raw_data, (EMetadataEncodingType)encoding_type)) + return EResult::MetadataDecodingError; + + return EResult::Success; +} + +EResult FileMetadataBlock::write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const +{ + Checksum cs(checksum_type); + + // write block header, payload + EResult res = BaseMetadataBlock::write(file, EBlockType::FileMetadata, compression_type, cs); + if (res != EResult::Success) + // propagate error + return res; + + // write block checksum + if (checksum_type != EChecksumType::None) + return cs.write(file); + + return EResult::Success; +} + +EResult FileMetadataBlock::read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header) +{ + // read block payload + EResult res = BaseMetadataBlock::read_data(file, block_header); + if (res != EResult::Success) + // propagate error + return res; + + const EChecksumType checksum_type = (EChecksumType)file_header.checksum_type; + if (checksum_type != EChecksumType::None) { + // read block checksum + Checksum cs(checksum_type); + res = cs.read(file); + if (res != EResult::Success) + // propagate error + return res; + } + + return EResult::Success; +} + +EResult ThumbnailBlock::write(FILE& file, EChecksumType checksum_type) const +{ + if (format >= thumbnail_formats_count()) + return EResult::InvalidThumbnailFormat; + if (width == 0) + return EResult::InvalidThumbnailWidth; + if (height == 0) + return EResult::InvalidThumbnailHeight; + if (data.size() == 0) + return EResult::InvalidThumbnailDataSize; + + // write block header + const BlockHeader block_header = { (uint16_t)EBlockType::Thumbnail, (uint16_t)ECompressionType::None, (uint32_t)data.size() }; + EResult res = block_header.write(file); + if (res != EResult::Success) + // propagate error + return res; + + // write block payload + if (!write_to_file(file, (const void*)&format, sizeof(format))) + return EResult::WriteError; + if (!write_to_file(file, (const void*)&width, sizeof(width))) + return EResult::WriteError; + if (!write_to_file(file, (const void*)&height, sizeof(height))) + return EResult::WriteError; + if (!write_to_file(file, (const void*)data.data(), data.size())) + return EResult::WriteError; + + if (checksum_type != EChecksumType::None) { + Checksum cs(checksum_type); + // update checksum with block header + block_header.update_checksum(cs); + // update checksum with block payload + update_checksum(cs); + // write block checksum + res = cs.write(file); + if (res != EResult::Success) + // propagate error + return res; + } + + return EResult::Success; +} + +EResult ThumbnailBlock::read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header) +{ + // read block payload + if (!read_from_file(file, (void*)&format, sizeof(format))) + return EResult::ReadError; + if (format >= thumbnail_formats_count()) + return EResult::InvalidThumbnailFormat; + if (!read_from_file(file, (void*)&width, sizeof(width))) + return EResult::ReadError; + if (width == 0) + return EResult::InvalidThumbnailWidth; + if (!read_from_file(file, (void*)&height, sizeof(height))) + return EResult::ReadError; + if (height == 0) + return EResult::InvalidThumbnailHeight; + if (block_header.uncompressed_size == 0) + return EResult::InvalidThumbnailDataSize; + data.resize(block_header.uncompressed_size); + if (!read_from_file(file, (void*)data.data(), block_header.uncompressed_size)) + return EResult::ReadError; + + const EChecksumType checksum_type = (EChecksumType)file_header.checksum_type; + if (checksum_type != EChecksumType::None) { + // read block checksum + Checksum cs(checksum_type); + const EResult res = cs.read(file); + if (res != EResult::Success) + // propagate error + return res; + } + + return EResult::Success; +} + +void ThumbnailBlock::update_checksum(Checksum& checksum) const +{ + checksum.append(encode((const void*)&format, sizeof(format))); + checksum.append(encode((const void*)&width, sizeof(width))); + checksum.append(encode((const void*)&height, sizeof(height))); + checksum.append(data); +} + +EResult PrinterMetadataBlock::write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const +{ + Checksum cs(checksum_type); + + // write block header, payload + EResult res = BaseMetadataBlock::write(file, EBlockType::PrinterMetadata, compression_type, cs); + if (res != EResult::Success) + // propagate error + return res; + + // write block checksum + if (checksum_type != EChecksumType::None) + return cs.write(file); + + return EResult::Success; +} + +EResult PrinterMetadataBlock::read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header) +{ + // read block payload + EResult res = BaseMetadataBlock::read_data(file, block_header); + if (res != EResult::Success) + // propagate error + return res; + + const EChecksumType checksum_type = (EChecksumType)file_header.checksum_type; + if (checksum_type != EChecksumType::None) { + // read block checksum + Checksum cs(checksum_type); + res = cs.read(file); + if (res != EResult::Success) + // propagate error + return res; + } + + return EResult::Success; +} + +EResult PrintMetadataBlock::write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const +{ + Checksum cs(checksum_type); + + // write block header, payload + EResult res = BaseMetadataBlock::write(file, EBlockType::PrintMetadata, compression_type, cs); + if (res != EResult::Success) + // propagate error + return res; + + // write block checksum + if (checksum_type != EChecksumType::None) + return cs.write(file); + + return EResult::Success; +} + +EResult PrintMetadataBlock::read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header) +{ + // read block payload + EResult res = BaseMetadataBlock::read_data(file, block_header); + if (res != EResult::Success) + // propagate error + return res; + + const EChecksumType checksum_type = (EChecksumType)file_header.checksum_type; + if (checksum_type != EChecksumType::None) { + // read block checksum + Checksum cs(checksum_type); + res = cs.read(file); + if (res != EResult::Success) + // propagate error + return res; + } + + return EResult::Success; +} + +EResult SlicerMetadataBlock::write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const +{ + Checksum cs(checksum_type); + + // write block header, payload + EResult res = BaseMetadataBlock::write(file, EBlockType::SlicerMetadata, compression_type, cs); + if (res != EResult::Success) + // propagate error + return res; + + // write block checksum + if (checksum_type != EChecksumType::None) + return cs.write(file); + + return EResult::Success; +} + +EResult SlicerMetadataBlock::read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header) +{ + // read block payload + EResult res = BaseMetadataBlock::read_data(file, block_header); + if (res != EResult::Success) + // propagate error + return res; + + const EChecksumType checksum_type = (EChecksumType)file_header.checksum_type; + if (checksum_type != EChecksumType::None) { + // read block checksum + Checksum cs(checksum_type); + res = cs.read(file); + if (res != EResult::Success) + // propagate error + return res; + } + + return EResult::Success; +} + +EResult GCodeBlock::write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const +{ + return EResult::Success; +} + +EResult GCodeBlock::read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header) +{ + return EResult::Success; +} + +#if ENABLE_CHECKSUM_BLOCK +EResult ChecksumBlock::write(FILE& file) const +{ + if (!data.empty()) { + const BlockHeader block_header = { (uint16_t)EBlockType::Checksum, (uint16_t)ECompressionType::None, (uint32_t)data.size() }; + // write block header + const EResult res = block_header.write(file); + if (res != EResult::Success) + // propagate error + return res; + // write block payload + if (!write_to_file(file, (const void*)data.data(), data.size())) + return EResult::WriteError; + } + + return EResult::Success; +} + +EResult ChecksumBlock::read_data(FILE& file, const BlockHeader& block_header) +{ + if (block_header.uncompressed_size > 0) { + data.resize(block_header.uncompressed_size); + if (!read_from_file(file, (void*)data.data(), block_header.uncompressed_size)) + return EResult::ReadError; + } + else + data.clear(); + + return EResult::Success; +} +#endif // ENABLE_CHECKSUM_BLOCK + +EResult Binarizer::initialize(FILE& file, EChecksumType checksum_type) +{ + if (!m_enabled) + return EResult::Success; + + // initialize checksum + m_checksum_type = checksum_type; +#if ENABLE_CHECKSUM_BLOCK + m_checksum = ChecksumBlock(); +#endif // ENABLE_CHECKSUM_BLOCK + + // save header + FileHeader file_header; + file_header.checksum_type = (uint16_t)m_checksum_type; + EResult res = file_header.write(file); + if (res != EResult::Success) + return res; + + // save file metadata block + res = m_binary_data.file_metadata.write(file, ECompressionType::None, m_checksum_type); + if (res != EResult::Success) + return res; + + // save printer metadata block + res = m_binary_data.printer_metadata.write(file, ECompressionType::None, m_checksum_type); + if (res != EResult::Success) + return res; + + // save thumbnail blocks + for (const ThumbnailBlock& block : m_binary_data.thumbnails) { + res = block.write(file, m_checksum_type); + if (res != EResult::Success) + return res; + } + + // save slicer metadata block + res = m_binary_data.slicer_metadata.write(file, ECompressionType::None, m_checksum_type); + if (res != EResult::Success) + return res; + + // save gcode block + + return EResult::Success; +} + +EResult Binarizer::finalize(FILE& file) +{ + if (!m_enabled) + return EResult::Success; + + // save print metadata block + EResult res = m_binary_data.print_metadata.write(file, ECompressionType::None, m_checksum_type); + if (res != EResult::Success) + return res; + +#if ENABLE_CHECKSUM_BLOCK + if (m_checksum_type != EChecksumType::None) { + // save checksum + // dummy checksum until it is not properly implemented + switch (m_checksum_type) + { + case EChecksumType::CRC32: + case EChecksumType::MD5: + { + m_checksum.data.clear(); + break; + } + } + + res = m_checksum.write(file); + if (res != EResult::Success) + return res; + } +#endif // ENABLE_CHECKSUM_BLOCK + + return EResult::Success; +} + +bool is_valid_binary_gcode(FILE& file) +{ + // cache file position + const long curr_pos = ftell(&file); + rewind(&file); + + std::array magic; + fread((void*)magic.data(), 1, magic.size(), &file); + if (ferror(&file)) + return false; + else { + // restore file position + fseek(&file, curr_pos, SEEK_SET); + return magic == MAGIC; + } +} + +EResult read_header(FILE& file, FileHeader& header, const uint32_t* const max_version) +{ + rewind(&file); + return header.read(file, max_version); +} + +static EResult checksums_match(FILE& file, const FileHeader& file_header, const BlockHeader& block_header) +{ + // cache file position + const long curr_pos = ftell(&file); + + Checksum curr_cs((EChecksumType)file_header.checksum_type); + // update block checksum block header + block_header.update_checksum(curr_cs); + + // read block payload + size_t remaining_payload_size = block_payload_size(block_header); + while (remaining_payload_size > 0) { + const size_t size_to_read = std::min(remaining_payload_size, g_checksum_max_cache_size); + std::vector payload(size_to_read); + if (!read_from_file(file, payload.data(), payload.size())) + return EResult::ReadError; + curr_cs.append(payload); + remaining_payload_size -= size_to_read; + } + + // read checksum + Checksum read_cs((EChecksumType)file_header.checksum_type); + EResult res = read_cs.read(file); + if (res != EResult::Success) + // propagate error + return res; + + // Verify checksum + if (!curr_cs.matches(read_cs)) + return EResult::InvalidChecksum; + + // restore file position + fseek(&file, curr_pos, SEEK_SET); + + return EResult::Success; +} + +EResult read_next_block_header(FILE& file, const FileHeader& file_header, BlockHeader& block_header, bool verify_checksum) +{ + if (verify_checksum && (EChecksumType)file_header.checksum_type != EChecksumType::None) { + const EResult res = block_header.read(file); + if (res != EResult::Success) + // propagate error + return res; + + return checksums_match(file, file_header, block_header); + } + else + return block_header.read(file); +} + +EResult read_next_block_header(FILE& file, const FileHeader& file_header, BlockHeader& block_header, EBlockType type, bool verify_checksum) +{ + // cache file position + const long curr_pos = ftell(&file); + + do { + EResult res = read_next_block_header(file, file_header, block_header, false); + if (res != EResult::Success) + // propagate error + return res; + else if (feof(&file)) { + // block not found + // restore file position + fseek(&file, curr_pos, SEEK_SET); + return EResult::BlockNotFound; + } + else if ((EBlockType)block_header.type == type) { + // block found + if (verify_checksum) { + res = checksums_match(file, file_header, block_header); + if (res != EResult::Success) + // propagate error + return res; + else + break; + } + } + + if (!feof(&file)) { + res = skip_block_content(file, file_header, block_header); + if (res != EResult::Success) + // propagate error + return res; + } + } while (true); + + return EResult::Success; +} + +EResult skip_block_payload(FILE& file, const BlockHeader& block_header) +{ + fseek(&file, (long)block_payload_size(block_header), SEEK_CUR); + return ferror(&file) ? EResult::ReadError : EResult::Success; +} + +EResult skip_block_content(FILE& file, const FileHeader& file_header, const BlockHeader& block_header) +{ + fseek(&file, (long)block_content_size(file_header, block_header), SEEK_CUR); + return ferror(&file) ? EResult::ReadError : EResult::Success; +} + +size_t block_parameters_size(EBlockType type) +{ + switch (type) + { + case EBlockType::FileMetadata: { return FileMetadataBlock::get_parameters_size(); } + case EBlockType::GCode: { return GCodeBlock::get_parameters_size(); } + case EBlockType::SlicerMetadata: { return SlicerMetadataBlock::get_parameters_size(); } + case EBlockType::PrinterMetadata: { return PrinterMetadataBlock::get_parameters_size(); } + case EBlockType::PrintMetadata: { return PrintMetadataBlock::get_parameters_size(); } + case EBlockType::Thumbnail: { return ThumbnailBlock::get_parameters_size(); } + } + return 0; +} + +size_t block_payload_size(const BlockHeader& block_header) +{ + size_t ret = block_parameters_size((EBlockType)block_header.type); + ret += ((ECompressionType)block_header.compression == ECompressionType::None) ? + block_header.uncompressed_size : block_header.compressed_size; + return ret; +} + +size_t checksum_size(EChecksumType type) +{ + switch (type) + { + case EChecksumType::None: { return 0; } + case EChecksumType::CRC32: { return 4; } + } + return 0; +} + +extern size_t block_content_size(const FileHeader& file_header, const BlockHeader& block_header) +{ +#if ENABLE_CHECKSUM_BLOCK + return ((EBlockType)block_header.type == EBlockType::Checksum) ? + block_payload_size(block_header) : block_payload_size(block_header) + checksum_size((EChecksumType)file_header.checksum_type); +#else + return block_payload_size(block_header) + checksum_size((EChecksumType)file_header.checksum_type); +#endif // ENABLE_CHECKSUM_BLOCK +} + +} // namespace BinaryGCode + diff --git a/src/libslic3r/GCode/GCodeBinarizer.hpp b/src/libslic3r/GCode/GCodeBinarizer.hpp new file mode 100644 index 0000000000..058cabd475 --- /dev/null +++ b/src/libslic3r/GCode/GCodeBinarizer.hpp @@ -0,0 +1,330 @@ +#ifndef slic3r_GCode_GCodeBinarizer_hpp_ +#define slic3r_GCode_GCodeBinarizer_hpp_ + +#ifdef _WIN32 +#define ENABLE_BINARIZED_GCODE_DEBUG 1 +#endif // _WIN32 + +#define ENABLE_CHECKSUM_BLOCK 0 + +#include +#include +#include +#include +#include + +namespace BinaryGCode { + +static const std::array MAGIC{ 'G', 'C', 'D', 'E' }; +static const uint32_t VERSION = 1; + +enum class EResult : uint16_t +{ + Success, + ReadError, + WriteError, + InvalidMagicNumber, + InvalidVersionNumber, + InvalidChecksumType, + InvalidBlockType, + InvalidCompressionType, + InvalidMetadataEncodingType, + DataCompressionError, + DataUncompressionError, + MetadataEncodingError, + MetadataDecodingError, + BlockNotFound, + InvalidChecksum, + InvalidThumbnailFormat, + InvalidThumbnailWidth, + InvalidThumbnailHeight, + InvalidThumbnailDataSize +}; + +// Returns a string description of the given result +extern std::string translate_result(BinaryGCode::EResult result); + +enum class EChecksumType : uint16_t +{ + None, + CRC32 +}; + +class Checksum +{ +public: + // Constructs a checksum of the given type. + // The checksum data are sized accordingly. + explicit Checksum(EChecksumType type); + + EChecksumType get_type() const; + + // Appends the given data to the cache and performs a checksum update if + // the size of the cache exceeds the max checksum cache size. + void append(const std::vector& data); + // Returns true if the given checksum is equal to this one + bool matches(Checksum& other); + + EResult write(FILE& file); + EResult read(FILE& file); + +private: + EChecksumType m_type; + std::vector m_cache; + std::vector m_checksum; + + void update(); +}; + +struct FileHeader +{ + uint32_t magic{ *(uint32_t*)(MAGIC.data()) }; + uint32_t version{ VERSION }; + uint16_t checksum_type{ (uint16_t)EChecksumType::None }; + + EResult write(FILE& file) const; + EResult read(FILE& file, const uint32_t* const max_version); +}; + +enum class EBlockType : uint16_t +{ +#if ENABLE_CHECKSUM_BLOCK + Checksum, +#endif // ENABLE_CHECKSUM_BLOCK + FileMetadata, + GCode, + SlicerMetadata, + PrinterMetadata, + PrintMetadata, + Thumbnail +}; + +enum class ECompressionType : uint16_t +{ + None, +}; + +struct BlockHeader +{ + uint16_t type{ 0 }; + uint16_t compression{ 0 }; + uint32_t uncompressed_size{ 0 }; + uint32_t compressed_size{ 0 }; + + // Updates the given checksum with the data of this BlockHeader + void update_checksum(Checksum& checksum) const; + + EResult write(FILE& file) const; + EResult read(FILE& file); +}; + +enum class EMetadataEncodingType : uint16_t +{ + INI, +}; + +struct BaseMetadataBlock +{ + // type of data encoding + uint16_t encoding_type{ 0 }; + // data in key/value form + std::vector> raw_data; + + // write block header and data in encoded format + EResult write(FILE& file, EBlockType block_type, ECompressionType compression_type, Checksum& checksum) const; + // read block data in encoded format + EResult read_data(FILE& file, const BlockHeader& block_header); + + static size_t get_parameters_size() { return sizeof(encoding_type); } +}; + +struct FileMetadataBlock : public BaseMetadataBlock +{ + // write block header and data + EResult write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const; + // read block data + EResult read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header); +}; + +enum class EThumbnailFormat : uint16_t +{ + PNG, + JPG, + QOI +}; + +struct ThumbnailBlock +{ + uint16_t format{ 0 }; + uint16_t width{ 0 }; + uint16_t height{ 0 }; + std::vector data; + + // write block header and data + EResult write(FILE& file, EChecksumType checksum_type) const; + // read block data + EResult read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header); + + static size_t get_parameters_size() { return sizeof(format) + sizeof(width) + sizeof(height); } + +private: + void update_checksum(Checksum& checksum) const; +}; + +struct PrinterMetadataBlock : public BaseMetadataBlock +{ + // write block header and data + EResult write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const; + // read block data + EResult read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header); +}; + +struct PrintMetadataBlock : public BaseMetadataBlock +{ + // write block header and data + EResult write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const; + // read block data + EResult read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header); +}; + +struct SlicerMetadataBlock : public BaseMetadataBlock +{ + // write block header and data + EResult write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const; + // read block data + EResult read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header); +}; + +struct GCodeBlock : public BaseMetadataBlock +{ + // write block header and data + EResult write(FILE& file, ECompressionType compression_type, EChecksumType checksum_type) const; + // read block data + EResult read_data(FILE& file, const FileHeader& file_header, const BlockHeader& block_header); +}; + +#if ENABLE_CHECKSUM_BLOCK +struct ChecksumBlock +{ + std::vector data; + + // write block header and data + EResult write(FILE& file) const; + // read block data + EResult read_data(FILE& file, const BlockHeader& block_header); +}; +#endif // ENABLE_CHECKSUM_BLOCK + +//===================================================================================================================================== +// +// PRUSASLICER INTERFACE +// +//===================================================================================================================================== + +struct BinaryData +{ + FileMetadataBlock file_metadata; + PrinterMetadataBlock printer_metadata; + std::vector thumbnails; + SlicerMetadataBlock slicer_metadata; + PrintMetadataBlock print_metadata; + + void reset() { + file_metadata.raw_data.clear(); + printer_metadata.raw_data.clear(); + thumbnails.clear(); + slicer_metadata.raw_data.clear(); + print_metadata.raw_data.clear(); + } +}; + +class Binarizer +{ +public: + bool is_enabled() const { return m_enabled; } + void set_enabled(bool enable) { m_enabled = enable; } + void set_compression_type(ECompressionType type) { m_compression_type = type; } + + BinaryData& get_binary_data() { return m_binary_data; } + const BinaryData& get_binary_data() const { return m_binary_data; } + + EResult initialize(FILE& file, EChecksumType checksum_type); + EResult finalize(FILE& file); + +private: + bool m_enabled{ false }; + + EChecksumType m_checksum_type{ EChecksumType::None }; + ECompressionType m_compression_type{ ECompressionType::None }; + + BinaryData m_binary_data; +#if ENABLE_CHECKSUM_BLOCK + ChecksumBlock m_checksum; +#endif // ENABLE_CHECKSUM_BLOCK +}; + +//===================================================================================================================================== +// +// FIRMWARE INTERFACE +// +//===================================================================================================================================== + +// Get the max size of the cache used to calculate checksums, in bytes +size_t get_checksum_max_cache_size(); +// Set the max size of the cache used to calculate checksums, in bytes +void set_checksum_max_cache_size(size_t size); + +// Returns true if the given file is a valid binary gcode +// Does not modify the file position +extern bool is_valid_binary_gcode(FILE& file); + +// Reads the file header. +// If max_version is not null, version is checked against the passed value +// If return == EResult::Success: +// - header will contain the file header +// - file position will be set at the start of the 1st block header +extern EResult read_header(FILE& file, FileHeader& header, const uint32_t* const max_version); + +// Reads next block header from the current file position. +// File position must be at the start of a block header. +// If return == EResult::Success: +// - block_header will contain the header of the block +// - file position will be set at the start of the block parameters data +extern EResult read_next_block_header(FILE& file, const FileHeader& file_header, BlockHeader& block_header, bool verify_checksum); + +// Searches and reads next block header with the given type from the current file position. +// File position must be at the start of a block header. +// If return == EResult::Success: +// - block_header will contain the header of the block with the required type +// - file position will be set at the start of the block parameters data +// otherwise: +// - file position will keep the current value +extern EResult read_next_block_header(FILE& file, const FileHeader& file_header, BlockHeader& block_header, EBlockType type, bool verify_checksum); + +// Skips the payload (parameters + data) of the block with the given block header. +// File position must be at the start of the block parameters. +// If return == EResult::Success: +// - file position will be set at the start of the block checksum, if present, or of next block header +extern EResult skip_block_payload(FILE& file, const BlockHeader& block_header); + +// Skips the content (parameters + data + checksum) of the block with the given block header. +// File position must be at the start of the block parameters. +// If return == EResult::Success: +// - file position will be set at the start of the next block header +extern EResult skip_block_content(FILE& file, const FileHeader& file_header, const BlockHeader& block_header); + +// Returns the size of the parameters of the given block type, in bytes. +extern size_t block_parameters_size(EBlockType type); + +// Returns the size of the payload (parameters + data) of the block with the given header, in bytes. +extern size_t block_payload_size(const BlockHeader& block_header); + +// Returns the size of the checksum of the given type, in bytes. +extern size_t checksum_size(EChecksumType type); + +// Returns the size of the content (parameters + data + checksum) of the block with the given header, in bytes. +extern size_t block_content_size(const FileHeader& file_header, const BlockHeader& block_header); + +} // namespace BinaryGCode + +#endif // slic3r_GCode_GCodeBinarizer_hpp_ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index f7fbb52a69..e6d97a3f8c 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -25,6 +25,10 @@ #endif #include +#if ENABLE_BINARIZED_GCODE_DEBUG +#include +#include +#endif // ENABLE_BINARIZED_GCODE_DEBUG static const float DEFAULT_TOOLPATH_WIDTH = 0.4f; static const float DEFAULT_TOOLPATH_HEIGHT = 0.2f; @@ -554,6 +558,10 @@ void GCodeProcessor::apply_config(const PrintConfig& config) { m_parser.apply_config(config); +#if ENABLE_BINARIZED_GCODE + m_binarizer.set_enabled(config.gcode_binary); +#endif // ENABLE_BINARIZED_GCODE + m_producer = EProducer::PrusaSlicer; m_flavor = config.gcode_flavor; @@ -1023,6 +1031,23 @@ static inline const char* remove_eols(const char *begin, const char *end) { // Load a G-code into a stand-alone G-code viewer. // throws CanceledException through print->throw_if_canceled() (sent by the caller as callback). void GCodeProcessor::process_file(const std::string& filename, std::function cancel_callback) +#if ENABLE_BINARIZED_GCODE +{ + FILE* file = boost::nowide::fopen(filename.c_str(), "rb"); + if (file == nullptr) + throw Slic3r::RuntimeError("Error opening the file: " + filename + "\n"); + + const bool is_binary = BinaryGCode::is_valid_binary_gcode(*file); + fclose(file); + + if (is_binary) + process_binary_file(filename, cancel_callback); + else + process_ascii_file(filename, cancel_callback); +} + +void GCodeProcessor::process_ascii_file(const std::string& filename, std::function cancel_callback) +#endif // ENABLE_BINARIZED_GCODE { CNumericLocalesSetter locales_setter; @@ -1090,6 +1115,163 @@ void GCodeProcessor::process_file(const std::string& filename, std::functionfinalize(false); } +#if ENABLE_BINARIZED_GCODE +void GCodeProcessor::process_binary_file(const std::string& filename, std::function cancel_callback) +{ + class ScopedFile + { + public: + explicit ScopedFile(FILE* file) : m_file(file) {} + ~ScopedFile() { if (m_file != nullptr) fclose(m_file); } + private: + FILE* m_file{ nullptr }; + }; + + FILE* file = boost::nowide::fopen(filename.c_str(), "rb"); + if (file == nullptr) + throw Slic3r::RuntimeError("Unable to open file: " + filename + "\n"); + + ScopedFile scoped_file(file); + + BinaryGCode::set_checksum_max_cache_size(1024); + + // read file header + BinaryGCode::FileHeader file_header; + BinaryGCode::EResult res = BinaryGCode::read_header(*file, file_header, &BinaryGCode::VERSION); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("File: " + filename + "does not contain a valid binary gcode\n Error: " + BinaryGCode::translate_result(res) + "\n"); + + const long first_block_header_position = ftell(file); + const bool verify_checksum = true; + + // read file metadata block + BinaryGCode::BlockHeader block_header; + res = BinaryGCode::read_next_block_header(*file, file_header, block_header, verify_checksum); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); + if ((BinaryGCode::EBlockType)block_header.type != BinaryGCode::EBlockType::FileMetadata) + throw Slic3r::RuntimeError("Unable to find file metadata block in file: " + filename + "\n"); + BinaryGCode::FileMetadataBlock file_metadata_block; + res = file_metadata_block.read_data(*file, file_header, block_header); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); + auto producer_it = std::find_if(file_metadata_block.raw_data.begin(), file_metadata_block.raw_data.end(), + [](const std::pair& item) { return item.first == "Producer"; }); + if (producer_it != file_metadata_block.raw_data.end() && boost::starts_with(producer_it->second, std::string(SLIC3R_APP_NAME))) + m_producer = EProducer::PrusaSlicer; + else + m_producer = EProducer::Unknown; + + // read printer metadata block + res = BinaryGCode::read_next_block_header(*file, file_header, block_header, verify_checksum); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); + if ((BinaryGCode::EBlockType)block_header.type != BinaryGCode::EBlockType::PrinterMetadata) + throw Slic3r::RuntimeError("Unable to find printer metadata block in file: " + filename + "\n"); + BinaryGCode::PrinterMetadataBlock printer_metadata_block; + res = printer_metadata_block.read_data(*file, file_header, block_header); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); +#if ENABLE_BINARIZED_GCODE_DEBUG + OutputDebugStringA("Printer metadata:\n"); + for (const auto& [key, value] : printer_metadata_block.raw_data) { + OutputDebugStringA(key.c_str()); + OutputDebugStringA("->"); + OutputDebugStringA(value.c_str()); + OutputDebugStringA("\n"); + } +#endif // ENABLE_BINARIZED_GCODE_DEBUG + + // read thumbnail blocks + res = BinaryGCode::read_next_block_header(*file, file_header, block_header, verify_checksum); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); + + while ((BinaryGCode::EBlockType)block_header.type == BinaryGCode::EBlockType::Thumbnail) { + BinaryGCode::ThumbnailBlock thumbnail_block; + res = thumbnail_block.read_data(*file, file_header, block_header); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); + +#if ENABLE_BINARIZED_GCODE_DEBUG + if (thumbnail_block.data.size() > 0) { + auto format_filename = [](const std::string& stem, const BinaryGCode::ThumbnailBlock& block) { + std::string ret = stem + "_" + std::to_string(block.width) + "x" + std::to_string(block.height); + switch ((BinaryGCode::EThumbnailFormat)block.format) + { + case BinaryGCode::EThumbnailFormat::PNG: { ret += ".png"; break; } + case BinaryGCode::EThumbnailFormat::JPG: { ret += ".jpg"; break; } + case BinaryGCode::EThumbnailFormat::QOI: { ret += ".qoi"; break; } + } + return ret; + }; + + const boost::filesystem::path path(filename); + const std::string out_path = path.parent_path().string(); + const std::string out_filename = out_path + "\\" + format_filename(path.stem().string(), thumbnail_block); + FILE* outfile = boost::nowide::fopen(out_filename.c_str(), "wb"); + if (outfile != nullptr) { + fwrite((const void*)thumbnail_block.data.data(), 1, thumbnail_block.data.size(), outfile); + fclose(outfile); + } + } +#endif // ENABLE_BINARIZED_GCODE_DEBUG + + res = BinaryGCode::read_next_block_header(*file, file_header, block_header, verify_checksum); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); + } + + // read slicer metadata block + if ((BinaryGCode::EBlockType)block_header.type != BinaryGCode::EBlockType::SlicerMetadata) + throw Slic3r::RuntimeError("Unable to find slicer metadata block in file: " + filename + "\n"); + BinaryGCode::SlicerMetadataBlock slicer_metadata_block; + res = slicer_metadata_block.read_data(*file, file_header, block_header); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); +#if ENABLE_BINARIZED_GCODE_DEBUG + OutputDebugStringA("Slicer metadata:\n"); + for (const auto& [key, value] : slicer_metadata_block.raw_data) { + OutputDebugStringA(key.c_str()); + OutputDebugStringA("->"); + OutputDebugStringA(value.c_str()); + OutputDebugStringA("\n"); + } +#endif // ENABLE_BINARIZED_GCODE_DEBUG + DynamicPrintConfig config; + config.apply(FullPrintConfig::defaults()); + std::string str; + for (const auto& [key, value] : slicer_metadata_block.raw_data) { + str += key + " = " + value + "\n"; + } + // Silently substitute unknown values by new ones for loading configurations from PrusaSlicer's own G-code. + // Showing substitution log or errors may make sense, but we are not really reading many values from the G-code config, + // thus a probability of incorrect substitution is low and the G-code viewer is a consumer-only anyways. + config.load_from_ini_string(str, ForwardCompatibilitySubstitutionRule::EnableSilent); + apply_config(config); + + // read print metadata block + res = BinaryGCode::read_next_block_header(*file, file_header, block_header, verify_checksum); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); + if ((BinaryGCode::EBlockType)block_header.type != BinaryGCode::EBlockType::PrintMetadata) + throw Slic3r::RuntimeError("Unable to find print metadata block in file: " + filename + "\n"); + BinaryGCode::PrintMetadataBlock print_metadata_block; + res = print_metadata_block.read_data(*file, file_header, block_header); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError("Error while reading file: " + filename + ": " + BinaryGCode::translate_result(res) + "\n"); +#if ENABLE_BINARIZED_GCODE_DEBUG + OutputDebugStringA("Print metadata:\n"); + for (const auto& [key, value] : print_metadata_block.raw_data) { + OutputDebugStringA(key.c_str()); + OutputDebugStringA("->"); + OutputDebugStringA(value.c_str()); + OutputDebugStringA("\n"); + } +#endif // ENABLE_BINARIZED_GCODE_DEBUG +} +#endif // ENABLE_BINARIZED_GCODE + void GCodeProcessor::initialize(const std::string& filename) { assert(is_decimal_separator_point()); @@ -3439,6 +3621,15 @@ void GCodeProcessor::post_process() throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for writing.\n")); } +#if ENABLE_BINARIZED_GCODE + if (m_binarizer.is_enabled()) { + BinaryGCode::set_checksum_max_cache_size(4096); + const BinaryGCode::EResult res = m_binarizer.initialize(*out.f, BinaryGCode::EChecksumType::CRC32); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError(std::string("Unable to initialize the gcode binarizer.\n")); + } +#endif // ENABLE_BINARIZED_GCODE + auto time_in_minutes = [](float time_in_seconds) { assert(time_in_seconds >= 0.f); return int((time_in_seconds + 0.5f) / 60.0f); @@ -3555,13 +3746,26 @@ void GCodeProcessor::post_process() size_t m_curr_g1_id{ 0 }; size_t m_out_file_pos{ 0 }; +#if ENABLE_BINARIZED_GCODE + BinaryGCode::Binarizer& m_binarizer; +#endif // ENABLE_BINARIZED_GCODE + public: +#if ENABLE_BINARIZED_GCODE + ExportLines(BinaryGCode::Binarizer& binarizer, EWriteType type, TimeMachine& machine) +#ifndef NDEBUG + : m_statistics(*this), m_binarizer(binarizer), m_write_type(type), m_machine(machine) {} +#else + : m_binarizer(binarizer), m_write_type(type), m_machine(machine) {} +#endif // NDEBUG +#else ExportLines(EWriteType type, TimeMachine& machine) #ifndef NDEBUG : m_statistics(*this), m_write_type(type), m_machine(machine) {} #else : m_write_type(type), m_machine(machine) {} #endif // NDEBUG +#endif // ENABLE_BINARIZED_GCODE void update(size_t lines_counter, size_t g1_lines_counter) { m_gcode_lines_map.push_back({ lines_counter, 0 }); @@ -3707,12 +3911,18 @@ void GCodeProcessor::post_process() private: void write_to_file(FilePtr& out, const std::string& out_string, GCodeProcessorResult& result, const std::string& out_path) { if (!out_string.empty()) { - fwrite((const void*)out_string.c_str(), 1, out_string.length(), out.f); - if (ferror(out.f)) { - out.close(); - boost::nowide::remove(out_path.c_str()); - throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nIs the disk full?\n")); +#if ENABLE_BINARIZED_GCODE + if (!m_binarizer.is_enabled()) { +#endif // ENABLE_BINARIZED_GCODE + fwrite((const void*)out_string.c_str(), 1, out_string.length(), out.f); + if (ferror(out.f)) { + out.close(); + boost::nowide::remove(out_path.c_str()); + throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nIs the disk full?\n")); + } +#if ENABLE_BINARIZED_GCODE } +#endif // ENABLE_BINARIZED_GCODE for (size_t i = 0; i < out_string.size(); ++i) { if (out_string[i] == '\n') result.lines_ends.emplace_back(m_out_file_pos + i + 1); @@ -3722,7 +3932,11 @@ void GCodeProcessor::post_process() } }; +#if ENABLE_BINARIZED_GCODE + ExportLines export_lines(m_binarizer, m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize, m_time_processor.machines[0]); +#else ExportLines export_lines(m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize, m_time_processor.machines[0]); +#endif // ENABLE_BINARIZED_GCODE // replace placeholder lines with the proper final value // gcode_line is in/out parameter, to reduce expensive memory allocation @@ -4035,6 +4249,46 @@ void GCodeProcessor::post_process() export_lines.flush(out, m_result, out_path); +#if ENABLE_BINARIZED_GCODE + if (m_binarizer.is_enabled()) { + // update print metadata + auto update_value = [](std::string& value, const std::vector& values) { + char buf[1024]; + value.clear(); + for (size_t i = 0; i < values.size(); ++i) { + sprintf(buf, i == values.size() - 1 ? " %.2lf" : " %.2lf,", values[i]); + value += buf; + } + }; + + // update binary data + BinaryGCode::BinaryData& binary_data = m_binarizer.get_binary_data(); + for (auto& [key, value] : binary_data.print_metadata.raw_data) { + if (key == "filament used [mm]") update_value(value, filament_mm); + else if (key == "filament used [g]") update_value(value, filament_g); + else if (key == "total filament used [g]") update_value(value, { filament_total_g }); + else if (key == "filament used [cm3]") update_value(value, filament_cm3); + else if (key == "filament cost") update_value(value, filament_cost); + else if (key == "total filament cost") update_value(value, { filament_total_cost }); + } + + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = m_time_processor.machines[i]; + PrintEstimatedStatistics::ETimeMode mode = static_cast(i); + if (mode == PrintEstimatedStatistics::ETimeMode::Normal || machine.enabled) { + char buf[128]; + sprintf(buf, "(%s mode)", (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent"); + binary_data.print_metadata.raw_data.push_back({ "estimated printing time " + std::string(buf), get_time_dhms(machine.time) }); + binary_data.print_metadata.raw_data.push_back({ "estimated first layer printing time " + std::string(buf), get_time_dhms(machine.layers_time.empty() ? 0.f : machine.layers_time.front()) }); + } + } + + const BinaryGCode::EResult res = m_binarizer.finalize(*out.f); + if (res != BinaryGCode::EResult::Success) + throw Slic3r::RuntimeError(std::string("Error while finalizing the gcode binarizer.\n")); + } +#endif // ENABLE_BINARIZED_GCODE + out.close(); in.close(); diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index fcba4321ae..fe2db07f99 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -7,6 +7,10 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/CustomGCode.hpp" +#if ENABLE_BINARIZED_GCODE +#include "GCodeBinarizer.hpp" +#endif // ENABLE_BINARIZED_GCODE + #include #include #include @@ -525,6 +529,9 @@ namespace Slic3r { private: GCodeReader m_parser; +#if ENABLE_BINARIZED_GCODE + BinaryGCode::Binarizer m_binarizer; +#endif // ENABLE_BINARIZED_GCODE EUnits m_units; EPositioningType m_global_positioning_type; @@ -622,6 +629,10 @@ namespace Slic3r { void apply_config(const PrintConfig& config); void set_print(Print* print) { m_print = print; } +#if ENABLE_BINARIZED_GCODE + BinaryGCode::BinaryData& get_binary_data() { return m_binarizer.get_binary_data(); } + const BinaryGCode::BinaryData& get_binary_data() const { return m_binarizer.get_binary_data(); } +#endif // ENABLE_BINARIZED_GCODE void enable_stealth_time_estimator(bool enabled); bool is_stealth_time_estimator_enabled() const { @@ -664,6 +675,11 @@ namespace Slic3r { void apply_config_kissslicer(const std::string& filename); void process_gcode_line(const GCodeReader::GCodeLine& line, bool producers_enabled); +#if ENABLE_BINARIZED_GCODE + void process_ascii_file(const std::string& filename, std::function cancel_callback = nullptr); + void process_binary_file(const std::string& filename, std::function cancel_callback = nullptr); +#endif // ENABLE_BINARIZED_GCODE + // Process tags embedded into comments void process_tags(const std::string_view comment, bool producers_enabled); bool process_producers_tags(const std::string_view comment); diff --git a/src/libslic3r/GCode/Thumbnails.hpp b/src/libslic3r/GCode/Thumbnails.hpp index 30bb6b653b..b2b5d60cb1 100644 --- a/src/libslic3r/GCode/Thumbnails.hpp +++ b/src/libslic3r/GCode/Thumbnails.hpp @@ -4,6 +4,9 @@ #include "../Point.hpp" #include "../PrintConfig.hpp" #include "ThumbnailData.hpp" +#if ENABLE_BINARIZED_GCODE +#include "GCode/GCodeBinarizer.hpp" +#endif // ENABLE_BINARIZED_GCODE #include #include @@ -55,6 +58,35 @@ inline void export_thumbnails_to_file(ThumbnailsGeneratorCallback &thumbnail_cb, } } +#if ENABLE_BINARIZED_GCODE +template +inline void generate_binary_thumbnails(ThumbnailsGeneratorCallback& thumbnail_cb, std::vector& out_thumbnails, + const std::vector& sizes, GCodeThumbnailsFormat format, ThrowIfCanceledCallback throw_if_canceled) +{ + out_thumbnails.clear(); + if (thumbnail_cb != nullptr) { + ThumbnailsList thumbnails = thumbnail_cb(ThumbnailsParams{ sizes, true, true, true, true }); + for (const ThumbnailData& data : thumbnails) { + if (data.is_valid()) { + auto compressed = compress_thumbnail(data, format); + if (compressed->data != nullptr && compressed->size > 0) { + BinaryGCode::ThumbnailBlock& block = out_thumbnails.emplace_back(BinaryGCode::ThumbnailBlock()); + block.width = (uint16_t)data.width; + block.height = (uint16_t)data.height; + switch (format) { + case GCodeThumbnailsFormat::PNG: { block.format = (uint16_t)BinaryGCode::EThumbnailFormat::PNG; break; } + case GCodeThumbnailsFormat::JPG: { block.format = (uint16_t)BinaryGCode::EThumbnailFormat::JPG; break; } + case GCodeThumbnailsFormat::QOI: { block.format = (uint16_t)BinaryGCode::EThumbnailFormat::QOI; break; } + } + block.data.resize(compressed->size); + memcpy(block.data.data(), compressed->data, compressed->size); + } + } + } + } +} +#endif // ENABLE_BINARIZED_GCODE + } // namespace Slic3r::GCodeThumbnails #endif // slic3r_GCodeThumbnails_hpp_ diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index f42b3f7708..5b1e86c321 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -449,7 +449,11 @@ static std::vector s_Preset_print_options { "support_tree_angle", "support_tree_angle_slow", "support_tree_branch_diameter", "support_tree_branch_diameter_angle", "support_tree_branch_diameter_double_wall", "support_tree_top_rate", "support_tree_branch_distance", "support_tree_tip_diameter", "dont_support_bridges", "thick_bridges", "notes", "complete_objects", "extruder_clearance_radius", +#if ENABLE_BINARIZED_GCODE + "extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "gcode_substitutions", "gcode_binary", "perimeter_extruder", +#else "extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "gcode_substitutions", "perimeter_extruder", +#endif // ENABLE_BINARIZED_GCODE "infill_extruder", "solid_infill_extruder", "support_material_extruder", "support_material_interface_extruder", "ooze_prevention", "standby_temperature_delta", "interface_shells", "extrusion_width", "first_layer_extrusion_width", "perimeter_extrusion_width", "external_perimeter_extrusion_width", "infill_extrusion_width", "solid_infill_extrusion_width", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 8d53dbb5a8..2bd92340d8 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -117,6 +117,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "perimeter_acceleration", "post_process", "gcode_substitutions", +#if ENABLE_BINARIZED_GCODE + "gcode_binary", +#endif // ENABLE_BINARIZED_GCODE "printer_notes", "retract_before_travel", "retract_before_wipe", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 3da804066b..dd9e4f0578 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1431,6 +1431,14 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionStrings()); +#if ENABLE_BINARIZED_GCODE + def = this->add("gcode_binary", coBool); + def->label = L("Export as binary G-code"); + def->tooltip = L("Exports the G-code in binary format."); + def->mode = comExpert; + def->set_default_value(new ConfigOptionBool(0)); +#endif // ENABLE_BINARIZED_GCODE + def = this->add("high_current_on_filament_swap", coBool); def->label = L("High extruder current on filament swap"); def->tooltip = L("It may be beneficial to increase the extruder motor current during the filament exchange" diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 54a835fe79..806cdc500f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -698,6 +698,9 @@ PRINT_CONFIG_CLASS_DEFINE( // i - case insensitive // w - whole word ((ConfigOptionStrings, gcode_substitutions)) +//#if ENABLE_BINARIZED_GCODE + ((ConfigOptionBool, gcode_binary)) +//#endif // ENABLE_BINARIZED_GCODE ((ConfigOptionString, layer_gcode)) ((ConfigOptionFloat, max_print_speed)) ((ConfigOptionFloat, max_volumetric_speed)) diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index 602654633c..92b6f8578c 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -62,4 +62,13 @@ #define ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR (1 && ENABLE_2_6_0_ALPHA1) +//==================== +// 2.6.1.alpha1 techs +//==================== +#define ENABLE_2_6_1_ALPHA1 1 + +// Enable export of binarized gcode +#define ENABLE_BINARIZED_GCODE (1 && ENABLE_2_6_1_ALPHA1) + + #endif // _prusaslicer_technologies_h_ diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index ad93afa879..5a3152df4c 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1673,6 +1673,9 @@ void TabPrint::build() Option option = optgroup->get_option("output_filename_format"); option.opt.full_width = true; optgroup->append_single_option_line(option); +#if ENABLE_BINARIZED_GCODE + optgroup->append_single_option_line("gcode_binary"); +#endif // ENABLE_BINARIZED_GCODE optgroup = page->new_optgroup(L("Other"));