-
Notifications
You must be signed in to change notification settings - Fork 1
/
pyphoto1.py
executable file
·246 lines (211 loc) · 10.2 KB
/
pyphoto1.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
"""
############################################################################
PyPhoto 1.1: thumbnail image viewer with resizing and saves.
Supports multiple image directory thumb windows - the initial img dir
is passed in as cmd arg, uses "images" default, or is selected via main
window button; later directories are opened by pressing "D" in image view
or thumbnail windows.
Viewer also scrolls popped-up images that are too large for the screen;
still to do: (1) rearrange thumbnails when window resized, based on current
window size; (2) [DONE] option to resize images to fit current window size?
(3) avoid scrolls if image size is less than window max size: use Label
if imgwide <= scrwide and imghigh <= scrhigh?
New in 1.1: updated to run in Python 3.1 and latest PIL;
New in 1.0: now does a form of (2) above: image is resized to one of the
display's dimensions if clicked, and zoomed in or out in 10% increments
on key presses; generalize me; caveat: seems to lose quality, pixels
after many resizes (this is probably a limitation of PIL)
The following scaler adapted from PIL's thumbnail code is similar to the
screen height scaler here, but only shrinks:
x, y = imgwide, imghigh
if x > scrwide: y = max(y * scrwide // x, 1); x = scrwide
if y > scrhigh: x = max(x * scrhigh // y, 1); y = scrhigh
############################################################################
"""
import sys, math, os
from Tkinter import *
import tkFileDialog
from tkFileDialog import SaveAs, Directory
import PIL
#from PIL import Image # PIL Image: also in Tkinter
from PIL.ImageTk import PhotoImage # PIL photo widget replacement
from viewer_thumbs import makeThumbs # developed earlier in book
# remember last dirs across all windows
saveDialog = SaveAs(title='Save As (filename gives image type)')
openDialog = Directory(title='Select Image Directory To Open')
trace = None
appname = 'PyPhoto 1.1: '
class ScrolledCanvas(Canvas):
"""
a canvas in a container that automatically makes
vertical and horizontal scroll bars for itself
"""
def __init__(self, container):
Canvas.__init__(self, container)
self.config(borderwidth=0)
vbar = Scrollbar(container)
hbar = Scrollbar(container, orient='horizontal')
vbar.pack(side=RIGHT, fill=Y) # pack canvas after bars
hbar.pack(side=BOTTOM, fill=X) # so clipped first
self.pack(side=TOP, fill=BOTH, expand=YES)
vbar.config(command=self.yview) # call on scroll move
hbar.config(command=self.xview)
self.config(yscrollcommand=vbar.set) # call on canvas move
self.config(xscrollcommand=hbar.set)
class ViewOne(Toplevel):
"""
open a single image in a pop-up window when created;
a class because photoimage obj must be saved, else
erased if reclaimed; scroll if too big for display;
on mouse clicks, resizes to window's height or width:
stretches or shrinks; on I/O keypress, zooms in/out;
both resizing schemes maintain original aspect ratio;
code is factored to avoid redundancy here as possible;
"""
def __init__(self, imgdir, imgfile, forcesize=()):
Toplevel.__init__(self)
helptxt = '(click L/R or press I/O to resize, S to save, D to open)'
self.title(appname + imgfile + ' ' + helptxt)
imgpath = os.path.join(imgdir, imgfile)
imgpil = Image.open(imgpath)
self.canvas = ScrolledCanvas(self)
self.drawImage(imgpil, forcesize)
self.canvas.bind('<Button-1>', self.onSizeToDisplayHeight)
self.canvas.bind('<Button-3>', self.onSizeToDisplayWidth)
self.bind('<KeyPress-i>', self.onZoomIn)
self.bind('<KeyPress-o>', self.onZoomOut)
self.bind('<KeyPress-s>', self.onSaveImage)
self.bind('<KeyPress-d>', onDirectoryOpen)
self.focus()
def drawImage(self, imgpil, forcesize=()):
imgtk = PhotoImage(image=imgpil) # not file=imgpath
scrwide, scrhigh = forcesize or self.maxsize() # wm screen size x,y
imgwide = imgtk.width() # size in pixels
imghigh = imgtk.height() # same as imgpil.size
fullsize = (0, 0, imgwide, imghigh) # scrollable
viewwide = min(imgwide, scrwide) # viewable
viewhigh = min(imghigh, scrhigh)
canvas = self.canvas
canvas.delete('all') # clear prior photo
canvas.config(height=viewhigh, width=viewwide) # viewable window size
canvas.config(scrollregion=fullsize) # scrollable area size
canvas.create_image(0, 0, image=imgtk, anchor=NW)
if imgwide <= scrwide and imghigh <= scrhigh: # too big for display?
self.state('normal') # no: win size per img
elif sys.platform[:3] == 'win': # do windows fullscreen
self.state('zoomed') # others use geometry()
self.saveimage = imgpil
self.savephoto = imgtk # keep reference on me
trace((scrwide, scrhigh), imgpil.size)
def sizeToDisplaySide(self, scaler):
# resize to fill one side of the display
imgpil = self.saveimage
scrwide, scrhigh = self.maxsize() # wm screen size x,y
imgwide, imghigh = imgpil.size # img size in pixels
newwide, newhigh = scaler(scrwide, scrhigh, imgwide, imghigh)
if (newwide * newhigh < imgwide * imghigh):
filter = Image.ANTIALIAS # shrink: antialias
else: # grow: bicub sharper
filter = Image.BICUBIC
imgnew = imgpil.resize((newwide, newhigh), filter)
self.drawImage(imgnew)
def onSizeToDisplayHeight(self, event):
def scaleHigh(scrwide, scrhigh, imgwide, imghigh):
newhigh = scrhigh
newwide = int(scrhigh * (imgwide / imghigh)) # 3.x true div
return (newwide, newhigh) # proportional
self.sizeToDisplaySide(scaleHigh)
def onSizeToDisplayWidth(self, event):
def scaleWide(scrwide, scrhigh, imgwide, imghigh):
newwide = scrwide
newhigh = int(scrwide * (imghigh / imgwide)) # 3.x true div
return (newwide, newhigh)
self.sizeToDisplaySide(scaleWide)
def zoom(self, factor):
# zoom in or out in increments
imgpil = self.saveimage
wide, high = imgpil.size
if factor < 1.0: # antialias best if shrink
filter = Image.ANTIALIAS # also nearest, bilinear
else:
filter = Image.BICUBIC
new = imgpil.resize((int(wide * factor), int(high * factor)), filter)
self.drawImage(new)
def onZoomIn(self, event, incr=.10):
self.zoom(1.0 + incr)
def onZoomOut(self, event, decr=.10):
self.zoom(1.0 - decr)
def onSaveImage(self, event):
# save current image state to file
filename = saveDialog.show()
if filename:
self.saveimage.save(filename)
def onDirectoryOpen(event):
"""
open a new image directory in new pop up
available in both thumb and img windows
"""
dirname = openDialog.show()
if dirname:
viewThumbs(dirname, kind=Toplevel)
def viewThumbs(imgdir, kind=Toplevel, numcols=None, height=400, width=500):
"""
make main or pop-up thumbnail buttons window;
uses fixed-size buttons, scrollable canvas;
sets scrollable (full) size, and places
thumbs at abs x,y coordinates in canvas;
no longer assumes all thumbs are same size:
gets max of all (x,y), some may be smaller;
"""
win = kind()
helptxt = '(press D to open other)'
win.title(appname + imgdir + ' ' + helptxt)
quit = Button(win, text='Quit', command=win.quit, bg='beige')
quit.pack(side=BOTTOM, fill=X)
canvas = ScrolledCanvas(win)
canvas.config(height=height, width=width) # init viewable window size
# changes if user resizes
thumbs = makeThumbs(imgdir) # [(imgfile, imgobj)]
numthumbs = len(thumbs)
if not numcols:
numcols = int(math.ceil(math.sqrt(numthumbs))) # fixed or N x N
numrows = int(math.ceil(numthumbs / numcols)) # 3.x true div
# max w|h: thumb=(name, obj), thumb.size=(width, height)
linksize = max(max(thumb[1].size) for thumb in thumbs)
fullsize = (0, 0, # upper left X,Y
(linksize * numcols), (linksize * numrows) ) # lower right X,Y
canvas.config(scrollregion=fullsize) # scrollable area size
rowpos = 0
savephotos = []
while thumbs:
thumbsrow, thumbs = thumbs[:numcols], thumbs[numcols:]
colpos = 0
for (imgfile, imgobj) in thumbsrow:
photo = PhotoImage(imgobj)
link = Button(canvas, image=photo)
def handler(savefile=imgfile):
ViewOne(imgdir, savefile)
link.config(command=handler, width=linksize, height=linksize)
link.pack(side=LEFT, expand=YES)
canvas.create_window(colpos, rowpos, anchor=NW,
window=link, width=linksize, height=linksize)
colpos += linksize
savephotos.append(photo)
rowpos += linksize
win.bind('<KeyPress-d>', onDirectoryOpen)
win.savephotos = savephotos
return win
if __name__ == '__main__':
"""
open dir = default or cmdline arg
else show simple window to select
"""
imgdir = tkFileDialog.askdirectory()
if os.path.exists(imgdir):
mainwin = viewThumbs(imgdir, kind=Tk)
else:
mainwin = Tk()
mainwin.title(appname + 'Open')
handler = lambda: onDirectoryOpen(None)
Button(mainwin, text='Open Image Directory', command=handler).pack()
mainwin.mainloop()