python: tkinter switch svg content

Viewed 396

I have a variety of SVG images, their filenames are in uniformed number format, such as 1.svg, 2.svg. I want to rename each file based on image content.

I coded a python tkinter app for me to do this, which it has a previous and next button and a text area, which allows me to go back and go previous and edit the filename.

Currently I succeed in displaying svg content by writing svg content into a temporary png file, then display the png file. However, I had trouble switching svg content.

Please have a look of following code:

# renamer.py
'''sh
pip install svglib
pip install Pillow
pip install tkinter
pip install reportlib
pip install lxml
'''


import os
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPDF, renderPM
from tkinter import *
from PIL import Image, ImageTk


# rename file by abs path
def rename(abs_path, new_name):
    os.rename(abs_path, new_name)


tk = Tk()

# ---------------------------------------- fill your path here
# SVG directory absolute path
directory = "";

svg_paths = []

# iterate .svg file and append to collector
for filename in os.listdir(directory):
    svg_paths.append(os.path.join(directory, filename));


# current manipulating index
at = 1


# feedback 
# how many finished
# how many to go
def percent():
    return f"{at} / {len(svg_paths)}"


vl = StringVar(tk, value=percent())


def go_previous():
    global at
    at -= 1
    if at == 0:
        # no more previous
        at += 1
    else:
        vl.set(percent())


def go_next():
    global at
    at += 1
    if at == len(svg_paths):
        # no more next
        at -= 1
    else:
        vl.set(percent())


label = Label(tk, textvariable=vl, font="Helvetica 30")
label.pack()



# tkinter doesnot support svg, therefore create a temp.png file 
# in same level as SVG folder as converted png file to display

canvas = Canvas(tk, width=500, height=500)
canvas.pack()

last_img = None



'''
update png is achieved by Tkinter.Canvas widget
canvas.delete() => delete last image content
canvas.create_image() => set up new image content

the file name is always temp.png however the content is different
since new svg will be overwritten into the same file name
'''

'''
if you call following function at the end, it doesnot work,
which the png will not be displayed on tkinter
but if we exec these code in global scope instead of function 
scope, it turns out working! I must use the function to update 
the image content when either previous or next button is clicked
'''

def refresh_image():
    global last_img, canvas

    if last_img is not None:
        canvas.delete(last_img)

    drawing = svg2rlg(svg_paths[at-1])
    renderPM.drawToFile(drawing, "temp.png", fmt="PNG")

    img = Image.open('temp.png')
    img = img.resize((400, 400), Image.ANTIALIAS)
    pimg = ImageTk.PhotoImage(img)

    last_img = canvas.create_image(0, 0, anchor='nw',image=pimg)


# display file name in input box
va = StringVar(tk, value='')
entry = Entry(tk, textvariable=va, font="Helvetica 30")
entry.pack(ipady=3)


# update file name
def refresh_entry():
    va.set(svg_paths[at-1].split('/')[-1])


# rename file
def assign_name(new_name):
    nm = directory + '/' + new_name
    rename(svg_paths[at-1], nm)
    # update recorded paths
    svg_paths[at] = nm


previous = Button(tk, text="previous", command=go_previous)
_next = Button(tk, text="next", command=go_next)

previous.pack()
_next.pack()


refresh_image()

'''
As I mentioned in refresh_image(),
if you hash the function above and unhash the code below,
it works.

I want to update image content according to svg_paths at `at` index
'''

# drawing = svg2rlg(svg_paths[at-1])
# renderPM.drawToFile(drawing, "temp.png", fmt="PNG")

# img = Image.open('temp.png')
# img = img.resize((400, 400), Image.ANTIALIAS)
# pimg = ImageTk.PhotoImage(img)

# last_img = canvas.create_image(0, 0, anchor='nw',image=pimg)

refresh_entry()

tk.mainloop()

What I want to do, is whenever I call refresh_image() function, tkinter canvas change the svg content based on at index, which rewrite current svg file into the temp.png file and display the png.

However, I find it wired that, it works if I write the inner part of refresh_image() function to global scope, but if it's in functional scope, it stops working!

the code example I provided above does not work, which it calls refresh_image() function. The following is the working version, instead of calling the function, I executed the code block in refresh_image() once at the end:

'''sh
pip install svglib
pip install Pillow
pip install tkinter
pip install reportlib
pip install lxml
'''


import os
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPDF, renderPM
from tkinter import *
from PIL import Image, ImageTk


# rename file by abs path
def rename(abs_path, new_name):
    os.rename(abs_path, new_name)


tk = Tk()

# ---------------------------------------- fill your path here
# SVG directory absolute path
directory = "";

svg_paths = []

# iterate .svg file and append to collector
for filename in os.listdir(directory):
    svg_paths.append(os.path.join(directory, filename));


# current manipulating index
at = 1


# feedback 
# how many finished
# how many to go
def percent():
    return f"{at} / {len(svg_paths)}"


vl = StringVar(tk, value=percent())


def go_previous():
    global at
    at -= 1
    if at == 0:
        # no more previous
        at += 1
    else:
        vl.set(percent())


def go_next():
    global at
    at += 1
    if at == len(svg_paths):
        # no more next
        at -= 1
    else:
        vl.set(percent())


label = Label(tk, textvariable=vl, font="Helvetica 30")
label.pack()



# tkinter doesnot support svg, therefore create a temp.png file 
# in same level as SVG folder as converted png file to display

canvas = Canvas(tk, width=500, height=500)
canvas.pack()

last_img = None



'''
update png is achieved by Tkinter.Canvas widget
canvas.delete() => delete last image content
canvas.create_image() => set up new image content

the file name is always temp.png however the content is different
since new svg will be overwritten into the same file name
'''

'''
if you call following function at the end, it doesnot work,
which the png will not be displayed on tkinter
but if we exec these code in global scope instead of function 
scope, it turns out working! I must use the function to update 
the image content when either previous or next button is clicked
'''

# def refresh_image():
#   global last_img, canvas

#   if last_img is not None:
#       canvas.delete(last_img)

#   drawing = svg2rlg(svg_paths[at-1])
#   renderPM.drawToFile(drawing, "temp.png", fmt="PNG")

#   img = Image.open('temp.png')
#   img = img.resize((400, 400), Image.ANTIALIAS)
#   pimg = ImageTk.PhotoImage(img)

#   last_img = canvas.create_image(0, 0, anchor='nw',image=pimg)


# display file name in input box
va = StringVar(tk, value='')
entry = Entry(tk, textvariable=va, font="Helvetica 30")
entry.pack(ipady=3)


# update file name
def refresh_entry():
    va.set(svg_paths[at-1].split('/')[-1])


# rename file
def assign_name(new_name):
    nm = directory + '/' + new_name
    rename(svg_paths[at-1], nm)
    # update recorded paths
    svg_paths[at] = nm


previous = Button(tk, text="previous", command=go_previous)
_next = Button(tk, text="next", command=go_next)

previous.pack()
_next.pack()


# refresh_image()

'''
As I mentioned in refresh_image(),
if you hash the function above and unhash the code below,
it works.

I want to update image content according to svg_paths at `at` index
'''

drawing = svg2rlg(svg_paths[at-1])
renderPM.drawToFile(drawing, "temp.png", fmt="PNG")

img = Image.open('temp.png')
img = img.resize((400, 400), Image.ANTIALIAS)
pimg = ImageTk.PhotoImage(img)

last_img = canvas.create_image(0, 0, anchor='nw',image=pimg)

refresh_entry()

tk.mainloop()

Please follow the testing instruction:

  1. I have put the code and 3 svg file examples in github, run following command

git clone https://github.com/Weilory/svg_renamer

  1. if you haven't install the packages:
pip install svglib
pip install Pillow
pip install tkinter
pip install reportlib
pip install lxml
  1. direct terminal to directory containing SVG folder and renamer.py, open renamer.py, assign directory variable to the absolute path of your SVG folder.

  2. run the code through terminal

python renamer.py

the renamer.py on github is the functional scope, which you should see NO image displaying in tkinter canvas.

  1. hash the refresh_image() function and the calling part, unhash the global single execution part at the end of file, run again.

python renamer.py

the image will show up. but without calling the function, I won't be able to switch svg content.

This issue has been annoying me all day, I will be so appreciated if someone offers me a solution or some suggestions.

1 Answers

after so many trials, the issue occurs at pimg = ImageTK.PhotoImage(img), which for some reason, if ImageTK.PhotoImage() doesn't return a value at global scope, the data will be forgotten, therefore, if we declare pimg as a global variable and assigning value in refresh_image() function it will work.

'''sh
pip install svglib
pip install Pillow
pip install tkinter
pip install reportlib
pip install lxml
'''


import os
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPDF, renderPM
from tkinter import *
from PIL import Image, ImageTk


# rename file by abs path
def rename(abs_path, new_name):
    os.rename(abs_path, new_name)


tk = Tk()

# ---------------------------------------- fill your path here
# SVG directory absolute path
directory = "C:/Users/QINY/OneDrive - Kardinia International College/Desktop/svg_conduct/SVG";

svg_paths = []

# iterate .svg file and append to collector
for filename in os.listdir(directory):
    svg_paths.append(os.path.join(directory, filename));


# current manipulating index
at = 1


# feedback 
# how many finished
# how many to go
def percent():
    return f"{at} / {len(svg_paths)}"


vl = StringVar(tk, value=percent())


def go_previous():
    global at
    at -= 1
    if at == 0:
        # no more previous
        at += 1
    else:
        vl.set(percent())


def go_next():
    global at
    at += 1
    if at == len(svg_paths):
        # no more next
        at -= 1
    else:
        vl.set(percent())


label = Label(tk, textvariable=vl, font="Helvetica 30")
label.pack()



# tkinter doesnot support svg, therefore create a temp.png file 
# in same level as SVG folder as converted png file to display

canvas = Canvas(tk, width=500, height=500)
canvas.pack()

last_img = None
pimg = None



'''
update png is achieved by Tkinter.Canvas widget
canvas.delete() => delete last image content
canvas.create_image() => set up new image content

the file name is always temp.png however the content is different
since new svg will be overwritten into the same file name
'''

'''
if you call following function at the end, it doesnot work,
which the png will not be displayed on tkinter
but if we exec these code in global scope instead of function 
scope, it turns out working! I must use the function to update 
the image content when either previous or next button is clicked
'''

def refresh_image():
    global last_img, canvas, renderPM, Image, ImageTk, tk, pimg

    if last_img is not None:
        canvas.delete(last_img)

    drawing = svg2rlg(svg_paths[at-1])
    renderPM.drawToFile(drawing, "temp.png", fmt="PNG")

    img = Image.open('temp.png')
    img = img.resize((400, 400), Image.ANTIALIAS)
    img.save('temp.png')

    pimg = ImageTk.PhotoImage(img)

    last_img = canvas.create_image(0, 0, anchor='nw',image=pimg)
    img.close()


# display file name in input box
va = StringVar(tk, value='')
entry = Entry(tk, textvariable=va, font="Helvetica 30")
entry.pack(ipady=3)


# update file name
def refresh_entry():
    va.set(svg_paths[at-1].split('/')[-1])


# rename file
def assign_name(new_name):
    nm = directory + '/' + new_name
    rename(svg_paths[at-1], nm)
    # update recorded paths
    svg_paths[at] = nm


previous = Button(tk, text="previous", command=go_previous)
_next = Button(tk, text="next", command=go_next)

previous.pack()
_next.pack()


refresh_image()

'''
As I mentioned in refresh_image(),
if you hash the function above and unhash the code below,
it works.

I want to update image content according to svg_paths at `at` index
'''

""" functional scope """
# drawing = svg2rlg(svg_paths[at-1])
# renderPM.drawToFile(drawing, "temp.png", fmt="PNG")

# img = Image.open('temp.png')
# img = img.resize((400, 400), Image.ANTIALIAS)
# img.save('temp.png')

# pimg = ImageTk.PhotoImage(img)
# last_img = canvas.create_image(0, 0, anchor='nw',image=pimg)



""" roll """
# img.close()
# canvas.delete(last_img)

# drawing = svg2rlg(svg_paths[at])
# renderPM.drawToFile(drawing, "temp.png", fmt="PNG")
# img = Image.open('temp.png')
# img = img.resize((400, 400), Image.ANTIALIAS)
# pimg = ImageTk.PhotoImage(img)

# last_img = canvas.create_image(0, 0, anchor='nw',image=pimg)


refresh_entry()

tk.mainloop()

I don't know why this happens but, be care when working with Pillow module, because some of the functions such as ImageTK requires assigning value to a global variable, in other words, if the class or function is initiated or declared only in functional scope, the data will be forgotten

if someone can explain why this happens, I would love to accept it as an answer.

Thanks for your time.

Related