Skip to content

Commit

Permalink
add MKS TFT/Lotmaxx thumbnail support
Browse files Browse the repository at this point in the history
  • Loading branch information
jneilliii committed May 12, 2021
1 parent 3203c3f commit 89c7b88
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 26 deletions.
111 changes: 87 additions & 24 deletions octoprint_prusaslicerthumbnails/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -70,36 +75,91 @@ 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

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

Expand All @@ -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():
Expand All @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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__
Expand All @@ -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
}

4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
###

.

setuptools
OctoPrint
Pillow
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 89c7b88

Please sign in to comment.