1

When I am trying to save an image from a tkinter canvas onto an external file, it doesn't keep the 16x16 resolution I would like which makes it blurry.

I was expecting no blur and it to keep the 16x16 resolution. I tried two methods of saving the file

The first method I tried is this:

self.sprite = tk.Canvas(self.root, width=16, height=16)

def get_clicked_pos(self, event):
        y, x = event.y, event.x
            
        gap = 288//16
        row = y//gap # row and column are the wrong way but i found it easier to keep it that way instead of changing it all
        col = x//gap
        self.spriteGrid[row][col].tile = self.colour
        self.spriteDraw(gap) # draws on the large canvas

        self.sprite.create_rectangle(col, row, col+1, row+1, fill=self.colour, outline="")

def save_as_png1(self):
        ps = self.sprite.postscript(width=16, height=16)

        image = PIL.Image.open(io.BytesIO(ps.encode('utf-8')))

        image.save('sprite.png')

In this method I made two canvases. One that is large and visible but a 16x16 grid so you can draw and one that is the correct size and not visible. When you draw on the large canvas it mimics it on the small canvas and when you save it turns the correct size canvas into a postscript and makes it a sprite. However, it doesn't work and creates this:

enter image description here method 1

The second method I tried is:

self.image1 = PIL.Image.new("RGB", (16, 16), "white")
self.draw = PIL.ImageDraw.Draw(self.image1)

def get_clicked_pos(self, event):
        y, x = event.y, event.x
            
        gap = 288//16
        row = y//gap # row and column are the wrong way but i found it easier to keep it that way instead of changing it all
        col = x//gap
        self.spriteGrid[row][col].tile = self.colour
        self.spriteDraw(gap)

        self.draw.rectangle([(col, row), (col+1, row+1)], fill=self.colour)
        self.sprite.create_rectangle(col, row, col+1, row+1, fill=self.colour, outline="")

def save_as_png2(self):
        self.image1.save("alternate.png")

In this method, it copies what you draw onto a PIL image which then gets saved when you call save_as_image2. This provides similar results but in a 16x16 file

enter image description here method 2

The whole code is:

import tkinter as tk
from tkinter.colorchooser import askcolor
import PIL.Image
import PIL.ImageDraw
import io

class DrawWindow():
    def __init__(self):
        self.root = tk.Tk()
        self.colour = "black"

        self.menubar = tk.Menu(self.root)

        self.optionsmenu = tk.Menu(self.menubar, tearoff=0)
        self.optionsmenu.add_command(label="Save v1", command=self.save_as_png1) 
        self.optionsmenu.add_command(label="Save v2", command=self.save_as_png2) 
        self.menubar.add_cascade(menu=self.optionsmenu, label="Options")

        self.root.config(menu=self.menubar)

        self.sprFrame = tk.Frame(self.root)
        self.sprFrame.pack()
        self.spriteCanvas = tk.Canvas(self.sprFrame, height=288, width=288, bg="white")
        self.spriteCanvas.pack(pady=5, padx=5)
        self.pickColour = tk.Button(self.sprFrame, text="Pick colour", command=self.changeColour)
        self.pickColour.pack(pady=5)
        self.spriteGrid = []
        for i in range(16):
            self.spriteGrid.append([])
            for j in range(16):
                spot = Spot(i, j, 1, 16)
                self.spriteGrid[i].append(spot)

        self.sprite = tk.Canvas(self.root, width=16, height=16)

        self.image1 = PIL.Image.new("RGB", (16, 16), "white")
        self.draw = PIL.ImageDraw.Draw(self.image1)

        self.spriteCanvas.bind("<Button-1>", self.get_clicked_pos)

        self.root.mainloop()

    def get_clicked_pos(self, event):
        y, x = event.y, event.x
            
        gap = 288//16
        row = y//gap # row and column are the wrong way but i found it easier to keep it that way instead of changing it all
        col = x//gap
        self.spriteGrid[row][col].tile = self.colour
        self.spriteDraw(gap)

        self.draw.rectangle([(col, row), (col+1, row+1)], fill=self.colour)
        self.sprite.create_rectangle(col, row, col+1, row+1, fill=self.colour, outline="")

    def changeColour(self):
        self.colour = askcolor(title="Sprite Colour")[1]

    def save_as_png1(self):
        ps = self.sprite.postscript(width=16, height=16)

        image = PIL.Image.open(io.BytesIO(ps.encode('utf-8')))

        image.save('sprite.png')

    def save_as_png2(self):
        self.image1.save("alternate.png")

    def spriteDraw(self, gap):
        for i in self.spriteGrid:
            for j in i:
                if not(j.tile==None):
                    self.spriteCanvas.create_rectangle(j.y*gap, j.x*gap, j.y*gap+gap, j.x*gap+gap, fill=j.tile)

class Spot:
    def __init__(self, row, col, width, total_rows):
        self.row = row
        self.col = col
        self.x = row * width
        self.y = col * width
        self.tile = None
        self.width = width
        self.total_rows = total_rows

DrawWindow()
37zak
  • 11
  • 2

1 Answers1

0

I figured it out, method two works but I was using self.draw.rectangle() when I should've been using self.image1.putpixel(). So for anyone wanting the finished code it is:

import tkinter as tk
from tkinter.colorchooser import askcolor
import PIL.Image
import PIL.ImageDraw

class DrawWindow():
    def __init__(self):
        self.root = tk.Tk()
        self.colour = ((0, 0, 0), "black")

        self.menubar = tk.Menu(self.root)

        self.optionsmenu = tk.Menu(self.menubar, tearoff=0)
        self.optionsmenu.add_command(label="Save image", command=self.save_as_png) 
        self.menubar.add_cascade(menu=self.optionsmenu, label="Options")

        self.root.config(menu=self.menubar)

        self.sprFrame = tk.Frame(self.root)
        self.sprFrame.pack()
        self.spriteCanvas = tk.Canvas(self.sprFrame, height=288, width=288, bg="white")
        self.spriteCanvas.pack(pady=5, padx=5)
        self.pickColour = tk.Button(self.sprFrame, text="Pick colour", command=self.changeColour)
        self.pickColour.pack(pady=5)
        self.spriteGrid = []
        for i in range(16):
            self.spriteGrid.append([])
            for j in range(16):
                spot = Spot(i, j, 1, 16)
                self.spriteGrid[i].append(spot)

        self.image1 = PIL.Image.new("RGB", (16, 16), "white")

        self.spriteCanvas.bind("<Button-1>", self.get_clicked_pos)

        self.root.mainloop()

    def get_clicked_pos(self, event):
        y, x = event.y, event.x
            
        gap = 288//16
        row = y//gap # row and column are the wrong way but i found it easier to keep it that way instead of changing it all
        col = x//gap
        self.spriteGrid[row][col].tile = self.colour[1]
        self.spriteDraw(gap)

        self.image1.putpixel((col, row), self.colour[0])

    def changeColour(self):
        self.colour = askcolor(title="Sprite Colour")
        print(self.colour)

    def save_as_png(self):
        self.image1.save("sprite.png")

    def spriteDraw(self, gap):
        for i in self.spriteGrid:
            for j in i:
                if not(j.tile==None):
                    self.spriteCanvas.create_rectangle(j.y*gap, j.x*gap, j.y*gap+gap, j.x*gap+gap, fill=j.tile)

class Spot:
    def __init__(self, row, col, width, total_rows):
        self.row = row
        self.col = col
        self.x = row * width
        self.y = col * width
        self.tile = None
        self.width = width
        self.total_rows = total_rows

DrawWindow()
37zak
  • 11
  • 2