# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import re # Regular expressions for parsing escape characters in the settings. import json from typing import Optional from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.InstanceContainer import InstanceContainer from UM.Logger import Logger from UM.i18n import i18nCatalog from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException catalog = i18nCatalog("cura") class GCodeProfileReader(ProfileReader): """A class that reads profile data from g-code files. It reads the profile data from g-code files and stores it in a new profile. This class currently does not process the rest of the g-code in any way. """ version = 3 """The file format version of the serialized g-code. It can only read settings with the same version as the version it was written with. If the file format is changed in a way that breaks reverse compatibility, increment this version number! """ escape_characters = { re.escape("\\\\"): "\\", # The escape character. re.escape("\\n"): "\n", # Newlines. They break off the comment. re.escape("\\r"): "\r" # Carriage return. Windows users may need this for visualisation in their editors. } """Dictionary that defines how characters are escaped when embedded in g-code. Note that the keys of this dictionary are regex strings. The values are not. """ def read(self, file_name): """Reads a g-code file, loading the profile from it. :param file_name: The name of the file to read the profile from. :return: The profile that was in the specified file, if any. If the specified file was no g-code or contained no parsable profile, None is returned. """ Logger.log("i", "Attempting to read a profile from the g-code") if file_name.split(".")[-1] != "gcode": return None prefix = ";SETTING_" + str(GCodeProfileReader.version) + " " prefix_length = len(prefix) # Loading all settings from the file. # They are all at the end, but Python has no reverse seek any more since Python3. # TODO: Consider moving settings to the start? serialized = "" # Will be filled with the serialized profile. try: with open(file_name, "r", encoding = "utf-8") as f: for line in f: if line.startswith(prefix): # Remove the prefix and the newline from the line and add it to the rest. serialized += line[prefix_length: -1] except IOError as e: Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e)) return None serialized = unescapeGcodeComment(serialized) serialized = serialized.strip() if not serialized: Logger.log("w", "No custom profile to import from this g-code: %s", file_name) raise NoProfileException() # Serialized data can be invalid JSON try: json_data = json.loads(serialized) except Exception as e: Logger.log("e", "Could not parse serialized JSON data from g-code %s, error: %s", file_name, e) return None profiles = [] global_profile = readQualityProfileFromString(json_data["global_quality"]) # This is a fix for profiles created with 2.3.0 For some reason it added the "extruder" property to the # global profile. # The fix is simple and safe, as a global profile should never have the extruder entry. if global_profile.getMetaDataEntry("extruder", None) is not None: global_profile.setMetaDataEntry("extruder", None) profiles.append(global_profile) for profile_string in json_data.get("extruder_quality", []): profiles.append(readQualityProfileFromString(profile_string)) return profiles def unescapeGcodeComment(string: str) -> str: """Unescape a string which has been escaped for use in a gcode comment. :param string: The string to unescape. :return: The unescaped string. """ # Un-escape the serialized profile. pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys())) # Perform the replacement with a regular expression. return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string) def readQualityProfileFromString(profile_string) -> Optional[InstanceContainer]: """Read in a profile from a serialized string. :param profile_string: The profile data in serialized form. :return: The resulting Profile object or None if it could not be read. """ # Create an empty profile - the id and name will be changed by the ContainerRegistry profile = InstanceContainer("") try: profile.deserialize(profile_string) except ContainerFormatError as e: Logger.log("e", "Corrupt profile in this g-code file: %s", str(e)) return None except Exception as e: # Not a valid g-code file. Logger.log("e", "Unable to serialise the profile: %s", str(e)) return None return profile