Navigation

    Duet3D Logo

    Duet3D

    • Register
    • Login
    • Search
    • Categories
    • Tags
    • Documentation
    • Order

    Cura gcode Save to Disk with Thumbnails

    General Discussion
    2
    3
    113
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • tfjield
      tfjield last edited by

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

      resam 1 Reply Last reply Reply Quote 1
      • resam
        resam @tfjield last edited by

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

        resam 1 Reply Last reply Reply Quote 1
        • resam
          resam @resam last edited by

          @tfjield I've pushed a change to the next branch at https://github.com/Kriechi/Cura-DuetRRFPlugin/tree/next

          Would love to hear your feedback if this works for you!
          Please follow the Manual Installation + Install from Source instructions in the readme.

          1 Reply Last reply Reply Quote 2
          • First post
            Last post
          Unless otherwise noted, all forum content is licensed under CC-BY-SA