diff --git a/octoprint_prusaslicerthumbnails/__init__.py b/octoprint_prusaslicerthumbnails/__init__.py index c4c170a..557574f 100644 --- a/octoprint_prusaslicerthumbnails/__init__.py +++ b/octoprint_prusaslicerthumbnails/__init__.py @@ -8,12 +8,15 @@ import octoprint.util import os import datetime +import io +from PIL import Image + class PrusaslicerthumbnailsPlugin(octoprint.plugin.SettingsPlugin, - octoprint.plugin.AssetPlugin, - octoprint.plugin.TemplatePlugin, - octoprint.plugin.EventHandlerPlugin, - octoprint.plugin.SimpleApiPlugin): + octoprint.plugin.AssetPlugin, + octoprint.plugin.TemplatePlugin, + octoprint.plugin.EventHandlerPlugin, + octoprint.plugin.SimpleApiPlugin): def __init__(self): self._fileRemovalTimer = None @@ -59,9 +62,11 @@ def _extract_thumbnail(self, gcode_filename, thumbnail_filename): import re import base64 regex = r"(?:^; thumbnail begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)" + regex_mks = re.compile('(?:;(?:simage|;gimage):).*?M10086 ;[\r\n]', re.DOTALL) lineNum = 0 collectedString = "" - with open(gcode_filename,"rb") as gcode_file: + use_mks = False + with open(gcode_filename, "rb") as gcode_file: for line in gcode_file: lineNum += 1 line = line.decode("utf-8", "ignore") @@ -70,18 +75,69 @@ def _extract_thumbnail(self, gcode_filename, thumbnail_filename): if gcode == "G1" and extrusionMatch: self._logger.debug("Line %d: Detected first extrusion. Read complete.", lineNum) break - if line.startswith(";") or line.startswith("\n"): + if line.startswith(";") or line.startswith("\n") or line.startswith("M10086 ;"): collectedString += line self._logger.debug(collectedString) - test_str = collectedString.replace(octoprint.util.to_native_str('\r\n'),octoprint.util.to_native_str('\n')) - test_str = test_str.replace(octoprint.util.to_native_str(';\n;\n'),octoprint.util.to_native_str(';\n\n;\n')) + test_str = collectedString.replace(octoprint.util.to_native_str('\r\n'), octoprint.util.to_native_str('\n')) + test_str = test_str.replace(octoprint.util.to_native_str(';\n;\n'), octoprint.util.to_native_str(';\n\n;\n')) matches = re.findall(regex, test_str, re.MULTILINE) + if len(matches) == 0: # MKS lottmaxx fallback + matches = regex_mks.findall(test_str) + if len(matches) > 0: + use_mks = True if len(matches) > 0: path = os.path.dirname(thumbnail_filename) if not os.path.exists(path): os.makedirs(path) - with open(thumbnail_filename,"wb") as png_file: - png_file.write(base64.b64decode(matches[-1:][0].replace("; ", "").encode())) + with open(thumbnail_filename, "wb") as png_file: + if use_mks: + png_file.write(self._extract_mks_thumbnail(matches)) + else: + png_file.write(base64.b64decode(matches[-1:][0].replace("; ", "").encode())) + + # Extracts a thumbnail from a gcode and returns png binary string + def _extract_mks_thumbnail(self, gcode_encoded_images): + + # Find the biggest thumbnail + encoded_image_dimensions, encoded_image = self.find_best_thumbnail(gcode_encoded_images) + + # Not found? + if encoded_image is None: + return None # What to return? Is None ok? + + # Remove M10086 ; and whitespaces + encoded_image = encoded_image.replace('M10086 ;', '').replace('\n', '').replace('\r', '').replace(' ', '') + + # Get bytes from hex + encoded_image = bytes(bytearray.fromhex(encoded_image)) + + # Load pixel data + image = Image.frombytes('RGB', encoded_image_dimensions, encoded_image, 'raw', 'BGR;16', 0, 1) + + # Save image as png + with io.BytesIO() as png_bytes: + image.save(png_bytes, "PNG") + png_bytes_string = png_bytes.getvalue() + + return png_bytes_string + + # Finds the biggest thumbnail + def find_best_thumbnail(self, gcode_encoded_images): + + # Check for gimage + for image in gcode_encoded_images: + if image.startswith(';;gimage:'): + # Return size and trimmed string + return (200, 200), image[9:] + + # Check for simage + for image in gcode_encoded_images: + if image.startswith(';simage:'): + # Return size and trimmed string + return (100, 100), image[8:] + + # Image not found + return None ##~~ EventHandlerPlugin mixin @@ -89,17 +145,21 @@ def on_event(self, event, payload): if event == "FolderRemoved" and payload["storage"] == "local": import shutil shutil.rmtree(self.get_plugin_data_folder() + "/" + payload["path"], ignore_errors=True) - if event in ["FileAdded","FileRemoved"] and payload["storage"] == "local" and "gcode" in payload["type"]: - thumbnail_filename = self.get_plugin_data_folder() + "/" + payload["path"].replace(".gcode",".png") + if event in ["FileAdded", "FileRemoved"] and payload["storage"] == "local" and "gcode" in payload["type"]: + thumbnail_filename = self.get_plugin_data_folder() + "/" + payload["path"].replace(".gcode", ".png") if os.path.exists(thumbnail_filename): os.remove(thumbnail_filename) if event == "FileAdded": gcode_filename = self._file_manager.path_on_disk("local", payload["path"]) self._extract_thumbnail(gcode_filename, thumbnail_filename) if os.path.exists(thumbnail_filename): - thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/" + payload["path"].replace(".gcode", ".png") + "?" + "{:%Y%m%d%H%M%S}".format(datetime.datetime.now()) - self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail", thumbnail_url.replace("//", "/"), overwrite=True) - self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src", self._identifier, overwrite=True) + thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/" + payload["path"].replace(".gcode", + ".png") + "?" + "{:%Y%m%d%H%M%S}".format( + datetime.datetime.now()) + self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail", + thumbnail_url.replace("//", "/"), overwrite=True) + self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src", + self._identifier, overwrite=True) ##~~ SimpleApiPlugin mixin @@ -110,11 +170,12 @@ def _process_gcode(self, gcode_file, results=[]): if gcode_file.get("thumbnail") == None: self._logger.debug("No Thumbnail for %s, attempting extraction" % gcode_file["path"]) results["no_thumbnail"].append(gcode_file["path"]) - self.on_event("FileAdded", dict(path=gcode_file["path"],storage="local",type=["gcode"])) + self.on_event("FileAdded", dict(path=gcode_file["path"], storage="local", type=["gcode"])) elif "prusaslicerthumbnails" in gcode_file.get("thumbnail") and not gcode_file.get("thumbnail_src"): self._logger.debug("No Thumbnail source for %s, adding" % gcode_file["path"]) results["no_thumbnail_src"].append(gcode_file["path"]) - self._file_manager.set_additional_metadata("local", gcode_file["path"], "thumbnail_src", self._identifier, overwrite=True) + self._file_manager.set_additional_metadata("local", gcode_file["path"], "thumbnail_src", + self._identifier, overwrite=True) elif gcode_file.get("type") == "folder" and not gcode_file.get("children") == None: children = gcode_file["children"] for key, file in children.items(): @@ -136,7 +197,7 @@ def on_api_command(self, command, data): FileList = self._file_manager.list_files(recursive=True) self._logger.info(FileList) LocalFiles = FileList["local"] - results = dict(no_thumbnail=[],no_thumbnail_src=[]) + results = dict(no_thumbnail=[], no_thumbnail_src=[]) for key, file in LocalFiles.items(): results = self._process_gcode(LocalFiles[key], results) return flask.jsonify(results) @@ -146,10 +207,11 @@ def route_hook(self, server_routes, *args, **kwargs): from octoprint.server.util.tornado import LargeResponseHandler, UrlProxyHandler, path_validation_factory from octoprint.util import is_hidden_path return [ - (r"thumbnail/(.*)", LargeResponseHandler, dict(path=self.get_plugin_data_folder(), - as_attachment=False, - path_validation=path_validation_factory(lambda path: not is_hidden_path(path),status_code=404))) - ] + (r"thumbnail/(.*)", LargeResponseHandler, dict(path=self.get_plugin_data_folder(), + as_attachment=False, + path_validation=path_validation_factory( + lambda path: not is_hidden_path(path), status_code=404))) + ] ##~~ Softwareupdate hook @@ -180,8 +242,10 @@ def get_update_information(self): ) ) + __plugin_name__ = "PrusaSlicer Thumbnails" -__plugin_pythoncompat__ = ">=2.7,<4" # python 2 and 3 +__plugin_pythoncompat__ = ">=2.7,<4" # python 2 and 3 + def __plugin_load__(): global __plugin_implementation__ @@ -192,4 +256,3 @@ def __plugin_load__(): "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, "octoprint.server.http.routes": __plugin_implementation__.route_hook } - diff --git a/requirements.txt b/requirements.txt index a1dc463..0179e5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,7 @@ ### . + +setuptools +OctoPrint +Pillow diff --git a/setup.py b/setup.py index db82282..73e8eb4 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ plugin_name = "PrusaSlicer Thumbnails" # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "0.1.5rc8" +plugin_version = "0.1.5rc9" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module @@ -33,7 +33,7 @@ plugin_license = "AGPLv3" # Any additional requirements besides OctoPrint should be listed here -plugin_requires = [] +plugin_requires = ["Pillow"] ### -------------------------------------------------------------------------------------------------------------------- ### More advanced options that you usually shouldn't have to touch follow after this point