Skip to content

Commit d4bc5ca

Browse files
authored
Merge pull request #102 from jneilliii/rc
2 parents 1563137 + 999f1f1 commit d4bc5ca

File tree

4 files changed

+73
-12
lines changed

4 files changed

+73
-12
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

5-
This plugin will extract embedded thumbnails from gcode files created from [PrusaSlicer](#PrusaSlicer), [SuperSlicer](#SuperSlicer), [Cura](#Cura), or [Simplify3D](#Simplify3D).
5+
This plugin will extract embedded thumbnails from gcode files created from [PrusaSlicer](#PrusaSlicer), [SuperSlicer](#SuperSlicer), [Cura](#Cura), [Simplify3D](#Simplify3D), [IdeaMaker](#IdeaMaker), or FlashPrint (FlashForge printers).
66

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

octoprint_prusaslicerthumbnails/__init__.py

+66-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from PIL import Image
1414
import re
1515
import base64
16+
import imghdr
1617

1718
from flask_babel import gettext
1819
from octoprint.access import ADMIN_GROUP
@@ -50,7 +51,7 @@ def get_settings_defaults(self):
5051
'inline_thumbnail_scale_value': "50", 'inline_thumbnail_position_left': False,
5152
'align_inline_thumbnail': False, 'inline_thumbnail_align_value': "left", 'state_panel_thumbnail': True,
5253
'state_panel_thumbnail_scale_value': "100", 'resize_filelist': False, 'filelist_height': "306",
53-
'scale_inline_thumbnail_position': False, 'sync_on_refresh': False}
54+
'scale_inline_thumbnail_position': False, 'sync_on_refresh': False, 'use_uploads_folder': False}
5455

5556
# ~~ AssetPlugin mixin
5657

@@ -65,16 +66,19 @@ def get_template_configs(self):
6566
]
6667

6768
def _extract_thumbnail(self, gcode_filename, thumbnail_filename):
68-
regex = r"(?:^; thumbnail begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)"
69+
regex = r"(?:^; thumbnail(?:_JPG)* begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail(?:_JPG)* end)"
6970
regex_mks = re.compile('(?:;(?:simage|;gimage):).*?M10086 ;[\r\n]', re.DOTALL)
7071
regex_weedo = re.compile('W221[\r\n](.*)[\r\n]W222', re.DOTALL)
7172
regex_luban = re.compile(';thumbnail: data:image/png;base64,(.*)[\r\n]', re.DOTALL)
7273
regex_qidi = re.compile('M4010.*\'(.*)\'', re.DOTALL)
74+
regex_creality = r"(?:^; jpg begin .*)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; jpg end)"
7375
lineNum = 0
7476
collectedString = ""
7577
use_mks = False
7678
use_weedo = False
7779
use_qidi = False
80+
use_flashprint = False
81+
use_creality = False
7882
with open(gcode_filename, "rb") as gcode_file:
7983
for line in gcode_file:
8084
lineNum += 1
@@ -109,6 +113,19 @@ def _extract_thumbnail(self, gcode_filename, thumbnail_filename):
109113
if len(matches) > 0:
110114
self._logger.debug("Found qidi thumbnail.")
111115
use_qidi = True
116+
if len(matches) == 0: # FlashPrint fallback
117+
with open(gcode_filename, "rb") as gcode_file:
118+
gcode_file.seek(58)
119+
thumbbytes = gcode_file.read(14454)
120+
if imghdr.what(file=None, h=thumbbytes) == 'bmp':
121+
self._logger.debug("Found flashprint thumbnail.")
122+
matches = [thumbbytes]
123+
use_flashprint = True
124+
if len(matches) == 0: # Creality Neo fallback
125+
matches = re.findall(regex_creality, test_str, re.MULTILINE)
126+
if len(matches) > 0:
127+
self._logger.debug("Found creality thumbnail.")
128+
use_creality = True
112129
if len(matches) > 0:
113130
maxlen=0
114131
choosen=-1
@@ -125,11 +142,38 @@ def _extract_thumbnail(self, gcode_filename, thumbnail_filename):
125142
png_file.write(self._extract_mks_thumbnail(matches))
126143
elif use_weedo:
127144
png_file.write(self._extract_weedo_thumbnail(matches))
145+
elif use_creality:
146+
png_file.write(self._extract_creality_thumbnail(matches[choosen]))
128147
elif use_qidi:
129148
self._logger.debug(matches)
149+
elif use_flashprint:
150+
png_file.write(self._extract_flashprint_thumbnail(matches))
130151
else:
131152
png_file.write(base64.b64decode(matches[choosen].replace("; ", "").encode()))
132153

154+
# Extracts a thumbnail from hex binary data usd by FlashPrint slicer
155+
def _extract_flashprint_thumbnail(self, gcode_encoded_images):
156+
encoded_image = gcode_encoded_images[0]
157+
158+
image = Image.open(io.BytesIO(encoded_image)).resize((160,120))
159+
rgba = image.convert("RGBA")
160+
pixels = rgba.getdata()
161+
newData = []
162+
163+
alphamaxvalue = 35
164+
for pixel in pixels:
165+
if pixel[0] >= 0 and pixel[0] <= alphamaxvalue and pixel[1] >= 0 and pixel[1] <= alphamaxvalue and pixel[2] >= 0 and pixel[2] <= alphamaxvalue : # finding black colour by its RGB value
166+
newData.append((255, 255, 255, 0)) # storing a transparent value when we find a black/dark colour
167+
else:
168+
newData.append(pixel) # other colours remain unchanged
169+
170+
rgba.putdata(newData)
171+
172+
with io.BytesIO() as png_bytes:
173+
rgba.save(png_bytes, "PNG")
174+
png_bytes_string = png_bytes.getvalue()
175+
return png_bytes_string
176+
133177
# Extracts a thumbnail from hex binary data usd by Qidi slicer
134178
def _extract_qidi_thumbnail(self, gcode_encoded_images):
135179
encoded_image = gcode_encoded_images[0].replace('W220 ', '').replace('\n', '').replace('\r', '').replace(' ', '')
@@ -160,7 +204,16 @@ def _extract_mks_thumbnail(self, gcode_encoded_images):
160204

161205
# Load pixel data
162206
image = Image.frombytes('RGB', encoded_image_dimensions, encoded_image, 'raw', 'BGR;16', 0, 1)
207+
return self._imageToPng(image)
208+
209+
# Extracts a thumbnail from hex binary data usd by Qidi slicer
210+
def _extract_creality_thumbnail(self, match):
211+
encoded_jpg = base64.b64decode(match.replace("; ", "").encode())
212+
with io.BytesIO(encoded_jpg) as jpg_bytes:
213+
image = Image.open(jpg_bytes)
214+
return self._imageToPng(image)
163215

216+
def _imageToPng(self, image):
164217
# Save image as png
165218
with io.BytesIO() as png_bytes:
166219
image.save(png_bytes, "PNG")
@@ -205,20 +258,23 @@ def on_event(self, event, payload):
205258
"type"] and payload.get("name", False):
206259
thumbnail_name = self.regex_extension.sub(".png", payload["name"])
207260
thumbnail_path = self.regex_extension.sub(".png", payload["path"])
208-
thumbnail_filename = "{}/{}".format(self.get_plugin_data_folder(), thumbnail_path)
261+
if not self._settings.get_boolean(["use_uploads_folder"]):
262+
thumbnail_filename = "{}/{}".format(self.get_plugin_data_folder(), thumbnail_path)
263+
else:
264+
thumbnail_filename = self._file_manager.path_on_disk("local", thumbnail_path)
209265

210266
if os.path.exists(thumbnail_filename):
211267
os.remove(thumbnail_filename)
212268
if event == "FileAdded":
213269
gcode_filename = self._file_manager.path_on_disk("local", payload["path"])
214270
self._extract_thumbnail(gcode_filename, thumbnail_filename)
215271
if os.path.exists(thumbnail_filename):
216-
thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/{}?{:%Y%m%d%H%M%S}".format(
217-
thumbnail_path.replace(thumbnail_name, quote(thumbnail_name)), datetime.datetime.now())
218-
self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail",
219-
thumbnail_url.replace("//", "/"), overwrite=True)
220-
self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src",
221-
self._identifier, overwrite=True)
272+
if not self._settings.get_boolean(["use_uploads_folder"]):
273+
thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/{}?{:%Y%m%d%H%M%S}".format(thumbnail_path.replace(thumbnail_name, quote(thumbnail_name)), datetime.datetime.now())
274+
else:
275+
thumbnail_url = "downloads/files/local/{}?{:%Y%m%d%H%M%S}".format(thumbnail_path.replace(thumbnail_name, quote(thumbnail_name)), datetime.datetime.now())
276+
self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail", thumbnail_url.replace("//", "/"), overwrite=True)
277+
self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src", self._identifier, overwrite=True)
222278

223279
# ~~ SimpleApiPlugin mixin
224280

@@ -258,7 +314,7 @@ def on_api_command(self, command, data):
258314
def scan_files(self):
259315
self._logger.debug("Crawling Files")
260316
file_list = self._file_manager.list_files(recursive=True)
261-
self._logger.info(file_list)
317+
self._logger.debug(file_list)
262318
local_files = file_list["local"]
263319
results = dict(no_thumbnail=[], no_thumbnail_src=[])
264320
for key, file in local_files.items():

octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails_settings.jinja2

+5
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
<input type="checkbox" data-bind="checked: settings.plugins.prusaslicerthumbnails.sync_on_refresh"> Rescan all files when pressing refresh in file list.
6161
</label>
6262
</div>
63+
<div class="controls">
64+
<label class="checkbox">
65+
<input type="checkbox" data-bind="checked: settings.plugins.prusaslicerthumbnails.use_uploads_folder"> Extract images into uploads folder.
66+
</label>
67+
</div>
6368
</div>
6469
<div class="row-fluid" data-bind="allowBindings: false">
6570
<div class="row-fluid" data-bind="allowBindings: true">

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
plugin_name = "Slicer Thumbnails"
1515

1616
# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
17-
plugin_version = "1.0.1"
17+
plugin_version = "1.0.2"
1818

1919
# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
2020
# module

0 commit comments

Comments
 (0)