Duet3D Logo Duet3D
    • Tags
    • Documentation
    • Order
    • Register
    • Login

    Cura gcode Save to Disk with Thumbnails

    Scheduled Pinned Locked Moved
    General Discussion
    3
    4
    725
    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.
    • tfjieldundefined
      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!

      resamundefined 1 Reply Last reply Reply Quote 1
      • resamundefined
        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!

        resamundefined 1 Reply Last reply Reply Quote 1
        • resamundefined
          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.

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

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

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