Skip to content

Commit

Permalink
1.0.0 (#76)
Browse files Browse the repository at this point in the history
* Fix Typo, `prusalicer` => `prusaslicer`

* bump version

* add file list height options and print button to popup, #49

* subscribe to state panel changes to automatically apply change

* add position inline thumbnail on left of text option, #68

* tweaks to scaling and positioning of inline thumbnail

* update state panel thumbnail for better text alignment when scaled

* disable modal popup in TouchUI

* update sponsors

* subscribe to upload knockout instead of filepath to resolve issues with overwriting selected file with new upload, #43

* update regular expression to recognize Cura 4.9 post processing script format.

* add MKS TFT/Lotmaxx thumbnail support

* bump major version
fix bug related to moving whole folders of files

* update readme

* update name in settings

* prepare for release

Co-authored-by: lightmaster <[email protected]>
  • Loading branch information
jneilliii and lightmaster authored May 19, 2021
1 parent ab359be commit 652277b
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 83 deletions.
38 changes: 23 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# PrusaSlicer Thumbnails
# Slicer Thumbnails

![GitHub Downloads](https://badgen.net/github/assets-dl/jneilliii/OctoPrint-PrusaSlicerThumbnails/)

This plugin will extract the embedded thumbnails from PrusaSlicer gcode files where the printer's profile ini file has the thumbnail option configured. This is default behavior for the Prusa Mini printer profile.

The thumbnail image extracted will always be the last resolution provided in the thumbnail setting. So for example the Prusa Mini setting is `thumbnails = 16x16,220x124` so the thumbnail that will be extracted will be 220x124 pixels as seen in the screenshots below. Check the Configuration section below for additional details.
This plugin will extract embedded thumbnails from gcode files created from [PrusaSlicer](#PrusaSlicer), [SuperSlicer](#SuperSlicer), [Cura](#Cura), or [Simplify3D](#Simplify3D).

The preview thumbnail can be shown in OctoPrint from the files list by clicking the newly added image button.

Expand All @@ -14,29 +12,37 @@ The thumbnail will open in a modal window.

![thumbnail](screenshot_thumbnail.png)

If enabled in settings the thumbnail can also be embedded as an inline thumbnail within the file list itself. If you use this option it's highly recommended to use Themeify to make the file list taller by adding the below custom style.

| Selector | CSS_Rule | Value |
|-----------------------------------------------------|------------|------------------|
| #files > div > div.gcode_files > div.scroll-wrapper | min-height | 800px !important |
If enabled in settings the thumbnail can also be embedded as an inline thumbnail within the file list itself. If you use this option it's highly recommended to also set the option to set file list height or position inline image to the left.

![thumbnail](screenshot_inline_thumbnail.png)

## Configuration

Since PrusaSlicer only enables thumbnails by default for the Prusa Mini you may need to manually update your configuration files. Those can be found by selecting `Show Configuration Folder` from the Help menu of the application and then inside the printers sub-folder you'll find your printer profiles.
### PrusaSlicer

Available via the UI since version 2.3, requires expert mode to be enabled in the upper right corner of the program to see the setting.

![PrusaSlicer](screenshot_prusaslicer.png)

**Warning**: the higher the resolution of the thumbnail you use in this setting the larger your gcode file will be when sliced.

**Update:** [SuperSlicer](https://github.com/supermerill/SuperSlicer), an advanced fork of PrusaSlicer, now has image options in the Printer Settings as of version 2.2.53, big shout out to the team there. You must enable expert mode in order to see it. This plugin will use the `big` reoslution configured.
### SuperSlicer

Available via the UI since version 2.2.53, requires expert mode to be enabled in the upper right corner of the program to see the setting.

![SuperSlicer](screenshot_superslicer.png)

**Note:** If you don't see your printer's ini file in the printers sub-folder; you are probably using one of the bundled Prusa Printer profiles (ie MK3S). If so you may need to create a copy of this printer profile to be able to have an ini file to edit. To do this in PrusaSlicer go to the Printer Settings tab and Click the save button next to the printer list and give it a new name. Alternatively, push Prusa Research to update their bundled profiles to match the Mini by commenting in the issue posted on their repository [here](https://github.com/prusa3d/PrusaSlicer/issues/3488).
**Warning**: the higher the resolution of the thumbnail you use in this setting the larger your gcode file will be when sliced.

### Cura

Open your desired printer profile in your favorite text editor and find the `thumbnails =` section and add the resolution that you would like to include in your sliced files, and therefore visible by this plugin. For example `thumbnails = 16x16,220x124` will be the equivalent of the Prusa Mini as described above.
A post-processing script has been bundled since version 4.9. For older versions you can manually add the post-processing script as described [here](https://gist.github.com/jneilliii/4034c84d1ec219c68c8877d0e794ec4e).

**Note:** Once you've made your changes you will need to restart PrusaSlicer in order for the changes to be used and embed the thumbnails in the exported gcode files.
![Cura](screenshot_cura.png)

**Warning**: the higher the resolution of the thumbnail you enter in this setting the larger your gcode file will be when sliced.
### Simplify3D

Available as a post-processing script for [windows](https://github.com/boweeble/s3d-thumbnail-generator) or [linux](https://github.com/NotExpectedYet/s3d-thumbnail-generator) thanks to [@boweeble](https://github.com/boweeble/) and [@NotExpectedYet](https://github.com/NotExpectedYet/).

## Get Help

Expand All @@ -57,6 +63,8 @@ Check out my other plugins [here](https://plugins.octoprint.org/by_author/#jneil
- [Andrew Beeman](https://github.com/Kiendeleo)
- [Calanish](https://github.com/calanish)
- [Will O](https://github.com/4wrxb)
- [Stephen Berry](https://github.com/berrystephenw)

### Support My Efforts
I, jneilliii, programmed this plugin for fun and do my best effort to support those that have issues with it, please return the favor and leave me a tip or become a Patron if you find this plugin helpful and want me to continue future development.

Expand Down
138 changes: 109 additions & 29 deletions octoprint_prusaslicerthumbnails/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@
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
self._fileRemovalLastDeleted = None
self._fileRemovalLastAdded = None
self._folderRemovalTimer = None
self._folderRemovalLastDeleted = {}
self._folderRemovalLastAdded = {}
self._waitForAnalysis = False
self._analysis_active = False

Expand All @@ -30,9 +36,14 @@ def get_settings_defaults(self):
inline_thumbnail=False,
scale_inline_thumbnail=False,
inline_thumbnail_scale_value="50",
inline_thumbnail_position_left=False,
align_inline_thumbnail=False,
inline_thumbnail_align_value="left",
state_panel_thumbnail=True
state_panel_thumbnail=True,
state_panel_thumbnail_scale_value="100",
resize_filelist=False,
filelist_height="306",
scale_inline_thumbnail_position=False
)

##~~ AssetPlugin mixin
Expand All @@ -53,10 +64,12 @@ def get_template_configs(self):
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 = 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 @@ -65,51 +78,116 @@ 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 not in ["FileAdded", "FileRemoved", "FolderRemoved", "FolderAdded"]:
return
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 == "FolderAdded" and payload["storage"] == "local":
file_list = self._file_manager.list_files(path=payload["path"], recursive=True)
local_files = file_list["local"]
results = dict(no_thumbnail=[], no_thumbnail_src=[])
for file_key, file in local_files.items():
results = self._process_gcode(local_files[file_key], results)
self._logger.debug("Scan results: {}".format(results))
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

def _process_gcode(self, gcode_file, results=[]):
self._logger.debug(gcode_file["path"])
if gcode_file.get("type") == "machinecode":
self._logger.debug(gcode_file.get("thumbnail"))
if gcode_file.get("thumbnail") == None:
if gcode_file.get("thumbnail") is None or not os.path.exists(gcode_file.get("thumbnail").split("?")[0]):
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 @@ -131,7 +209,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 @@ -141,17 +219,18 @@ 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

def get_update_information(self):
return dict(
prusaslicerthumbnails=dict(
displayName="PrusaSlicer Thumbnails",
displayName="Slicer Thumbnails",
displayVersion=self._plugin_version,

# version check: github repository
Expand All @@ -175,8 +254,10 @@ def get_update_information(self):
)
)

__plugin_name__ = "PrusaSlicer Thumbnails"
__plugin_pythoncompat__ = ">=2.7,<4" # python 2 and 3

__plugin_name__ = "Slicer Thumbnails"
__plugin_pythoncompat__ = ">=2.7,<4" # python 2 and 3


def __plugin_load__():
global __plugin_implementation__
Expand All @@ -187,4 +268,3 @@ def __plugin_load__():
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
"octoprint.server.http.routes": __plugin_implementation__.route_hook
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,19 @@
#prusa_thumbnail_viewer h3 {
overflow: hidden;
}

#files .gcode_files .entry {
overflow: hidden;
}

#files .gcode_files .entry img, #prusaslicer_state_thumbnail img {
width: auto;
}

.inline_prusa_thumbnail.pull-left, #prusaslicer_state_thumbnail.pull-left {
padding-right: 5px;
}

#prusaslicer_state_thumbnail.pull-left img {
width: 100%;
}
Loading

0 comments on commit 652277b

Please sign in to comment.