UFPWriter.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import json
  4. from typing import cast, List, Dict
  5. from Charon.VirtualFile import VirtualFile # To open UFP files.
  6. from Charon.OpenMode import OpenMode # To indicate that we want to write to UFP files.
  7. from Charon.filetypes.OpenPackagingConvention import OPCError
  8. from io import StringIO # For converting g-code to bytes.
  9. from PyQt6.QtCore import QBuffer
  10. from UM.Application import Application
  11. from UM.Logger import Logger
  12. from UM.Mesh.MeshWriter import MeshWriter # The writer we need to implement.
  13. from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
  14. from UM.PluginRegistry import PluginRegistry # To get the g-code writer.
  15. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  16. from UM.Scene.SceneNode import SceneNode
  17. from UM.Settings.InstanceContainer import InstanceContainer
  18. from cura.CuraApplication import CuraApplication
  19. from cura.Settings.CuraStackBuilder import CuraStackBuilder
  20. from cura.Settings.GlobalStack import GlobalStack
  21. from cura.Utils.Threading import call_on_qt_thread
  22. from UM.i18n import i18nCatalog
  23. METADATA_OBJECTS_PATH = "metadata/objects"
  24. SLICE_METADATA_PATH = "Cura/slicemetadata.json"
  25. catalog = i18nCatalog("cura")
  26. class UFPWriter(MeshWriter):
  27. def __init__(self):
  28. super().__init__(add_to_recent_files = False)
  29. MimeTypeDatabase.addMimeType(
  30. MimeType(
  31. name = "application/x-ufp",
  32. comment = "Ultimaker Format Package",
  33. suffixes = ["ufp"]
  34. )
  35. )
  36. # This needs to be called on the main thread (Qt thread) because the serialization of material containers can
  37. # trigger loading other containers. Because those loaded containers are QtObjects, they must be created on the
  38. # Qt thread. The File read/write operations right now are executed on separated threads because they are scheduled
  39. # by the Job class.
  40. @call_on_qt_thread
  41. def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
  42. archive = VirtualFile()
  43. archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
  44. try:
  45. self._writeObjectList(archive)
  46. # Store the g-code from the scene.
  47. archive.addContentType(extension = "gcode", mime_type = "text/x-gcode")
  48. except EnvironmentError as e:
  49. error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
  50. self.setInformation(error_msg)
  51. Logger.error(error_msg)
  52. return False
  53. gcode_textio = StringIO() # We have to convert the g-code into bytes.
  54. gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
  55. success = gcode_writer.write(gcode_textio, None)
  56. if not success: # Writing the g-code failed. Then I can also not write the gzipped g-code.
  57. self.setInformation(gcode_writer.getInformation())
  58. return False
  59. try:
  60. gcode = archive.getStream("/3D/model.gcode")
  61. gcode.write(gcode_textio.getvalue().encode("UTF-8"))
  62. archive.addRelation(virtual_path = "/3D/model.gcode",
  63. relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
  64. except EnvironmentError as e:
  65. error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
  66. self.setInformation(error_msg)
  67. Logger.error(error_msg)
  68. return False
  69. # Write settings
  70. try:
  71. archive.addContentType(extension="json", mime_type="application/json")
  72. setting_textio = StringIO()
  73. json.dump(self._getSliceMetadata(), setting_textio, separators=(", ", ": "), indent=4)
  74. steam = archive.getStream(SLICE_METADATA_PATH)
  75. steam.write(setting_textio.getvalue().encode("UTF-8"))
  76. except EnvironmentError as e:
  77. error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
  78. self.setInformation(error_msg)
  79. Logger.error(error_msg)
  80. return False
  81. # Attempt to store the thumbnail, if any:
  82. backend = CuraApplication.getInstance().getBackend()
  83. snapshot = None if getattr(backend, "getLatestSnapshot", None) is None else backend.getLatestSnapshot()
  84. if snapshot:
  85. try:
  86. archive.addContentType(extension = "png", mime_type = "image/png")
  87. thumbnail = archive.getStream("/Metadata/thumbnail.png")
  88. thumbnail_buffer = QBuffer()
  89. thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
  90. snapshot.save(thumbnail_buffer, "PNG")
  91. thumbnail.write(thumbnail_buffer.data())
  92. archive.addRelation(virtual_path = "/Metadata/thumbnail.png",
  93. relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail",
  94. origin = "/3D/model.gcode")
  95. except EnvironmentError as e:
  96. error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
  97. self.setInformation(error_msg)
  98. Logger.error(error_msg)
  99. return False
  100. else:
  101. Logger.log("w", "Thumbnail not created, cannot save it")
  102. # Store the material.
  103. application = CuraApplication.getInstance()
  104. machine_manager = application.getMachineManager()
  105. container_registry = application.getContainerRegistry()
  106. global_stack = machine_manager.activeMachine
  107. material_extension = "xml.fdm_material"
  108. material_mime_type = "application/x-ultimaker-material-profile"
  109. try:
  110. archive.addContentType(extension = material_extension, mime_type = material_mime_type)
  111. except OPCError:
  112. Logger.log("w", "The material extension: %s was already added", material_extension)
  113. added_materials = []
  114. for extruder_stack in global_stack.extruderList:
  115. material = extruder_stack.material
  116. try:
  117. material_file_name = material.getMetaData()["base_file"] + ".xml.fdm_material"
  118. except KeyError:
  119. Logger.log("w", "Unable to get base_file for the material %s", material.getId())
  120. continue
  121. material_file_name = "/Materials/" + material_file_name
  122. # The same material should not be added again.
  123. if material_file_name in added_materials:
  124. continue
  125. material_root_id = material.getMetaDataEntry("base_file")
  126. material_root_query = container_registry.findContainers(id = material_root_id)
  127. if not material_root_query:
  128. Logger.log("e", "Cannot find material container with root id {root_id}".format(root_id = material_root_id))
  129. return False
  130. material_container = material_root_query[0]
  131. try:
  132. serialized_material = material_container.serialize()
  133. except NotImplementedError:
  134. Logger.log("e", "Unable serialize material container with root id: %s", material_root_id)
  135. return False
  136. try:
  137. material_file = archive.getStream(material_file_name)
  138. material_file.write(serialized_material.encode("UTF-8"))
  139. archive.addRelation(virtual_path = material_file_name,
  140. relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material",
  141. origin = "/3D/model.gcode")
  142. except EnvironmentError as e:
  143. error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
  144. self.setInformation(error_msg)
  145. Logger.error(error_msg)
  146. return False
  147. added_materials.append(material_file_name)
  148. try:
  149. archive.close()
  150. except EnvironmentError as e:
  151. error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
  152. self.setInformation(error_msg)
  153. Logger.error(error_msg)
  154. return False
  155. return True
  156. @staticmethod
  157. def _writeObjectList(archive):
  158. """Write a json list of object names to the METADATA_OBJECTS_PATH metadata field
  159. To retrieve, use: `archive.getMetadata(METADATA_OBJECTS_PATH)`
  160. """
  161. objects_model = CuraApplication.getInstance().getObjectsModel()
  162. object_metas = []
  163. for item in objects_model.items:
  164. object_metas.extend(UFPWriter._getObjectMetadata(item["node"]))
  165. data = {METADATA_OBJECTS_PATH: object_metas}
  166. archive.setMetadata(data)
  167. @staticmethod
  168. def _getObjectMetadata(node: SceneNode) -> List[Dict[str, str]]:
  169. """Get object metadata to write for a Node.
  170. :return: List of object metadata dictionaries.
  171. Might contain > 1 element in case of a group node.
  172. Might be empty in case of nonPrintingMesh
  173. """
  174. return [{"name": item.getName()}
  175. for item in DepthFirstIterator(node)
  176. if item.getMeshData() is not None and not item.callDecoration("isNonPrintingMesh")]
  177. def _getSliceMetadata(self) -> Dict[str, Dict[str, Dict[str, str]]]:
  178. """Get all changed settings and all settings. For each extruder and the global stack"""
  179. print_information = CuraApplication.getInstance().getPrintInformation()
  180. settings = {
  181. "material": {
  182. "length": print_information.materialLengths,
  183. "weight": print_information.materialWeights,
  184. "cost": print_information.materialCosts,
  185. },
  186. "global": {
  187. "changes": {},
  188. "all_settings": {},
  189. }
  190. }
  191. global_stack = cast(GlobalStack, Application.getInstance().getGlobalContainerStack())
  192. # Add global user or quality changes
  193. global_flattened_changes = InstanceContainer.createMergedInstanceContainer(global_stack.userChanges, global_stack.qualityChanges)
  194. for setting in global_flattened_changes.getAllKeys():
  195. settings["global"]["changes"][setting] = global_flattened_changes.getProperty(setting, "value")
  196. # Get global all settings values without user or quality changes
  197. for setting in global_stack.getAllKeys():
  198. settings["global"]["all_settings"][setting] = global_stack.getProperty(setting, "value")
  199. for i, extruder in enumerate(global_stack.extruderList):
  200. # Add extruder fields to settings dictionary
  201. settings[f"extruder_{i}"] = {
  202. "changes": {},
  203. "all_settings": {},
  204. }
  205. # Add extruder user or quality changes
  206. extruder_flattened_changes = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder.qualityChanges)
  207. for setting in extruder_flattened_changes.getAllKeys():
  208. settings[f"extruder_{i}"]["changes"][setting] = extruder_flattened_changes.getProperty(setting, "value")
  209. # Get extruder all settings values without user or quality changes
  210. for setting in extruder.getAllKeys():
  211. settings[f"extruder_{i}"]["all_settings"][setting] = extruder.getProperty(setting, "value")
  212. return settings