UFPWriter.py 9.4 KB

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