Cura gcode Save to Disk with Thumbnails
-
@resam
Per the conversation in the other thread, I was looking for a an option in Cura to save a gcode file to disk with QOI thumbnails. I took your thumbnail code from github and incorporated it into this post-processing script:import os import base64 import traceback from io import StringIO from PyQt6 import QtCore from PyQt6.QtCore import QCoreApplication, QBuffer from PyQt6.QtGui import QImage from UM.Application import Application from UM.Logger import Logger from UM.Math.Matrix import Matrix from cura.Snapshot import Snapshot from cura.PreviewPass import PreviewPass from ..Script import Script from qoi import QOIEncoder class CreateQOIThumbnail(Script): def __init__(self): super().__init__() def render_scene(self): scene = Application.getInstance().getController().getScene() active_camera = scene.getActiveCamera() render_width, render_height = active_camera.getWindowSize() render_width = int(render_width) render_height = int(render_height) Logger.log("d", f"Found active camera with {render_width=} {render_height=}") QCoreApplication.processEvents() preview_pass = PreviewPass(render_width, render_height) fovy = 30 satisfied = False zooms = 0 while not satisfied and zooms < 5: preview_pass.render() pixel_output = preview_pass.getOutput().convertToFormat(QImage.Format.Format_ARGB32) # pixel_output.save(os.path.expanduser(f"~/Downloads/foo-a-zoom-{zooms}.png"), "PNG") min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(pixel_output) size = max((max_x - min_x) / render_width, (max_y - min_y) / render_height) if size > 0.5 or satisfied: satisfied = True else: # make it big and allow for some empty space around zooms += 1 fovy *= 0.75 projection_matrix = Matrix() projection_matrix.setPerspective(fovy, render_width / render_height, 1, 500) active_camera.setProjectionMatrix(projection_matrix) Logger.log("d", f"Rendered thumbnail: {zooms=}, {size=}, {min_x=}, {max_x=}, {min_y=}, {max_y=}, {fovy=}") # crop to content pixel_output = pixel_output.copy(min_x, min_y, max_x - min_x, max_y - min_y) Logger.log("d", f"Cropped thumbnail to {min_x}, {min_y}, {max_x - min_x}, {max_y - min_y}.") # pixel_output.save(os.path.expanduser("~/Downloads/foo-b-cropped.png"), "PNG") Logger.log("d", "Successfully rendered scene.") return pixel_output def render_thumbnail(self, pixel_output, width, height): # scale to desired width and height pixel_output = pixel_output.scaled( width, height, aspectRatioMode=QtCore.Qt.AspectRatioMode.KeepAspectRatio, transformMode=QtCore.Qt.TransformationMode.SmoothTransformation ) Logger.log("d", f"Scaled thumbnail to {width=}, {height=}.") # pixel_output.save(os.path.expanduser("~/Downloads/foo-c-scaled.png"), "PNG") # center image within desired width and height if one dimension is too small if pixel_output.width() < width: d = int((width - pixel_output.width()) / 2. + 0.5) pixel_output = pixel_output.copy(-d, 0, width, pixel_output.height()) Logger.log("d", f"Centered thumbnail horizontally {d=}.") if pixel_output.height() < height: d = int((height - pixel_output.height()) / 2. + 0.5) pixel_output = pixel_output.copy(0, -d, pixel_output.width(), height) Logger.log("d", f"Centered thumbnail vertically {d=}.") # pixel_output.save(os.path.expanduser("~/Downloads/foo-d-aspect-fixed.png"), "PNG") Logger.log("d", f"Successfully rendered {width}x{height} thumbnail.") return pixel_output def encode_as_qoi(self, thumbnail): # https://qoiformat.org/qoi-specification.pdf pixels = [thumbnail.pixel(x, y) for y in range(thumbnail.height()) for x in range(thumbnail.width())] pixels = [(unsigned_p ^ (1 << 31)) - (1 << 31) for unsigned_p in pixels] encoder = QOIEncoder() r = encoder.encode( width=thumbnail.width(), height=thumbnail.height(), pixels=pixels, alpha=thumbnail.hasAlphaChannel(), linear_colorspace=False ) if not r: raise ValueError("image size unsupported") Logger.log("d", f"Successfully encoded {thumbnail.width()}x{thumbnail.height()} thumbnail in QOI format.") size = encoder.get_encoded_size() return encoder.get_encoded()[:size] def encode_as_png(self, thumbnail): buffer = QBuffer() buffer.open(QBuffer.ReadWrite) thumbnail.save(buffer, "PNG") buffer.close() return buffer.data() def generate_thumbnail(self): thumbnail_stream = StringIO() Logger.log("d", "Rendering thumbnail image...") try: scene = self.render_scene() # PanelDue: 480×272 (4.3" displays) or 800×480 pixels (5" and 7" displays) # ref https://forum.duet3d.com/post/270550 and https://forum.duet3d.com/post/270553 thumbnail_sizes = [ (48, 48), (128, 128), (160, 160), (256, 256), ] for width, height in thumbnail_sizes: thumbnail = self.render_thumbnail(scene, width, height) qoi_data = self.encode_as_qoi(thumbnail) b64_data = base64.b64encode(qoi_data).decode('ascii') b64_encoded_size = len(b64_data) thumbnail_stream.write(f"; thumbnail_QOI begin {width}x{height} {b64_encoded_size}\n") max_row_length = 78 for i in range(0, b64_encoded_size, max_row_length): s = b64_data[i:i+max_row_length] thumbnail_stream.write(f"; {s}\n") thumbnail_stream.write(f"; thumbnail_QOI end\n") Logger.log("d", "Successfully encoded thumbnails as base64 into gcode comments.") return thumbnail_stream except Exception as e: Logger.log("e", "failed to create snapshot: " + str(e)) Logger.log("e", traceback.format_stack()) # continue without the QOI snapshot return StringIO() def getSettingDataString(self): return """{ "name": "Create QOI Thumbnail", "key": "CreateQOIThumbnail", "metadata": {}, "version": 2, "settings": { "enabled": { "label": "Enabled", "description": "Enable to generate thumbnails.", "type": "bool", "default_value": true } } }""" def execute(self, data): if not self.getSettingValueByKey("enabled"): return data; snapshot = self.generate_thumbnail() if snapshot: for layer in data: layer_index = data.index(layer) lines = data[layer_index].split("\n") for line in lines: if line.startswith(";Generated with Cura"): Logger.log("d", "Found appropriate place in gcode file.") line_index = lines.index(line) insert_index = line_index + 1 lines[insert_index:insert_index] = snapshot.getvalue()[:-1].split("\n") break final_lines = "\n".join(lines) data[layer_index] = final_lines return data
This is just your code, slightly edited to work as a script, and it works well for me. I should have done this up on github, but I've never created a repository there and it would take me more time to figure it out than I wanted to spend today! lol. Besides, I'm just a hack, so if you wanted to take this (or do it better) please do!
Thanks for doing the heavy lifting!
-
@tfjield thanks! I'll take a look on how to best integrate it into the existing plugin!
If anyone wants to give it a try and send me a pull request - I'd be happy to review and merge it!
-
@tfjield I've pushed a change to the
next
branch at https://github.com/Kriechi/Cura-DuetRRFPlugin/tree/nextWould love to hear your feedback if this works for you!
Please follow the Manual Installation + Install from Source instructions in the readme. -
@resam Sorry for the late reply! I am no longer using RRF, so I can't test your update. Thank you for keeping me in mind, though!