Рубрики
Без рубрики

Добавление Viewer Exif на изображение просмотра изображения

Получите практические, реальные навыки Python на наших ресурсах и пути

Автор оригинала: Mike Driscoll.

На днях мы создали Простое изображение просмотра Отказ Сегодня мы создадим вторичный диалог для отображения данных EXIF Image, если у него есть какие-либо. Мы сделаем это таким образом, чтобы это окно обновило, когда мы изменим картинки, используя круто Pubsub модуль. Мы будем использовать версию, которая включена в WXPYPHON для этого приложения, но не стесняйтесь использовать автономную версию.

Отображение EXIF: простой способ

Сначала мы создадим действительно простую версию нового кадра, которую мы будем использовать для отображения данных EXIF для наших изображений. Это будет всего 11 кадров данных, включая имя файла и размер файла. Как только мы посмотрим, как работает этот код, мы добавим функциональность для отображения всех данных, возвращенных из нашего анализатора EXIF. Давайте начнем!

import os
import wx
from wx.lib.pubsub import Publisher

pil_flag = False
pyexif_flag = False

try:
    import exif
    pyexif_flag = True
except ImportError:
    try:
        from PIL import Image
        from PIL.ExifTags import TAGS
        pil_flag = True
    except ImportError:
        pass

#----------------------------------------------------------------------
def getExifData(photo):
    """
    Extracts the EXIF information from the provided photo
    """
    if pyexif_flag:
        exif_data = exif.parse(photo)
    elif pil_flag:
        exif_data  = {}
        i = Image.open(photo)
        info = i._getexif()
        for tag, value in info.items():
            decoded = TAGS.get(tag, tag)
            exif_data[decoded] = value
    else:
        raise Exception("PyExif and PIL not found!")
    return exif_data

#----------------------------------------------------------------------
def getPhotoSize(photo):
    """
    """
    photo_size = os.path.getsize(photo)
    photo_size = photo_size / 1024.0
    if photo_size > 1000:
        # photo is larger than 1 MB
        photo_size = photo_size / 1024.0
        size = "%0.2f MB" % photo_size
    else:
        size = "%d KB" % photo_size
    return size
    
########################################################################
class Photo:
    """"""

    #----------------------------------------------------------------------
    def __init__(self, photo):
        """Constructor"""
        self.exif_data = getExifData(photo)
        self.filename = os.path.basename(photo)
        self.filesize = getPhotoSize(photo)
    
    
########################################################################
class MainPanel(wx.Panel):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent, photo):
        """Constructor"""
        wx.Panel.__init__(self, parent)
        
        # dict of Exif keys and static text labels
        self.photo_data = {"ApertureValue":"Aperture", "DateTime":"Creation Date",
                           "ExifImageHeight":"Height", "ExifImageWidth":"Width",
                           "ExposureTime":"Exposure", "FNumber":"F-Stop",
                           "Flash":"Flash", "FocalLength":"Focal Length", 
                           "ISOSpeedRatings":"ISO", "Model":"Camera Model", 
                           "ShutterSpeedValue":"Shutter Speed"}
        
        # TODO: Display filesize too!
        self.exif_data = photo.exif_data
        self.filename = photo.filename
        self.filesize = photo.filesize
        Publisher().subscribe(self.updatePanel, ("update"))
        
        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
        self.layoutWidgets()
        self.SetSizer(self.mainSizer)
        
    #----------------------------------------------------------------------
    def layoutWidgets(self):
        """
        """
        ordered_widgets = ["Model", "ExifImageWidth", "ExifImageHeight",
                           "DateTime", "static_line",
                           "ApertureValue", "ExposureTime", "FNumber",
                           "Flash", "FocalLength", "ISOSpeedRatings",
                           "ShutterSpeedValue"
                           ]
        
        self.buildRow("Filename", self.filename, "Filename")
        self.buildRow("File Size", self.filesize, "FileSize")
        for key in ordered_widgets:
            if key not in self.exif_data and key != "static_line":
                continue
            if (key != "static_line"):
                self.buildRow(self.photo_data[key], self.exif_data[key], key)
            else:
                print "Adding staticLine"
                self.mainSizer.Add(wx.StaticLine(self), 0, wx.ALL|wx.EXPAND, 5)
        
    #----------------------------------------------------------------------
    def buildRow(self, label, value, txtName):
        """"""
        
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        lbl = wx.StaticText(self, label=label, size=(75, -1))
        txt = wx.TextCtrl(self, value=value, size=(150,-1),
                          style=wx.TE_READONLY, name=txtName)
        sizer.Add(lbl, 0, wx.ALL|wx.CENTER, 5)
        sizer.Add(txt, 0, wx.ALL, 5)
        self.mainSizer.Add(sizer)
        
    #----------------------------------------------------------------------
    def updatePanel(self, msg):
        """"""
        photo = msg.data
        self.exif_data = photo.exif_data
        
        children = self.GetChildren()
        for child in children:
            if isinstance(child, wx.TextCtrl):
                self.update(photo, child)
                    
    #----------------------------------------------------------------------
    def update(self, photo, txtWidget):
        """"""
        key = txtWidget.GetName()
        
        if key in self.exif_data:
            value = self.exif_data[key]
        else:
            value = "No Data"
            
        if key == "Filename":
            txtWidget.SetValue(photo.filename)
        elif key == "FileSize":
            txtWidget.SetValue(photo.filesize)
        else:
            txtWidget.SetValue(value)
    
########################################################################
class PhotoInfo(wx.Frame):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, photo_path):
        """Constructor"""
        wx.Frame.__init__(self, None, title="Image Information")
        photo = Photo(photo_path)
        panel = MainPanel(self, photo)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        sizer.Fit(self)
        
        Publisher().subscribe(self.updateDisplay, ("update display"))
        self.Show()
        
    #----------------------------------------------------------------------
    def updateDisplay(self, msg):
        """
        """
        photo = msg.data
        new_photo = Photo(photo)
        Publisher().sendMessage(("update"), new_photo)
        
#----------------------------------------------------------------------
if __name__ == "__main__":
    import wx.lib.inspection
    app = wx.PySimpleApp()
    photo = "path/to/test/photo.jpg"
    frame = PhotoInfo(photo)
    wx.lib.inspection.InspectionTool().Show()
    app.MainLoop()

Это много кода, чтобы перейти, поэтому мы просто достигли умеренных достопримечательностей. Во-первых, нам нужно импортировать немного библиотеки Python Exif. Я предпочитаю Pyexif, так как на этом легко начать, но если это не удается, то я проверю библиотеку визуализации Python (Pil) и импортировать его вместо этого. Если у вас есть какая-то другая библиотека, которую вам нравится, не стесняйтесь изменять код по мере необходимости. Как только у нас есть библиотека, которую мы хотим загружены, мы установили флаг, который мы будем использовать в нашем getexifdata функция. Вся эта функция делает анализ пропущенного на фотографии, чтобы получить данные exif (если у него есть), а затем возвращает эти данные как Диктовать Отказ У нас также есть GetPhotosize Функция, которую мы используем для расчета размера файла фото. Наконец, последнее произведение, прежде чем мы доберемся до нашего wxpython-кода, это Фото класс. Мы используем этот класс, чтобы пропустить данные по фотографии немного проще. Каждый экземпляр фото будет иметь следующие три атрибута:

  • exif_data.
  • имя файла
  • размер файла

Когда я впервые начал работать над этим кодом, я пытался пройти кучу данных о файлах, и он получил довольно раздражающую и грязную. Это намного чище, хотя, вероятно, было бы даже лучше, если мы поставим эти функции внутри самого класса фото. Я оставлю это как упражнение для читателя, хотя.

В нашем Mainpanel Класс, мы создаем photo_data Словарь, который содержит данные exif, которые, вероятно, наиболее полезны для пользователя. Вы можете изменить Dict по мере необходимости, если вы думаете, что там должно быть больше. В любом случае, ключевые ключи Диктома соответствуют некоторым ключам, возвращаемыми из нашего дампа EXIF. Значения станут ярлыками. В LayoutWidgets Метод, мы переиграем ключей Dict, и создаем «строки» двух виджетов кусочка: метка (WX.Statictext) и управление текстом (WX.TextCTRL). Мы используем ключ для параметра имени TextCtrls. Это важно при обновлении данных при выключании пользователей.

UpdatePanel и Обновить Методы, очевидно, используются для обновления виджетов панели, когда пользователь меняет фотографии в основном контроле. Это работает через приемник Pubsub, который мы создали в Mainpanel’s __init__. В этом случае мы фактически используем приемник Pubsub в Фотоинфо Объект кадров, чтобы вызвать приемник Mainpanel. Это было в основном, чтобы убедиться, что мы получили новый экземпляр фотографий, созданный в правой точке, но снова глядя на код, я думаю, что мы могли бы вырезать второй приемник в кадре и делать все в методах на панели. Есть еще один забавный проект обучения для вас, дорогой читатель.

Отображение данных EXIF: используйте ноутбук!

Есть много данных, которые мы просто игнорируем, если мы остановимся в этот момент. Тем не менее, есть настолько много дополнительных данных, что она не очень хорошо помещается в диалог этого размера. Одним простом решением было бы поставить данные на прокручиваемую панель, но мы собираемся пойти с виджетом ноутбука, так как многие люди не любят прокрутку. Конечно, вы можете бесплатно отправиться в приключение и использовать виджет ScrolledPanel сам по себе или в сочетании с идеей ноутбука. На данный момент, давайте сделаем гундук в этом сложном зверь:

import os
import wx
from wx.lib.pubsub import Publisher

pil_flag = False
pyexif_flag = False

try:
    import exif
    pyexif_flag = True
except ImportError:
    try:
        from PIL import Image
        from PIL.ExifTags import TAGS
        pil_flag = True
    except ImportError:
        pass

#----------------------------------------------------------------------
def getExifData(photo):
    """
    Extracts the EXIF information from the provided photo
    """
    if pyexif_flag:
        exif_data = exif.parse(photo)
    elif pil_flag:
        exif_data  = {}
        i = Image.open(photo)
        info = i._getexif()
        for tag, value in info.items():
            decoded = TAGS.get(tag, tag)
            exif_data[decoded] = value
    else:
        raise Exception("PyExif and PIL not found!")
    return exif_data

#----------------------------------------------------------------------
def getPhotoSize(photo):
    """
    Takes a photo path and returns the size of the photo
    """
    photo_size = os.path.getsize(photo)
    photo_size = photo_size / 1024.0
    if photo_size > 1000:
        # photo is larger than 1 MB
        photo_size = photo_size / 1024.0
        size = "%0.2f MB" % photo_size
    else:
        size = "%d KB" % photo_size
    return size
    
########################################################################
class Photo:
    """
    Class to hold information about the passed in photo
    """

    #----------------------------------------------------------------------
    def __init__(self, photo):
        """Constructor"""
        self.exif_data = getExifData(photo)
        self.filename = os.path.basename(photo)
        self.filesize = getPhotoSize(photo)
    
    
########################################################################
class NBPanel(wx.Panel):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent, photo, panelOne=False):
        """Constructor"""
        wx.Panel.__init__(self, parent)
        
        self.panelOne = panelOne
        # dict of Exif keys and static text labels
        self.photo_data = {"ApertureValue":"Aperture", "DateTime":"Creation Date",
                           "ExifImageHeight":"Height", "ExifImageWidth":"Width",
                           "ExposureTime":"Exposure", "FNumber":"F-Stop",
                           "Flash":"Flash", "FocalLength":"Focal Length", 
                           "ISOSpeedRatings":"ISO", "Model":"Camera Model", 
                           "ShutterSpeedValue":"Shutter Speed"}
       
        self.exif_data = photo.exif_data
        self.filename = photo.filename
        self.filesize = photo.filesize
        Publisher().subscribe(self.updatePanel, ("update"))
        
        self.mainSizer = wx.BoxSizer(wx.VERTICAL)
        self.layoutWidgets()
        self.SetSizer(self.mainSizer)
        
    #----------------------------------------------------------------------
    def layoutWidgets(self):
        """
        Create and layout the various widgets on the panel
        """
        ordered_widgets = ["Model", "ExifImageWidth", "ExifImageHeight",
                           "DateTime", "static_line",
                           "ApertureValue", "ExposureTime", "FNumber",
                           "Flash", "FocalLength", "ISOSpeedRatings",
                           "ShutterSpeedValue"
                           ]
        if self.panelOne:
            self.buildRow("Filename", self.filename, "Filename")
            self.buildRow("File Size", self.filesize, "FileSize")
            for key in ordered_widgets:
                if key not in self.exif_data:
                    continue
                if key != "static_line":
                    self.buildRow(self.photo_data[key], self.exif_data[key], key)
        else:
            keys = self.exif_data.keys()
            keys.sort()
            print "keys for second panel:"
            print keys
            for key in keys:
                if key not in self.exif_data:
                    continue
                if key not in ordered_widgets and "Tag" not in key:
                    self.buildRow(key, self.exif_data[key], key)
        
    #----------------------------------------------------------------------
    def buildRow(self, label, value, txtName):
        """
        Build a two widget row and add it to the main sizer
        """
        
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        if self.panelOne:
            lbl = wx.StaticText(self, label=label, size=(75, -1))
        else: 
            lbl = wx.StaticText(self, label=label, size=(150, -1))
        txt = wx.TextCtrl(self, value=value, size=(150,-1),
                          style=wx.TE_READONLY, name=txtName)
        sizer.Add(lbl, 0, wx.ALL|wx.CENTER, 5)
        sizer.Add(txt, 0, wx.ALL, 5)
        self.mainSizer.Add(sizer)
        
    #----------------------------------------------------------------------
    def updatePanel(self, msg):
        """
        Iterate over the children widgets in the panel and update the 
        text control's values via the "update" method
        """
        photo = msg.data
        self.exif_data = photo.exif_data
        
        children = self.GetChildren()
        for child in children:
            if isinstance(child, wx.TextCtrl):
                self.update(photo, child)
                    
    #----------------------------------------------------------------------
    def update(self, photo, txtWidget):
        """
        Updates the text control's values
        """
        key = txtWidget.GetName()
        
        if key in self.exif_data:
            value = self.exif_data[key]
        else:
            value = "No Data"
            
        if self.panelOne:
            if key == "Filename":
                txtWidget.SetValue(photo.filename)
            elif key == "FileSize":
                txtWidget.SetValue(photo.filesize)
            else:
                txtWidget.SetValue(value)
        else:
            txtWidget.SetValue(value)
        
########################################################################
class InfoNotebook(wx.Notebook):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, parent, photo):
        """Constructor"""
        wx.Notebook.__init__(self, parent, style=wx.BK_BOTTOM)
        
        self.tabOne = NBPanel(self, photo, panelOne=True)
        self.tabTwo = NBPanel(self, photo)
        self.AddPage(self.tabOne, "Main Info")
        self.AddPage(self.tabTwo, "More Info")
        Publisher().subscribe(self.updateDisplay, ("update display"))
        
    #----------------------------------------------------------------------
    def updateDisplay(self, msg):
        """
        Catches the PubSub's "event", creates a new photo instance and
        passes that info to the panel so it can update
        """
        photo = msg.data
        new_photo = Photo(photo)
        Publisher().sendMessage(("update"), new_photo)
    
    
########################################################################
class PhotoInfo(wx.Frame):
    """"""

    #----------------------------------------------------------------------
    def __init__(self, photo_path):
        """Constructor"""
        wx.Frame.__init__(self, None, title="Image Information")
        photo = Photo(photo_path)
        panel = wx.Panel(self)
        notebook = InfoNotebook(panel, photo)
        
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(notebook, 1, wx.EXPAND)
        panel.SetSizer(sizer)
        
        mainSizer.Add(panel)
        self.SetSizer(mainSizer)
        mainSizer.Fit(self)
        self.Show()
        
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    photo = r'path/to/photo.jpg'
    frame = PhotoInfo(photo)
    app.MainLoop()    

Этот код может сделать с хорошим рефакторингом, но он работает довольно хорошо. Давайте перейдем на различия между этой версией и последним. Во-первых, мы меняем Mainpanel к Nbpanel Потому что теперь это будет панель/вкладка ноутбука. Мы также добавляем дополнительный аргумент в init, а также позволить нам сделать первую панель отличаться от второго (то есть. Panelone ). Тогда в LayoutWidgets Метод, мы используем флаг Panelone, чтобы поставить ordered_widgets На первой вкладке и остальной части данных EXIF на втором. Мы также изменили Обновить метод почти так же. Наконец, мы добавили Infonotebook Класс для создания виджета ноутбука, и мы поместим его экземпляра на нашем кадре. Если вы наступили с данными EXIF, то вы знаете, что есть пара полей, которые достаточно долго. Мы должны поставить их в многострочные текстовые элементы управления. Вот один из способов сделать это, изменив наши BuildRow Способ слегка:

def buildRow(self, label, value, txtName):
    """
    Build a two widget row and add it to the main sizer
    """
    
    sizer = wx.BoxSizer(wx.HORIZONTAL)
    if self.panelOne:
        lbl = wx.StaticText(self, label=label, size=(75, -1))
    else: 
        lbl = wx.StaticText(self, label=label, size=(150, -1))
    if len(value) < 40:
        txt = wx.TextCtrl(self, value=value, size=(150,-1),
                          style=wx.TE_READONLY, name=txtName)
    else:
        txt = wx.TextCtrl(self, value=value, size=(150,60),
                          style=wx.TE_READONLY|wx.TE_MULTILINE,
                          name=txtName)
    sizer.Add(lbl, 0, wx.ALL|wx.CENTER, 5)
    sizer.Add(txt, 0, wx.ALL, 5)
    self.mainSizer.Add(sizer)

В этом фрагменте мы только что добавили заявление, чтобы проверить, было ли длина значения менее 40 символов. Если это так, мы создали нормальный текстовый контроль; В противном случае мы создали многослойный текстовый контроль. Теперь не так просто? Вот что выглядит вторая вкладка:

Обертывание

Последняя часть, на которой нам нужно посмотреть, – это то, что можно изменить в основной программе. По сути, нам просто нужно добавить кнопку «Информация», создать наш Viewer EXIF и показать его в обработчике событий кнопки. Что-то вроде этого будет работать:

frame = photoInfo.PhotoInfo(self.currentPicturePath)
frame.Show()

Чтобы обновить Viewer EXIF, нам нужно отправить ему сообщение PUBSUB. Мы могли бы отправить сообщение в предыдущие и следующие события кнопки, но тогда нам придется поддерживать код в двух местах. Вместо этого мы поставим новый код в LoadImage Способ и отправьте сообщение подобное:

Publisher().sendMessage("update display", self.currentPicturePath)

Это все, что есть к этому. Я включил полный исходный код ниже в разделе Загрузки. Надеюсь, это помогло вам увидеть, насколько легко добавить новую функцию в проект GUI, а также как отобразить данные EXIF с помощью WXPYHTHON.

Загрузки

  • exif_viewer.zip
  • exif_viewer.tar.