GCodeProfileReader.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. # Copyright (c) 2020 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import re # Regular expressions for parsing escape characters in the settings.
  4. import json
  5. from typing import Optional
  6. from UM.Settings.ContainerFormatError import ContainerFormatError
  7. from UM.Settings.InstanceContainer import InstanceContainer
  8. from UM.Logger import Logger
  9. from UM.i18n import i18nCatalog
  10. from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException
  11. catalog = i18nCatalog("cura")
  12. class GCodeProfileReader(ProfileReader):
  13. """A class that reads profile data from g-code files.
  14. It reads the profile data from g-code files and stores it in a new profile.
  15. This class currently does not process the rest of the g-code in any way.
  16. """
  17. version = 3
  18. """The file format version of the serialized g-code.
  19. It can only read settings with the same version as the version it was
  20. written with. If the file format is changed in a way that breaks reverse
  21. compatibility, increment this version number!
  22. """
  23. escape_characters = {
  24. re.escape("\\\\"): "\\", # The escape character.
  25. re.escape("\\n"): "\n", # Newlines. They break off the comment.
  26. re.escape("\\r"): "\r" # Carriage return. Windows users may need this for visualisation in their editors.
  27. }
  28. """Dictionary that defines how characters are escaped when embedded in
  29. g-code.
  30. Note that the keys of this dictionary are regex strings. The values are
  31. not.
  32. """
  33. def read(self, file_name):
  34. """Reads a g-code file, loading the profile from it.
  35. :param file_name: The name of the file to read the profile from.
  36. :return: The profile that was in the specified file, if any. If the
  37. specified file was no g-code or contained no parsable profile,
  38. None is returned.
  39. """
  40. Logger.log("i", "Attempting to read a profile from the g-code")
  41. if file_name.split(".")[-1] != "gcode":
  42. return None
  43. prefix = ";SETTING_" + str(GCodeProfileReader.version) + " "
  44. prefix_length = len(prefix)
  45. # Loading all settings from the file.
  46. # They are all at the end, but Python has no reverse seek any more since Python3.
  47. # TODO: Consider moving settings to the start?
  48. serialized = "" # Will be filled with the serialized profile.
  49. try:
  50. with open(file_name, "r", encoding = "utf-8") as f:
  51. for line in f:
  52. if line.startswith(prefix):
  53. # Remove the prefix and the newline from the line and add it to the rest.
  54. serialized += line[prefix_length: -1]
  55. except IOError as e:
  56. Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
  57. return None
  58. serialized = unescapeGcodeComment(serialized)
  59. serialized = serialized.strip()
  60. if not serialized:
  61. Logger.log("w", "No custom profile to import from this g-code: %s", file_name)
  62. raise NoProfileException()
  63. # Serialized data can be invalid JSON
  64. try:
  65. json_data = json.loads(serialized)
  66. except Exception as e:
  67. Logger.log("e", "Could not parse serialized JSON data from g-code %s, error: %s", file_name, e)
  68. return None
  69. profiles = []
  70. global_profile = readQualityProfileFromString(json_data["global_quality"])
  71. # This is a fix for profiles created with 2.3.0 For some reason it added the "extruder" property to the
  72. # global profile.
  73. # The fix is simple and safe, as a global profile should never have the extruder entry.
  74. if global_profile.getMetaDataEntry("extruder", None) is not None:
  75. global_profile.setMetaDataEntry("extruder", None)
  76. profiles.append(global_profile)
  77. for profile_string in json_data.get("extruder_quality", []):
  78. profiles.append(readQualityProfileFromString(profile_string))
  79. return profiles
  80. def unescapeGcodeComment(string: str) -> str:
  81. """Unescape a string which has been escaped for use in a gcode comment.
  82. :param string: The string to unescape.
  83. :return: The unescaped string.
  84. """
  85. # Un-escape the serialized profile.
  86. pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys()))
  87. # Perform the replacement with a regular expression.
  88. return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string)
  89. def readQualityProfileFromString(profile_string) -> Optional[InstanceContainer]:
  90. """Read in a profile from a serialized string.
  91. :param profile_string: The profile data in serialized form.
  92. :return: The resulting Profile object or None if it could not be read.
  93. """
  94. # Create an empty profile - the id and name will be changed by the ContainerRegistry
  95. profile = InstanceContainer("")
  96. try:
  97. profile.deserialize(profile_string)
  98. except ContainerFormatError as e:
  99. Logger.log("e", "Corrupt profile in this g-code file: %s", str(e))
  100. return None
  101. except Exception as e: # Not a valid g-code file.
  102. Logger.log("e", "Unable to serialise the profile: %s", str(e))
  103. return None
  104. return profile