Merge pull request #1263 from brucejackson/Imagemetadata-grampet
| Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 289 KiB | 
| Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 607 KiB | 
| Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 408 KiB | 
| Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 145 KiB | 
| Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 645 KiB | 
							
								
								
									
										22
									
								
								example/gramps/image_credits.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | |||||||
|  | # Credits for Images used in Gramps Example database # | ||||||
|  |  | ||||||
|  | # O0.jpg ## | ||||||
|  | "Riga Old Man" by liber is marked with CC BY-SA 2.0. To view the terms, visit https://creativecommons.org/licenses/by-sa/2.0/?ref=openverse  | ||||||
|  | - https://www.flickr.com/photos/51035655291@N01/200858281 | ||||||
|  |  | ||||||
|  | ## O2.jpg ## | ||||||
|  | "Brothers by Robert Boning (c.1870)" by pellethepoet is marked with CC BY 2.0. To view the terms, visit https://creativecommons.org | ||||||
|  | - https://www.flickr.com/photos/47201412@N02/12675131334 | ||||||
|  |  | ||||||
|  | ## O3.jpg ## | ||||||
|  | "Portrait of a Canon-man" by mescon is marked with CC BY 2.0. To view the terms, visit https://creativecommons.org/licenses/by/2.0/?ref=openverse  | ||||||
|  | - https://www.flickr.com/photos/23666014@N08/3790571906 | ||||||
|  |  | ||||||
|  | ## 04.jpg ## | ||||||
|  | "Woman Photographer" by pedrosimoes7 is marked with CC BY 2.0. To view the terms, visit https://creativecommons.org/licenses/by/2.0/?ref=openverse  | ||||||
|  | - https://www.flickr.com/photos/46944516@N00/6872425924 | ||||||
|  |  | ||||||
|  | ## O5.jpg ## | ||||||
|  | "Happy couple" by pedrosimoes7 is marked with CC BY 2.0. To view the terms, visit https://creativecommons.org/licenses/by/2.0/?ref=openverse  | ||||||
|  | - https://www.flickr.com/photos/46944516@N00/4198095383 | ||||||
|  |  | ||||||
| @@ -2,8 +2,9 @@ | |||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| # Gramps - a GTK+/GNOME based genealogy program | # Gramps - a GTK+/GNOME based genealogy program | ||||||
| # | # | ||||||
| # Copyright (C) 2011      Nick Hall | # Copyright (C) 2011,2014  Nick Hall | ||||||
| # Copyright (C) 2011       Rob G. Healey <robhealey1@gmail.com> | # Copyright (C) 2011       Rob G. Healey <robhealey1@gmail.com> | ||||||
|  | # Copyright (C) 2022       Bruce Jackson | ||||||
| # | # | ||||||
| # This program is free software; you can redistribute it and/or modify | # This program is free software; you can redistribute it and/or modify | ||||||
| # it under the terms of the GNU General Public License as published by | # it under the terms of the GNU General Public License as published by | ||||||
| @@ -25,6 +26,10 @@ | |||||||
| # | # | ||||||
| #------------------------------------------------------------------------- | #------------------------------------------------------------------------- | ||||||
| import os | import os | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | _LOG = logging.getLogger(".libmetadata") | ||||||
|  |  | ||||||
|  |  | ||||||
| #------------------------------------------------------------------------- | #------------------------------------------------------------------------- | ||||||
| # | # | ||||||
| @@ -35,6 +40,10 @@ from gi.repository import Gtk | |||||||
| import gi | import gi | ||||||
| gi.require_version('GExiv2', '0.10') | gi.require_version('GExiv2', '0.10') | ||||||
| from gi.repository import GExiv2 | from gi.repository import GExiv2 | ||||||
|  | from gi.repository import Gdk | ||||||
|  | from gi.repository import GdkPixbuf | ||||||
|  | from gi.repository import GObject | ||||||
|  |  | ||||||
|  |  | ||||||
| #------------------------------------------------------------------------- | #------------------------------------------------------------------------- | ||||||
| # | # | ||||||
| @@ -42,7 +51,7 @@ from gi.repository import GExiv2 | |||||||
| # | # | ||||||
| #------------------------------------------------------------------------- | #------------------------------------------------------------------------- | ||||||
|  |  | ||||||
| from gramps.gui.listmodel import ListModel | from gramps.gui.listmodel import ListModel, NOSORT, IMAGE as COL_IMAGE | ||||||
| from gramps.gen.const import GRAMPS_LOCALE as glocale | from gramps.gen.const import GRAMPS_LOCALE as glocale | ||||||
| _ = glocale.translation.gettext | _ = glocale.translation.gettext | ||||||
| from gramps.gen.utils.place import conv_lat_lon | from gramps.gen.utils.place import conv_lat_lon | ||||||
| @@ -51,15 +60,19 @@ from gramps.gen.lib import Date | |||||||
| from gramps.gen.datehandler import displayer | from gramps.gen.datehandler import displayer | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
|  | THUMBNAIL_IMAGE_SIZE = (50, 50) | ||||||
|  |  | ||||||
| def format_datetime(datestring): | def format_datetime(datestring): | ||||||
|     """ |     """ | ||||||
|     Convert an exif timestamp into a string for display, using the |     Convert an exif timestamp into a string for display, using the | ||||||
|     standard Gramps date format. |     standard Gramps date format.  Function not used for XMP Date Metatags: | ||||||
|  |     https://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#date-value-type | ||||||
|     """ |     """ | ||||||
|     try: |     try: | ||||||
|         timestamp = datetime.strptime(datestring, '%Y:%m:%d %H:%M:%S') |         timestamp = datetime.strptime(datestring, '%Y:%m:%d %H:%M:%S') | ||||||
|     except ValueError: |     except ValueError: | ||||||
|         return _('Invalid format') |         return _('Invalid format') | ||||||
|  |  | ||||||
|     date_part = Date() |     date_part = Date() | ||||||
|     date_part.set_yr_mon_day(timestamp.year, timestamp.month, timestamp.day) |     date_part.set_yr_mon_day(timestamp.year, timestamp.month, timestamp.day) | ||||||
|     date_str = displayer.display(date_part) |     date_str = displayer.display(date_part) | ||||||
| @@ -97,23 +110,61 @@ def format_gps(raw_dms, nsew): | |||||||
|  |  | ||||||
|     return result if result is not None else _('Invalid format') |     return result if result is not None else _('Invalid format') | ||||||
|  |  | ||||||
| DESCRIPTION = _('Description') |  | ||||||
| IMAGE = _('Image') |  | ||||||
| CAMERA = _('Camera') | DESCRIPTION = _('Descriptive Tags') | ||||||
| GPS = _('GPS') | DATE = _('Date and Time Tags') | ||||||
| ADVANCED = _('Advanced') | PEOPLE = _('People Tags') | ||||||
|  | EVENT = _('Event Tags') | ||||||
|  | IMAGE = _('Image Tags') | ||||||
|  | CAMERA = _('Camera Information') | ||||||
|  | LOCATION = _('Location Tags') | ||||||
|  | ADVANCED = _('Advanced Tags') | ||||||
|  | RIGHTS = _('Rights Tags') | ||||||
|  | TAGGING = _('Keyword Tags') | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | List of tags available to plugin can be found at the Exiv2 project | ||||||
|  | https://www.exiv2.org/metadata.html | ||||||
|  | """ | ||||||
|  |  | ||||||
| TAGS = [(DESCRIPTION, 'Exif.Image.ImageDescription', None, None), | TAGS = [(DESCRIPTION, 'Exif.Image.ImageDescription', None, None), | ||||||
|         (DESCRIPTION, 'Exif.Image.Artist', None, None), |  | ||||||
|         (DESCRIPTION, 'Exif.Image.Copyright', None, None), |  | ||||||
|         (DESCRIPTION, 'Exif.Photo.DateTimeOriginal', None, format_datetime), |  | ||||||
|         (DESCRIPTION, 'Exif.Photo.DateTimeDigitized', None, format_datetime), |  | ||||||
|         (DESCRIPTION, 'Exif.Image.DateTime', None, format_datetime), |  | ||||||
|         (DESCRIPTION, 'Exif.Image.TimeZoneOffset', None, None), |  | ||||||
|         (DESCRIPTION, 'Exif.Image.XPSubject', None, None), |         (DESCRIPTION, 'Exif.Image.XPSubject', None, None), | ||||||
|         (DESCRIPTION, 'Exif.Image.XPComment', None, None), |         (DESCRIPTION, 'Exif.Image.XPComment', None, None), | ||||||
|         (DESCRIPTION, 'Exif.Image.XPKeywords', None, None), |  | ||||||
|         (DESCRIPTION, 'Exif.Image.Rating', None, None), |         (DESCRIPTION, 'Exif.Image.Rating', None, None), | ||||||
|  |         (DESCRIPTION, 'Xmp.dc.title', None, None), | ||||||
|  |         (DESCRIPTION, 'Xmp.dc.description', None, None), | ||||||
|  |         (DESCRIPTION, 'Xmp.dc.subject', None, None), | ||||||
|  |         (DESCRIPTION, 'Xmp.acdsee.caption', None, None), | ||||||
|  |         (DESCRIPTION, 'Xmp.acdsee.notes', None, None), | ||||||
|  |         (DESCRIPTION, 'Iptc.Application2.Caption', None, None), | ||||||
|  |         (DESCRIPTION, 'Exif.Photo.UserComment', None, None), | ||||||
|  |         (DESCRIPTION, 'Xmp.iptcExt.AOTitle', None, None), | ||||||
|  |         (DATE, 'Exif.Photo.DateTimeOriginal', None, format_datetime), | ||||||
|  |         (DATE, 'Exif.Photo.DateTimeDigitized', None, format_datetime), | ||||||
|  |         (DATE, 'Exif.Image.DateTime', None, format_datetime), | ||||||
|  |         (DATE, 'Exif.Image.TimeZoneOffset', None, None), | ||||||
|  |         (DATE, 'Xmp.Xmp.CreateDate', None, None), | ||||||
|  |         (DATE, 'Xmp.photoshop.DateCreated', None, None), | ||||||
|  |         (PEOPLE, 'Xmp.mwg-rs.Regions/mwg-rs:RegionList[1]/mwg-rs:Name', None, None), | ||||||
|  |         (PEOPLE, 'Xmp.mwg-rs.Regions', None, None), | ||||||
|  |         (PEOPLE, 'Xmp.iptcExt.PersonInImage', None, None), | ||||||
|  |         (EVENT, 'Xmp.iptcExt.Event', None, None), | ||||||
|  |         (LOCATION, 'Xmp.iptcExt.LocationShown', None, None), | ||||||
|  |         (LOCATION, 'Exif.GPSInfo.GPSLatitude',  'Exif.GPSInfo.GPSLatitudeRef', format_gps), | ||||||
|  |         (LOCATION, 'Exif.GPSInfo.GPSLongitude', 'Exif.GPSInfo.GPSLongitudeRef', format_gps), | ||||||
|  |         (LOCATION, 'Exif.GPSInfo.GPSAltitude', 'Exif.GPSInfo.GPSAltitudeRef', None), | ||||||
|  |         (LOCATION, 'Exif.GPSInfo.GPSTimeStamp', None, None), | ||||||
|  |         (LOCATION, 'Exif.GPSInfo.GPSSatellites', None, None), | ||||||
|  |         (TAGGING, 'Exif.Image.XPKeywords', None, None), | ||||||
|  |         (TAGGING, 'Iptc.Application2.Keywords', None, None), | ||||||
|  |         (TAGGING, 'Xmp.mwg-kw.Hierarchy', None, None), | ||||||
|  |         (TAGGING, 'Xmp.mwg-kw.Keywords', None, None), | ||||||
|  |         (TAGGING, 'Xmp.digiKam.TagsList', None, None), | ||||||
|  |         (TAGGING, 'Xmp.MicrosoftPhoto.LastKeywordXMP', None, None), | ||||||
|  |         (TAGGING, 'Xmp.MicrosoftPhoto.LastKeywordIPTC', None, None), | ||||||
|  |         (TAGGING, 'Xmp.lr.hierarchicalSubject', None, None), | ||||||
|  |         (TAGGING, 'Xmp.acdsee.categories', None, None), | ||||||
|         (IMAGE, 'Exif.Image.DocumentName', None, None), |         (IMAGE, 'Exif.Image.DocumentName', None, None), | ||||||
|         (IMAGE, 'Exif.Photo.PixelXDimension', None, None), |         (IMAGE, 'Exif.Photo.PixelXDimension', None, None), | ||||||
|         (IMAGE, 'Exif.Photo.PixelYDimension', None, None), |         (IMAGE, 'Exif.Photo.PixelYDimension', None, None), | ||||||
| @@ -126,6 +177,11 @@ TAGS = [(DESCRIPTION, 'Exif.Image.ImageDescription', None, None), | |||||||
|         (IMAGE, 'Exif.Image.Compression', None, None), |         (IMAGE, 'Exif.Image.Compression', None, None), | ||||||
|         (IMAGE, 'Exif.Photo.CompressedBitsPerPixel', None, None), |         (IMAGE, 'Exif.Photo.CompressedBitsPerPixel', None, None), | ||||||
|         (IMAGE, 'Exif.Image.PhotometricInterpretation', None, None), |         (IMAGE, 'Exif.Image.PhotometricInterpretation', None, None), | ||||||
|  |         (RIGHTS, 'Exif.Image.Copyright', None, None), | ||||||
|  |         (RIGHTS, 'Exif.Image.Artist', None, None), | ||||||
|  |         (RIGHTS, 'Xmp.xmpRights.Owner', None, None), | ||||||
|  |         (RIGHTS, 'Xmp.xmpRights.UsageTerms', None, None), | ||||||
|  |         (RIGHTS, 'Xmp.xmpRights.WebStatement', None, None), | ||||||
|         (CAMERA, 'Exif.Image.Make', None, None), |         (CAMERA, 'Exif.Image.Make', None, None), | ||||||
|         (CAMERA, 'Exif.Image.Model', None, None), |         (CAMERA, 'Exif.Image.Model', None, None), | ||||||
|         (CAMERA, 'Exif.Photo.FNumber', None, None), |         (CAMERA, 'Exif.Photo.FNumber', None, None), | ||||||
| @@ -147,14 +203,6 @@ TAGS = [(DESCRIPTION, 'Exif.Image.ImageDescription', None, None), | |||||||
|         (CAMERA, 'Exif.Photo.Sharpness', None, None), |         (CAMERA, 'Exif.Photo.Sharpness', None, None), | ||||||
|         (CAMERA, 'Exif.Photo.WhiteBalance', None, None), |         (CAMERA, 'Exif.Photo.WhiteBalance', None, None), | ||||||
|         (CAMERA, 'Exif.Photo.DigitalZoomRatio', None, None), |         (CAMERA, 'Exif.Photo.DigitalZoomRatio', None, None), | ||||||
|         (GPS, 'Exif.GPSInfo.GPSLatitude', |  | ||||||
|               'Exif.GPSInfo.GPSLatitudeRef', format_gps), |  | ||||||
|         (GPS, 'Exif.GPSInfo.GPSLongitude', |  | ||||||
|               'Exif.GPSInfo.GPSLongitudeRef', format_gps), |  | ||||||
|         (GPS, 'Exif.GPSInfo.GPSAltitude', |  | ||||||
|               'Exif.GPSInfo.GPSAltitudeRef', None), |  | ||||||
|         (GPS, 'Exif.GPSInfo.GPSTimeStamp', None, None), |  | ||||||
|         (GPS, 'Exif.GPSInfo.GPSSatellites', None, None), |  | ||||||
|         (ADVANCED, 'Exif.Image.Software', None, None), |         (ADVANCED, 'Exif.Image.Software', None, None), | ||||||
|         (ADVANCED, 'Exif.Photo.ImageUniqueID', None, None), |         (ADVANCED, 'Exif.Photo.ImageUniqueID', None, None), | ||||||
|         (ADVANCED, 'Exif.Image.CameraSerialNumber', None, None), |         (ADVANCED, 'Exif.Image.CameraSerialNumber', None, None), | ||||||
| @@ -169,51 +217,66 @@ class MetadataView(Gtk.TreeView): | |||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         Gtk.TreeView.__init__(self) |         Gtk.TreeView.__init__(self) | ||||||
|         self.sections = {} |         self.sections = {} | ||||||
|         titles = [(_('Key'), 1, 235), |         titles = [(_('Namespace'), 0, 150), | ||||||
|                   (_('Value'), 2, 325)] |                   (_('Label'), 1, 150), | ||||||
|  |                   (_(' '), NOSORT, 60, COL_IMAGE), | ||||||
|  |                   (_('Value'), NOSORT, 325)] | ||||||
|  |  | ||||||
|         self.model = ListModel(self, titles, list_mode="tree") |         self.model = ListModel(self, titles, list_mode="tree") | ||||||
|  |  | ||||||
|     def display_exif_tags(self, full_path): |  | ||||||
|  |     def display_exif_tags(self, image_path): | ||||||
|         """ |         """ | ||||||
|         Display the exif tags. |         Display the exif tags. | ||||||
|         """ |         """ | ||||||
|         self.sections = {} |         self.sections = {} | ||||||
|  |  | ||||||
|  |         # set fixed_height_mode to FALSE so thumbnails are not truncated. | ||||||
|  |         self.model.tree.set_fixed_height_mode(False) | ||||||
|         self.model.clear() |         self.model.clear() | ||||||
|  |  | ||||||
|         if not os.path.exists(full_path): |         if not os.path.exists(image_path): | ||||||
|             return False |             return False | ||||||
|  |  | ||||||
|         retval = False |         retval = False | ||||||
|         with open(full_path, 'rb') as fd: |         with open(image_path, 'rb') as fd: | ||||||
|             try: |             try: | ||||||
|                 buf = fd.read() |                 buf = fd.read() | ||||||
|                 metadata = GExiv2.Metadata() |                 metadata = GExiv2.Metadata() | ||||||
|                 metadata.open_buf(buf) |                 metadata.open_buf(buf) | ||||||
|  |                 self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path) | ||||||
|                 get_human = metadata.get_tag_interpreted_string |                 get_human = metadata.get_tag_interpreted_string | ||||||
|  |  | ||||||
|                 for section, key, key2, func in TAGS: |                 for section, key, key2, func in TAGS: | ||||||
|                     if not key in metadata.get_exif_tags(): |                     if not key in self.__get_all_tags(metadata): | ||||||
|                         continue |                         continue | ||||||
|  |  | ||||||
|                     if func is not None: |                     if func is not None: | ||||||
|                         if key2 is None: |                         if key2 is None: | ||||||
|                             human_value = func(metadata[key]) |                             human_value = func(metadata[key]) | ||||||
|                         else: |                         else: | ||||||
|                             if key2 in metadata.get_exif_tags(): |                             if key2 in self.__get_all_tags(metadata): | ||||||
|                                 human_value = func(metadata[key], metadata[key2]) |                                 human_value = func(metadata[key], metadata[key2]) | ||||||
|                             else: |                             else: | ||||||
|                                 human_value = func(metadata[key], None) |                                 human_value = func(metadata[key], None) | ||||||
|                     else: |                     else: | ||||||
|                         human_value = get_human(key) |                         human_value = get_human(key) | ||||||
|                         if key2 in metadata.get_exif_tags(): |                         if key2 in self.__get_all_tags(metadata): | ||||||
|                             human_value += ' ' + get_human(key2) |                             human_value += ' ' + get_human(key2) | ||||||
|  |  | ||||||
|                     label = metadata.get_tag_label(key) |  | ||||||
|                     node = self.__add_section(section) |  | ||||||
|                     if human_value is None: |                     if human_value is None: | ||||||
|                         human_value = '' |                         human_value = '' | ||||||
|                     self.model.add((label, human_value), node=node) |  | ||||||
|  |                     # If first named region is found - find all named regions | ||||||
|  |                     if key == 'Xmp.mwg-rs.Regions/mwg-rs:RegionList[1]/mwg-rs:Name': | ||||||
|  |                         self.__get_named_regions(metadata) | ||||||
|  |                         continue | ||||||
|  |  | ||||||
|  |                     label = metadata.get_tag_label(key) | ||||||
|  |                     namespace = self.__get_tag_namespace(key) | ||||||
|  |  | ||||||
|  |                     node = self.__add_section(section) | ||||||
|  |                     self.model.add([namespace, label, None, human_value], node=node) | ||||||
|  |  | ||||||
|                     self.model.tree.expand_all() |                     self.model.tree.expand_all() | ||||||
|                     retval = self.model.count > 0 |                     retval = self.model.count > 0 | ||||||
| @@ -227,29 +290,131 @@ class MetadataView(Gtk.TreeView): | |||||||
|         Add the section heading node to the model. |         Add the section heading node to the model. | ||||||
|         """ |         """ | ||||||
|         if section not in self.sections: |         if section not in self.sections: | ||||||
|             node = self.model.add([section, '']) |             node = self.model.add([section, '', None, '']) | ||||||
|             self.sections[section] = node |             self.sections[section] = node | ||||||
|         else: |         else: | ||||||
|             node = self.sections[section] |             node = self.sections[section] | ||||||
|         return node |         return node | ||||||
|  |  | ||||||
|     def get_has_data(self, full_path): |     def get_has_data(self, image_path): | ||||||
|         """ |         """ | ||||||
|         Return True if the gramplet has data, else return False. |         Return True if the gramplet has data, else return False. | ||||||
|         """ |         """ | ||||||
|         if not os.path.exists(full_path): |         if not os.path.exists(image_path): | ||||||
|             return False |             return False | ||||||
|         with open(full_path, 'rb') as fd: |         with open(image_path, 'rb') as fd: | ||||||
|             retval = False |             retval = False | ||||||
|             try: |             try: | ||||||
|                 buf = fd.read() |                 buf = fd.read() | ||||||
|                 metadata = GExiv2.Metadata() |                 metadata = GExiv2.Metadata() | ||||||
|                 metadata.open_buf(buf) |                 metadata.open_buf(buf) | ||||||
|                 for tag in TAGS: |                 for tag in TAGS: | ||||||
|                     if tag in metadata.get_exif_tags(): |                     if tag in self.__get_all_tags(metadata): | ||||||
|                         retval = True |                         retval = True | ||||||
|                         break |                         break | ||||||
|             except: |             except: | ||||||
|                 pass |                 pass | ||||||
|  |  | ||||||
|         return retval |         return retval | ||||||
|  |  | ||||||
|  |     def __get_all_tags(self, metadata): | ||||||
|  |         """ | ||||||
|  |         Return a list of all XMP, IPTC and EXIF tags in the media file | ||||||
|  |         """ | ||||||
|  |         tag_list = metadata.get_exif_tags() + metadata.get_xmp_tags() + metadata.get_iptc_tags() | ||||||
|  |  | ||||||
|  |         return tag_list | ||||||
|  |  | ||||||
|  |     def __get_named_regions(self, metadata): | ||||||
|  |         """ | ||||||
|  |         Retrieve all XMP named regions in an image and populate the treeview row. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         region_tag = 'Xmp.mwg-rs.Regions/mwg-rs:RegionList[%s]/' | ||||||
|  |         region_name = region_tag + 'mwg-rs:Name' | ||||||
|  |         region_x = region_tag + 'mwg-rs:Area/stArea:x' | ||||||
|  |         region_y = region_tag + 'mwg-rs:Area/stArea:y' | ||||||
|  |         region_w = region_tag + 'mwg-rs:Area/stArea:w' | ||||||
|  |         region_h = region_tag + 'mwg-rs:Area/stArea:h' | ||||||
|  |  | ||||||
|  |         pixbuf_width = self.pixbuf.get_width() | ||||||
|  |         pixbuf_height = self.pixbuf.get_height() | ||||||
|  |  | ||||||
|  |         i = 1 | ||||||
|  |         while True: | ||||||
|  |             name = metadata.get(region_name % i) | ||||||
|  |             region_name_display = region_name % i | ||||||
|  |  | ||||||
|  |             if name is None: | ||||||
|  |                 break | ||||||
|  |             try: | ||||||
|  |                 x = float(metadata.get(region_x % i)) * pixbuf_width | ||||||
|  |                 y = float(metadata.get(region_y % i)) * pixbuf_height | ||||||
|  |                 w = float(metadata.get(region_w % i)) * pixbuf_width | ||||||
|  |                 h = float(metadata.get(region_h % i)) * pixbuf_height | ||||||
|  |             except ValueError: | ||||||
|  |                 x = pixbuf_width /2 | ||||||
|  |                 y = pixbuf_height / 2 | ||||||
|  |                 w = pixbuf_width | ||||||
|  |                 h = pixbuf_height | ||||||
|  |  | ||||||
|  |             # ensure region does not exceed bounds of image | ||||||
|  |             region_p1 = x - (w / 2) | ||||||
|  |             if region_p1 < 0: | ||||||
|  |                 region_p1 = 0 | ||||||
|  |             region_p2 = y - (h / 2) | ||||||
|  |             if region_p2 < 0: | ||||||
|  |                 region_p2 = 0 | ||||||
|  |             region_p3 = x + (w / 2) | ||||||
|  |             if region_p3 > pixbuf_width: | ||||||
|  |                 region_p3 = pixbuf_width | ||||||
|  |             region_p4 =  y + (h / 2) | ||||||
|  |             if region_p4 > pixbuf_height: | ||||||
|  |                 region_p4 = pixbuf_height | ||||||
|  |  | ||||||
|  |             region = (region_p1, region_p2, region_p3, region_p4) | ||||||
|  |             person_thumbnail = self.__get_thumbnail(region, THUMBNAIL_IMAGE_SIZE) | ||||||
|  |  | ||||||
|  |             label = metadata.get_tag_label(region_name % i) | ||||||
|  |             namespace = self.__get_tag_namespace(region_name % i) | ||||||
|  |  | ||||||
|  |             node = self.__add_section(PEOPLE) | ||||||
|  |             self.model.add([namespace, label, person_thumbnail, name], node=node) | ||||||
|  |  | ||||||
|  |             i += 1 | ||||||
|  |  | ||||||
|  |     def __get_thumbnail(self, region, thumbnail_size): | ||||||
|  |         """ | ||||||
|  |         Returns the thumbnail of the given region. | ||||||
|  |         """ | ||||||
|  |         w = region[2] - region[0] | ||||||
|  |         h = region[3] - region[1] | ||||||
|  |  | ||||||
|  |         if w <= self.pixbuf.get_width() and h <= self.pixbuf.get_height() and self.pixbuf: | ||||||
|  |             subpixbuf = self.pixbuf.new_subpixbuf(region[0], region[1], w, h) | ||||||
|  |             size = self.__resize_keep_aspect(w, h, *thumbnail_size) | ||||||
|  |             return subpixbuf.scale_simple(size[0], size[1], | ||||||
|  |                                       GdkPixbuf.InterpType.BILINEAR) | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  |  | ||||||
|  |     def __resize_keep_aspect(self, orig_x, orig_y, target_x, target_y): | ||||||
|  |         """ | ||||||
|  |         Calculates the dimensions of the rectangle obtained from | ||||||
|  |         the rectangle orig_x * orig_y by scaling to fit | ||||||
|  |         target_x * target_y keeping the aspect ratio. | ||||||
|  |         """ | ||||||
|  |         orig_aspect = orig_x / orig_y | ||||||
|  |         target_aspect = target_x / target_y | ||||||
|  |         if orig_aspect > target_aspect: | ||||||
|  |             return (target_x, target_x * orig_y // orig_x) | ||||||
|  |         else: | ||||||
|  |             return (target_y * orig_x // orig_y, target_y) | ||||||
|  |  | ||||||
|  |     def __get_tag_namespace(self, key): | ||||||
|  |  | ||||||
|  |         x = key.split(".") | ||||||
|  |         del x[-1] | ||||||
|  |         namespace =  '.'.join(x) | ||||||
|  |  | ||||||
|  |         return namespace | ||||||
|   | |||||||