5

Orientation:

I have created the following functions to allow the user to change the turtle to an image of the his/her choosing and then stamp it to the canvas at any point:

def TurtleShape():
    try:
        # Tkinter buttons related to turtle manipulation
        manipulateimage.config(state = NORMAL)
        flipButton.config(state = NORMAL)
        mirrorButton.config(state = NORMAL)
        originalButton.config(state = NORMAL)
        resetturtle.config(state = NORMAL)
        rotateButton.config(state = NORMAL)
        # Ask user for file name from tkinter file dialog, and return file name as `klob`
        global klob
        klob = filedialog.askopenfilename()
        global im
        # Open `klob` and return as `im`
        im = Image.open(klob)
        # Append `im` to pictures deque
        pictures.append(im)
        # Clear `edited` deque
        edited.clear()
        # Save `im` as an image, then register image as shape, and finally set image as turtle shape
        im.save(klob + '.gif', "GIF")
        register_shape(klob + '.gif')
        shape(klob + '.gif')
        update()
    except:
        # If user selects cancel in file dialog, then pass
        pass

def StampPic():
    stamp()
    draw_space() # Go forward 100 pixels with pen up after every stamp
    update()

The image can also be manipulated to the user's choosing by these other functions:

Resize function – This function works either as a first or secondary function. First meaning that it is called initially, and secondary meaning it edits an already edited image. So, if ONLY called first, this function will take the image appended to the pictures deque, resize that, and output the edited image as a .gif image, which will be the new shape of the turtle. However, if called two times or more in a row, because of an issue where resizing the same picture more than once will result in a distorted image, I had to create another deque jiop which saves the original item from the pictures deque, and whenever this function is called more than once in a row, that original image is resized every time, instead of the same image each time. But, if ONLY called as a secondary function, then the function will simply take the current image from the edited deque, resize that image, and then set that as the turtle's new shape:

def TurtleImageResize():
    if not hasattr(TurtleImageResize, "counter"):
        TurtleImageResize.counter = 0
    TurtleImageResize.counter += 1
    # width = original size of image
    width = im.size[0]
    # height = original height of image
    height = im.size[1]
    # Allow user to enter new width for image
    NewOne2 = numinput('Width of Image', 'Set the width of the image: ', minval = 1)
    # Allow user to enter new height for image
    NewOne = numinput('Height of Image', 'Set the height of your image: ', minval = 1)
    # Set width to user input if user input is NOT nothing. Otherwise, use `width` as picture width.
    Picwidth = NewOne2 if NewOne2 != None else width
    # Set height to user input if user input is NOT None. Otherwise, use `height` as picture height.
    Picheight = NewOne if NewOne != None else height
    try:
        # Secondary Step: Take ORIGINAL image appended to `jiop` (from `except:` code block succeeding `try:` code block) and resize THAT image each time this function is called twice in a row. Otherwise, if ONLY called as a secondary step, take previously edited image from `edited` deque, resize that, and append newly edited image to the `edited` deque.
        try:
            # `jiop` is a deque
            hye = jiop.pop()
            jiop.append(hye)
            print("Jiop")
        except:
            hye = edited.pop()
            jiop.append(hye)
            print("Edited")
        # Resize Image to Picwidth and Picheight
        editpic = hye.resize((int(Picwidth), int(Picheight)), Image.ANTIALIAS)
        edited.append(editpic) 
        print("Hooplah!")
    except:
        # Intial step: Take image appended to `pictures` deque from `TurtleShape` function, then edit that and append newly edited image to both `editpic` and `pictures`
        geer = pictures.pop()
        # Resize Image to Picwidth and Picheight
        editpic = geer.resize((int(Picwidth), int(Picheight)), Image.ANTIALIAS)
        jiop.append(geer)
        edited.append(editpic)
        pictures.append(editpic)
        print("Normal")
    # Save image as `.gif`
    editpic.save(klob + str(TurtleImageResize.counter) + '.gif', 'GIF')
    # Register image as a shape, and use it as shape of turtle
    register_shape(klob + str(TurtleImageResize.counter) + '.gif')
    shape(klob + str(TurtleImageResize.counter) + '.gif')
    update()

Flip, Rotate, and Mirror functions - These work rather simpler than the resize function above. If called initially, they each will take the image from the pictures deque, manipulate it, append that edited image to the edited deque, then change the turtle "shape" to that new image. However, if called second, they each will take the image from the edited deque, manipulate that, re-append the manipulated image back to the edited deque, then set that as the turtle's new "shape". These functions are shown below:

def flippic():
    if not hasattr(flippic, "counter"):
        flippic.counter = 0
    flippic.counter += 1
    try:
        # Secondary step: Take previously edited image from `edited` deque, manipulate that, and append newly edited image to the `edited` deque
        jiop.clear()
        ghy = edited.pop()
        # Flip image over horizontal line
        kpl = ImageOps.flip(ghy)
        edited.append(kpl)
        print("Jlop")
    except:
        # Initial step: Take image appended to `pictures` deque from `TurtleShape` function, then edit that and append newly edited image to both `editpic` and `pictures`
        neer = pictures.pop()
        # Flip image over horizontal line
        kpl = ImageOps.flip(neer)
        pictures.append(kpl)
        edited.append(kpl)
        print("Yup")
    # Save image as `.gif`
    kpl.save(klob + str(flippic.counter) + '.gif', "GIF")
    # Register image as a shape, and use it as shape of turtle
    register_shape(klob + str(flippic.counter) + '.gif')
    shape(klob + str(flippic.counter) + '.gif')
    update()

def mirror():
    if not hasattr(mirror, "counter"):
        mirror.counter = 0
    mirror.counter += 1
    try:
        jiop.clear()
        jui = edited.pop()
        # Flip image over vertical line
        fgrt = ImageOps.mirror(jui)
        edited.append(fgrt)
    except:
        bbc = pictures.pop()
        # Flip image over vertical line
        fgrt = ImageOps.mirror(bbc)
        pictures.append(fgrt)
        edited.append(fgrt)
    fgrt.save(klob + str(mirror.counter) + ".gif")
    register_shape(klob + str(mirror.counter) + ".gif")
    shape(klob + str(mirror.counter) + ".gif")
    update()

def rotatePic():
    if not hasattr(rotatePic, "counter"):
        rotatePic.counter = 0
    rotatePic.counter += 1
    try:
        jiop.clear()
        lmcb = edited.pop()
        # Rotate image 90º right
        fetch = lmcb.rotate(-90, expand = True)
        edited.append(fetch)
    except:
        bolt = pictures.pop()
        # Rotate image 90º right
        fetch = bolt.rotate(-90, expand = True)
        pictures.append(fetch)
        edited.append(fetch)
    fetch.save(klob + str(rotatePic.counter) + ".gif")
    register_shape(klob + str(rotatePic.counter) + ".gif")
    shape(klob + str(rotatePic.counter) + ".gif")
    update()

This way ALL the editing functions work together on essentially the same fundamental image.

The Issue:

Now, consider that the user wants to take the turtle image and then resize it to the size, for instance, 800x400, and stamp it to a specific spot on the canvas. After that, the user decides to move the turtle image to another spot on the canvas, flip the image, and then stamp the image there. There should now be two images right? One stamped, and the other flipped? However, with my program, for some reason, that is not the case. Instead, the stamped image disappears the moment the user flips the turtle image, even though there is no clear() function to be found anywhere (to show you what I mean, refer to the edit below). Apparently this issue ONLY occurs after the TurtleImageResize function is called.

What is wrong in my TurtleImageResize function that is leading to this issue? I had completely revamped the turtle shape's image management process to what it is right now in hopes that it will fix this issue that I was also experiencing with my previous setup, but apparently, that is STILL not the case. Therefore, any help with this issue is greatly appreciated!

EDIT: Below is a minimal, complete, and verifiable way to reproduce the issue I am having (MUST have PIL (or Pillow) and GhostScript installed in order for this to work):

import os,shutil,subprocess, sys
her = sys.platform
if her == "win32":
    print("Windows is your Operating System")
    win_gs = ["gs","gswin32c","gswin64c"]
    if all( shutil.which(gs_version) is None for gs_version in win_gs ):
        paths = ["C:\\Program Files\\gs\\gs9.18\\bin","C:\\Program Files (x86)\\gs\\gs9.18\\bin"]
        for path in (x for x in paths if os.path.exists(x)):
            os.environ["PATH"] += ";" + path
            break
        if any( shutil.which(gs_version) for gs_version in win_gs ):
            print("GhostScript 9.18 for Windows found and utilized")
        else:
            print("You do not have GhostScript 9.18 installed for Windows. Please install it.")
            sys.exit(0)
    else:
        print("GhostScript 9.18 for Windows found and utilized")
elif her == 'darwin':
    print("Macintosh is your Operating System")
    if shutil.which("gs") is None:
        os.environ["PATH"] += ":/usr/local/bin"
        if shutil.which("gs") is None:
            print("You do not have GhostScript installed for Macintosh. Please install it.")
            sys.exit(0)
        else:
            print("GhostScript for Macintosh found and utilized")

from turtle import *
from tkinter import *
try:
    import tkinter.filedialog as filedialog
except ImportError:
    pass
import collections
from PIL import Image, ImageEnhance, ImageOps


jiop = collections.deque()
pictures = collections.deque()
edited = collections.deque()
picwidth = collections.deque()
picheight = collections.deque()

def draw_space():
    # Draw a space 200 pixels wide.
    penup()
    forward(200)
    pendown()

def TurtleShape():
   try:
       manipulateimage.config(state = NORMAL)
       flipButton.config(state = NORMAL)
       mirrorButton.config(state = NORMAL)
       rotateButton.config(state = NORMAL)
       global klob
       klob = filedialog.askopenfilename()
       global im
       im = Image.open(klob)
       pictures.append(im)
       edited.clear()
       im.save(klob + '.gif', "GIF")
       register_shape(klob + '.gif')
       shape(klob + '.gif')
       update()
   except AttributeError:
       pass

def TurtleImageResize():
    if not hasattr(TurtleImageResize, "counter"):
        TurtleImageResize.counter = 0
    TurtleImageResize.counter += 1
    width = im.size[0]
    height = im.size[1]
    NewOne2 = numinput('Width of Image', 'Set the width of the image: ', minval = 1)
    NewOne = numinput('Height of Image', 'Set the height of your image: ', minval = 1)
    Picwidth = NewOne2 if NewOne2 != None else width
    picwidth.append(Picwidth)
    Picheight = NewOne if NewOne != None else height
    picheight.append(Picheight)
    try:
        try:
            hye = jiop.pop()
            jiop.append(hye)
        except:
            hye = edited.pop()
            jiop.append(hye)
        editpic = hye.resize((int(Picwidth), int(Picheight)), Image.ANTIALIAS)
        edited.append(editpic)
        pictures.append(editpic)
    except:
        geer = pictures.pop()
        editpic = geer.resize((int(Picwidth), int(Picheight)), Image.ANTIALIAS)
        jiop.append(geer)
        edited.append(editpic)
        pictures.append(editpic)
    editpic.save(klob + str(TurtleImageResize.counter) + '.gif', 'GIF')
    register_shape(klob + str(TurtleImageResize.counter) + '.gif')
    shape(klob + str(TurtleImageResize.counter) + '.gif')
    update()

def flippic():
    if not hasattr(flippic, "counter"):
        flippic.counter = 0
    flippic.counter += 1
    try:
        jiop.clear()
        ghy = edited.pop()
        kpl = ImageOps.flip(ghy)
        edited.append(kpl)
        pictures.append(kpl)
        print("Jlop")
    except:
        neer = pictures.pop()
        kpl = ImageOps.flip(neer)
        pictures.append(kpl)
        edited.append(kpl)
        print("Yup")
    kpl.save(klob + str(flippic.counter) + '.gif', "GIF")
    register_shape(klob + str(flippic.counter) + '.gif')
    shape(klob + str(flippic.counter) + '.gif')
    update()

def mirror():
    if not hasattr(mirror, "counter"):
        mirror.counter = 0
    mirror.counter += 1
    try:
        jiop.clear()
        jui = edited.pop()
        fgrt = ImageOps.mirror(jui)
        edited.append(fgrt)
        pictures.append(fgrt)
    except:
        bbc = pictures.pop()
        fgrt = ImageOps.mirror(bbc)
        pictures.append(fgrt)
        edited.append(fgrt)
    fgrt.save(klob + str(mirror.counter) + ".gif")
    register_shape(klob + str(mirror.counter) + ".gif")
    shape(klob + str(mirror.counter) + ".gif")
    update()

def rotatePic():
    if not hasattr(rotatePic, "counter"):
        rotatePic.counter = 0
    rotatePic.counter += 1
    try:
        jiop.clear()
        lmcb = edited.pop()
        fetch = lmcb.rotate(-90, expand = True)
        edited.append(fetch)
        pictures.append(fetch)
    except:
        bolt = pictures.pop()
        fetch = bolt.rotate(-90, expand = True)
        pictures.append(fetch)
        edited.append(fetch)
    fetch.save(klob + str(rotatePic.counter) + ".gif")
    register_shape(klob + str(rotatePic.counter) + ".gif")
    shape(klob + str(rotatePic.counter) + ".gif")
    update()

def StampPic():
   stamp()
   draw_space()
   update()

def move_turtle():
    # Pick up the turtle and move it to its starting location.
    penup()
    goto(-200, 100)
    pendown()

def settings():
    #  Tkinter buttons
    turtlepic = Button(text = "Set Turtle Image", command = TurtleShape)
    turtlepic.pack(side = 'left')

    stampimage = Button(text = "Stamp", command = StampPic)
    stampimage.pack(side = 'left')

    global manipulateimage
    manipulateimage = Button(text = "Resize Turtle Image", command = TurtleImageResize, state = DISABLED)
    manipulateimage.pack(side = 'left')

    global flipButton
    flipButton = Button(text = "Flip image", command = flippic, state = DISABLED)
    flipButton.pack(side = 'left')

    global mirrorButton
    mirrorButton = Button(text = "Mirror Image", command = mirror, state = DISABLED)
    mirrorButton.pack(side = 'left')

    global rotateButton
    rotateButton = Button(text = "Rotate Image", command = rotatePic, state = DISABLED)
    rotateButton.pack(side = 'left')

def skip(x, y):
    penup()
    goto(x, y)
    pendown()
    update()

move_turtle()
settings()
speed(0)
tracer(0, 0)
onscreenclick(skip)

if sys.platform == 'win32':
   input()
else:
   pass

When/if you have both GhostScript and PIL (or Pillow) installed on your system, to reproduce my issue, please do the following (All steps required except step # 4):

  1. Click the Set Turtle Image button at bottom of window, select any image you want the turtle to be, then press Open. The turtle gets set to that image.

  2. Resize the Image to 800x400 (or any other size you want) by pressing the Resize turtle Image button at the bottom of the screen. Two dialogs will pop up in succession. Enter the width of 800 (or your own width) in the first dialog, and then enter the height of 400 (or your own height) in the second dialog, and after you finish, the image will change size according to the dimensions provided (or set image back to the original dimension(s) depending on whether or not you pressed cancel).

  3. Select the Stamp button at the bottom of the window. The image is stamped onto the canvas, and the turtle moves forward 400 pixels "behind" the stamped image.

  4. OPTIONAL: Click anywhere on the canvas to take the turtle to that spot.

  5. Flip/mirror/rotate the image.

As you can see, after doing all this, just as you flip/mirror/rotate the image, the stamped image just disappears. What is wrong with my TurtleImageResize function that is causing this to occur?

EDIT # 2: Just in case this information is useful, I am running Python 3.5.1 on a Macintosh with OS version 10.11.2 (El Capitan).

R. Kap
  • 599
  • 1
  • 10
  • 33
  • @tobias_k Did you run the code in its entirety? Also, if you are using Windows, did you run it through the Command Prompt? – R. Kap Jan 28 '16 at 10:08
  • @tobias_k Do you have `GhostScript` installed? If not, you will need that to reproduce my issue. Go to http://www.ghostscript.com/download/gsdnld.html to download the correct version for your copy of Windows. Then, after you do that, please run the updated code from my post. – R. Kap Jan 28 '16 at 10:46
  • I'm running Linux and ghostscript is installed. Can you verify that your MCVE runs on your machine? Maybe you missed a line when copying it into the post? Update: You should never do `except: pass`; turns out the `filedialog` module was not imported. – tobias_k Jan 28 '16 at 10:51
  • @tobias_k Try commenting out (or removing) the `try:` and `except:` code from the `TurtleShape` function, and dedent the code block which was previously inside the (now commented out/removed) `try:` code block. What error do you get when you press the `Set Turtle Image` button? – R. Kap Jan 28 '16 at 10:55
  • Had to add `import tkinter.filedialog as filedialog`, now it runs, and I kindof can reproduce the problem. – tobias_k Jan 28 '16 at 10:57
  • @tobias_k So what do you think is causing the issue? – R. Kap Jan 28 '16 at 10:59
  • No idea yet, but I noticed that the problem only occurs with flip, and at the same time the flip, rotate, and mirror functions are _very_ similar. I suggest you try to replace those with just one (parameterized) function. – tobias_k Jan 28 '16 at 11:19
  • @tobias_k Actually, it occurs only after the `TurtleImageResize` function is called. – R. Kap Jan 29 '16 at 09:53

1 Answers1

4

The problem seems to be that by having individual counters for the different functions, you overwrite the files created by previous operations. Let's say you have a picture named test.gif and apply the flip transformation. The result will be saved as test.gif1.gif. If you now apply a rotate transformation, the rotated picture is also saved as test.gif1.gif, overwriting the existing file, and the previous picture disappears.

So one way to fix this bug is to use a single counter for all the pictures, instead of one per function, e.g. using itertools.count or just an int. This will also make your code somewhat shorter.


However, there are a few more issues that I'd like to point out:

  • you have quite a high amount of code duplication, particularly in your different transformation functions; you can refactor those to take the actual transformation as a function parameter
  • don't do except: as much, much less except: pass; this way you never know that happened in case something goes wrong
  • also, IMHO exceptions should only be used for exceptional behaviour, while you use them for normal behaviour, such as when the lists are still empty
  • I don't really see what all those different queues are for; the code works just as well if you put all the pictures into a single queue, or no queues at all (just saving them on disk); but maybe you need those for something that was not part of your example code

Here's my version of the code:

import turtle
import tkinter
import tkinter.filedialog as filedialog
import itertools
from PIL import Image, ImageEnhance, ImageOps

count = itertools.count()
img = None

def turtleShape():
   global img
   klob = filedialog.askopenfilename()
   img = Image.open(klob)
   saveAndUpdate(img)

def turtleImageResize():
    def resize(img):
        picwidth = turtle.numinput('Width of Image', 'Set the width of the image: ', minval=1) or img.size[0]
        picheight = turtle.numinput('Height of Image', 'Set the height of your image: ', minval=1) or img.size[1]
        return img.resize((int(picwidth), int(picheight)), Image.ANTIALIAS)
    manipulate(resize)

def manipulate(function):
    global img
    if img:
        img = function(img)
        saveAndUpdate(img)
    else:
        print("No picture selected")

def flippic():
    manipulate(ImageOps.flip)

def mirror():
    manipulate(ImageOps.mirror)

def rotatePic():
    manipulate(lambda img: img.rotate(-90, expand=True))

def saveAndUpdate(img):
    name = "pic_" + str(next(count)) + ".gif"
    img.save(name, 'GIF')
    turtle.register_shape(name)
    turtle.shape(name)
    turtle.update()

def stampPic():
    turtle.stamp()
    turtle.penup()
    turtle.forward(200)
    turtle.pendown()

def settings():
    tkinter.Button(text="Set Turtle Image", command=turtleShape).pack(side='left')
    tkinter.Button(text="Stamp", command=stampPic).pack(side = 'left')
    tkinter.Button(text="Resize Turtle Image", command=turtleImageResize).pack(side='left')
    tkinter.Button(text="Flip image", command=flippic).pack(side='left')
    tkinter.Button(text="Mirror Image", command=mirror).pack(side='left')
    tkinter.Button(text="Rotate Image", command=rotatePic).pack(side='left')

def skip(x, y):
    turtle.penup()
    turtle.goto(x, y)
    turtle.pendown()
    turtle.update()

skip(-200, 100)
settings()
turtle.speed(0)
turtle.tracer(0, 0)
turtle.onscreenclick(skip)
turtle.mainloop()
tobias_k
  • 81,265
  • 12
  • 120
  • 179
  • I notice that in your code you do a lot of forward declaring. For instance, in your version of the `turtleshape` function, you call `saveAndUpdate` which is defined later on in the code. How is that working without first calling it when the program runs? – R. Kap Jan 29 '16 at 18:07
  • Also, yeah, I do need those `deques` for something not in the MCVE. Otherwise, I would not have set it up the way I did. But thanks for all the other tips though! It really helped! :) – R. Kap Jan 29 '16 at 20:24
  • @R.Kap It is working because the called function is only needed when the calling function is being evaluated, not when it is first parsed. If the function is not declared then, you'd get an AttributeError, but by the time the function is called it is declared, so everything is fine. And yes, I did remove some parts from your code (e.g. the buttons being deactivated at first), but the only part you really need to change is the counter. And I suggest you also try to use that generic `manipulate` function in some form. – tobias_k Jan 29 '16 at 22:20
  • Your proposed solution world perfectly! The issue occurs no more. Thank you! :) – R. Kap Jan 30 '16 at 21:36