geekdoc-python-zh/docs/pythonlibrary/adding-an-exif-viewer-to-th...

21 KiB
Raw Permalink Blame History

将 EXIF 浏览器添加到图像浏览器

原文:https://www.blog.pythonlibrary.org/2010/04/10/adding-an-exif-viewer-to-the-image-viewer/

前几天,我们创建了一个简单的图像查看器。今天,我们将创建一个辅助对话框来显示图像的 EXIF 数据,如果有的话。我们将这样做,当我们使用酷的发布模块改变图片时,这个窗口将会更新。对于这个应用程序,我们将使用 wxPython 中包含的版本,但是也可以随意使用独立版本。

展示 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 数据(如果有的话),然后将该数据作为一个 dict 返回。我们还有一个 getPhotoSize 函数,用来计算照片的文件大小。最后,在我们进入 wxPython 代码之前的最后一部分是 Photo 类。我们使用这个类来使照片数据的传递变得更加容易。每个照片实例将具有以下三个属性:

  • exif_data
  • 文件名
  • filesize

当我第一次开始编写这段代码时,我试图传递一堆关于文件的数据,这变得非常烦人和混乱。这要干净得多,尽管如果我们将这些函数放在 Photo 类本身中可能会更好。不过,我将把它留给读者作为练习。

在我们的 MainPanel 类中,我们创建了一个 photo_data 字典,其中保存了可能对用户最有用的 EXIF 数据。如果你认为应该有更多的你可以根据需要修改字典。总之dict 的键对应于从 exif 转储中返回的一些键。这些值将成为标签。在 layoutWidgets 方法中,我们迭代 dict 的键并创建由两个小部件组成的“行”:一个标签(wx。StaticText)和一个文本控件(wx。TextCtrl)。我们使用 TextCtrls 的 name 参数的键。这在用户切换图片时更新数据时很重要。

updatePanelupdate 方法显然是用来在用户更改主控件中的照片时更新面板的小部件。这通过我们在主面板的 init 中创建的 pubsub 接收器来实现。在这种情况下,我们实际上使用了 PhotoInfo frame 对象中的 pubsub 接收器来调用主面板的接收器。这主要是为了确保我们在正确的点上创建了一个新的照片实例,但是再看一下代码,我认为我们可以去掉框架中的第二个接收器,在面板的方法中做所有的事情。亲爱的读者,你还有一个有趣的学习项目。

显示 EXIF 数据:使用笔记本!

如果我们就此打住,就会忽略很多数据。然而,有太多的额外数据,它不太适合这种大小的对话框。一个简单的解决方案是将数据放在一个可滚动的面板上,但是我们打算用一个笔记本小部件来代替,因为很多人不喜欢滚动。当然,您可以自由地进行一次冒险,单独使用 ScrolledPanel 小部件或将其与 notebook 的想法结合使用。不过现在,让我们来看看这个复杂的野兽:


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()    

这段代码可以做一个很好的重构,但它工作得很好。让我们来看看这个版本和上一个版本之间的区别。首先,我们将主面板改为次面板,因为它现在将是一个笔记本面板/选项卡。我们还向 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)

在这个代码片段中,我们只是添加了一个 IF 语句来检查值的长度是否少于 40 个字符。如果是这样,我们创建了一个普通的文本控件;否则我们创建了一个多行文本控件。这不是很容易吗?下面是第二个选项卡现在的样子:

包扎

我们需要查看的最后一部分是主程序中需要更改的内容。基本上,我们只需要添加一个信息按钮,实例化我们的 EXIF 浏览器,并在按钮的事件处理程序中显示它。类似这样的事情会起作用:


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

要更新 EXIF 浏览器,我们需要向它发送一条 Pubsub 消息。我们可以在 previous 和 next 按钮事件中发送消息,但是我们必须在两个地方维护代码。相反,我们将把新代码放在 loadImage 方法中,并像这样发送消息:


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

这就是全部了。我在下面的下载小节中包含了完整的源代码。希望这有助于您了解向 GUI 项目添加新特性是多么容易,以及如何使用 wxPython 显示 EXIF 数据。

下载