mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-07-24 23:23:59 -06:00
Follow-up to 7c01ddf996
1) Starting with this commit, configuration block exported into G-code is delimited by "; prusaslicer_config = begin" and "; prusaslicer_config = end". These delimiters look like any other key / value configuration pairs on purpose to be compatible with older PrusaSlicer config parsing from G-code. 2) Config parser from G-code newly searches for "; generated by ..." comment over the complete G-code, thus it is compatible with various post processing scripts extending the G-code at the start. 3) Config parser from G-code parses PrusaSlicer version from the "; generated by PrusaSlicer ...." header and if the G-code was generated by PrusaSlicer 2.4.0-alpha0 and newer, it expects that the G-code already contains the "; prusaslicer_config = begin / end" tags and it relies on these tags to extract configuration. 4) A new simple and robust parser was written for reading project configuration from 3MF / AMF, while a heuristic parser to read config from G-code located at the end of the G-code file was used before.
This commit is contained in:
parent
ac86c7c022
commit
e947a29fc8
8 changed files with 251 additions and 45 deletions
|
@ -625,7 +625,7 @@ void ConfigBase::setenv_() const
|
|||
ConfigSubstitutions ConfigBase::load(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule)
|
||||
{
|
||||
return is_gcode_file(file) ?
|
||||
this->load_from_gcode_file(file, true /* check header */, compatibility_rule) :
|
||||
this->load_from_gcode_file(file, compatibility_rule) :
|
||||
this->load_from_ini(file, compatibility_rule);
|
||||
}
|
||||
|
||||
|
@ -637,6 +637,54 @@ ConfigSubstitutions ConfigBase::load_from_ini(const std::string &file, ForwardCo
|
|||
return this->load(tree, compatibility_rule);
|
||||
}
|
||||
|
||||
ConfigSubstitutions ConfigBase::load_from_ini_string(const std::string &data, ForwardCompatibilitySubstitutionRule compatibility_rule)
|
||||
{
|
||||
boost::property_tree::ptree tree;
|
||||
std::istringstream iss(data);
|
||||
boost::property_tree::read_ini(iss, tree);
|
||||
return this->load(tree, compatibility_rule);
|
||||
}
|
||||
|
||||
// Loading a "will be one day a legacy format" of configuration stored into 3MF or AMF.
|
||||
// Accepts the same data as load_from_ini_string(), only with each configuration line possibly prefixed with a semicolon (G-code comment).
|
||||
ConfigSubstitutions ConfigBase::load_from_ini_string_commented(std::string &&data, ForwardCompatibilitySubstitutionRule compatibility_rule)
|
||||
{
|
||||
// Convert the "data" string into INI format by removing the semi-colons at the start of a line.
|
||||
// Also the "; generated by PrusaSlicer ..." comment line will be removed.
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; i < data.size();)
|
||||
if (i == 0 || data[i] == '\n') {
|
||||
// Start of a line.
|
||||
if (i != 0) {
|
||||
// Consume LF.
|
||||
assert(data[i] == '\n');
|
||||
// Don't keep empty lines.
|
||||
if (j != 0 && data[j] != '\n')
|
||||
data[j ++] = data[i ++];
|
||||
}
|
||||
// Skip all leading spaces;
|
||||
for (; i < data.size() && (data[i] == ' ' || data[i] == '\t'); ++ i) ;
|
||||
// Skip the semicolon (comment indicator).
|
||||
if (i < data.size() && data[i] == ';')
|
||||
++ i;
|
||||
// Skip all leading spaces after semicolon.
|
||||
for (; i < data.size() && (data[i] == ' ' || data[i] == '\t'); ++ i) ;
|
||||
if (strncmp(data.data() + i, "generated by ", 13) == 0) {
|
||||
// Skip the "; generated by ..." line.
|
||||
for (; i < data.size() && data[i] != '\n'; ++ i);
|
||||
}
|
||||
} else if (data[i] == '\r' && i + 1 < data.size() && data[i + 1] == '\n') {
|
||||
// Skip CR.
|
||||
++ i;
|
||||
} else {
|
||||
// Consume the rest of the data.
|
||||
data[j ++] = data[i ++];
|
||||
}
|
||||
data.erase(data.begin() + j, data.end());
|
||||
|
||||
return this->load_from_ini_string(data, compatibility_rule);
|
||||
}
|
||||
|
||||
ConfigSubstitutions ConfigBase::load(const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule)
|
||||
{
|
||||
ConfigSubstitutionContext substitutions_ctxt(compatibility_rule);
|
||||
|
@ -651,37 +699,8 @@ ConfigSubstitutions ConfigBase::load(const boost::property_tree::ptree &tree, Fo
|
|||
return std::move(substitutions_ctxt.substitutions);
|
||||
}
|
||||
|
||||
// Load the config keys from the tail of a G-code file.
|
||||
ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, bool check_header, ForwardCompatibilitySubstitutionRule compatibility_rule)
|
||||
{
|
||||
// Read a 64k block from the end of the G-code.
|
||||
boost::nowide::ifstream ifs(file);
|
||||
if (check_header) {
|
||||
const char slic3r_gcode_header[] = "; generated by Slic3r ";
|
||||
const char prusaslicer_gcode_header[] = "; generated by PrusaSlicer ";
|
||||
std::string firstline;
|
||||
std::getline(ifs, firstline);
|
||||
if (strncmp(slic3r_gcode_header, firstline.c_str(), strlen(slic3r_gcode_header)) != 0 &&
|
||||
strncmp(prusaslicer_gcode_header, firstline.c_str(), strlen(prusaslicer_gcode_header)) != 0)
|
||||
throw Slic3r::RuntimeError("Not a PrusaSlicer / Slic3r PE generated g-code.");
|
||||
}
|
||||
ifs.seekg(0, ifs.end);
|
||||
auto file_length = ifs.tellg();
|
||||
auto data_length = std::min<std::fstream::pos_type>(65535, file_length);
|
||||
ifs.seekg(file_length - data_length, ifs.beg);
|
||||
std::vector<char> data(size_t(data_length) + 1, 0);
|
||||
ifs.read(data.data(), data_length);
|
||||
ifs.close();
|
||||
|
||||
ConfigSubstitutionContext substitutions_ctxt(compatibility_rule);
|
||||
size_t key_value_pairs = load_from_gcode_string(data.data(), substitutions_ctxt);
|
||||
if (key_value_pairs < 80)
|
||||
throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs));
|
||||
return std::move(substitutions_ctxt.substitutions);
|
||||
}
|
||||
|
||||
// Load the config keys from the given string.
|
||||
size_t ConfigBase::load_from_gcode_string(const char* str, ConfigSubstitutionContext& substitutions)
|
||||
static inline size_t load_from_gcode_string_legacy(ConfigBase &config, const char *str, ConfigSubstitutionContext &substitutions)
|
||||
{
|
||||
if (str == nullptr)
|
||||
return 0;
|
||||
|
@ -704,7 +723,7 @@ size_t ConfigBase::load_from_gcode_string(const char* str, ConfigSubstitutionCon
|
|||
if (end - (++ start) < 10 || start[0] != ';' || start[1] != ' ')
|
||||
break;
|
||||
const char *key = start + 2;
|
||||
if (!(*key >= 'a' && *key <= 'z') || (*key >= 'A' && *key <= 'Z'))
|
||||
if (!((*key >= 'a' && *key <= 'z') || (*key >= 'A' && *key <= 'Z')))
|
||||
// A key must start with a letter.
|
||||
break;
|
||||
const char *sep = key;
|
||||
|
@ -726,7 +745,7 @@ size_t ConfigBase::load_from_gcode_string(const char* str, ConfigSubstitutionCon
|
|||
if (key == nullptr)
|
||||
break;
|
||||
try {
|
||||
this->set_deserialize(std::string(key, key_end), std::string(value, end), substitutions);
|
||||
config.set_deserialize(std::string(key, key_end), std::string(value, end), substitutions);
|
||||
++num_key_value_pairs;
|
||||
}
|
||||
catch (UnknownOptionException & /* e */) {
|
||||
|
@ -735,7 +754,175 @@ size_t ConfigBase::load_from_gcode_string(const char* str, ConfigSubstitutionCon
|
|||
end = start;
|
||||
}
|
||||
|
||||
return num_key_value_pairs;
|
||||
return num_key_value_pairs;
|
||||
}
|
||||
|
||||
// Reading a config from G-code back to front for performance reasons: We don't want to scan
|
||||
// hundreds of MB file for a short config block, which we expect to find at the end of the G-code.
|
||||
class ReverseLineReader
|
||||
{
|
||||
public:
|
||||
using pos_type = boost::nowide::ifstream::pos_type;
|
||||
|
||||
// Stop at file_start
|
||||
ReverseLineReader(boost::nowide::ifstream &ifs, pos_type file_start) : m_ifs(ifs), m_file_start(file_start)
|
||||
{
|
||||
m_ifs.seekg(0, m_ifs.end);
|
||||
m_file_pos = m_ifs.tellg();
|
||||
m_block.assign(m_block_size, 0);
|
||||
}
|
||||
|
||||
bool getline(std::string &out) {
|
||||
out.clear();
|
||||
for (;;) {
|
||||
if (m_block_len == 0) {
|
||||
// Read the next block.
|
||||
m_block_len = size_t(std::min<std::fstream::pos_type>(m_block_size, m_file_pos - m_file_start));
|
||||
if (m_block_len == 0)
|
||||
return false;
|
||||
m_file_pos -= m_block_len;
|
||||
m_ifs.seekg(m_file_pos, m_ifs.beg);
|
||||
if (! m_ifs.read(m_block.data(), m_block_len))
|
||||
return false;
|
||||
}
|
||||
|
||||
assert(m_block_len > 0);
|
||||
// Non-empty buffer. Find another LF.
|
||||
int i = int(m_block_len) - 1;
|
||||
for (; i >= 0; -- i)
|
||||
if (m_block[i] == '\n')
|
||||
break;
|
||||
// i is position of LF or -1 if not found.
|
||||
if (i == -1) {
|
||||
// LF not found. Just make a backup of the buffer and continue.
|
||||
out.insert(out.begin(), m_block.begin(), m_block.begin() + m_block_len);
|
||||
m_block_len = 0;
|
||||
} else {
|
||||
assert(i >= 0);
|
||||
// Copy new line to the output. It may be empty.
|
||||
out.insert(out.begin(), m_block.begin() + i + 1, m_block.begin() + m_block_len);
|
||||
// Block length without the newline.
|
||||
m_block_len = i;
|
||||
// Remove CRLF from the end of the block.
|
||||
if (m_block_len > 0 && m_block[m_block_len - 1] == '\r')
|
||||
-- m_block_len;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
boost::nowide::ifstream &m_ifs;
|
||||
std::vector<char> m_block;
|
||||
size_t m_block_size = 65536;
|
||||
size_t m_block_len = 0;
|
||||
pos_type m_file_start;
|
||||
pos_type m_file_pos = 0;
|
||||
};
|
||||
|
||||
// Load the config keys from the tail of a G-code file.
|
||||
ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, ForwardCompatibilitySubstitutionRule compatibility_rule)
|
||||
{
|
||||
// Read a 64k block from the end of the G-code.
|
||||
boost::nowide::ifstream ifs(file);
|
||||
// Look for Slic3r or PrusaSlicer header.
|
||||
// Look for the header across the whole file as the G-code may have been extended at the start by a post-processing script or the user.
|
||||
bool has_delimiters = false;
|
||||
{
|
||||
static constexpr const char slic3r_gcode_header[] = "; generated by Slic3r ";
|
||||
static constexpr const char prusaslicer_gcode_header[] = "; generated by PrusaSlicer ";
|
||||
std::string header;
|
||||
bool header_found = false;
|
||||
while (std::getline(ifs, header)) {
|
||||
if (strncmp(slic3r_gcode_header, header.c_str(), strlen(slic3r_gcode_header)) == 0) {
|
||||
header_found = true;
|
||||
break;
|
||||
} else if (strncmp(prusaslicer_gcode_header, header.c_str(), strlen(prusaslicer_gcode_header)) == 0) {
|
||||
// Parse PrusaSlicer version.
|
||||
size_t i = strlen(prusaslicer_gcode_header);
|
||||
for (; i < header.size() && header[i] == ' '; ++ i) ;
|
||||
size_t j = i;
|
||||
for (; j < header.size() && header[j] != ' '; ++ j) ;
|
||||
try {
|
||||
Semver semver(header.substr(i, j - i));
|
||||
has_delimiters = semver >= Semver(2, 4, 0, nullptr, "alpha0");
|
||||
} catch (const RuntimeError &) {
|
||||
}
|
||||
header_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (! header_found)
|
||||
throw Slic3r::RuntimeError("Not a PrusaSlicer / Slic3r PE generated g-code.");
|
||||
}
|
||||
|
||||
auto header_end_pos = ifs.tellg();
|
||||
ConfigSubstitutionContext substitutions_ctxt(compatibility_rule);
|
||||
size_t key_value_pairs = 0;
|
||||
|
||||
if (has_delimiters)
|
||||
{
|
||||
// PrusaSlicer starting with 2.4.0-alpha0 delimits the config section stored into G-code with
|
||||
// ; prusaslicer_config = begin
|
||||
// ...
|
||||
// ; prusaslicer_config = end
|
||||
// The begin / end tags look like any other key / value pairs on purpose to be compatible with older G-code viewer.
|
||||
// Read the file in reverse line by line.
|
||||
ReverseLineReader reader(ifs, header_end_pos);
|
||||
// Read the G-code file by 64k blocks back to front.
|
||||
bool begin_found = false;
|
||||
bool end_found = false;
|
||||
std::string line;
|
||||
while (reader.getline(line))
|
||||
if (line == "; prusaslicer_config = end") {
|
||||
end_found = true;
|
||||
break;
|
||||
}
|
||||
if (! end_found)
|
||||
throw Slic3r::RuntimeError(format("Configuration block closing tag \"; prusaslicer_config = end\" not found when reading %1%", file));
|
||||
std::string key, value;
|
||||
while (reader.getline(line)) {
|
||||
if (line == "; prusaslicer_config = begin") {
|
||||
begin_found = true;
|
||||
break;
|
||||
}
|
||||
// line should be a valid key = value pair.
|
||||
auto pos = line.find('=');
|
||||
if (pos != std::string::npos && pos > 1 && line.front() == ';') {
|
||||
key = line.substr(1, pos - 1);
|
||||
value = line.substr(pos + 1);
|
||||
boost::trim(key);
|
||||
boost::trim(value);
|
||||
try {
|
||||
this->set_deserialize(key, value, substitutions_ctxt);
|
||||
++ key_value_pairs;
|
||||
} catch (UnknownOptionException & /* e */) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! begin_found)
|
||||
throw Slic3r::RuntimeError(format("Configuration block opening tag \"; prusaslicer_config = begin\" not found when reading %1%", file));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Slic3r or PrusaSlicer older than 2.4.0-alpha0 do not emit any delimiter.
|
||||
// Try a heuristics reading the G-code from back.
|
||||
ifs.seekg(0, ifs.end);
|
||||
auto file_length = ifs.tellg();
|
||||
auto data_length = std::min<std::fstream::pos_type>(65535, file_length - header_end_pos);
|
||||
ifs.seekg(file_length - data_length, ifs.beg);
|
||||
std::vector<char> data(size_t(data_length) + 1, 0);
|
||||
ifs.read(data.data(), data_length);
|
||||
ifs.close();
|
||||
key_value_pairs = load_from_gcode_string_legacy(*this, data.data(), substitutions_ctxt);
|
||||
}
|
||||
|
||||
if (key_value_pairs < 80)
|
||||
throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs));
|
||||
return std::move(substitutions_ctxt.substitutions);
|
||||
}
|
||||
|
||||
void ConfigBase::save(const std::string &file) const
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue