Поскольку PyPhoto просто расширяет и повторно использует приемы и программный код, с которыми мы встречались ранее в книге, здесь мы опустим детальное обсуждение исходных текстов. За исходными сведениями обращайтесь к обсуждению приемов обработки изображений и применения PIL в главе 8 и к описанию виджета холста в главе 9. В двух словах отмечу, что PyPhoto использует холсты в двух случаях: для отображения коллекций миниатюр и для вывода открываемых изображений. Для вывода миниатюр используется тот же прием компоновки, что и раньше, в примере 9.15. Для вывода полноразмерных изображений также используется холст, прокручиваемая (полная) область которого соответствует размеру изображения, а видимая область вычисляется как минимум из размера физического экрана и размера самого изображения. Физический размер экрана можно определить вызовом метода maxsize() окна Toplevel. Благодаря этому полноразмерное изображение можно прокручивать, что очень удобно при просмотре изображений, размеры которых слишком велики, чтобы уместиться на экране (что весьма характерно для фотографий, снятых новейшими цифровыми фотокамерами).
Кроме того, PyPhoto выполняет привязку событий от клавиатуры и мыши для реализации операций изменения размеров и масштабирования. Благодаря PIL эти операции реализуются очень просто — мы сохраняем оригинальное изображение в объекте изображения PIL, вызываем его метод resize, передавая новые размеры, и перерисовываем изображение на холсте. Программа PyPhoto также использует диалоги открытия и сохранения файла, чтобы запомнить последний посещенный каталог.
Расширение PIL поддерживает дополнительные операции, которыми мы могли бы расширить набор обрабатываемых событий, но для просмотра изображений вполне достаточно изменения размеров. В настоящее время PyPhoto не использует потоки выполнения, чтобы с их помощью избежать блокирования во время выполнения продолжительных операций (например, операция первого открытия большого каталога). Такие расширения я оставляю для самостоятельного упражнения.
Программа PyPhoto реализована в виде единого файла, представленного в примере 11.5, хотя она получает бесплатно некоторую дополнительную функциональность от повторного использования функции, генерирующей миниатюры, из модуля viewer_thumbs, который мы написали в конце главы 8, в примере 8.45. Чтобы не заставлять вас листать страницы взад и вперед, ниже приводится фрагмент программного кода импортируемой функции создания миниатюр, используемой здесь:
# импортировано из главы 8…
def makeThumbs(imgdir, size=(100, 100), subdir=’thumbs’):
# возвращает список кортежей
# (имя_файла_изображения, объект_миниатюры_изображения);
thumbdir = os.path.join(imgdir, subdir)
if not os.path.exists(thumbdir):
os.mkdir(thumbdir)
thumbs = []
for imgfile in os.listdir(imgdir):
thumbpath = os.path.join(thumbdir, imgfile)
if os.path.exists(thumbpath):
thumbobj = Image.open(thumbpath) # использовать созданные ранее
thumbs.append((imgfile, thumbobj)) else:
print(‘making’, thumbpath)
imgpath = os.path.join(imgdir, imgfile)
try:
imgobj = Image.open(imgpath) # создать миниатюру
imgobj.thumbnail(size, Image.ANTIALIAS) # фильтр, дающий
# лучшее качество при
# уменьшении размеров
imgobj.save(thumbpath) # тип определяется
thumbs.append((imgfile, imgobj)) # расширением
except: print(“Skipping: “, imgpath) return thumbs
Программный код, реализующий окно выбора миниатюр, также очень схож с представленным в главе 9 примером с прокручиваемой коллекцией миниатюр, но он не импортируется этим файлом, а просто повторяется в нем, чтобы обеспечить будущее его развитие (и его статус в главе 9 — функционального подмножества — здесь понижен до уровня прототипа).
При изучении этого файла особое внимание обратите на организацию программного кода в виде набора функций и методов многократного пользования, которая позволяет избежать избыточности, — если нам, например, когда-нибудь придется изменить реализацию операции изменения размеров, нам достаточно будет изменить один метод, а не два. Кроме того, обратите внимание на класс ScrolledCanvas — компонент многократного пользования, который обеспечивает автоматическое связывание полос прокрутки и холстов.
Пример 11.5. PP4E\Gui\PIL\pyphoto1.py
############################################################################ PyPhoto 1.1: программа просмотра миниатюр изображений с возможностью изменения размеров и сохранения.
Позволяет открывать несколько окон для просмотра миниатюр из разных каталогов — в качестве начального каталога с изображениями принимается аргумент командной строки, каталог по умолчанию “images” или выбранный щелчком на кнопке в главном окне; последующие каталоги могут открываться нажатием клавиши “D” в окне с миниатюрами или в окне просмотра полноразмерного изображения.
Программа также позволяет прокручивать изображения, если они слишком большие и не умещаются на экране;
все еще необходимо: (1) реализовать переупорядочение миниатюр при изменении размеров окна, исходя из текущего размера окна; (2) [ВЫПОЛНЕНО] возможность изменения размеров изображения в соответствии с текущими размерами окна?
(3) отключать прокрутку, если размер изображения меньше максимального размера окна: использовать Label, если шир_изобр <= шир_окна и выс_изобр <= выс_окна?
Новое в версии 1.1: работает под управлением Python 3.1 и с последней версией PIL;
Новое в версии 1.0: реализован пункт (2) выше: щелчок мышью изменяет размер изображения в соответствии с одним из размеров экрана, и предусмотрена возможность увеличения и уменьшения масштаба изображения с шагом 10% нажатием клавиши; требуется поискать более универсальные решения; ВНИМАНИЕ: похоже, что после многократного изменения размеров теряется качество изображения (вероятно, это ограничение PIL)
Следующий алгоритм масштабирования, миниатюр средствами PIL, напоминает используемый в программе, но только x, y = imgwide, imghigh
if x > scrwide: y = max(y * scrwide
if y > scrhigh: x = max(x * scrhigh // y, 1); y = scrhigh
############################################################################ import sys, math, os from tkinter import * from tkinter.filedialog import SaveAs, Directory
from PIL import Image # PIL Image: также имеется в tkinter
from PIL.ImageTk import PhotoImage # версия виджета PhotoImage из PIL from viewer_thumbs import makeThumbs # разработан ранее в книге
# запомнить последний открытый каталог
saveDialog = SaveAs(title=’Save As (filename gives image type)’) openDialog = Directory(title=’Select Image Directory To Open’)
trace = print # or lambda *x: None
appname = ‘PyPhoto 1.1: ‘
class ScrolledCanvas(Canvas):
холст в контейнере, который автоматически создает вертикальную и горизонтальную полосы прокрутки
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) # холст прикрепляется после
hbar.pack(side=BOTTOM, fill=X) # полос, чтобы обрезался первым
self.pack(side=TOP, fill=BOTH, expand=YES)
vbar.config(command=self.yview) # вызвать при перемещении полосы
hbar.config(command=self.xview) # прокрутки
self.config(yscrollcommand=vbar.set) # вызвать при прокрутке холста
self.config(xscrollcommand=hbar.set)
class ViewOne(Toplevel):
при создании открывает единственное изображение во всплывающем окне; реализовано в виде класса, потому что объект PhotoImage должен сохраняться, иначе изображение будет стерто при утилизации; обеспечивает прокрутку больших изображений; щелчок мыши изменяет размер изображения в соответствии с высотой или шириной окна: растягивает или сжимает; нажатие клавиш I и O увеличивает и уменьшает размеры изображения; оба алгоритма изменения размеров предусматривают сохранение оригинального отношения сторон; программный код организован так, чтобы избежать избыточности, насколько это возможно;
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) |
# |
file != imgpath |
scrwide, |
scrhigh = forcesize or self.maxsize() |
# |
размеры x,y экрана |
imgwide |
= imgtk.width() |
# |
размеры в пикселях |
imghigh |
= imgtk.height() |
# |
то же, |
|
|
# |
что и imgpil.size |
fullsize |
= (0, 0, imgwide, imghigh) |
# |
прокручиваемая |
viewwide |
= min(imgwide, scrwide) |
# |
видимая |
viewhigh |
= min(imghigh, scrhigh) |
|
|
canvas = |
self.canvas |
|
|
canvas.delete(‘all’) |
# |
удалить предыд. изобр |
canvas.config(height=viewhigh, width=viewwide) # видимые размеры окна canvas.config(scrollregion=fullsize) # размер прокр. области canvas.create_image(0, 0, image=imgtk, anchor=NW)
if imgwide <= scrwide and imghigh <= scrhigh: # слишком велико?
self.state(‘normal’) |
# нет: размер окна по изобр |
elif sys.platform[:3] == ‘win’: |
# в Windows на весь экран |
self.state(‘zoomed’) |
# в других исп. geometry() |
self.saveimage = imgpil |
|
self.savephoto = imgtk |
# сохранить ссылку на меня |
trace((scrwide, scrhigh), imgpil.size) |
|
def sizeToDisplaySide(self, scaler):
# изменить размер, чтобы полностью заполнить одну сторону экрана imgpil = self.saveimage
scrwide, scrhigh = self.maxsize() # размеры x,y экрана
imgwide, imghigh = imgpil.size # размеры изображения в пикселях
newwide, newhigh = scaler(scrwide, scrhigh, imgwide, imghigh)
if (newwide * newhigh < imgwide * imghigh):
filter = Image.ANTIALIAS # сжатие: со сглаживанием
else: # растягивание: бикубическая
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)) # истинное деление
return (newwide, newhigh) # пропорциональные
self.sizeToDisplaySide(scaleHigh)
def onSizeToDisplayWidth(self, event):
def scaleWide(scrwide, scrhigh, imgwide, imghigh): newwide = scrwide
newhigh = int(scrwide * (imghigh / imgwide)) # истинное деление return (newwide, newhigh)
self.sizeToDisplaySide(scaleWide)
def zoom(self, factor):
# уменьшить или увеличить масштаб с шагом
imgpil = self.saveimage
wide, high = imgpil.size
if factor < 1.0: # сглаживание дает лучшее качество
filter = Image.ANTIALIAS # при сжатии, также можно
else: # использовать NEAREST, BILINEAR
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):
# сохранить изображение в текущем виде в файл
filename = saveDialog.show()
if filename:
self.saveimage.save(filename)
def onDirectoryOpen(event):
открывает новый каталог с изображениями в новом окне
может вызываться в обоих окнах, с изображением и с миниатюрами
dirname = openDialog.show()
if dirname:
viewThumbs(dirname, kind=Toplevel)
def viewThumbs(imgdir, kind=Toplevel, numcols=None, height=400, width=500):
создает окно и кнопки с миниатюрами;
использует кнопки фиксированного размера, прокручиваемый холст;
устанавливает прокручиваемый (полный) размер и размещает миниатюры
в холсте по абсолютным координатам x,y;
больше не предполагает, что все миниатюры имеют одинаковые размеры: за основу берет максимальные размеры (x,y) среди всех миниатюр, некоторые могут быть меньше;
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) # видимый размер окна, может # изменяться пользователем
thumbs = makeThumbs(imgdir) # [(imgfile, imgobj)]
numthumbs = len(thumbs) if not numcols:
numcols = int(math.ceil(math.sqrt(numthumbs))) # фиксир. или N x N numrows = int(math.ceil(numthumbs / numcols)) # истинное деление
# максимальная шир|выс: thumb=(name, obj), thumb.size=(width, height) linksize = max(max(thumb[1].size) for thumb in thumbs) trace(linksize)
fullsize = (0, 0, # X,Y верхн. левого угла
(linksize*numcols),(linksize*numrows)) # X,Y прав. нижнего угла
canvas.config(scrollregion=fullsize) # размер прокруч. области
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__’:
открываемый каталог = по умолчанию или из аргумента командной строки, иначе вывести простое окно с кнопкой для выбора каталога
imgdir = ‘images’
if len(sys.argv) > 1: imgdir = sys.argv[1]
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()
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011