File: pygadgets-products/unzipped/_PyPhoto/PIL/_PriorCode2.1/pyphoto.py
"""
############################################################################
PyPhoto 2.1: a basic but open-source and portable image viewer,
with cached thumbnails for speed, scrolling, resizing, and saves.
Copyright: 2017 M.Lutz, http://learning-python.com.
License: provided freely, but with no warranties of any kind.
Originally from the book "Programming Python, 4th Edition".
VERSIONS:
[SA] 2.1, Nov-2017: rereleased with new PyGadgets, to store a folder's
thumbnails in a single pickle file, instead of individual image files in
a subfolder. This has the same performance, but avoids extra files (15k
images formerly meant 15k thumbs); multiple file loads and saves; and
nasty modtime-copy issues for backup programs. See viewer_thumbs.py.
Version 2.0 users: run the included delete-pyphoto2.0-thumbs-folders.py
(or its executable), to delete all 2.0 subfolders when upgrading to 2.1.
[SA] 2.0, Sep-2017: Standalone release of PyCalc, PyClock, PyPhoto, PyToe.
Multiple changes (see ahead), included in the PyGadgets standalone release.
[PP4E] 1.X, 2006: the original version appeared in the book named above.
OVERVIEW:
This program displays cached image-folder thumbnails which open images
when clicked, scrolls both images and thumbnail pages, and provides a
variety of viewing functionality (see the Help popup).
It supports multiple image-directory thumb windows; the initial image
folder is passed in via configs, uses the "images-mixed" default, or is
selected via a main window button. Later directories are opened by
pressing "D" in either image-view or thumbnail-view windows.
For startup speed, folder thumbnails are cached in a pickle file in
writeable folders (subfolder caching is available for other programs).
The image viewer resizes and scrolls images too large for the screen.
This program is still something of a placeholder for more photo editing
ops, but it demonstrates PIL basics well enough to ship as is.
VERSION DETAILS:
----
New in 2.1: store thumbnails in a single pickle file, not as multiple
thumb-image files in a subfolder, and apply other minor improvements
to thumbnail processing; see viewer_thumbs.py for details. Here, also
catch/report images with errors, whose thumbnails are no longer omitted.
Version 2.1 also reports save errors in GUI popups, prints all excs,
and supports a new config setting to ignore modtime-based change tests.
----
New in 2.0 [SA]: multiple/major extensions to boost utility:
- New help dialog, tweak hints in window titles
- Mac OS port: menus, reopens, focus, clicks
- Auto-resize large images to screen size
- Bind arrow keys to scroll both thumbs and images
- Add N/P=next/prior image in folder (as displayed)
- Add A/S=actual/scaled size (a=prior viewer sole mode)
- Old key S (save) is now W (write)
- Show busy message in window while creating thumbnails
- Use LANCZOS resize filter in all resize contexts in newer Pillows:
now the best quality (if slowest) for both shrinking and expanding:
pillow.readthedocs.io/en/4.2.x/handbook/concepts.html#concept-filters
- Move to upper left corner if too large for display or partially
off-screen, but do not center if smaller than display (user choice)
- Get screen max size reliably (this is a bit loose in Tk)
- Implement ViewSize fixed-size option for image views and scaling
- Workaround a C libs hardcrash for tiff thumbnail saves: no compress
PyPhoto is still a bit primitive, and something of a PIL demo
(e.g. images are not resized on window resizes, and there is no
cropping, etc.), but is also illustrative of general PIL use.
----
Older PP4E book versions
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).
----
NOTES
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
----
CAVEATS/TBDS
1) TIFF drawing quality on Mac OS El Capitan
On test and development machines, TIFF images and their thumbnails
render poorly (with artifacts) in Mac OS apps and source-code on
El Capitan, but accurately on Mac OS Sierra in all modes (and always
flawlessly on Windows and Linux). This may indicate a newer shared
libtiff on Sierra, which might be fixed by installs on El Capitan.
2) Image-save limitations
Saves ("W") work well in typical roles, but are not always as simple
as giving a ".xxx" filename extension (e.g., this may discard GIF
transparencies), and some image-type saves may not work at all due
to the Pillow library's constraints, especially for conversions
(e.g., they may leave zero-length files). To be improved if usage
warrants. Version 2.1 at least adds a GUI popup on save errors.
3) Image auto-placement is one-sided
The automatic move-to-upper-left-corner works only when the image
is off-screen to the right, not left; this could be generalized
to work on either screen side, but that seems a bit superfluous.
4) Mac OS screenshot shadows on Mac OS El Capitan
The default shadows that Mac OS adds around its screenshots sometimes
render as all-black in Pillow images and thumbnails as they're used here.
Oddly, this has been observed ONLY on a Mac OS El Capitan machine; it
does not occur on a Mac OS Sierra machine, and Windows and Linux seem
to always render the shadow properly too. Hence, this appears to be
Mac-only, and specific to either Mac OS version or a single machine.
(See also #1 above: El Capitan's image-drawing stack seems flaky.)
If not, this may be prohibitively complex. A magic-but-sadly-undocumented
Pillow remedy seems unlikely, and removing shadows manually seems absurd:
https://stackoverflow.com/questions/18293290/
programmatically-remove-apple-screen-capture-shadow-borders
5) And so on
Other TBDs include (1) rearrange thumbnails when window resized, based on
current window size? (2) [DONE] resize images to fit display/window size?
(3) avoid scrolls if image size is less than window max size: use Label
if imgwide <= scrwide and imghigh <= scrhigh? (4) cropping, annotation,...?
############################################################################
"""
import sys, math, os, traceback
from tkinter import *
from tkinter.filedialog import SaveAs, Directory
from tkinter.messagebox import showerror
# [SA] require PIL/Pillow install
pillowerror = """
Pillow 3rd-party package is not installed.
...This package is required by PyPhoto for both thumbnails generation
...and image file types not supported by Pythons without Pillow. Any
...Python supports GIFs, and Pythons using Tk 8.6 or later add PNGs, but
...thumbnails expect Pillow. See https://pypi.python.org/pypi/Pillow.
"""
try:
from PIL import Image # get image wrapper + widget
from PIL.ImageTk import PhotoImage # replaces tkinter's version
except ImportError:
print(pillowerror)
sys.exit(0)
# don't continue: some image types may load, but need PIL to make thumbnails
# Pillow install required for source-code only: apps/exes have PIL "baked in"
# thumbnail generation code, developed earlier in book
from viewer_thumbs import makeThumbs, isImageFileName, sortedDisplayOrder
# [SA] Mac port (and other backports)
RunningOnMac = sys.platform.startswith('darwin')
RunningOnWindows = sys.platform.startswith('win')
RunningOnLinux = sys.platform.startswith('linux')
# [SA]: set window icons on Windows and Linux
from windowicons import trySetWindowIcon
# remember last dirs across all windows
saveDialog = SaveAs(title='Save As (filename extension gives image type)')
if RunningOnMac:
openargs = dict(message='Select Image Directory To Open') # [SA]
else:
openargs = dict(title='Select Image Directory To Open')
openDialog = Directory(**openargs)
trace = print # or lambda *x: None
appname = 'PyPhoto 2.1'
############################################################################
# Canvas with dual scroll bars, used by both thumbnail and image windows
############################################################################
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)
############################################################################
# View a single image selected in thumbnails window
############################################################################
class ViewOne(Toplevel):
"""
--------------------------------------------------------------
A pop-up window that opens a single image when created.
Scrollable vertically and horizontally if too big for display.
This was initially coded as a class because PhotoImage objects
must be saved, else images may be erased from GUI if reclaimed.
On mouse clicks, resizes to display's height or width, either
stretching or shrinking. On I/O keypress, zooms image in/out.
Both resizing schemes maintain the original aspect ratio; its
code is factored to avoid redundancy here as possible.
[SA] See 2.0 changes list in main docstring for updates:
there were too many extensions to list here.
[2.1] Catch and report exceptions for images with errors,
whose thumbnails are no longer omitted in 2.1. Pillow uses
a "lazy" model in which open() just identifies the file, but
the complete load() is deferred until later processing. This
can be a major problem, as exceptions might occur anywhere and
anytime for images with errors. Here, and on next/prior image,
call load() explicitly to force errors to happen immediately.
--------------------------------------------------------------
"""
def __init__(self,
imgdir, imgfile, # image path+name to display
dirwinsize=(), # thumbs: pass along on "D"
viewsize=(), # fixed display size
opener=None, # refocus on errors?
nothumbchanges=False): # thumbs: pass along on "D"
Toplevel.__init__(self)
self.setTitle(imgfile)
trySetWindowIcon(self, 'icons', 'pygadgets') # [SA] for win+lin
self.viewsize = viewsize # fixed scaling size
# try to load image
imgpath = os.path.join(imgdir, imgfile) # img file to open
try:
imgpil = Image.open(imgpath) # img object to be reused by ops
imgpil.load() # [2.1] load now so errors here
except:
# [2.1] can fail on OSError+ in Pillow
traceback.print_exc()
self.destroy()
showerror('PyPhoto: Image Load',
'Cannot load image file:\n%s' % imgpath)
if opener:
opener.focus_force() # abandon after error popup
if RunningOnLinux: opener.lift() # else root win stays above
return # abandon after error popup
self.trueimage = imgpil # for all ops till N/P
self.canvas = ScrolledCanvas(self) # tk canvas to be reused
self.drawImageFirst() # show scaled or actual now
# bind keys/events for this image-view window
self.canvas.bind('<Button-1>', self.onSizeToDisplayHeight)
self.canvas.bind('<Button-3>', self.onSizeToDisplayWidth)
if RunningOnMac:
self.canvas.bind('<Button-2>', self.onSizeToDisplayWidth) # [SA]
self.bind('<Control-Button-1>', self.onSizeToDisplayWidth)
self.bind('<KeyPress-i>', self.onZoomIn)
self.bind('<KeyPress-o>', self.onZoomOut)
self.bind('<KeyPress-w>', self.onSaveImage)
self.bind('<KeyPress-d>',
lambda event: onDirectoryOpen(self, dirwinsize, viewsize, nothumbchanges))
# [SA] question=? but portable, help key in all gadgets
self.bind('<KeyPress-question>', lambda event: onHelp(self))
# [SA] bind arrow keys to scroll canvas too (tbd: increment?)
self.bind('<Up>', lambda event: self.canvas.yview_scroll(-1, 'units'))
self.bind('<Down>', lambda event: self.canvas.yview_scroll(+1, 'units'))
self.bind('<Left>', lambda event: self.canvas.xview_scroll(-1, 'units'))
self.bind('<Right>', lambda event: self.canvas.xview_scroll(+1, 'units'))
# [SA] add next/prior image in this image's folder
self.bind('<KeyPress-n>', self.onNextImage)
self.bind('<KeyPress-p>', self.onPrevImage)
self.imgdir, self.imgfile, self.dirwinsize = imgdir, imgfile, dirwinsize
# [SA] add actual/scaled size (actual=former version's only mode)
self.bind('<KeyPress-a>', lambda event: self.drawImageSized(self.trueimage))
self.bind('<KeyPress-s>', lambda event: self.drawImageFirst())
self.focus() # on Windows, make sure new window catches events now
# [SA] set min size as partial fix for odd window shinkage on zoomout
# on Mac; later made mostly moot by auto-resize to screen/fixed size
"""
self.minsize(500, 500) # w, h
"""
def setTitle(self, imgfile):
"""
[SA] split off from constructor for next/prior
"""
helptxt = 'I/O=zoom, N/P=move, A/S=size, arrows=scroll'
self.title('%s: %s (%s)' % (appname, imgfile, helptxt))
def getMaxSize(self, constrained=True):
"""
[SA] fullscreen (W, H) size, factored to common code;
viewsize is experimental, but suffices to limit scaling max;
oddness: Tk's self.maxsize() height varies across calls, and
on Mac is < self.winfo_screenwidth()/.winfo_screenheight();
"""
#trace('==>', self.maxsize())
#trace('==>', (self.winfo_screenwidth(), self.winfo_screenheight()))
scrwide, scrhigh = self.winfo_screenwidth(), self.winfo_screenheight()
if not constrained:
return (scrwide, scrhigh) # wm screen size (x,y)
else:
return self.viewsize or (scrwide, scrhigh) # user limit, or wm
#
# Drawing
#
def drawImageFirst(self):
"""
[SA] draw from self.trueimage, ether scaled or actual;
called on first open, next/prior, and S=scale key, but
trueimage is set by the first two of these only, and is
used by most ops (viewimage is used by zooms and saves);
resize large images to screen or fixed size initially;
smaller images are still drawn by actual size, as before;
viewsize is still experimental (scaling to screen size in
onSizeToDisplayBoth is better), but suffices to limit max;
[SA] imgpil.width and imgpil.height are not available in
earlier Pillows: use the older .size tuple-pair instead;
"""
imgpil = self.trueimage
imgwide, imghigh = imgpil.size # size in pixels (x,y)
scrwide, scrhigh = self.getMaxSize() # wm screen size (x,y)
if imgwide <= scrwide and imghigh <= scrhigh: # too big for display?
self.drawImageSized(imgpil) # no: win size per img
else: # yes: resize to screen
self.onSizeToDisplayBoth() # scale to screen W and H
"""
# this else was not as good...
diffwide = imgwide - scrwide # use most-exceeded edge
diffhigh = imghigh - scrhigh
if diffwide > diffhigh: # either may be negative
self.onSizeToDisplayWidth(event=None)
else:
self.onSizeToDisplayHeight(event=None)
"""
def drawImageSized(self, imgpil):
"""
draw imgpil, as it is sized, in current window/canvas;
imgpil may be the original actual size, or a temp resize;
"""
imgtk = PhotoImage(image=imgpil) # not file=imgpath
imgwide = imgtk.width() # size in pixels
imghigh = imgtk.height() # same as imgpil.size
scrwide, scrhigh = self.getMaxSize() # wm screen size (x,y)
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)
self.savephoto = imgtk # keep reference on me
self.viewimage = imgpil # currently-shown size
trace((scrwide, scrhigh), imgpil.size)
# [SA] move to upper-left if too big or partially off-screen
self.update()
coords = self.geometry() # e.g., '721x546+521+23'
wmsizeX, wmsizeY = \
[int(x) for x in coords.split('+')[0].split('x')]
upleftX, upleftY = \
[int(x) for x in coords.split('+')[1:3]]
truewide, truehigh = self.getMaxSize(constrained=False)
if (upleftX + wmsizeX > truewide) or (upleftY + wmsizeY > truehigh):
self.geometry('+0+0')
#trace((upleftX, wmsizeX, truewide), (upleftY, wmsizeY, truehigh))
# [SA] the point of this seems lost in translation...
"""
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()
else: # [SA] Mac is different...
win.wm_attributes('-fullscreen', 1)
"""
#
# Scaling
#
def sizeToDisplaySide(self, scaler):
"""
resize to fill one side (or both) of the display;
[SA] start from self.trueimage, not last view size;
[SA] lanczos filter not available in earlier Pillows;
"""
imgpil = self.trueimage # scale from full size
imgwide, imghigh = imgpil.size # img size in pixels
scrwide, scrhigh = self.getMaxSize() # wm screen size (x,y)
newwide, newhigh = scaler(scrwide, scrhigh, imgwide, imghigh)
if hasattr(Image, 'LANCZOS'):
filter = Image.LANCZOS # [SA] best for all, if available
else:
if (newwide * newhigh < imgwide * imghigh):
filter = Image.ANTIALIAS # shrink: antialias
else: # grow: bicub sharper
filter = Image.BICUBIC
newimg = imgpil.resize((newwide, newhigh), filter)
self.drawImageSized(newimg)
def onSizeToDisplayHeight(self, event):
def scaleHigh(scrwide, scrhigh, imgwide, imghigh):
newwide = int(imgwide * (scrhigh / imghigh)) # 3.x true div
newhigh = scrhigh # [SA] -border
return (newwide, newhigh) # proportional
self.sizeToDisplaySide(scaleHigh)
def onSizeToDisplayWidth(self, event):
def scaleWide(scrwide, scrhigh, imgwide, imghigh):
newhigh = int(imghigh * (scrwide / imgwide)) # 3.x true div
newwide = scrwide # [SA] -border
return (newwide, newhigh)
self.sizeToDisplaySide(scaleWide)
def onSizeToDisplayBoth(self):
"""
[SA] scale to fit both height and width of screen;
the 90% scale-down is meant allow for window borders:
should this happen in sizeToDisplaySide for all (tbd)?
"""
def scaleBoth(scrwide, scrhigh, imgwide, imghigh):
scrwide, scrhigh = (int(x * .90) for x in (scrwide, scrhigh))
newwide, newhigh = imgwide, imghigh
if imgwide > scrwide:
newhigh = int(imghigh * (scrwide / imgwide))
newwide = scrwide
if imghigh > scrhigh:
newwide = int(imgwide * (scrhigh / imghigh))
newhigh = scrhigh
return (newwide, newhigh)
self.sizeToDisplaySide(scaleBoth)
#
# Zooming
#
def zoom(self, factor):
"""
zoom in or out in increments;
[SA] must use viewimage here, else not cumulative;
[SA] lanczos filter not available in earlier Pillows;
"""
imgpil = self.viewimage # zoom from last display size
wide, high = imgpil.size # may be scaled, actual, zoomed
if hasattr(Image, 'LANCZOS'):
filter = Image.LANCZOS # [SA] best for all, if available
else:
if factor < 1.0: # antialias best if shrink
filter = Image.ANTIALIAS # also nearest, bilinear
else:
filter = Image.BICUBIC
newimg = imgpil.resize((int(wide * factor), int(high * factor)), filter)
self.drawImageSized(newimg)
def onZoomIn(self, event, incr=.10):
self.zoom(1.0 + incr)
def onZoomOut(self, event, decr=.10):
self.zoom(1.0 - decr)
#
# Saving
#
def onSaveImage(self, event):
"""
save current image size/state to a file;
per PIL, fiename extension gives image type;
[SA] save from viewimage: currently-shown size;
[SA] set initialfile name for convenience;
[2.1] catch/report save errors, console+GUI
"""
filename = saveDialog.show(initialfile=self.imgfile)
if filename:
try:
self.viewimage.save(filename)
except:
traceback.print_exc()
print('Error saving image file: not saved')
showerror('PyPhoto: Image Save',
'Cannot save image file:\n%s' % filename)
self.focus_force() # [SA] for Mac
#
# Navigating
#
def switchimage(self, ixmod):
"""
[SA] add next/prior image in this image's folder;
redraws in same window/canvas, to avoid flicker if
draw in a new window and erase the old one (and the
"ViewOne" class name now really means one at a time);
[2.1] catch/report images with errors (whose thumbs
are no longer omitted); error imgs stop the next/prior
progression: users must click a new thumb past it;
[2.1] must call viewer_thumb's sortedDisplayOrder(),
not os.listdir() directly, so the next/prior order
implemented here matches thumbs-display order;
"""
currdir, currfile = self.imgdir, self.imgfile
allfiles = sortedDisplayOrder(currdir)
imgfiles = list(filter(isImageFileName, allfiles))
currix = imgfiles.index(currfile)
newix = currix + ixmod
newix = len(imgfiles)-1 if newix < 0 else newix % len(imgfiles)
nextfile = imgfiles[newix] # wrapped around
imgpath = os.path.join(currdir, nextfile) # img file to open
try:
nextimgpil = Image.open(imgpath) # img object to be reused
nextimgpil.load() # [2.1] load now so errors here
except:
# [2.1] can fail on OSError+ in Pillow
traceback.print_exc()
showerror('PyPhoto: Image Load',
'Cannot load image file:\n%s' % imgpath)
self.focus_force()
if RunningOnLinux: self.lift() # else root stays above
else:
# move iff image loaded
self.imgfile = nextfile
self.setTitle(nextfile)
self.trueimage = nextimgpil # save for ops on image
self.drawImageFirst() # new image, same win/canvas
"""
# or new window: this worked but was too twitchy...
ViewOne(currdir, nextfile, self.dirwinsize, (plus new stuff))
self.destroy()
"""
def onNextImage(self, event):
self.switchimage(+1)
def onPrevImage(self, event):
self.switchimage(-1)
############################################################################
# View the thumbnails window for an initial or chosen directory
############################################################################
def viewThumbs(imgdir, # open this folder
kind=Toplevel, # thumbs window: Tk or Toplevel
dirwinsize=(), # size of this thumbs window
viewsize=(), # size of each image-view window
numcols=None, # fixed, else per #thumbnails
nothumbchanges=False): # don't detect image changes?
"""
--------------------------------------------------------------
Make main (Tk) or pop-up (Toplevel) thumbnail-buttons window.
Uses fixed-size buttons, and a bi-scrollable canvas.
Sets scrollable (full) size of canvas, and places thumbs
at absoute x,y coordinates in the canvas.
No longer assumes that all thumbs are the same size:
uses max of all (x,y) for all, as some may be smaller.
[SA] See 2.0 changes list in main docstring for updates:
there were too many extensions to list here.
CAUTION: changing thumb size can defeat backup programs;
delete all thumbs on size changes (see viewer_thumbs.py).
--------------------------------------------------------------
"""
win = kind()
helptxt = 'D=open'
win.title('%s: %s (%s)' % (appname, imgdir, helptxt))
trySetWindowIcon(win, 'icons', 'pygadgets') # [SA] for win+lin
# [SA] add new Help button
# [SA] Quit=destroy (this window only unless last Tk), not quit (entire app)
tools = Frame(win, bg='beige')
tools.pack(side=BOTTOM, fill=X)
quit = Button(tools, text=' Quit ', command=win.destroy) # [SA] no bg= on Mac
quit.pack(side=RIGHT, expand=YES)
help = Button(tools, text=' Help ', command=lambda: onHelp(win))
help.pack(side=LEFT, expand=YES)
# [SA] question=? but portable, help key in all gadgets
win.bind('<KeyPress-question>', lambda event: onHelp(win))
# make or load thumbs ==> [(imgfile, imgobj)]
thumbs = makeThumbs(imgdir, # all images in folder
size=(128, 128), # fixed thumbnails size
busywindow=win, # announce in GUI
nothumbchanges=nothumbchanges) # don't detect changes?
numthumbs = len(thumbs)
if numthumbs == 0: # no-image dir?
numcols = numrows = 0 # [SA] avoid / 0 exc
else:
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
width, height = dirwinsize # [SA] new configs model
canvas = ScrolledCanvas(win) # init viewable window size
canvas.config(height=height, width=width) # changes if user resizes
# max w|h: thumb=(name, obj), obj.size=(width, height)
if numthumbs == 0:
linksize = 0 # [SA] avoid empty-seq max() exc
else:
linksize = max(max(thumb[1].size) for thumb in thumbs)
trace(linksize)
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(_imgfile=imgfile):
ViewOne(imgdir, _imgfile, dirwinsize, viewsize, win, nothumbchanges)
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.savephotos = savephotos # keep references to all to avoid gc
# bind keys/events for this directory-view window
win.bind('<KeyPress-d>',
lambda event: onDirectoryOpen(win, dirwinsize, viewsize, nothumbchanges))
# [SA] bind arrow keys to scroll canvas too (tbd: increment?)
win.bind('<Up>', lambda event: canvas.yview_scroll(-1, 'units'))
win.bind('<Down>', lambda event: canvas.yview_scroll(+1, 'units'))
win.bind('<Left>', lambda event: canvas.xview_scroll(-1, 'units'))
win.bind('<Right>', lambda event: canvas.xview_scroll(+1, 'units'))
win.focus() # [SA] on Windows, make sure new window catches events now
return win
############################################################################
# Utilities, having multiple class and non-class clients
############################################################################
def onDirectoryOpen(parentwin, dirwinsize, viewsize, nothumbchanges):
"""
open a new image directory in a new main window;
available via "D" in both thumb and img windows
"""
dirname = openDialog.show()
if dirname:
viewThumbs(dirname, Toplevel, dirwinsize, viewsize,
nothumbchanges=nothumbchanges)
else:
parentwin.focus_force() # [SA] for Mac
def onHelp(parentwin):
"""
[SA] new help dialog - simple but sufficient;
used for 'help' button click, '?' keyboard press, Mac menus;
'?' keypress and Mac menus work in both thumb and image windows;
"""
from helpmessage import showhelp
showhelp(parentwin, 'PyPhoto', HelpText, forcetext=False,
setwinicon=lambda win:
trySetWindowIcon(win, 'icons', 'pygadgets'))
#parentwin.focus_force() # now done in helpmessage
HelpText = ('PyPhoto 2.1\n'
'\n'
'A Python/tkinter/PIL image-viewer GUI.\n'
'For Mac OS, Windows, and Linux.\n'
'From the book Programming Python.\n'
'Author and © M. Lutz 2006-2017.\n'
'\n'
'In directory windows:\n'
'▶ Key D opens another image directory\n'
'▶ Clicking an image\'s thumbnail opens it in an '
'image-view window at scaled size\n'
'\n'
'In image-view windows:\n'
'▶ Key D opens another image directory\n'
'▶ Keys N/P open the next/prior image\n'
'▶ Keys I/O zoom the image in/out\n'
'▶ Keys A/S show actual/scaled size\n'
'▶ Key W saves (writes) the current image\n'
'▶ Leftclick and rightclick resize to screen height '
'and width, respectively (on Macs, rightclick=two-finger '
'click or control+click)\n'
'\n'
'In both window types, key ?=Help, and the Up/Down '
'and Left/Right arrow keys scroll vertically and '
'horizontally, respectively.\n'
'\n'
'For W saves, the filename\'s ".xxx" extension gives its '
'image type. For example, to both shrink a PNG and convert '
'it to GIF, zoom out with O and save as ".gif" with W.\n'
'\n'
'A "_PyPhoto-thumbs.pkl" file is created in each opened image '
'folder when possible, to store image thumbnails for fast access. '
'Its thumbs are kept in sync with images.\n'
'\n'
'PyPhoto source-code distributions (but not apps or '
'executables) require installation of the Pillow extension '
'package from https://pypi.python.org/pypi/Pillow.\n'
'\n'
'For downloads and more apps, visit:\n'
'http://learning-python.com/programs.html'
)
############################################################################
# Top-level logic: get configs, get first folder, display thumbnails
############################################################################
if __name__ == '__main__':
"""
open dir = default or cmdline arg
else show simple window to select
"""
from getConfigs import getConfigs # [SA] new gadgets utility
defaults = dict(InitialSize='500x400', # size of dir/thumbs window
InitialFolder='images-mixed', # None = ask for dir
ViewSize=None, # None = scale to screen
NoThumbChanges=False) # True = skip change detection [2.1]
configs = getConfigs('PyPhoto', defaults) # load from file or args
imgdir = configs.InitialFolder
dirwinsize = configs.InitialSize.split('x') # 'WidthxHeight'
dirwinsize = [int(x) for x in dirwinsize] # (Width, Height)
viewsize = configs.ViewSize
viewsize = viewsize.split('x') if viewsize else () # e.g., '800x600'
viewsize = list(map(int, viewsize)) # (800, 600)
nothumbchanges = configs.NoThumbChanges
if imgdir and os.path.exists(imgdir):
mainwin = viewThumbs(imgdir, Tk, dirwinsize, viewsize,
nothumbchanges=nothumbchanges)
else:
mainwin = Tk()
mainwin.geometry('250x50')
mainwin.title(appname + 'Open')
handler = lambda: onDirectoryOpen(mainwin, dirwinsize, viewsize, nothumbchanges)
Button(mainwin, text='Open Image Directory',
command=handler).pack(expand=YES, fill=BOTH)
trySetWindowIcon(mainwin, 'icons', 'pygadgets') # [SA] for win+lin
if RunningOnMac:
# Mac requires menus, deiconifies, focus
# [SA] on Mac, customize app-wide automatic top-of-display menu
from guimaker_pp4e import fixAppleMenuBar
fixAppleMenuBar(window=mainwin,
appname='PyPhoto',
helpaction=lambda: onHelp(mainwin),
aboutaction=None,
quitaction=mainwin.quit) # immediate, bound method
# [SA] reopen auto on dock/app click and fix tk focus loss on deiconify
def onReopen():
mainwin.lift()
mainwin.update()
temp = Toplevel()
temp.lower()
temp.destroy()
mainwin.createcommand('::tk::mac::ReopenApplication', onReopen)
mainwin.mainloop()