├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml ├── stale.yml └── workflows │ └── stale.yml ├── .gitignore ├── MANIFEST.in ├── README.md ├── babel.cfg ├── extras ├── PrusaSlicerThumbnails.md └── README.txt ├── octoprint_prusaslicerthumbnails ├── __init__.py ├── static │ ├── css │ │ └── prusaslicerthumbnails.css │ └── js │ │ └── prusaslicerthumbnails.js └── templates │ ├── prusaslicerthumbnails.jinja2 │ └── prusaslicerthumbnails_settings.jinja2 ├── patreon-with-text-new.png ├── paypal-with-text.png ├── requirements.txt ├── screenshot_button.png ├── screenshot_cura.png ├── screenshot_ideamaker.png ├── screenshot_inline_thumbnail.png ├── screenshot_prusaslicer.png ├── screenshot_simplify3d.png ├── screenshot_superslicer.png ├── screenshot_thumbnail.png ├── setup.py └── translations └── README.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [**.py] 13 | indent_style = tab 14 | 15 | [**.js] 16 | indent_style = space 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jneilliii] 2 | patreon: jneilliii 3 | custom: ['https://www.paypal.me/jneilliii'] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Check for updates to GitHub Actions every week 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 14 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - enhancement 8 | - bug 9 | - solved 10 | - documentation 11 | # Label to use when marking an issue as stale 12 | staleLabel: stale 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | activity in 14 days. It will be closed if no further activity occurs in 7 days. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark Stale Issues 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | # permissions: 7 | # actions: write 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/stale@v9 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had activity in 14 days. It will be closed if no further activity occurs in 7 days' 16 | days-before-stale: 14 17 | days-before-close: 7 18 | stale-issue-label: 'stale' 19 | days-before-issue-stale: 14 20 | days-before-pr-stale: -1 21 | days-before-issue-close: 7 22 | days-before-pr-close: -1 23 | exempt-issue-labels: 'bug,enhancement' 24 | # - uses: actions/checkout@v4 25 | # - uses: gautamkrishnar/keepalive-workflow@v2 26 | # with: 27 | # use_api: true 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .idea 4 | *.iml 5 | build 6 | dist 7 | *.egg* 8 | .DS_Store 9 | *.zip 10 | /git-flow-plus.config 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include octoprint_prusaslicerthumbnails/templates * 3 | recursive-include octoprint_prusaslicerthumbnails/translations * 4 | recursive-include octoprint_prusaslicerthumbnails/static * 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slicer Thumbnails 2 | 3 | ![GitHub Downloads](https://badgen.net/github/assets-dl/jneilliii/OctoPrint-PrusaSlicerThumbnails/) 4 | 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). 6 | 7 | The preview thumbnail can be shown in OctoPrint from the files list by clicking the newly added image button. 8 | 9 | ![button](screenshot_button.png) 10 | 11 | The thumbnail will open in a modal window. 12 | 13 | ![thumbnail](screenshot_thumbnail.png) 14 | 15 | 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. 16 | 17 | ![thumbnail](screenshot_inline_thumbnail.png) 18 | 19 | ## Configuration 20 | 21 | ### PrusaSlicer 22 | 23 | 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. 24 | 25 | ![PrusaSlicer](screenshot_prusaslicer.png) 26 | 27 | **Warning**: the higher the resolution of the thumbnail you use in this setting the larger your gcode file will be when sliced. 28 | 29 | ### SuperSlicer 30 | 31 | 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. 32 | 33 | ![SuperSlicer](screenshot_superslicer.png) 34 | 35 | **Warning**: the higher the resolution of the thumbnail you use in this setting the larger your gcode file will be when sliced. 36 | 37 | ### Cura 38 | 39 | 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). 40 | 41 | ![Cura](screenshot_cura.png) 42 | 43 | ### Simplify3D 44 | 45 | **Simplify3D 5.1** 46 | 47 | Now integrated as an option in newer versions of Simplify3D 48 | ![Simplify3D](screenshot_simplify3d.png) 49 | 50 | **Older versions** 51 | 52 | Available as a post-processing script for [windows](https://github.com/boweeble/s3d-thumbnail-generator), [linux](https://github.com/NotExpectedYet/s3d-thumbnail-generator), or [macos](https://github.com/idcrook/s3d-thumbnail-generator-macos) thanks to [@boweeble](https://github.com/boweeble/), [@NotExpectedYet](https://github.com/NotExpectedYet/), and [@idcrook](https://github.com/idcrook/). 53 | 54 | Alernatively there is another solution that utilizes OpenSCAD to geerate the thumbnail thanks to [@fabiorinaldus399](https://github.com/fabiorinaldus399/) that can be found [here](https://github.com/fabiorinaldus399/gcode-tumbnail-generator-for-Simplify3D). 55 | 56 | ### IdeaMaker 57 | 58 | Available since version 2.4.1: Users can enable GCode Thumbnails for OctoPrint and Mainsail under “Printer Settings” -> “Advanced”. 59 | 60 | ![IdeaMaker](screenshot_ideamaker.png) 61 | 62 | ## Get Help 63 | 64 | If you experience issues with this plugin or need assistance please use the issue tracker by clicking issues above. 65 | 66 | ### Additional Plugins 67 | 68 | Check out my other plugins [here](https://plugins.octoprint.org/by_author/#jneilliii) 69 | 70 | ### Sponsors 71 | - Andreas Lindermayr 72 | - [@TheTuxKeeper](https://github.com/thetuxkeeper) 73 | - [@tideline3d](https://github.com/tideline3d/) 74 | - [SimplyPrint](https://simplyprint.io/) 75 | - [Andrew Beeman](https://github.com/Kiendeleo) 76 | - [Calanish](https://github.com/calanish) 77 | - [Lachlan Bell](https://lachy.io/) 78 | - [Jonny Bergdahl](https://github.com/bergdahl) 79 | ## Support My Efforts 80 | 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. 81 | 82 | [![Patreon](patreon-with-text-new.png)](https://www.patreon.com/jneilliii) [![paypal](paypal-with-text.png)](https://paypal.me/jneilliii) 83 | 84 | No paypal.me? Send funds via PayPal to jneilliii@gmail.com 85 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: */**.py] 2 | [jinja2: */**.jinja2] 3 | extensions=jinja2.ext.autoescape, jinja2.ext.with_ 4 | 5 | [javascript: */**.js] 6 | extract_messages = gettext, ngettext 7 | -------------------------------------------------------------------------------- /extras/PrusaSlicerThumbnails.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: plugin 3 | 4 | id: PrusaSlicerThumbnails 5 | title: OctoPrint-PrusaSlicerThumbnails 6 | description: Plugin that extracts thumbnails from uploaded gcode files sliced by PrusaSlicer. 7 | author: jneilliii 8 | license: AGPLv3 9 | 10 | # TODO 11 | date: today's date in format YYYY-MM-DD, e.g. 2015-04-21 12 | 13 | homepage: https://github.com/jneilliii/OctoPrint-PrusaSlicerThumbnails 14 | source: https://github.com/jneilliii/OctoPrint-PrusaSlicerThumbnails 15 | archive: https://github.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/archive/master.zip 16 | 17 | # TODO 18 | # Set this to true if your plugin uses the dependency_links setup parameter to include 19 | # library versions not yet published on PyPi. SHOULD ONLY BE USED IF THERE IS NO OTHER OPTION! 20 | #follow_dependency_links: false 21 | 22 | # TODO 23 | tags: 24 | - a list 25 | - of tags 26 | - that apply 27 | - to your plugin 28 | - (take a look at the existing plugins for what makes sense here) 29 | 30 | # TODO 31 | screenshots: 32 | - url: url of a screenshot, /assets/img/... 33 | alt: alt-text of a screenshot 34 | caption: caption of a screenshot 35 | - url: url of another screenshot, /assets/img/... 36 | alt: alt-text of another screenshot 37 | caption: caption of another screenshot 38 | - ... 39 | 40 | # TODO 41 | featuredimage: url of a featured image for your plugin, /assets/img/... 42 | 43 | # TODO 44 | # You only need the following if your plugin requires specific OctoPrint versions or 45 | # specific operating systems to function - you can safely remove the whole 46 | # "compatibility" block if this is not the case. 47 | 48 | compatibility: 49 | 50 | # List of compatible versions 51 | # 52 | # A single version number will be interpretated as a minimum version requirement, 53 | # e.g. "1.3.1" will show the plugin as compatible to OctoPrint versions 1.3.1 and up. 54 | # More sophisticated version requirements can be modelled too by using PEP440 55 | # compatible version specifiers. 56 | # 57 | # You can also remove the whole "octoprint" block. Removing it will default to all 58 | # OctoPrint versions being supported. 59 | 60 | octoprint: 61 | - 1.2.0 62 | 63 | # List of compatible operating systems 64 | # 65 | # Valid values: 66 | # 67 | # - windows 68 | # - linux 69 | # - macos 70 | # - freebsd 71 | # 72 | # There are also two OS groups defined that get expanded on usage: 73 | # 74 | # - posix: linux, macos and freebsd 75 | # - nix: linux and freebsd 76 | # 77 | # You can also remove the whole "os" block. Removing it will default to all 78 | # operating systems being supported. 79 | 80 | os: 81 | - linux 82 | - windows 83 | - macos 84 | - freebsd 85 | 86 | # Compatible Python version 87 | # 88 | # Plugins should aim for compatibility for Python 2 and 3 for now, in which case the value should be ">=2.7,<4". 89 | # 90 | # Plugins that only wish to support Python 3 should set it to ">=3,<4". 91 | # 92 | # If your plugin only supports Python 2 (worst case, not recommended for newly developed plugins since Python 2 93 | # is EOL), leave at ">=2.7,<3" 94 | 95 | python: ">=2.7,<3" 96 | 97 | --- 98 | 99 | **TODO**: Longer description of your plugin, configuration examples etc. This part will be visible on the page at 100 | http://plugins.octoprint.org/plugin/PrusaSlicerThumbnails/ 101 | -------------------------------------------------------------------------------- /extras/README.txt: -------------------------------------------------------------------------------- 1 | Currently Cookiecutter generates the following helpful extras to this folder: 2 | 3 | PrusaSlicerThumbnails.md 4 | Data file for plugins.octoprint.org. Fill in the missing TODOs once your 5 | plugin is ready for release and file a PR as described at 6 | http://plugins.octoprint.org/help/registering/ to get it published. 7 | 8 | This folder may be safely removed if you don't need it. 9 | -------------------------------------------------------------------------------- /octoprint_prusaslicerthumbnails/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import absolute_import 3 | from __future__ import unicode_literals 4 | 5 | import flask 6 | import octoprint.plugin 7 | import octoprint.filemanager 8 | import octoprint.filemanager.util 9 | import octoprint.util 10 | import os 11 | import datetime 12 | import io 13 | from PIL import Image 14 | import re 15 | import base64 16 | import imghdr 17 | 18 | from flask_babel import gettext 19 | from octoprint.access import ADMIN_GROUP 20 | from octoprint.access.permissions import Permissions 21 | 22 | try: 23 | from urllib import quote, unquote 24 | except ImportError: 25 | from urllib.parse import quote, unquote 26 | 27 | 28 | class PrusaslicerthumbnailsPlugin(octoprint.plugin.SettingsPlugin, 29 | octoprint.plugin.AssetPlugin, 30 | octoprint.plugin.TemplatePlugin, 31 | octoprint.plugin.EventHandlerPlugin, 32 | octoprint.plugin.SimpleApiPlugin): 33 | 34 | def __init__(self): 35 | self.file_scanner = None 36 | self.syncing = False 37 | self._fileRemovalTimer = None 38 | self._fileRemovalLastDeleted = None 39 | self._fileRemovalLastAdded = None 40 | self._folderRemovalTimer = None 41 | self._folderRemovalLastDeleted = {} 42 | self._folderRemovalLastAdded = {} 43 | self._waitForAnalysis = False 44 | self._analysis_active = False 45 | self.regex_extension = re.compile("\.(?:gco(?:de)?|tft)$") 46 | 47 | # ~~ SettingsPlugin mixin 48 | 49 | def get_settings_defaults(self): 50 | return {'installed': True, 'inline_thumbnail': False, 'scale_inline_thumbnail': False, 51 | 'inline_thumbnail_scale_value': "50", 'inline_thumbnail_position_left': False, 52 | 'align_inline_thumbnail': False, 'inline_thumbnail_align_value': "left", 'state_panel_thumbnail': True, 53 | 'state_panel_thumbnail_scale_value': "100", 'resize_filelist': False, 'filelist_height': "306", 54 | 'scale_inline_thumbnail_position': False, 'sync_on_refresh': False, 'use_uploads_folder': False, 55 | 'relocate_progress': False} 56 | 57 | # ~~ AssetPlugin mixin 58 | 59 | def get_assets(self): 60 | return {'js': ["js/prusaslicerthumbnails.js"], 'css': ["css/prusaslicerthumbnails.css"]} 61 | 62 | # ~~ TemplatePlugin mixin 63 | 64 | def get_template_configs(self): 65 | return [ 66 | {'type': "settings", 'custom_bindings': False, 'template': "prusaslicerthumbnails_settings.jinja2"}, 67 | ] 68 | 69 | def _extract_thumbnail(self, gcode_filename, thumbnail_filename): 70 | regex = r"(?:^; thumbnail(?:_JPG)* begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail(?:_JPG)* end)" 71 | regex_mks = re.compile('(?:;(?:simage|;gimage):).*?M10086 ;[\r\n]', re.DOTALL) 72 | regex_weedo = re.compile('W221[\r\n](.*)[\r\n]W222', re.DOTALL) 73 | regex_luban = re.compile(';[Tt]humbnail: ?data:image/png;base64,(.*)[\r\n]', re.DOTALL) 74 | regex_qidi = re.compile('M4010.*\'(.*)\'', re.DOTALL) 75 | regex_creality = r"(?:^; jpg begin .*)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; jpg end)" 76 | regex_buddy = r"(?:^; thumbnail(?:_QOI)* begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail(?:_QOI)* end)" 77 | lineNum = 0 78 | collectedString = "" 79 | use_mks = False 80 | use_weedo = False 81 | use_qidi = False 82 | use_flashprint = False 83 | use_creality = False 84 | use_buddy = False 85 | 86 | with open(gcode_filename, "r", encoding="utf8", errors="ignore") as gcode_file: 87 | for line in gcode_file: 88 | lineNum += 1 89 | gcode = octoprint.util.comm.gcode_command_for_cmd(line) 90 | extrusion_match = octoprint.util.comm.regexes_parameters["floatE"].search(line) 91 | if gcode == "G1" and extrusion_match: 92 | self._logger.debug("Line %d: Detected first extrusion. Read complete.", lineNum) 93 | break 94 | if line.startswith(";") or line.startswith("\n") or line.startswith("M10086 ;") or line[0:4] in ["W220", "W221", "W222"]: 95 | collectedString += line 96 | self._logger.debug(collectedString) 97 | test_str = collectedString.replace(octoprint.util.to_unicode('\r\n'), octoprint.util.to_unicode('\n')) 98 | test_str = test_str.replace(octoprint.util.to_unicode(';\n;\n'), octoprint.util.to_unicode(';\n\n;\n')) 99 | matches = re.findall(regex, test_str, re.MULTILINE) 100 | if len(matches) == 0: # MKS lottmaxx fallback 101 | matches = regex_mks.findall(test_str) 102 | if len(matches) > 0: 103 | self._logger.debug("Found mks thumbnail.") 104 | use_mks = True 105 | if len(matches) == 0: # Weedo fallback 106 | matches = regex_weedo.findall(test_str) 107 | if len(matches) > 0: 108 | self._logger.debug("Found weedo thumbnail.") 109 | use_weedo = True 110 | if len(matches) == 0: # luban fallback 111 | matches = regex_luban.findall(test_str) 112 | if len(matches) > 0: 113 | self._logger.debug("Found luban thumbnail.") 114 | if len(matches) == 0: # Qidi fallback 115 | matches = regex_qidi.findall(test_str) 116 | if len(matches) > 0: 117 | self._logger.debug("Found qidi thumbnail.") 118 | use_qidi = True 119 | if len(matches) == 0: # FlashPrint fallback 120 | with open(gcode_filename, "rb") as gcode_file: 121 | gcode_file.seek(58) 122 | thumbbytes = gcode_file.read(14454) 123 | if imghdr.what(file=None, h=thumbbytes) == 'bmp': 124 | self._logger.debug("Found flashprint thumbnail.") 125 | matches = [thumbbytes] 126 | use_flashprint = True 127 | if len(matches) == 0: # Creality Neo fallback 128 | matches = re.findall(regex_creality, test_str, re.MULTILINE) 129 | if len(matches) > 0: 130 | self._logger.debug("Found creality thumbnail.") 131 | use_creality = True 132 | if len(matches) == 0: # Prusa buddy fallback 133 | matches = re.findall(regex_buddy, test_str, re.MULTILINE) 134 | if len(matches) > 0: 135 | self._logger.debug("Found Prusa Buddy QOI thumbnail.") 136 | use_buddy = True 137 | if len(matches) > 0: 138 | maxlen=0 139 | choosen=-1 140 | for i in range(len(matches)): 141 | curlen=len(matches[i]) 142 | if maxlen= 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 182 | newData.append((255, 255, 255, 0)) # storing a transparent value when we find a black/dark colour 183 | else: 184 | newData.append(pixel) # other colours remain unchanged 185 | 186 | rgba.putdata(newData) 187 | 188 | with io.BytesIO() as png_bytes: 189 | rgba.save(png_bytes, "PNG") 190 | png_bytes_string = png_bytes.getvalue() 191 | return png_bytes_string 192 | 193 | # Extracts a thumbnail from hex binary data usd by Qidi slicer 194 | def _extract_qidi_thumbnail(self, gcode_encoded_images): 195 | encoded_image = gcode_encoded_images[0].replace('W220 ', '').replace('\n', '').replace('\r', '').replace(' ', '') 196 | encoded_image = bytes(bytearray.fromhex(encoded_image)) 197 | return encoded_image 198 | 199 | # Extracts a thumbnail from hex binary data usd by Weedo printers 200 | def _extract_weedo_thumbnail(self, gcode_encoded_images): 201 | encoded_image = gcode_encoded_images[0].replace('W220 ', '').replace('\n', '').replace('\r', '').replace(' ', '') 202 | encoded_image = bytes(bytearray.fromhex(encoded_image)) 203 | return encoded_image 204 | 205 | # Extracts a thumbnail from a gcode and returns png binary string 206 | def _extract_mks_thumbnail(self, gcode_encoded_images): 207 | 208 | # Find the biggest thumbnail 209 | encoded_image_dimensions, encoded_image = self.find_best_thumbnail(gcode_encoded_images) 210 | 211 | # Not found? 212 | if encoded_image is None: 213 | return None # What to return? Is None ok? 214 | 215 | # Remove M10086 ; and whitespaces 216 | encoded_image = encoded_image.replace('M10086 ;', '').replace('\n', '').replace('\r', '').replace(' ', '') 217 | 218 | # Get bytes from hex 219 | encoded_image = bytes(bytearray.fromhex(encoded_image)) 220 | 221 | # Load pixel data 222 | image = Image.frombytes('RGB', encoded_image_dimensions, encoded_image, 'raw', 'BGR;16', 0, 1) 223 | return self._imageToPng(image) 224 | 225 | # Extracts a thumbnail from hex binary data usd by Qidi slicer 226 | def _extract_creality_thumbnail(self, match): 227 | encoded_jpg = base64.b64decode(match.replace("; ", "").encode()) 228 | with io.BytesIO(encoded_jpg) as jpg_bytes: 229 | image = Image.open(jpg_bytes) 230 | return self._imageToPng(image) 231 | 232 | def _imageToPng(self, image): 233 | # Save image as png 234 | with io.BytesIO() as png_bytes: 235 | image.save(png_bytes, "PNG") 236 | png_bytes_string = png_bytes.getvalue() 237 | 238 | return png_bytes_string 239 | 240 | # Finds the biggest thumbnail 241 | def find_best_thumbnail(self, gcode_encoded_images): 242 | 243 | # Check for gimage 244 | for image in gcode_encoded_images: 245 | if image.startswith(';;gimage:'): 246 | # Return size and trimmed string 247 | return (200, 200), image[9:] 248 | 249 | # Check for simage 250 | for image in gcode_encoded_images: 251 | if image.startswith(';simage:'): 252 | # Return size and trimmed string 253 | return (100, 100), image[8:] 254 | 255 | # Image not found 256 | return None 257 | 258 | # ~~ EventHandlerPlugin mixin 259 | 260 | def on_event(self, event, payload): 261 | if event not in ["FileAdded", "FileRemoved", "FolderRemoved", "FolderAdded"]: 262 | return 263 | if event == "FolderRemoved" and payload["storage"] == "local": 264 | import shutil 265 | shutil.rmtree(self.get_plugin_data_folder() + "/" + payload["path"], ignore_errors=True) 266 | if event == "FolderAdded" and payload["storage"] == "local": 267 | file_list = self._file_manager.list_files(path=payload["path"], recursive=True) 268 | local_files = file_list["local"] 269 | results = dict(no_thumbnail=[], no_thumbnail_src=[]) 270 | for file_key, file in local_files.items(): 271 | results = self._process_gcode(local_files[file_key], results) 272 | self._logger.debug("Scan results: {}".format(results)) 273 | if event in ["FileAdded", "FileRemoved"] and payload["storage"] == "local" and "gcode" in payload[ 274 | "type"] and payload.get("name", False): 275 | thumbnail_name = self.regex_extension.sub(".png", payload["name"]) 276 | thumbnail_path = self.regex_extension.sub(".png", payload["path"]) 277 | if not self._settings.get_boolean(["use_uploads_folder"]): 278 | thumbnail_filename = "{}/{}".format(self.get_plugin_data_folder(), thumbnail_path) 279 | else: 280 | thumbnail_filename = self._file_manager.path_on_disk("local", thumbnail_path) 281 | 282 | if os.path.exists(thumbnail_filename): 283 | os.remove(thumbnail_filename) 284 | if event == "FileAdded": 285 | gcode_filename = self._file_manager.path_on_disk("local", payload["path"]) 286 | self._extract_thumbnail(gcode_filename, thumbnail_filename) 287 | if os.path.exists(thumbnail_filename): 288 | thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/{}?{:%Y%m%d%H%M%S}".format(thumbnail_path.replace(thumbnail_name, quote(thumbnail_name)), datetime.datetime.now()) 289 | self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail", thumbnail_url.replace("//", "/"), overwrite=True) 290 | self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src", self._identifier, overwrite=True) 291 | 292 | # ~~ SimpleApiPlugin mixin 293 | 294 | def _process_gcode(self, gcode_file, results=None): 295 | if results is None: 296 | results = [] 297 | self._logger.debug(gcode_file["path"]) 298 | if gcode_file.get("type") == "machinecode": 299 | self._logger.debug(gcode_file.get("thumbnail")) 300 | if gcode_file.get("thumbnail") is None or not os.path.exists("{}/{}".format(self.get_plugin_data_folder(), self.regex_extension.sub(".png", gcode_file["path"]))): 301 | self._logger.debug("No Thumbnail for %s, attempting extraction" % gcode_file["path"]) 302 | results["no_thumbnail"].append(gcode_file["path"]) 303 | self.on_event("FileAdded", {'path': gcode_file["path"], 'storage': "local", 'type': ["gcode"], 304 | 'name': gcode_file["name"]}) 305 | elif "prusaslicerthumbnails" in gcode_file.get("thumbnail") and not gcode_file.get("thumbnail_src"): 306 | self._logger.debug("No Thumbnail source for %s, adding" % gcode_file["path"]) 307 | results["no_thumbnail_src"].append(gcode_file["path"]) 308 | self._file_manager.set_additional_metadata("local", gcode_file["path"], "thumbnail_src", 309 | self._identifier, overwrite=True) 310 | elif gcode_file.get("type") == "folder" and not gcode_file.get("children") == None: 311 | children = gcode_file["children"] 312 | for key, file in children.items(): 313 | self._process_gcode(children[key], results) 314 | return results 315 | 316 | def get_api_commands(self): 317 | return dict(crawl_files=[]) 318 | 319 | def on_api_command(self, command, data): 320 | import flask 321 | if not Permissions.PLUGIN_PRUSASLICERTHUMBNAILS_SCAN.can(): 322 | return flask.make_response("Insufficient rights", 403) 323 | 324 | if command == "crawl_files": 325 | return flask.jsonify(self.scan_files()) 326 | 327 | def scan_files(self): 328 | self._logger.debug("Crawling Files") 329 | file_list = self._file_manager.list_files(recursive=True) 330 | self._logger.debug(file_list) 331 | local_files = file_list["local"] 332 | results = dict(no_thumbnail=[], no_thumbnail_src=[]) 333 | for key, file in local_files.items(): 334 | results = self._process_gcode(local_files[key], results) 335 | self.file_scanner = None 336 | return results 337 | 338 | # ~~ extension_tree hook 339 | def get_extension_tree(self, *args, **kwargs): 340 | return dict( 341 | machinecode=dict( 342 | gcode=["tft"] 343 | ) 344 | ) 345 | 346 | # ~~ Routes hook 347 | def route_hook(self, server_routes, *args, **kwargs): 348 | from octoprint.server.util.tornado import LargeResponseHandler, path_validation_factory 349 | from octoprint.util import is_hidden_path 350 | thumbnail_root_path = self._file_manager.path_on_disk("local", "") if self._settings.get_boolean(["use_uploads_folder"]) else self.get_plugin_data_folder() 351 | return [ 352 | (r"thumbnail/(.*)", LargeResponseHandler, 353 | {'path': thumbnail_root_path, 'as_attachment': False, 'path_validation': path_validation_factory( 354 | lambda path: not is_hidden_path(path), status_code=404)}) 355 | ] 356 | 357 | # ~~ Server API Before Request Hook 358 | 359 | def hook_octoprint_server_api_before_request(self, *args, **kwargs): 360 | return [self.update_file_list] 361 | 362 | def update_file_list(self): 363 | if self._settings.get_boolean(["sync_on_refresh"]) and flask.request.path.startswith( 364 | '/api/files') and flask.request.method == 'GET' and not self.file_scanner: 365 | from threading import Thread 366 | self.file_scanner = Thread(target=self.scan_files, daemon=True) 367 | self.file_scanner.start() 368 | 369 | # ~~ Access Permissions Hook 370 | 371 | def get_additional_permissions(self, *args, **kwargs): 372 | return [ 373 | {'key': "SCAN", 'name': "Scan Files", 'description': gettext("Allows access to scan files."), 374 | 'roles': ["admin"], 'dangerous': True, 'default_groups': [ADMIN_GROUP]} 375 | ] 376 | 377 | # ~~ Softwareupdate hook 378 | 379 | def get_update_information(self): 380 | return {'prusaslicerthumbnails': {'displayName': "Slicer Thumbnails", 'displayVersion': self._plugin_version, 381 | 'type': "github_release", 'user': "jneilliii", 382 | 'repo': "OctoPrint-PrusaSlicerThumbnails", 'current': self._plugin_version, 383 | 'stable_branch': {'name': "Stable", 'branch': "master", 384 | 'comittish': ["master"]}, 'prerelease_branches': [ 385 | {'name': "Release Candidate", 'branch': "rc", 'comittish': ["rc", "master"]} 386 | ], 'pip': "https://github.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/archive/{target_version}.zip"}} 387 | 388 | # ~~ Backup hook 389 | 390 | def additional_backup_excludes(self, excludes, *args, **kwargs): 391 | if "uploads" in excludes: 392 | return ["."] 393 | return [] 394 | 395 | 396 | __plugin_name__ = "Slicer Thumbnails" 397 | __plugin_pythoncompat__ = ">=2.7,<4" # python 2 and 3 398 | 399 | 400 | def __plugin_load__(): 401 | global __plugin_implementation__ 402 | __plugin_implementation__ = PrusaslicerthumbnailsPlugin() 403 | 404 | global __plugin_hooks__ 405 | __plugin_hooks__ = { 406 | "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, 407 | "octoprint.filemanager.extension_tree": __plugin_implementation__.get_extension_tree, 408 | "octoprint.server.http.routes": __plugin_implementation__.route_hook, 409 | "octoprint.server.api.before_request": __plugin_implementation__.hook_octoprint_server_api_before_request, 410 | "octoprint.access.permissions": __plugin_implementation__.get_additional_permissions, 411 | "octoprint.plugin.backup.additional_excludes": __plugin_implementation__.additional_backup_excludes, 412 | } 413 | -------------------------------------------------------------------------------- /octoprint_prusaslicerthumbnails/static/css/prusaslicerthumbnails.css: -------------------------------------------------------------------------------- 1 | /* #prusa_thumbnail_viewer { 2 | width: 330px; 3 | margin-left: -165px; 4 | } */ 5 | 6 | #prusa_thumbnail_viewer h3 { 7 | overflow: hidden; 8 | } 9 | 10 | #files .gcode_files .entry { 11 | overflow: hidden; 12 | } 13 | 14 | #files .gcode_files .entry img, #prusaslicer_state_thumbnail img { 15 | width: auto; 16 | } 17 | 18 | .inline_prusa_thumbnail.pull-left, #prusaslicer_state_thumbnail.pull-left { 19 | padding-right: 5px; 20 | } 21 | 22 | #prusaslicer_state_thumbnail.pull-left img { 23 | width: 100%; 24 | } 25 | -------------------------------------------------------------------------------- /octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js: -------------------------------------------------------------------------------- 1 | /* 2 | * View model for OctoPrint-PrusaSlicerThumbnails 3 | * 4 | * Author: jneilliii 5 | * License: AGPLv3 6 | */ 7 | $(function() { 8 | function PrusaslicerthumbnailsViewModel(parameters) { 9 | var self = this; 10 | 11 | self.settingsViewModel = parameters[0]; 12 | self.filesViewModel = parameters[1]; 13 | self.printerStateViewModel = parameters[2]; 14 | 15 | self.thumbnail_url = ko.observable('/static/img/tentacle-20x20.png'); 16 | self.thumbnail_title = ko.observable(''); 17 | self.inline_thumbnail = ko.observable(); 18 | self.file_details = ko.observable(); 19 | self.crawling_files = ko.observable(false); 20 | self.crawl_results = ko.observableArray([]); 21 | 22 | self.filesViewModel.prusaslicerthumbnails_open_thumbnail = function(data) { 23 | if(data.thumbnail_src === "prusaslicerthumbnails"){ 24 | var thumbnail_title = data.name.replace(/\.(?:gco(?:de)?|tft)$/,''); 25 | self.thumbnail_url(data.thumbnail); 26 | self.thumbnail_title(thumbnail_title); 27 | self.file_details(data); 28 | $('div#prusa_thumbnail_viewer').modal("show"); 29 | } 30 | }; 31 | 32 | self.DEFAULT_THUMBNAIL_SCALE = "100%"; 33 | self.filesViewModel.thumbnailScaleValue = ko.observable(self.DEFAULT_THUMBNAIL_SCALE); 34 | 35 | self.DEFAULT_THUMBNAIL_ALIGN = "left"; 36 | self.filesViewModel.thumbnailAlignValue = ko.observable(self.DEFAULT_THUMBNAIL_ALIGN); 37 | 38 | self.DEFAULT_THUMBNAIL_POSITION = false; 39 | self.filesViewModel.thumbnailPositionLeft = ko.observable(self.DEFAULT_THUMBNAIL_POSITION); 40 | 41 | self.crawl_files = function(){ 42 | self.crawling_files(true); 43 | self.crawl_results([]); 44 | $.ajax({ 45 | url: API_BASEURL + "plugin/prusaslicerthumbnails", 46 | type: "POST", 47 | dataType: "json", 48 | data: JSON.stringify({ 49 | command: "crawl_files" 50 | }), 51 | contentType: "application/json; charset=UTF-8" 52 | }).done(function(data){ 53 | for (key in data) { 54 | if(data[key].length){ 55 | self.crawl_results.push({name: ko.observable(key), files: ko.observableArray(data[key])}); 56 | } 57 | } 58 | if(self.crawl_results().length === 0){ 59 | self.crawl_results.push({name: ko.observable('No convertible files found'), files: ko.observableArray([])}); 60 | } 61 | self.filesViewModel.requestData({force: true}); 62 | self.crawling_files(false); 63 | }).fail(function(data){ 64 | self.crawling_files(false); 65 | }); 66 | }; 67 | 68 | self.onBeforeBinding = function() { 69 | // inject filelist thumbnail into template 70 | 71 | let regex = /
([\s\S]*)<.div>/mi; 72 | let template = ''; 73 | 74 | let inline_thumbnail_template = '
' + 76 | '
'; 81 | 82 | 83 | $("#files_template_machinecode").text(function () { 84 | var return_value = inline_thumbnail_template + $(this).text(); 85 | return_value = return_value.replace(regex, '
$1 ' + template + '>
'); 86 | return return_value; 87 | }); 88 | 89 | // assign initial scaling 90 | if (self.settingsViewModel.settings.plugins.prusaslicerthumbnails.scale_inline_thumbnail()==true){ 91 | self.filesViewModel.thumbnailScaleValue(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_scale_value() + "%"); 92 | } 93 | 94 | // assign initial alignment 95 | if (self.settingsViewModel.settings.plugins.prusaslicerthumbnails.align_inline_thumbnail()==true){ 96 | self.filesViewModel.thumbnailAlignValue(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_align_value()); 97 | } 98 | 99 | // assign initial filelist height 100 | if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.resize_filelist()) { 101 | $('#files > div > div.gcode_files > div.scroll-wrapper').css({'height': self.settingsViewModel.settings.plugins.prusaslicerthumbnails.filelist_height() + 'px'}); 102 | } 103 | 104 | // assign initial position 105 | if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_position_left()==true) { 106 | self.filesViewModel.thumbnailPositionLeft(true); 107 | } 108 | 109 | // observe scaling changes 110 | self.settingsViewModel.settings.plugins.prusaslicerthumbnails.scale_inline_thumbnail.subscribe(function(newValue){ 111 | if (newValue == false){ 112 | self.filesViewModel.thumbnailScaleValue(self.DEFAULT_THUMBNAIL_SCALE); 113 | } else { 114 | self.filesViewModel.thumbnailScaleValue(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_scale_value() + "%"); 115 | } 116 | }); 117 | self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_scale_value.subscribe(function(newValue){ 118 | self.filesViewModel.thumbnailScaleValue(newValue + "%"); 119 | }); 120 | self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value.subscribe(function(newValue){ 121 | $('#prusaslicer_state_thumbnail').attr({'width': self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value() + '%'}); 122 | if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value() !== 100) { 123 | $('#prusaslicer_state_thumbnail').addClass('pull-left').next('hr').remove(); 124 | } 125 | }); 126 | 127 | // observe alignment changes 128 | self.settingsViewModel.settings.plugins.prusaslicerthumbnails.align_inline_thumbnail.subscribe(function(newValue){ 129 | if (newValue == false){ 130 | self.filesViewModel.thumbnailAlignValue(self.DEFAULT_THUMBNAIL_SCALE); 131 | } else { 132 | self.filesViewModel.thumbnailAlignValue(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_align_value()); 133 | } 134 | }); 135 | self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_align_value.subscribe(function(newValue){ 136 | self.filesViewModel.thumbnailAlignValue(newValue); 137 | }); 138 | 139 | // observe position changes 140 | self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_position_left.subscribe(function(newValue){ 141 | self.filesViewModel.thumbnailPositionLeft(newValue); 142 | }); 143 | 144 | // observe file list height changes 145 | self.settingsViewModel.settings.plugins.prusaslicerthumbnails.filelist_height.subscribe(function(newValue){ 146 | if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.resize_filelist()) { 147 | $('#files > div > div.gcode_files > div.scroll-wrapper').css({'height': self.settingsViewModel.settings.plugins.prusaslicerthumbnails.filelist_height() + 'px'}); 148 | } 149 | }); 150 | 151 | self.printerStateViewModel.dateString.subscribe(function(data){ 152 | if(data && data != "unknown"){ 153 | OctoPrint.files.get('local',self.printerStateViewModel.filepath()) 154 | .done(function(file_data){ 155 | if(file_data){ 156 | if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail() && file_data.thumbnail && file_data.thumbnail_src == 'prusaslicerthumbnails'){ 157 | if($('#prusaslicer_state_thumbnail').length) { 158 | $('#prusaslicer_state_thumbnail').attr('src', file_data.thumbnail); 159 | } else { 160 | $('#state > div > hr:first').after(''); 161 | if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value() == 100) { 162 | $('#prusaslicer_state_thumbnail').removeClass('pull-left').after('
'); 163 | } 164 | if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.relocate_progress()) { 165 | $('#state > div > div.progress.progress-text-centered').css({'margin-bottom': 'inherit'}).insertBefore('#prusaslicer_state_thumbnail').after('
'); 166 | } 167 | } 168 | } else { 169 | $('#prusaslicer_state_thumbnail').remove(); 170 | } 171 | } 172 | }) 173 | .fail(function(file_data){ 174 | if($('#prusaslicer_state_thumbnail').length) { 175 | $('#prusaslicer_state_thumbnail').remove(); 176 | } 177 | }); 178 | } else { 179 | $('#prusaslicer_state_thumbnail').remove(); 180 | if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value() == 100) { 181 | $('#prusaslicer_state_hr').remove(); 182 | } 183 | } 184 | }); 185 | }; 186 | 187 | } 188 | 189 | OCTOPRINT_VIEWMODELS.push({ 190 | construct: PrusaslicerthumbnailsViewModel, 191 | dependencies: ['settingsViewModel', 'filesViewModel', 'printerStateViewModel'], 192 | elements: ['div#prusa_thumbnail_viewer', '#crawl_files', '#crawl_files_results'] 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails.jinja2: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails_settings.jinja2: -------------------------------------------------------------------------------- 1 |

{{ _('Slicer Thumbnails') }}

2 |
3 |
4 | 7 |
8 |
9 | 10 | 11 | % 12 | 13 |
14 |
15 | 18 |
19 |
20 | 23 |
24 |
25 | 28 |
29 |
30 | 31 | 32 | % 33 | 34 |
35 |
36 | 39 |
40 |
41 | 44 |
45 |
46 | 51 |
52 |
53 | 56 |
57 |
58 | 59 | 60 | px 61 | 62 |
63 |
64 | 67 |
68 |
69 | 72 |
73 |
74 |
75 |
76 |
77 |
78 | 79 |
80 |
81 |
82 |
83 |

Scan Results

84 |
    85 | 86 |
  • 87 | 88 | 89 |
  • 90 |
      91 |
    • 92 | 93 |
    • 94 |
    95 |
  • 96 | 97 |
98 |
99 |
100 |
101 |
102 | -------------------------------------------------------------------------------- /patreon-with-text-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/patreon-with-text-new.png -------------------------------------------------------------------------------- /paypal-with-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/paypal-with-text.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ### 2 | # This file is only here to make sure that something like 3 | # 4 | # pip install -e . 5 | # 6 | # works as expected. Requirements can be found in setup.py. 7 | ### 8 | 9 | . 10 | 11 | setuptools 12 | OctoPrint 13 | Pillow 14 | -------------------------------------------------------------------------------- /screenshot_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/screenshot_button.png -------------------------------------------------------------------------------- /screenshot_cura.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/screenshot_cura.png -------------------------------------------------------------------------------- /screenshot_ideamaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/screenshot_ideamaker.png -------------------------------------------------------------------------------- /screenshot_inline_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/screenshot_inline_thumbnail.png -------------------------------------------------------------------------------- /screenshot_prusaslicer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/screenshot_prusaslicer.png -------------------------------------------------------------------------------- /screenshot_simplify3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/screenshot_simplify3d.png -------------------------------------------------------------------------------- /screenshot_superslicer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/screenshot_superslicer.png -------------------------------------------------------------------------------- /screenshot_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneilliii/OctoPrint-PrusaSlicerThumbnails/361f91825b214c47f149b8e184e761c531709032/screenshot_thumbnail.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | ######################################################################################################################## 4 | ### Do not forget to adjust the following variables to your own plugin. 5 | 6 | # The plugin's identifier, has to be unique 7 | plugin_identifier = "prusaslicerthumbnails" 8 | 9 | # The plugin's python package, should be "octoprint_", has to be unique 10 | plugin_package = "octoprint_prusaslicerthumbnails" 11 | 12 | # The plugin's human readable name. Can be overwritten within OctoPrint's internal data via __plugin_name__ in the 13 | # plugin module 14 | plugin_name = "Slicer Thumbnails" 15 | 16 | # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module 17 | plugin_version = "1.0.8" 18 | 19 | # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin 20 | # module 21 | plugin_description = """Plugin that extracts thumbnails from uploaded gcode files sliced by PrusaSlicer.""" 22 | 23 | # The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module 24 | plugin_author = "jneilliii" 25 | 26 | # The plugin's author's mail address. 27 | plugin_author_email = "jneilliii+github@gmail.com" 28 | 29 | # The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module 30 | plugin_url = "https://github.com/jneilliii/OctoPrint-PrusaSlicerThumbnails" 31 | 32 | # The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module 33 | plugin_license = "AGPLv3" 34 | 35 | # Any additional requirements besides OctoPrint should be listed here 36 | plugin_requires = ["Pillow>=9.5.0"] 37 | 38 | ### -------------------------------------------------------------------------------------------------------------------- 39 | ### More advanced options that you usually shouldn't have to touch follow after this point 40 | ### -------------------------------------------------------------------------------------------------------------------- 41 | 42 | # Additional package data to install for this plugin. The subfolders "templates", "static" and "translations" will 43 | # already be installed automatically if they exist. Note that if you add something here you'll also need to update 44 | # MANIFEST.in to match to ensure that python setup.py sdist produces a source distribution that contains all your 45 | # files. This is sadly due to how python's setup.py works, see also http://stackoverflow.com/a/14159430/2028598 46 | plugin_additional_data = [] 47 | 48 | # Any additional python packages you need to install with your plugin that are not contained in .* 49 | plugin_additional_packages = [] 50 | 51 | # Any python packages within .* you do NOT want to install with your plugin 52 | plugin_ignored_packages = [] 53 | 54 | # Additional parameters for the call to setuptools.setup. If your plugin wants to register additional entry points, 55 | # define dependency links or other things like that, this is the place to go. Will be merged recursively with the 56 | # default setup parameters as provided by octoprint_setuptools.create_plugin_setup_parameters using 57 | # octoprint.util.dict_merge. 58 | # 59 | # Example: 60 | # plugin_requires = ["someDependency==dev"] 61 | # additional_setup_parameters = {"dependency_links": ["https://github.com/someUser/someRepo/archive/master.zip#egg=someDependency-dev"]} 62 | additional_setup_parameters = {} 63 | 64 | ######################################################################################################################## 65 | 66 | from setuptools import setup 67 | 68 | try: 69 | import octoprint_setuptools 70 | except: 71 | print("Could not import OctoPrint's setuptools, are you sure you are running that under " 72 | "the same python installation that OctoPrint is installed under?") 73 | import sys 74 | sys.exit(-1) 75 | 76 | setup_parameters = octoprint_setuptools.create_plugin_setup_parameters( 77 | identifier=plugin_identifier, 78 | package=plugin_package, 79 | name=plugin_name, 80 | version=plugin_version, 81 | description=plugin_description, 82 | author=plugin_author, 83 | mail=plugin_author_email, 84 | url=plugin_url, 85 | license=plugin_license, 86 | requires=plugin_requires, 87 | additional_packages=plugin_additional_packages, 88 | ignored_packages=plugin_ignored_packages, 89 | additional_data=plugin_additional_data 90 | ) 91 | 92 | if len(additional_setup_parameters): 93 | from octoprint.util import dict_merge 94 | setup_parameters = dict_merge(setup_parameters, additional_setup_parameters) 95 | 96 | setup(**setup_parameters) 97 | -------------------------------------------------------------------------------- /translations/README.txt: -------------------------------------------------------------------------------- 1 | Your plugin's translations will reside here. The provided setup.py supports a 2 | couple of additional commands to make managing your translations easier: 3 | 4 | babel_extract 5 | Extracts any translateable messages (marked with Jinja's `_("...")` or 6 | JavaScript's `gettext("...")`) and creates the initial `messages.pot` file. 7 | babel_refresh 8 | Reruns extraction and updates the `messages.pot` file. 9 | babel_new --locale= 10 | Creates a new translation folder for locale ``. 11 | babel_compile 12 | Compiles the translations into `mo` files, ready to be used within 13 | OctoPrint. 14 | babel_pack --locale= [ --author= ] 15 | Packs the translation for locale `` up as an installable 16 | language pack that can be manually installed by your plugin's users. This is 17 | interesting for languages you can not guarantee to keep up to date yourself 18 | with each new release of your plugin and have to depend on contributors for. 19 | 20 | If you want to bundle translations with your plugin, create a new folder 21 | `octoprint_prusaslicerthumbnails/translations`. When that folder exists, 22 | an additional command becomes available: 23 | 24 | babel_bundle --locale= 25 | Moves the translation for locale `` to octoprint_prusaslicerthumbnails/translations, 26 | effectively bundling it with your plugin. This is interesting for languages 27 | you can guarantee to keep up to date yourself with each new release of your 28 | plugin. 29 | --------------------------------------------------------------------------------