@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!