Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package Photini for openSUSE:Factory checked 
in at 2021-07-14 23:58:50
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/Photini (Old)
 and      /work/SRC/openSUSE:Factory/.Photini.new.2625 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "Photini"

Wed Jul 14 23:58:50 2021 rev:23 rq:906278 version:2021.7.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/Photini/Photini.changes  2021-06-16 
20:37:07.839354622 +0200
+++ /work/SRC/openSUSE:Factory/.Photini.new.2625/Photini.changes        
2021-07-14 23:59:16.593386797 +0200
@@ -1,0 +2,11 @@
+Mon Jul 12 10:54:42 UTC 2021 - Luigi Baldoni <aloi...@gmx.com>
+
+- Update to version 2021.7.0
+  * Added a tab for 'ownership' and copyright details.
+  * Added 'tooltip' hints to some text fields.
+  * Try to ensure only one instance of Photini runs at a time.
+  * Added menu option to make thumbnails for all images that
+    have none.
+  * Other minor improvements and bug fixes.
+
+-------------------------------------------------------------------

Old:
----
  Photini-2021.6.0.tar.gz

New:
----
  Photini-2021.7.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ Photini.spec ++++++
--- /var/tmp/diff_new_pack.xrVMGc/_old  2021-07-14 23:59:17.205382316 +0200
+++ /var/tmp/diff_new_pack.xrVMGc/_new  2021-07-14 23:59:17.209382287 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           Photini
-Version:        2021.6.0
+Version:        2021.7.0
 Release:        0
 Summary:        Digital photograph metadata (EXIF, IPTC, XMP) editing 
application
 License:        GPL-3.0-or-later

++++++ Photini-2021.6.0.tar.gz -> Photini-2021.7.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/CHANGELOG.txt 
new/Photini-2021.7.0/CHANGELOG.txt
--- old/Photini-2021.6.0/CHANGELOG.txt  2021-06-16 13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/CHANGELOG.txt  2021-07-12 11:49:32.000000000 +0200
@@ -16,6 +16,13 @@
 along with this program.  If not, see
 <http://www.gnu.org/licenses/>.
 
+Changes in v2021.7.0:
+  1/ Added a tab for 'ownership' and copyright details.
+  2/ Added 'tooltip' hints to some text fields.
+  3/ Try to ensure only one instance of Photini runs at a time.
+  4/ Added menu option to make thumbnails for all images that have none.
+  5/ Other minor improvements and bug fixes.
+
 Changes in v2021.6.0:
   1/ Show IPTC-IIM data length limits in text fields.
   2/ Drop use of Python FlickrAPI library.
Binary files old/Photini-2021.6.0/src/doc/images/screenshot_200.png and 
new/Photini-2021.7.0/src/doc/images/screenshot_200.png differ
Binary files old/Photini-2021.6.0/src/doc/images/screenshot_201.png and 
new/Photini-2021.7.0/src/doc/images/screenshot_201.png differ
Binary files old/Photini-2021.6.0/src/doc/images/screenshot_202.png and 
new/Photini-2021.7.0/src/doc/images/screenshot_202.png differ
Binary files old/Photini-2021.6.0/src/doc/images/screenshot_203.png and 
new/Photini-2021.7.0/src/doc/images/screenshot_203.png differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/doc/index.rst 
new/Photini-2021.7.0/src/doc/index.rst
--- old/Photini-2021.6.0/src/doc/index.rst      2021-06-16 13:18:18.000000000 
+0200
+++ new/Photini-2021.7.0/src/doc/index.rst      2021-07-12 11:49:32.000000000 
+0200
@@ -1,5 +1,5 @@
 .. This is part of the Photini documentation.
-   Copyright (C)  2012-19  Jim Easterbrook.
+   Copyright (C)  2012-21  Jim Easterbrook.
    See the file DOC_LICENSE.txt for copying conditions.
 
 Photini documentation
@@ -20,7 +20,7 @@
    misc/index
    other/reading
 
-Copyright (C)  2012-19  Jim Easterbrook.
+Copyright (C)  2012-21  Jim Easterbrook.
 
 Permission is granted to copy, distribute and/or modify this document under 
the terms of the GNU Free Documentation License, Version 1.3 or any later 
version published by the Free Software Foundation; with no Invariant Sections, 
no Front-Cover Texts, and no Back-Cover Texts.
 A copy of the license is included in the section entitled "GNU Free 
Documentation License".
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Photini-2021.6.0/src/doc/manual/descriptive_metadata.rst 
new/Photini-2021.7.0/src/doc/manual/descriptive_metadata.rst
--- old/Photini-2021.6.0/src/doc/manual/descriptive_metadata.rst        
2021-06-16 13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/src/doc/manual/descriptive_metadata.rst        
2021-07-12 11:49:32.000000000 +0200
@@ -24,6 +24,7 @@
 (Some software calls this field "author" or "byline".)
 Now Photini will ask for the name of the creator.
 Edit the name if required, then click ``OK``.
+(More detailed ownership information can be added with the 
:doc:`ownership_metadata` tab.)
 
 .. |hazard| unicode:: U+026A1
 
@@ -76,3 +77,29 @@
 This is stored in the metadata as a rating value of -1.
 
 .. image:: ../images/screenshot_14.png
+
+More information about the data fields
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Click on any field name below to see the IPTC definition and user notes for 
that field.
+Although these fields are defined in an `IPTC standard`_, they are all stored 
in XMP metadata.
+Most of them are also stored in "legacy" IPTC-IIM data.
+
+`Title / Object Name 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#title>`_
+  IPTC ``Headline`` data, if present, is merged into this field.
+  Not stored in Exif.
+`Description / Caption 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#description>`_
+  The who, what and why of what the image depicts.
+`Keywords 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#keywords>`_
+  Separate words or phrases with ``;`` characters. Not stored in Exif.
+`Rating 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#image-rating>`_
+  Not stored in Exif or IPTC-IIM.
+`Copyright 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#copyright-notice>`_
+  Who owns the copyright.
+  This shows the same information as the :doc:`ownership_metadata` ``Copyright 
Notice`` field.
+`Creator / Artist 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#creator>`_
+  Usually the photographer's name.
+  If there is more than one creator, separate them with a ``;`` character.
+  This shows the same information as the :doc:`ownership_metadata` ``Creator`` 
field.
+
+.. _IPTC standard:            
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/doc/manual/image_selector.rst 
new/Photini-2021.7.0/src/doc/manual/image_selector.rst
--- old/Photini-2021.6.0/src/doc/manual/image_selector.rst      2021-06-16 
13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/src/doc/manual/image_selector.rst      2021-07-12 
11:49:32.000000000 +0200
@@ -69,3 +69,15 @@
 .. image:: ../images/screenshot_009.png
 
 The same menu items also appear in the main ``File`` menu.
+
+Using Photini with other programs
+---------------------------------
+
+If you use other applications that can display or edit image metadata then you 
need to be careful when using them with Photini.
+Just like with a word processor or text editor it can be risky to have a file 
open for editing in more than one program.
+If you make changes in Photini you should save them before getting another 
program to reload or reopen the file.
+If you make changes in another program you should use the context menu 
described above to reload the file in Photini.
+
+You may also want to experiment with how other programs display the metadata 
you create in Photini and *vice versa*.
+Be aware that other programs might not store their metadata in the picture 
files, but use a database or separate files (other than XMP sidecars).
+Such programs are not compatible with Photini, unless they can be configured 
to use metadata standards.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/doc/manual/index.rst 
new/Photini-2021.7.0/src/doc/manual/index.rst
--- old/Photini-2021.6.0/src/doc/manual/index.rst       2021-06-16 
13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/src/doc/manual/index.rst       2021-07-12 
11:49:32.000000000 +0200
@@ -1,5 +1,5 @@
 .. This is part of the Photini documentation.
-   Copyright (C)  2012-19  Jim Easterbrook.
+   Copyright (C)  2012-21  Jim Easterbrook.
    See the file ../DOC_LICENSE.txt for copying conditions.
 
 User manual
@@ -13,6 +13,7 @@
 
    image_selector
    descriptive_metadata
+   ownership_metadata
    technical_metadata
    map
    address
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Photini-2021.6.0/src/doc/manual/ownership_metadata.rst 
new/Photini-2021.7.0/src/doc/manual/ownership_metadata.rst
--- old/Photini-2021.6.0/src/doc/manual/ownership_metadata.rst  1970-01-01 
01:00:00.000000000 +0100
+++ new/Photini-2021.7.0/src/doc/manual/ownership_metadata.rst  2021-07-12 
11:49:32.000000000 +0200
@@ -0,0 +1,60 @@
+.. This is part of the Photini documentation.
+   Copyright (C)  2021  Jim Easterbrook.
+   See the file ../DOC_LICENSE.txt for copying condidions.
+
+Ownership metadata
+==================
+
+The ``Ownership metadata`` tab (keyboard shortcut ``Alt+O``) allows you to 
edit ownership and copyright information about your photographs.
+
+.. image:: ../images/screenshot_200.png
+
+Most of this data will be the same for all your photographs, so Photini uses a 
"template" to apply the same text to all the selected images.
+The ``Edit template`` button opens the dialog shown below.
+
+.. image:: ../images/screenshot_201.png
+
+Fill in any of the fields you want to use on every photograph.
+The field labels are copied from the `IPTC standard`_, as is the help text 
which should pop up if you hover your mouse over a field.
+If you have already set your copyright holder and creator names on the 
:doc:`descriptive_metadata` tab then this information should already be on the 
form.
+
+.. image:: ../images/screenshot_202.png
+
+Note that you can insert the year in which a photograph was taken with ``%Y``.
+This is probably only useful in the ``Copyright Notice``, but is available for 
all fields.
+(You can actually use any directive recognised by the `Python strftime 
function`_, such as ``%m`` for month number or ``%B`` for the month name.)
+
+.. image:: ../images/screenshot_203.png
+
+The ``Apply template`` button copies the template data to all the selected 
images, setting the correct year in the ``Copyright Notice``.
+You can then add more information, or edit the existing information, in the 
usual way.
+
+More information about the data fields
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Click on any field name below to see the IPTC definition and user notes for 
that field.
+Although these fields are defined in an `IPTC standard`_, they are all stored 
in XMP metadata.
+Most of them are also stored in "legacy" IPTC-IIM data.
+
+`Creator 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#creator>`_
+  Usually the photographer's name.
+  If there is more than one creator, separate them with a ``;`` character.
+  This shows the same information as the :doc:`descriptive_metadata` ``Creator 
/ Artist`` field.
+`Creator's Jobtitle 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#creators-jobtitle>`_
+  If there is more than one creator, there should be a matching number of 
creator jobtitles, separated by ``;`` characters.
+`Credit Line 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#credit-line>`_
+  Usually the photographer's name, but could be their employer or client.
+`Copyright Notice 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#copyright-notice>`_
+  Who owns the copyright.
+  This shows the same information as the :doc:`descriptive_metadata` 
``Copyright`` field.
+`Rights Usage Terms 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#rights-usage-terms>`_
+  Not stored in IPTC-IIM.
+`Instructions 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#instructions>`_
+  Notes to a publisher of the image.
+`Contact Information 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#creators-contact-info>`_
+  Only the `Address 
<http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#address>`_
 is stored in IPTC-IIM.
+  Multiple email addresses, URLs, or phone numbers should be separated by 
commas.
+
+
+.. _IPTC standard:            
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata
+.. _Python strftime function: 
https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/doc/manual/tags.rst 
new/Photini-2021.7.0/src/doc/manual/tags.rst
--- old/Photini-2021.6.0/src/doc/manual/tags.rst        2021-06-16 
13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/src/doc/manual/tags.rst        2021-07-12 
11:49:32.000000000 +0200
@@ -18,52 +18,60 @@
 These tags are where Photini stores its metadata.
 (Legacy IPTC-IIM data is only used if it already exists in the file, in line 
with the MWG guidelines, unless the "write unconditionally" user setting is 
enabled.)
 
-Note that "Title / Object Name" and "Keywords" are not stored in Exif.
+Note that some fields, such as "Title / Object Name" and "Keywords", are not 
stored in Exif.
 You may prefer not to use these fields to ensure compatibility with software 
that only handles Exif.
 
-=====================  ================================  
==============================  ==================
-Photini field          Exif tag                          XMP tag               
          IPTC-IIM tag
-=====================  ================================  
==============================  ==================
-Title / Object Name                                      Xmp.dc.title          
          Iptc.Application2.ObjectName
-Description / Caption  Exif.Image.ImageDescription       Xmp.dc.description    
          Iptc.Application2.Caption
-Keywords                                                 Xmp.dc.subject        
          Iptc.Application2.Keywords
-Rating                                                   Xmp.xmp.Rating
-Copyright              Exif.Image.Copyright              Xmp.dc.rights         
          Iptc.Application2.Copyright
-Creator / Artist       Exif.Image.Artist                 Xmp.dc.creator        
          Iptc.Application2.Byline
-Date / time Taken      Exif.Photo.DateTimeOriginal       
Xmp.photoshop.DateCreated       Iptc.Application2.DateCreated
-                       Exif.Photo.SubSecTimeOriginal                           
          Iptc.Application2.TimeCreated
-Date / time Digitised  Exif.Photo.DateTimeDigitized      Xmp.xmp.CreateDate    
          Iptc.Application2.DigitizationDate
-                       Exif.Photo.SubSecTimeDigitized                          
          Iptc.Application2.DigitizationTime
-Date / time Modified   Exif.Image.DateTime               Xmp.xmp.ModifyDate
-                       Exif.Photo.SubSecTime
-Orientation            Exif.Image.Orientation
-Camera maker name      Exif.Image.Make
-Camera model name      Exif.Image.Model
-Camera serial number   Exif.Photo.BodySerialNumber
-Lens maker name        Exif.Photo.LensMake
-Lens model name        Exif.Photo.LensModel
-Lens serial number     Exif.Photo.LensSerialNumber
-Lens specification     Exif.Photo.LensSpecification
-Focal length           Exif.Photo.FocalLength
-35mm equiv             Exif.Photo.FocalLengthIn35mmFilm
-Aperture               Exif.Photo.FNumber
-Latitude, longitude    Exif.GPSInfo.GPSLatitude
-                       Exif.GPSInfo.GPSLatitudeRef
-                       Exif.GPSInfo.GPSLongitude
-                       Exif.GPSInfo.GPSLongitudeRef
-Altitude               Exif.GPSInfo.GPSAltitude
-                       Exif.GPSInfo.GPSAltitudeRef
-Camera address                                           
Xmp.iptcExt.LocationCreated
-                                                         Xmp.iptc.Location     
          Iptc.Application2.SubLocation
-                                                         Xmp.photoshop.City    
          Iptc.Application2.City
-                                                         Xmp.photoshop.State   
          Iptc.Application2.ProvinceState
-                                                         Xmp.photoshop.Country 
          Iptc.Application2.CountryName
-                                                         Xmp.iptc.CountryCode  
          Iptc.Application2.CountryCode
-Subject address                                          
Xmp.iptcExt.LocationShown
-Thumbnail image        Exif.Thumbnail.Compression
-                       Exif.Thumbnail.ImageWidth
-                       Exif.Thumbnail.ImageLength
-=====================  ================================  
==============================  ==================
+Some of the field names in the table below lingk to their definition in the 
IPTC standard.
+You may find this useful when deciding what to write in those fields.
+
+========================  ================================  
==============================  ==================
+Photini field             Exif tag                          XMP tag            
             IPTC-IIM tag
+========================  ================================  
==============================  ==================
+`Title / Object Name`_                                      Xmp.dc.title       
             Iptc.Application2.ObjectName
+`Description / Caption`_  Exif.Image.ImageDescription       Xmp.dc.description 
             Iptc.Application2.Caption
+Keywords_                                                   Xmp.dc.subject     
             Iptc.Application2.Keywords
+Rating_                                                     Xmp.xmp.Rating
+`Creator / Artist`_       Exif.Image.Artist                 Xmp.dc.creator     
             Iptc.Application2.Byline
+`Creator's Jobtitle`_                                       
Xmp.photoshop.AuthorsPosition   Iptc.Application2.BylineTitle
+`Credit Line`_                                              
Xmp.photoshop.Credit            Iptc.Application2.Credit
+`Copyright Notice`_       Exif.Image.Copyright              Xmp.dc.rights      
             Iptc.Application2.Copyright
+`Rights Usage Terms`_                                       
Xmp.xmpRights.UsageTerms
+Instructions_                                               
Xmp.photoshop.Instructions      Iptc.Application2.SpecialInstructions
+`Contact Information`_                                      
Xmp.iptc.CreatorContactInfo     Iptc.Application2.Contact
+`Date / time Taken`_      Exif.Photo.DateTimeOriginal       
Xmp.photoshop.DateCreated       Iptc.Application2.DateCreated
+                          Exif.Photo.SubSecTimeOriginal                        
             Iptc.Application2.TimeCreated
+Date / time Digitised     Exif.Photo.DateTimeDigitized      Xmp.xmp.CreateDate 
             Iptc.Application2.DigitizationDate
+                          Exif.Photo.SubSecTimeDigitized                       
             Iptc.Application2.DigitizationTime
+Date / time Modified      Exif.Image.DateTime               Xmp.xmp.ModifyDate
+                          Exif.Photo.SubSecTime
+Orientation               Exif.Image.Orientation
+Camera maker name         Exif.Image.Make
+Camera model name         Exif.Image.Model
+Camera serial number      Exif.Photo.BodySerialNumber
+Lens maker name           Exif.Photo.LensMake
+Lens model name           Exif.Photo.LensModel
+Lens serial number        Exif.Photo.LensSerialNumber
+Lens specification        Exif.Photo.LensSpecification
+Focal length              Exif.Photo.FocalLength
+35mm equiv                Exif.Photo.FocalLengthIn35mmFilm
+Aperture                  Exif.Photo.FNumber
+Latitude_, longitude_     Exif.GPSInfo.GPSLatitude
+                          Exif.GPSInfo.GPSLatitudeRef
+                          Exif.GPSInfo.GPSLongitude
+                          Exif.GPSInfo.GPSLongitudeRef
+Altitude_                 Exif.GPSInfo.GPSAltitude
+                          Exif.GPSInfo.GPSAltitudeRef
+`Camera address`_                                           
Xmp.iptcExt.LocationCreated
+                                                            Xmp.iptc.Location  
             Iptc.Application2.SubLocation
+                                                            Xmp.photoshop.City 
             Iptc.Application2.City
+                                                            
Xmp.photoshop.State             Iptc.Application2.ProvinceState
+                                                            
Xmp.photoshop.Country           Iptc.Application2.CountryName
+                                                            
Xmp.iptc.CountryCode            Iptc.Application2.CountryCode
+`Subject address`_                                          
Xmp.iptcExt.LocationShown
+Thumbnail image           Exif.Thumbnail.Compression
+                          Exif.Thumbnail.ImageWidth
+                          Exif.Thumbnail.ImageLength
+========================  ================================  
==============================  ==================
 
 Secondary tags
 --------------
@@ -74,15 +82,15 @@
 =====================  ===============================  
==============================  ==================
 Photini field          Exif tag                         XMP tag                
         IPTC-IIM tag
 =====================  ===============================  
==============================  ==================
-Title / Object Name    Exif.Image.XPTitle                                      
         Iptc.Application2.Headline
+Title / Object Name    Exif.Image.XPTitle               Xmp.photoshop.Headline 
         Iptc.Application2.Headline
 Description / Caption  Exif.Image.XPComment             
Xmp.tiff.ImageDescription
                        Exif.Image.XPSubject
                        Exif.Photo.UserComment
 Keywords               Exif.Image.XPKeywords
 Rating                 Exif.Image.Rating                
Xmp.MicrosoftPhoto.Rating
                        Exif.Image.RatingPercent
-Copyright                                               Xmp.tiff.Copyright
 Creator / Artist       Exif.Image.XPAuthor              Xmp.tiff.Artist
+Copyright                                               Xmp.tiff.Copyright
 Date / time Taken      Exif.Image.DateTimeOriginal      
Xmp.exif.DateTimeOriginal
 Date / time Digitised                                   
Xmp.exif.DateTimeDigitized
 Date / time Modified                                    Xmp.tiff.DateTime
@@ -153,3 +161,21 @@
 
 [1] The time zone offset is not directly presented to the user.
 It is applied to the Date / time Taken, Date / time Digitised and Date / time 
Modified fields if no other time zone information is available.
+
+.. _Altitude:              
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#gps-altitude
+.. _Camera address:        
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#location-created
+.. _Contact Information:   
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#creators-contact-info
+.. _Copyright Notice:      
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#copyright-notice
+.. _Creator / Artist:      
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#creator
+.. _Creator's Jobtitle:    
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#creators-jobtitle
+.. _Credit Line:           
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#credit-line
+.. _Date / time Taken:     
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#date-created
+.. _Description / Caption: 
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#description
+.. _Instructions:          
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#instructions
+.. _Keywords:              
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#keywords
+.. _Latitude:              
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#gps-latitude
+.. _longitude:             
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#gps-longitude
+.. _Rating:                
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#image-rating
+.. _Rights Usage Terms:    
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#rights-usage-terms
+.. _Subject address:       
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#location-shown-in-the-image
+.. _Title / Object Name:   
http://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata#title
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/__init__.py 
new/Photini-2021.7.0/src/photini/__init__.py
--- old/Photini-2021.6.0/src/photini/__init__.py        2021-06-16 
13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/src/photini/__init__.py        2021-07-12 
11:49:32.000000000 +0200
@@ -1,4 +1,4 @@
 from __future__ import unicode_literals
 
-__version__ = '2021.6.0'
-build = '1713 (b4a7af3)'
+__version__ = '2021.7.0'
+build = '1751 (9df716c)'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/address.py 
new/Photini-2021.7.0/src/photini/address.py
--- old/Photini-2021.6.0/src/photini/address.py 2021-06-16 13:18:18.000000000 
+0200
+++ new/Photini-2021.7.0/src/photini/address.py 2021-07-12 11:49:32.000000000 
+0200
@@ -49,6 +49,19 @@
                 length_check=ImageMetadata.max_bytes(key))
             self.members[key].editingFinished.connect(self.editing_finished)
         self.members['CountryCode'].setMaximumWidth(40)
+        self.members['SubLocation'].setToolTip(translate(
+            'AddressTab', 'Enter the name of the sublocation.'))
+        self.members['City'].setToolTip(translate(
+            'AddressTab', 'Enter the name of the city.'))
+        self.members['ProvinceState'].setToolTip(translate(
+            'AddressTab', 'Enter the name of the province or state.'))
+        self.members['CountryName'].setToolTip(translate(
+            'AddressTab', 'Enter the name of the country.'))
+        self.members['CountryCode'].setToolTip(translate(
+            'AddressTab',
+            'Enter the 2 or 3 letter ISO 3166 country code of the country.'))
+        self.members['WorldRegion'].setToolTip(translate(
+            'AddressTab', 'Enter the name of the world region.'))
         for j, text in enumerate((
                 translate('AddressTab', 'Street'),
                 translate('AddressTab', 'City'),
@@ -255,9 +268,14 @@
     def set_tab_text(self, idx):
         if idx == 0:
             text = translate('AddressTab', 'camera')
+            tip = translate('AddressTab', 'Enter the details about a location'
+                            ' where this image was created.')
         else:
             text = translate('AddressTab', 'subject {}').format(idx)
+            tip = translate('AddressTab', 'Enter the details about a location'
+                            ' which is shown in this image.')
         self.location_info.setTabText(idx, text)
+        self.location_info.setTabToolTip(idx, tip)
 
     @QtSlot()
     @catch_all
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/descriptive.py 
new/Photini-2021.7.0/src/photini/descriptive.py
--- old/Photini-2021.6.0/src/photini/descriptive.py     2021-06-16 
13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/src/photini/descriptive.py     2021-07-12 
11:49:32.000000000 +0200
@@ -69,8 +69,10 @@
         self.layout().addWidget(self.slider)
         # display
         self.display = QtWidgets.QLineEdit()
+        self.display.setStyleSheet("* {background-color:rgba(0,0,0,0);}")
         self.display.setFrame(False)
         self.display.setReadOnly(True)
+        self.display.setContextMenuPolicy(Qt.NoContextMenu)
         self.display.setFocusPolicy(Qt.NoFocus)
         self.layout().addWidget(self.display)
         # adopt child methods/signals
@@ -200,6 +202,9 @@
         self.widgets['title'] = SingleLineEdit(
             spell_check=True,
             length_check=ImageMetadata.max_bytes('title'))
+        self.widgets['title'].setToolTip(translate(
+            'DescriptiveTab', 'Enter a short verbal and human readable name'
+            ' for the image, this may be the file name.'))
         self.widgets['title'].editingFinished.connect(self.new_title)
         self.form.addRow(translate(
             'DescriptiveTab', 'Title / Object Name'), self.widgets['title'])
@@ -207,13 +212,24 @@
         self.widgets['description'] = MultiLineEdit(
             spell_check=True,
             length_check=ImageMetadata.max_bytes('description'))
-        
self.widgets['description'].editingFinished.connect(self.new_description)
-        self.form.addRow(translate(
-            'DescriptiveTab', 'Description / Caption'), 
self.widgets['description'])
+        self.widgets['description'].setToolTip(translate(
+            'DescriptiveTab', 'Enter a "caption" describing the who, what,'
+            ' and why of what is happening in this image,\nthis might include'
+            ' names of people, and/or their role in the action that is taking'
+            ' place within the image.'))
+        self.widgets['description'].editingFinished.connect(
+            self.new_description)
+        self.form.addRow(
+            translate('DescriptiveTab', 'Description / Caption'),
+            self.widgets['description'])
         # keywords
         self.widgets['keywords'] = KeywordsEditor(
             spell_check=True, length_check=ImageMetadata.max_bytes('keywords'),
             multi_string=True)
+        self.widgets['keywords'].setToolTip(translate(
+            'DescriptiveTab', 'Enter any number of keywords, terms or phrases'
+            ' used to express the subject matter in the image.'
+            '\nSeparate them with ";" characters.'))
         self.widgets['keywords'].editingFinished.connect(self.new_keywords)
         self.form.addRow(translate(
             'DescriptiveTab', 'Keywords'), self.widgets['keywords'])
@@ -226,6 +242,9 @@
         # copyright
         self.widgets['copyright'] = LineEditWithAuto(
             length_check=ImageMetadata.max_bytes('copyright'))
+        self.widgets['copyright'].setToolTip(translate(
+            'OwnerTab', 'Enter a notice on the current owner of the'
+            ' copyright for this image, such as "??2008 Jane Doe".'))
         self.widgets['copyright'].editingFinished.connect(self.new_copyright)
         self.widgets['copyright'].autoComplete.connect(self.auto_copyright)
         self.form.addRow(translate(
@@ -233,6 +252,9 @@
         # creator
         self.widgets['creator'] = LineEditWithAuto(
             length_check=ImageMetadata.max_bytes('creator'), multi_string=True)
+        self.widgets['creator'].setToolTip(translate(
+            'OwnerTab',
+            'Enter the name of the person that created this image.'))
         self.widgets['creator'].editingFinished.connect(self.new_creator)
         self.widgets['creator'].autoComplete.connect(self.auto_creator)
         self.form.addRow(translate(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/editor.py 
new/Photini-2021.7.0/src/photini/editor.py
--- old/Photini-2021.6.0/src/photini/editor.py  2021-06-16 13:18:18.000000000 
+0200
+++ new/Photini-2021.7.0/src/photini/editor.py  2021-07-12 11:49:32.000000000 
+0200
@@ -38,8 +38,8 @@
 from photini.loggerwindow import LoggerWindow
 from photini.opencage import OpenCage
 from photini.pyqt import (
-    catch_all, Qt, QtCore, QtGui, QNetworkProxy, QtSlot, QtWidgets, qt_version,
-    width_for_text)
+    catch_all, Qt, QtCore, QtGui, QtNetwork, QNetworkProxy, QtSignal, QtSlot,
+    QtWidgets, qt_version, width_for_text)
 from photini.spelling import SpellCheck, spelling_version
 
 try:
@@ -56,7 +56,7 @@
 class QTabBar(QtWidgets.QTabBar):
     def tabSizeHint(self, index):
         size = super(QTabBar, self).tabSizeHint(index)
-        size.setWidth(max(size.width(), width_for_text(self, 'x' * 13)))
+        size.setWidth(max(size.width(), width_for_text(self, 'x' * 10)))
         return size
 
 
@@ -84,6 +84,79 @@
         super(ConfigStore, self).save()
 
 
+class ServerSocket(QtCore.QObject):
+    new_files = QtSignal(list)
+
+    def __init__(self, socket, *arg, **kw):
+        super(ServerSocket, self).__init__(*arg, **kw)
+        self.socket = socket
+        self.data = b''
+        self.socket.setParent(self)
+        self.socket.readyRead.connect(self.read_data)
+        self.socket.disconnected.connect(self.deleteLater)
+
+    @QtSlot()
+    @catch_all
+    def read_data(self):
+        file_list = []
+        while self.socket.bytesAvailable():
+            self.data += self.socket.readAll().data()
+            while b'\n' in self.data:
+                line, sep, self.data = self.data.partition(b'\n')
+                string = line.decode('utf-8')
+                file_list.append(string)
+        if file_list:
+            self.new_files.emit(file_list)
+
+
+class InstanceServer(QtNetwork.QTcpServer):
+    new_files = QtSignal(list)
+
+    def __init__(self, *arg, **kw):
+        super(InstanceServer, self).__init__(*arg, **kw)
+        config = BaseConfigStore('instance')
+        self.newConnection.connect(self.new_connection)
+        if not self.listen(QtNetwork.QHostAddress.LocalHost):
+            logger.error('Failed to start instance server:', 
self.errorString())
+            return
+        config.set('server', 'port', self.serverPort())
+        config.save()
+
+    @QtSlot()
+    @catch_all
+    def new_connection(self):
+        window = self.parent().window()
+        window.raise_()
+        window.activateWindow()
+        while self.hasPendingConnections():
+            socket = self.nextPendingConnection()
+            socket = ServerSocket(socket, parent=self)
+            socket.new_files.connect(self.new_files)
+
+
+def SendToInstance(files):
+    config = BaseConfigStore('instance')
+    port = config.get('server', 'port')
+    if not port:
+        return False
+    socket = QtNetwork.QTcpSocket()
+    socket.connectToHost(
+        QtNetwork.QHostAddress.LocalHost, int(port), 
QtCore.QIODevice.WriteOnly)
+    if not socket.waitForConnected(1000):
+        logger.info('Connect to server: %s', socket.errorString())
+        return False
+    for path in files:
+        data = os.path.abspath(path).encode('utf-8') + b'\n'
+        while data:
+            count = socket.write(data)
+            if count < 1:
+                logger.error('Write to server: %s', socket.errorString())
+                break
+            data = data[count:]
+    socket.waitForBytesWritten()
+    socket.close()
+    return True
+
 class MainWindow(QtWidgets.QMainWindow):
     def __init__(self, options, initial_files):
         super(MainWindow, self).__init__()
@@ -117,7 +190,12 @@
         # image selector
         self.image_list = ImageList()
         self.image_list.selection_changed.connect(self.new_selection)
+        self.image_list.image_list_changed.connect(self.new_image_list)
         self.image_list.new_metadata.connect(self.new_metadata)
+        # start instance server
+        instance_server = InstanceServer(parent=self)
+        instance_server.new_files.connect(
+            self.image_list.open_file_list, Qt.QueuedConnection)
         # update config file
         if self.app.config_store.config.has_section('tabs'):
             conv = {
@@ -139,7 +217,8 @@
                     self.app.config_store.config.remove_option('tabs', key)
         # prepare list of tabs and associated stuff
         self.tab_list = []
-        default_modules = ['photini.descriptive',  'photini.technical',
+        default_modules = ['photini.descriptive',  'photini.ownership',
+                           'photini.technical',
                            'photini.googlemap',    'photini.bingmap',
                            'photini.mapboxmap',    'photini.openstreetmap',
                            'photini.address',      'photini.flickr',
@@ -172,6 +251,11 @@
         self.save_action.setShortcuts(QtGui.QKeySequence.Save)
         self.save_action.setEnabled(False)
         self.save_action.triggered.connect(self.image_list.save_files)
+        self.fix_thumbs_action = file_menu.addAction(
+            translate('MenuBar', 'Fix missing thumbnails'))
+        self.fix_thumbs_action.setEnabled(False)
+        self.fix_thumbs_action.triggered.connect(
+            self.image_list.fix_missing_thumbs)
         action = file_menu.addAction(translate('MenuBar', 'Close all files'))
         action.triggered.connect(self.image_list.close_all_files)
         sep = file_menu.addAction(translate('MenuBar', 'Selected images'))
@@ -389,6 +473,16 @@
             self.import_gpx_action.setEnabled(len(selection) > 0)
         self.tabs.currentWidget().new_selection(selection)
 
+    @QtSlot()
+    @catch_all
+    def new_image_list(self):
+        for image in self.image_list.images:
+            thumb = image.metadata.thumbnail
+            if not thumb or not thumb['image']:
+                self.fix_thumbs_action.setEnabled(True)
+                return
+        self.fix_thumbs_action.setEnabled(False)
+
     @QtSlot(bool)
     @catch_all
     def new_metadata(self, unsaved_data):
@@ -451,6 +545,10 @@
         '-v', '--verbose', action='count', default=0,
         help=translate('CLIHelp', 'increase number of logging messages'))
     options, args = parser.parse_args()
+    # if an instance of Photini is already running, send it the list of
+    # files to open
+    if SendToInstance(args):
+        return 0
     # ensure warnings are visible in test mode
     if options.test:
         warnings.simplefilter('default')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/exiv2.py 
new/Photini-2021.7.0/src/photini/exiv2.py
--- old/Photini-2021.6.0/src/photini/exiv2.py   2021-06-16 13:18:18.000000000 
+0200
+++ new/Photini-2021.7.0/src/photini/exiv2.py   2021-07-12 11:49:32.000000000 
+0200
@@ -48,9 +48,10 @@
 # versions may not recognise them. The xapGImg URL is invalid, but
 # Photini doesn't write xapGImg so it doesn't matter.
 for prefix, name in (
-        ('exifEX',  'http://cipa.jp/exif/1.0/'),
-        ('xapGImg', 'http://ns.adobe.com/xxx/'),
-        ('xmpGImg', 'http://ns.adobe.com/xap/1.0/g/img/')):
+        ('exifEX',    'http://cipa.jp/exif/1.0/'),
+        ('xapGImg',   'http://ns.adobe.com/xxx/'),
+        ('xmpGImg',   'http://ns.adobe.com/xap/1.0/g/img/'),
+        ('xmpRights', 'http://ns.adobe.com/xap/1.0/rights/')):
     GExiv2.Metadata.register_xmp_namespace(name, prefix)
 
 # Gexiv2 won't register the 'Iptc4xmpExt' namespace as its abbreviated
@@ -325,8 +326,8 @@
 
     def set_group(self, tag, value, idx=1):
         sub_tag = self._multi_tags[tag][0].format(idx=idx)
-        if any(value) and '/' in sub_tag:
-            # create XMP structure/container
+        if any(value) and '[' in sub_tag:
+            # create XMP array
             for t in self.get_xmp_tags():
                 if t.startswith(tag):
                     # container already exists
@@ -348,20 +349,24 @@
 
     # maximum length of Iptc data
     _max_bytes = {
-        'Iptc.Application2.Byline'           :   32,
-        'Iptc.Application2.Caption'          : 2000,
-        'Iptc.Application2.City'             :   32,
-        'Iptc.Application2.Copyright'        :  128,
-        'Iptc.Application2.CountryCode'      :    3,
-        'Iptc.Application2.CountryName'      :   64,
-        'Iptc.Application2.Headline'         :  256,
-        'Iptc.Application2.Keywords'         :   64,
-        'Iptc.Application2.ObjectName'       :   64,
-        'Iptc.Application2.Program'          :   32,
-        'Iptc.Application2.ProgramVersion'   :   10,
-        'Iptc.Application2.ProvinceState'    :   32,
-        'Iptc.Application2.SubLocation'      :   32,
-        'Iptc.Envelope.CharacterSet'         :   32,
+        'Iptc.Application2.Byline'             :   32,
+        'Iptc.Application2.BylineTitle'        :   32,
+        'Iptc.Application2.Caption'            : 2000,
+        'Iptc.Application2.City'               :   32,
+        'Iptc.Application2.Contact'            :  128,
+        'Iptc.Application2.Copyright'          :  128,
+        'Iptc.Application2.CountryCode'        :    3,
+        'Iptc.Application2.CountryName'        :   64,
+        'Iptc.Application2.Credit'             :   32,
+        'Iptc.Application2.Headline'           :  256,
+        'Iptc.Application2.Keywords'           :   64,
+        'Iptc.Application2.ObjectName'         :   64,
+        'Iptc.Application2.Program'            :   32,
+        'Iptc.Application2.ProgramVersion'     :   10,
+        'Iptc.Application2.ProvinceState'      :   32,
+        'Iptc.Application2.SpecialInstructions':  256,
+        'Iptc.Application2.SubLocation'        :   32,
+        'Iptc.Envelope.CharacterSet'           :   32,
         }
 
     @classmethod
@@ -432,13 +437,23 @@
         OK = True
         saved_tags = self.open_old(self._path).get_all_tags()
         for tag in self.get_all_tags():
-            if tag in ('Exif.Image.GPSTag',):
+            if tag in saved_tags:
+                continue
+            if tag in ('Exif.Image.GPSTag', 'Exif.MakerNote.ByteOrder',
+                       'Exif.MakerNote.Offset', 'Exif.Photo.MakerNote'):
                 # some tags disappear with good reason
                 continue
-            if tag not in saved_tags:
+            family, group, tagname = tag.split('.')
+            if family == 'Exif' and group[:5] in (
+                    'Canon', 'Casio', 'Fujif', 'Minol', 'Nikon', 'Olymp',
+                    'Panas', 'Penta', 'Samsu', 'Sigma', 'Sony1'):
+                # maker note tags are often not saved
                 logger.warning(
                     '%s: tag not saved: %s', os.path.basename(self._path), tag)
-                OK = False
+                continue
+            logger.error(
+                '%s: tag not saved: %s', os.path.basename(self._path), tag)
+            OK = False
         return OK
 
     def get_all_tags(self):
@@ -505,6 +520,7 @@
             'Exif.{ifd}.FocalPlaneResolutionUnit'),
         'Exif.{ifd}.ImageDimensions': (
             'Exif.{ifd}.ImageWidth', 'Exif.{ifd}.ImageLength'),
+        'Iptc.Application2.Contact': ('Iptc.Application2.Contact',),
         'Iptc.Application2.DateCreated': (
             'Iptc.Application2.DateCreated', 'Iptc.Application2.TimeCreated'),
         'Iptc.Application2.DigitizationDate': (
@@ -531,6 +547,16 @@
         'Xmp.exifEX.LensMake': (
             'Xmp.exifEX.LensMake', 'Xmp.exifEX.LensModel',
             'Xmp.exifEX.LensSerialNumber'),
+        'Xmp.iptc.CreatorContactInfo': (
+            'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrExtadr',
+            'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCity',
+            'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrCtry',
+            'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiEmailWork',
+            'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork',
+            'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrPcode',
+            'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiAdrRegion',
+            'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiUrlWork',
+            ),
         'Xmp.iptc.Location': (
             'Xmp.iptc.Location', 'Xmp.photoshop.City', 'Xmp.photoshop.State',
             'Xmp.photoshop.Country', 'Xmp.iptc.CountryCode'),
@@ -583,6 +609,8 @@
                             ('WN', 'Exif.OlympusEq.CameraType'),
                             ('WN', 'Exif.Pentax.ModelID'),
                             ('WN', 'Xmp.aux.SerialNumber')),
+        'contact_info'   : (('WA', 'Xmp.iptc.CreatorContactInfo'),
+                            ('WA', 'Iptc.Application2.Contact')),
         'copyright'      : (('WA', 'Exif.Image.Copyright'),
                             ('WA', 'Xmp.dc.rights'),
                             ('W0', 'Xmp.tiff.Copyright'),
@@ -592,6 +620,10 @@
                             ('WA', 'Xmp.dc.creator'),
                             ('W0', 'Xmp.tiff.Artist'),
                             ('WA', 'Iptc.Application2.Byline')),
+        'creator_title'  : (('WA', 'Xmp.photoshop.AuthorsPosition'),
+                            ('WA', 'Iptc.Application2.BylineTitle')),
+        'credit_line'    : (('WA', 'Xmp.photoshop.Credit'),
+                            ('WA', 'Iptc.Application2.Credit')),
         'date_digitised' : (('WA', 'Exif.Photo.DateTimeDigitized'),
                             ('WA', 'Xmp.xmp.CreateDate'),
                             ('W0', 'Xmp.exif.DateTimeDigitized'),
@@ -617,6 +649,8 @@
                             ('WX', 'Xmp.exif.FocalLength')),
         'focal_length_35': (('WA', 'Exif.Photo.FocalLengthIn35mmFilm'),
                             ('WX', 'Xmp.exif.FocalLengthIn35mmFilm')),
+        'instructions'   : (('WA', 'Xmp.photoshop.Instructions'),
+                            ('WA', 'Iptc.Application2.SpecialInstructions')),
         'keywords'       : (('WA', 'Xmp.dc.subject'),
                             ('WA', 'Iptc.Application2.Keywords'),
                             ('W0', 'Exif.Image.XPKeywords')),
@@ -666,9 +700,11 @@
                             ('WN', 'Exif.CanonTi.TimeZone'),
                             ('WN', 'Exif.NikonWt.Timezone')),
         'title'          : (('WA', 'Xmp.dc.title'),
+                            ('W0', 'Xmp.photoshop.Headline'),
                             ('WA', 'Iptc.Application2.ObjectName'),
                             ('W0', 'Exif.Image.XPTitle'),
                             ('W0', 'Iptc.Application2.Headline')),
+        'usageterms'     : (('WA', 'Xmp.xmpRights.UsageTerms'),),
         }
 
     def read(self, name, type_):
@@ -720,6 +756,25 @@
             else:
                 value.write(self, tag)
 
+    # Exiv2 uses the Exif.Image.Make value to decode Exif.Photo.MakerNote
+    # If we change Exif.Image.Make we should delete Exif.Photo.MakerNote
+    def camera_change_ok(self, camera_model):
+        if not (self.has_tag('Exif.Photo.MakerNote')
+                and self.has_tag('Exif.Image.Make')):
+            return True
+        if not camera_model:
+            return False
+        return self.get_string('Exif.Image.Make') == camera_model['make']
+
+    def delete_makernote(self, camera_model):
+        if self.camera_change_ok(camera_model):
+            return
+        self._clear_value('Exif.Image.Make')
+        self.save_file(self._path)
+        self.open_path(self._path)
+        self._clear_value('Exif.Photo.MakerNote')
+        self.save_file(self._path)
+
 
 class ImageMetadata(Exiv2Metadata):
     _iptc_encodings = {
@@ -826,25 +881,6 @@
                 if value:
                     self.set_tag_multiple(tag, value)
 
-    # Exiv2 uses the Exif.Image.Make value to decode Exif.Photo.MakerNote
-    # If we change Exif.Image.Make we should delete Exif.Photo.MakerNote
-    def camera_change_ok(self, camera_model):
-        if not (self.has_tag('Exif.Photo.MakerNote')
-                and self.has_tag('Exif.Image.Make')):
-            return True
-        if not camera_model:
-            return False
-        return self.get_string('Exif.Image.Make') == camera_model['make']
-
-    def delete_makernote(self, camera_model):
-        if self.camera_change_ok(camera_model):
-            return
-        self._clear_value('Exif.Image.Make')
-        self.save_file(self._path)
-        self.open_path(self._path)
-        self._clear_value('Exif.Photo.MakerNote')
-        self.save_file(self._path)
-
 
 class Preview(object):
     def __init__(self, md, buf):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/flickr.py 
new/Photini-2021.7.0/src/photini/flickr.py
--- old/Photini-2021.6.0/src/photini/flickr.py  2021-06-16 13:18:18.000000000 
+0200
+++ new/Photini-2021.7.0/src/photini/flickr.py  2021-07-12 11:49:32.000000000 
+0200
@@ -498,11 +498,13 @@
                 'description': image.metadata.description or '',
                 }
             # keywords
-            params['tags'] = {'tags': 'uploaded:by=photini'}
+            params['tags'] = {'tags': 'uploaded:by=photini,'}
             for keyword in image.metadata.keywords or []:
                 if not keyword.startswith(ID_TAG):
-                    params['tags']['tags'] += ' "{}"'.format(
-                        keyword.replace('"', ''))
+                    keyword = keyword.replace('"', "'")
+                    if ',' in keyword:
+                        keyword = '"' + keyword + '"'
+                    params['tags']['tags'] += keyword + ','
             # date_taken
             date_taken = image.metadata.date_taken
             if date_taken:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/imagelist.py 
new/Photini-2021.7.0/src/photini/imagelist.py
--- old/Photini-2021.6.0/src/photini/imagelist.py       2021-06-16 
13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/src/photini/imagelist.py       2021-07-12 
11:49:32.000000000 +0200
@@ -109,25 +109,25 @@
     def regenerate_thumbnail(self):
         # DCF spec says thumbnail must be 160 x 120, so other aspect
         # ratios are padded with black
-        # first try using FFmpeg to make thumbnail
-        data = self.make_thumb_ffmpeg()
-        if data:
-            self.metadata.thumbnail = {'data': data}
-            return True
-        # use PIL or Qt
+        # try using PIL first, good quality and quick
         qt_im = self.get_qt_image()
-        if not qt_im or qt_im.isNull():
-            return False
-        if PIL:
+        if qt_im and PIL:
             data = self.make_thumb_PIL(qt_im)
             if data:
                 self.metadata.thumbnail = {'data': data}
                 return True
-        qt_im = self.make_thumb_Qt(qt_im)
-        if not qt_im or qt_im.isNull():
-            return False
-        self.metadata.thumbnail = {'image': qt_im}
-        return True
+        # next try using FFmpeg, good quality but slower
+        data = self.make_thumb_ffmpeg()
+        if data:
+            self.metadata.thumbnail = {'data': data}
+            return True
+        # lastly use Qt, quick but not high quality
+        if qt_im:
+            qt_im = self.make_thumb_Qt(qt_im)
+            if qt_im:
+                self.metadata.thumbnail = {'image': qt_im}
+                return True
+        return False
 
     def make_thumb_ffmpeg(self):
         # get input dimensions
@@ -167,8 +167,7 @@
         reader.setAutoTransform(False)
         qt_im = reader.read()
         if not qt_im or qt_im.isNull():
-            logger.error('Cannot read %s image data from %s',
-                         self.file_type, self.path)
+            logger.error('Image read: %s: %s', self.path, reader.errorString())
             return None
         w = qt_im.width()
         h = qt_im.height()
@@ -183,21 +182,15 @@
         if w >= h:
             new_h = int(0.5 + (float(w * 3) / 4.0))
             new_w = int(0.5 + (float(h * 4) / 3.0))
-            if new_h > h:
-                pad = (new_h - h) // 2
-                qt_im = qt_im.copy(0, -pad, w, new_h)
-            elif new_w > w:
-                pad = (new_w - w) // 2
-                qt_im = qt_im.copy(-pad, 0, new_w, h)
         else:
             new_h = int(0.5 + (float(w * 4) / 3.0))
             new_w = int(0.5 + (float(h * 3) / 4.0))
-            if new_w > w:
-                pad = (new_w - w) // 2
-                qt_im = qt_im.copy(-pad, 0, new_w, h)
-            elif new_h > h:
-                pad = (new_h - h) // 2
-                qt_im = qt_im.copy(0, -pad, w, new_h)
+        if new_w > w:
+            pad = (new_w - w) // 2
+            qt_im = qt_im.copy(-pad, 0, new_w, h)
+        elif new_h > h:
+            pad = (new_h - h) // 2
+            qt_im = qt_im.copy(0, -pad, w, new_h)
         return qt_im
 
     def make_thumb_PIL(self, qt_im):
@@ -763,7 +756,8 @@
             new_md = image.metadata
             old_md = Metadata(image.path, utf_safe=self.app.options.utf_safe)
             for key in ('title', 'description', 'keywords', 'rating',
-                        'copyright', 'creator',
+                        'creator', 'creator_title', 'credit_line', 'copyright',
+                        'usageterms', 'instructions', 'contact_info',
                         'date_taken', 'date_digitised', 'date_modified',
                         'orientation', 'camera_model',
                         'lens_model', 'lens_spec',
@@ -817,6 +811,17 @@
 
     @QtSlot()
     @catch_all
+    def fix_missing_thumbs(self):
+        with Busy():
+            for image in self.get_images():
+                thumb = image.metadata.thumbnail
+                if not thumb or not thumb['image']:
+                    if image.regenerate_thumbnail():
+                        image.load_thumbnail()
+        self.image_list_changed.emit()
+
+    @QtSlot()
+    @catch_all
     def close_selected_files(self):
         self.close_files(False)
 
@@ -853,6 +858,7 @@
         self._save_files(self.images)
 
     def _save_files(self, images=[]):
+        self._flush_editing()
         if_mode = eval(self.app.config_store.get('files', 'image', 'True'))
         sc_mode = self.app.config_store.get('files', 'sidecar', 'auto')
         force_iptc = eval(
@@ -870,16 +876,13 @@
                 image.metadata.save(
                     if_mode=if_mode, sc_mode=sc_mode,
                     force_iptc=force_iptc, file_times=file_times)
-        unsaved = False
-        for image in self.images:
-            if image.metadata.changed():
-                unsaved = True
-                break
+        unsaved = any([image.metadata.changed() for image in self.images])
         self.new_metadata.emit(unsaved)
 
     def unsaved_files_dialog(
             self, all_files=False, with_cancel=True, with_discard=True):
         """Return true if OK to continue with close or quit or whatever"""
+        self._flush_editing()
         for image in self.images:
             if image.metadata.changed() and (all_files or image.selected):
                 break
@@ -903,6 +906,12 @@
             return True
         return result == QtWidgets.QMessageBox.Discard
 
+    def _flush_editing(self):
+        # finish any editing in progress
+        current_focus = self.app.focusWidget()
+        if current_focus:
+            current_focus.clearFocus()
+
     def get_selected_images(self):
         selection = []
         for image in self.images:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/metadata.py 
new/Photini-2021.7.0/src/photini/metadata.py
--- old/Photini-2021.6.0/src/photini/metadata.py        2021-06-16 
13:18:18.000000000 +0200
+++ new/Photini-2021.7.0/src/photini/metadata.py        2021-07-12 
11:49:32.000000000 +0200
@@ -195,6 +195,9 @@
 
     @staticmethod
     def convert(value):
+        for key in value:
+            if isinstance(value[key], str):
+                value[key] = value[key].strip() or None
         return value
 
     def __setattr__(self, name, value):
@@ -342,16 +345,32 @@
         return this, False, True
 
 
+class ContactInformation(MD_Dict_Mergeable):
+    # stores IPTC contact information
+    _keys = ('CiAdrExtadr', 'CiAdrCity', 'CiAdrCtry', 'CiEmailWork',
+             'CiTelWork', 'CiAdrPcode', 'CiAdrRegion', 'CiUrlWork')
+
+    def __str__(self):
+        result = []
+        for key in self._keys:
+            if self[key]:
+                result.append('{}: {}'.format(key, self[key]))
+        return '\n'.join(result)
+
+    @staticmethod
+    def merge_item(this, other):
+        if other in this:
+            return this, False, False
+        return this + ' // ' + other, True, False
+
+
 class Location(MD_Dict_Mergeable):
     # stores IPTC defined location heirarchy
     _keys = ('SubLocation', 'City', 'ProvinceState',
              'CountryName', 'CountryCode', 'WorldRegion')
 
-    @staticmethod
-    def convert(value):
-        for key in value:
-            if value[key] and not value[key].strip():
-                value[key] = None
+    def convert(self, value):
+        value = super(Location, self).convert(value)
         if value['CountryCode']:
             value['CountryCode'] = value['CountryCode'].upper()
         return value
@@ -446,12 +465,8 @@
     _keys = ('make', 'model', 'serial_no')
     _quiet = True
 
-    @staticmethod
-    def convert(value):
-        for key in value:
-            if value[key]:
-                value[key] = value[key].strip()
-            value[key] = value[key] or None
+    def convert(self, value):
+        value = super(CameraModel, self).convert(value)
         if value['model'] == 'unknown':
             value['model'] = None
         return value
@@ -480,12 +495,8 @@
     _keys = ('make', 'model', 'serial_no')
     _quiet = True
 
-    @staticmethod
-    def convert(value):
-        for key in value:
-            if value[key]:
-                value[key] = value[key].strip()
-            value[key] = value[key] or None
+    def convert(self, value):
+        value = super(LensModel, self).convert(value)
         if value['model'] == 'n/a':
             value['model'] = None
         if value['serial_no'] == '0000000000':
@@ -551,6 +562,9 @@
 
     @staticmethod
     def convert(value):
+        if value['data']:
+            # don't keep reference to what might be an entire image file
+            value['data'] = bytes(value['data'])
         if value['data'] and not value['image']:
             buf = QtCore.QBuffer()
             buf.setData(value['data'])
@@ -561,14 +575,17 @@
             if value['image'].isNull():
                 logger.error('thumbnail: %s', reader.errorString())
                 value['image'] = None
-            # don't keep reference to what might be an entire image file
-            value['data'] = None
         if value['image'] and not value['data']:
             buf = QtCore.QBuffer()
             buf.open(buf.WriteOnly)
             value['fmt'] = 'JPEG'
-            value['image'].save(buf, value['fmt'])
-            value['data'] = buf.data().data()
+            quality = 95
+            while True:
+                value['image'].save(buf, value['fmt'], quality)
+                value['data'] = buf.data().data()
+                if len(value['data']) < 50000:
+                    break
+                quality -= 1
         if value['image']:
             value['w'] = value['image'].width()
             value['h'] = value['image'].height()
@@ -618,7 +635,8 @@
             handler.set_group(tag, [str(self['w']), str(self['h'])])
 
     def __str__(self):
-        return '{fmt} thumbnail, {w}x{h}'.format(**self)
+        return '{fmt} thumbnail, {w}x{h}, {size} bytes'.format(
+            size=len(self['data']), **self)
 
 
 class DateTime(MD_Dict):
@@ -778,11 +796,15 @@
         date_string, time_string = file_value
         if not date_string:
             return None
-        # remove missing date values
-        while len(date_string) > 4 and date_string[-2:] == '00':
-            date_string = date_string[:-3]
-        if date_string == '0000':
-            return None
+        # remove missing date values, allowing for GIMP not writing
+        # leading zeros
+        parts = [int(x) for x in date_string.split('-')]
+        while parts[-1] == 0:
+            parts = parts[:-1]
+            if not parts:
+                return None
+        date_string = '-'.join(['{:04d}'.format(parts[0])]
+                               + ['{:02d}'.format(x) for x in parts[1:]])
         # ignore time if date is not full precision
         if len(date_string) < 10:
             time_string = None
@@ -894,7 +916,10 @@
 
 class MD_String(MD_Value, str):
     def __new__(cls, value):
-        return super(MD_String, cls).__new__(cls, value.strip())
+        value = value.strip()
+        if not value:
+            return None
+        return super(MD_String, cls).__new__(cls, value)
 
     @classmethod
     def from_exiv2(cls, file_value, tag):
@@ -1091,14 +1116,18 @@
         'altitude'       : Altitude,
         'aperture'       : Aperture,
         'camera_model'   : CameraModel,
+        'contact_info'   : ContactInformation,
         'copyright'      : MD_String,
         'creator'        : MultiString,
+        'creator_title'  : MultiString,
+        'credit_line'    : MD_String,
         'date_digitised' : DateTime,
         'date_modified'  : DateTime,
         'date_taken'     : DateTime,
         'description'    : MD_String,
         'focal_length'   : MD_Rational,
         'focal_length_35': MD_Int,
+        'instructions'   : MD_String,
         'keywords'       : MultiString,
         'latlong'        : LatLon,
         'lens_model'     : LensModel,
@@ -1113,6 +1142,7 @@
         'thumbnail'      : Thumbnail,
         'timezone'       : Timezone,
         'title'          : MD_String,
+        'usageterms'     : MD_String,
         }
 
     def __init__(self, path, notify=None, utf_safe=False):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/ownership.py 
new/Photini-2021.7.0/src/photini/ownership.py
--- old/Photini-2021.6.0/src/photini/ownership.py       1970-01-01 
01:00:00.000000000 +0100
+++ new/Photini-2021.7.0/src/photini/ownership.py       2021-07-12 
11:49:32.000000000 +0200
@@ -0,0 +1,387 @@
+##  Photini - a simple photo metadata editor.
+##  http://github.com/jim-easterbrook/Photini
+##  Copyright (C) 2021  Jim Easterbrook  j...@jim-easterbrook.me.uk
+##
+##  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 the Free Software Foundation, either version 3 of the
+##  License, or (at your option) any later version.
+##
+##  This program is distributed in the hope that it will be useful,
+##  but WITHOUT ANY WARRANTY; without even the implied warranty of
+##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+##  General Public License for more details.
+##
+##  You should have received a copy of the GNU General Public License
+##  along with this program.  If not, see
+##  <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+
+from datetime import datetime
+import logging
+
+from photini.exiv2 import ImageMetadata
+from photini.pyqt import (
+    catch_all, MultiLineEdit, QtCore, QtSlot, QtWidgets, SingleLineEdit)
+
+logger = logging.getLogger(__name__)
+translate = QtCore.QCoreApplication.translate
+
+
+class TabWidget(QtWidgets.QWidget):
+    @staticmethod
+    def tab_name():
+        return translate('OwnerTab', '&Ownership metadata')
+
+    def __init__(self, image_list, *arg, **kw):
+        super(TabWidget, self).__init__(*arg, **kw)
+        self.config_store = QtWidgets.QApplication.instance().config_store
+        self.image_list = image_list
+        self.setLayout(QtWidgets.QHBoxLayout())
+        self.layout().setContentsMargins(0, 0, 0, 0)
+        # construct widgets
+        self.enableable = []
+        ## data fields
+        form, self.widgets = self.data_form()
+        self.enableable.append(form.widget())
+        for key in self.widgets:
+            self.widgets[key].editingFinished.connect(
+                getattr(self, 'new_' + key))
+        self.layout().addWidget(form)
+        ## buttons
+        buttons = QtWidgets.QVBoxLayout()
+        buttons.addStretch(1)
+        edit_template = QtWidgets.QPushButton(
+            translate('OwnerTab', 'Edit\ntemplate'))
+        edit_template.clicked.connect(self.edit_template)
+        buttons.addWidget(edit_template)
+        apply_template = QtWidgets.QPushButton(
+            translate('OwnerTab', 'Apply\ntemplate'))
+        apply_template.clicked.connect(self.apply_template)
+        self.enableable.append(apply_template)
+        buttons.addWidget(apply_template)
+        self.layout().addLayout(buttons)
+        # disable data entry until an image is selected
+        self.set_enabled(False)
+
+    def data_form(self):
+        widgets = {}
+        scrollarea = QtWidgets.QScrollArea()
+        scrollarea.setFrameStyle(QtWidgets.QFrame.NoFrame)
+        scrollarea.setWidgetResizable(True)
+        form = QtWidgets.QWidget()
+        form.setLayout(QtWidgets.QFormLayout())
+        # creator
+        widgets['creator'] = SingleLineEdit(
+            length_check=ImageMetadata.max_bytes('creator'),
+            spell_check=True, multi_string=True)
+        widgets['creator'].setToolTip(translate(
+            'OwnerTab',
+            'Enter the name of the person that created this image.'))
+        form.layout().addRow(translate(
+            'OwnerTab', 'Creator'), widgets['creator'])
+        # creator title
+        widgets['creator_title'] = SingleLineEdit(
+            length_check=ImageMetadata.max_bytes('creator_title'),
+            spell_check=True, multi_string=True)
+        widgets['creator_title'].setToolTip(translate(
+            'OwnerTab',
+            'Enter the job title of the person listed in the Creator field.'))
+        form.layout().addRow(translate(
+            'OwnerTab', "Creator's Jobtitle"), widgets['creator_title'])
+        # credit line
+        widgets['credit_line'] = SingleLineEdit(
+            length_check=ImageMetadata.max_bytes('credit_line'),
+            spell_check=True)
+        widgets['credit_line'].setToolTip(translate(
+            'OwnerTab',
+            'Enter who should be credited when this image is published.'))
+        form.layout().addRow(translate(
+            'OwnerTab', 'Credit Line'), widgets['credit_line'])
+        # copyright
+        widgets['copyright'] = SingleLineEdit(
+            length_check=ImageMetadata.max_bytes('copyright'), 
spell_check=True)
+        widgets['copyright'].setToolTip(translate(
+            'OwnerTab', 'Enter a notice on the current owner of the'
+            ' copyright for this image, such as "??2008 Jane Doe".'))
+        form.layout().addRow(translate(
+            'OwnerTab', 'Copyright Notice'), widgets['copyright'])
+        # usage terms
+        widgets['usageterms'] = SingleLineEdit(
+            length_check=ImageMetadata.max_bytes('usageterms'),
+            spell_check=True)
+        widgets['usageterms'].setToolTip(translate(
+            'OwnerTab',
+            'Enter instructions on how this image can legally be used.'))
+        form.layout().addRow(translate(
+            'OwnerTab', 'Rights Usage Terms'), widgets['usageterms'])
+        # special instructions
+        widgets['instructions'] = SingleLineEdit(
+            length_check=ImageMetadata.max_bytes('instructions'),
+            spell_check=True)
+        widgets['instructions'].setToolTip(translate(
+            'OwnerTab', 'Enter information about embargoes, or other'
+            ' restrictions not covered by the Rights Usage Terms field.'))
+        form.layout().addRow(translate(
+            'OwnerTab', 'Instructions'), widgets['instructions'])
+        ## contact information
+        contact_group = QtWidgets.QGroupBox()
+        contact_group.setLayout(QtWidgets.QFormLayout())
+        # email addresses
+        widgets['CiEmailWork'] = SingleLineEdit()
+        widgets['CiEmailWork'].setToolTip(translate(
+            'OwnerTab', 'Enter the work email address(es) for the person'
+            ' that created this image, such as n...@domain.com.'))
+        contact_group.layout().addRow(translate(
+            'OwnerTab', 'Email(s)'), widgets['CiEmailWork'])
+        # URLs
+        widgets['CiUrlWork'] = SingleLineEdit()
+        widgets['CiUrlWork'].setToolTip(translate(
+            'OwnerTab', 'Enter the work Web URL(s) for the person'
+            ' that created this image, such as http://www.domain.com/.'))
+        contact_group.layout().addRow(translate(
+            'OwnerTab', 'Web URL(s)'), widgets['CiUrlWork'])
+        # phone numbers
+        widgets['CiTelWork'] = SingleLineEdit()
+        widgets['CiTelWork'].setToolTip(translate(
+            'OwnerTab', 'Enter the work phone number(s) for the person'
+            ' that created this image, using the international format,'
+            ' such as +1 (123) 456789.'))
+        contact_group.layout().addRow(translate(
+            'OwnerTab', 'Phone(s)'), widgets['CiTelWork'])
+        # address
+        widgets['CiAdrExtadr'] = MultiLineEdit(
+            length_check=ImageMetadata.max_bytes('contact_info'),
+            spell_check=True)
+        widgets['CiAdrExtadr'].setToolTip(translate(
+            'OwnerTab',
+            'Enter address for the person that created this image.'))
+        contact_group.layout().addRow(translate(
+            'OwnerTab', 'Address'), widgets['CiAdrExtadr'])
+        # city
+        widgets['CiAdrCity'] = SingleLineEdit(spell_check=True)
+        widgets['CiAdrCity'].setToolTip(translate(
+            'OwnerTab', 'Enter the city for the address of the person'
+            ' that created this image.'))
+        contact_group.layout().addRow(translate(
+            'OwnerTab', 'City'), widgets['CiAdrCity'])
+        # postcode
+        widgets['CiAdrPcode'] = SingleLineEdit()
+        widgets['CiAdrPcode'].setToolTip(translate(
+            'OwnerTab', 'Enter the postal code for the address of the person'
+            ' that created this image.'))
+        contact_group.layout().addRow(translate(
+            'OwnerTab', 'Postal Code'), widgets['CiAdrPcode'])
+        # region
+        widgets['CiAdrRegion'] = SingleLineEdit(spell_check=True)
+        widgets['CiAdrRegion'].setToolTip(translate(
+            'OwnerTab', 'Enter the state for the address of the person'
+            ' that created this image.'))
+        contact_group.layout().addRow(translate(
+            'OwnerTab', 'State/Province'), widgets['CiAdrRegion'])
+        # country
+        widgets['CiAdrCtry'] = SingleLineEdit(spell_check=True)
+        widgets['CiAdrCtry'].setToolTip(translate(
+            'OwnerTab', 'Enter the country name for the address of the person'
+            ' that created this image.'))
+        contact_group.layout().addRow(translate(
+            'OwnerTab', 'Country'), widgets['CiAdrCtry'])
+        form.layout().addRow(translate(
+            'OwnerTab', 'Contact Information'), contact_group)
+        scrollarea.setWidget(form)
+        return scrollarea, widgets
+
+    def set_enabled(self, enabled):
+        for widget in self.enableable:
+            widget.setEnabled(enabled)
+
+    def refresh(self):
+        self.new_selection(self.image_list.get_selected_images())
+
+    def do_not_close(self):
+        return False
+
+    @QtSlot()
+    @catch_all
+    def image_list_changed(self):
+        pass
+
+    @QtSlot()
+    @catch_all
+    def new_creator(self):
+        self._new_value('creator')
+
+    @QtSlot()
+    @catch_all
+    def new_creator_title(self):
+        self._new_value('creator_title')
+
+    @QtSlot()
+    @catch_all
+    def new_credit_line(self):
+        self._new_value('credit_line')
+
+    @QtSlot()
+    @catch_all
+    def new_copyright(self):
+        self._new_value('copyright')
+
+    @QtSlot()
+    @catch_all
+    def new_usageterms(self):
+        self._new_value('usageterms')
+
+    @QtSlot()
+    @catch_all
+    def new_instructions(self):
+        self._new_value('instructions')
+
+    @QtSlot()
+    @catch_all
+    def new_CiEmailWork(self):
+        self._new_value('CiEmailWork')
+
+    @QtSlot()
+    @catch_all
+    def new_CiUrlWork(self):
+        self._new_value('CiUrlWork')
+
+    @QtSlot()
+    @catch_all
+    def new_CiTelWork(self):
+        self._new_value('CiTelWork')
+
+    @QtSlot()
+    @catch_all
+    def new_CiAdrExtadr(self):
+        self._new_value('CiAdrExtadr')
+
+    @QtSlot()
+    @catch_all
+    def new_CiAdrCity(self):
+        self._new_value('CiAdrCity')
+
+    @QtSlot()
+    @catch_all
+    def new_CiAdrPcode(self):
+        self._new_value('CiAdrPcode')
+
+    @QtSlot()
+    @catch_all
+    def new_CiAdrRegion(self):
+        self._new_value('CiAdrRegion')
+
+    @QtSlot()
+    @catch_all
+    def new_CiAdrCtry(self):
+        self._new_value('CiAdrCtry')
+
+    def _new_value(self, key):
+        images = self.image_list.get_selected_images()
+        if not self.widgets[key].is_multiple():
+            value = self.widgets[key].get_value()
+            for image in images:
+                self._set_value(image, key, value)
+        self._update_widget(key, images)
+
+    def _update_widget(self, key, images):
+        if not images:
+            return
+        values = []
+        for image in images:
+            value = self._get_value(image, key)
+            if value not in values:
+                values.append(value)
+        if len(values) > 1:
+            self.widgets[key].set_multiple(choices=filter(None, values))
+        else:
+            self.widgets[key].set_value(values[0])
+
+    @QtSlot()
+    @catch_all
+    def edit_template(self):
+        dialog = QtWidgets.QDialog(parent=self)
+        dialog.setFixedSize(min(800, self.window().width()),
+                            min(600, self.window().height()))
+        dialog.setWindowTitle(self.tr('Photini: ownership template'))
+        dialog.setLayout(QtWidgets.QVBoxLayout())
+        # main dialog area
+        form, widgets = self.data_form()
+        widgets['copyright'].setToolTip(
+            widgets['copyright'].toolTip() + ' ' +
+            translate('OwnerTab',
+                      'Use %Y to insert the year the photograph was taken.'))
+        for key in widgets:
+            value = None
+            if key == 'copyright':
+                name = self.config_store.get('user', 'copyright_name') or ''
+                text = (self.config_store.get('user', 'copyright_text') or
+                        translate('DescriptiveTab', 'Copyright ??{year} 
{name}.'
+                                  ' All rights reserved.'))
+                value = text.format(year='%Y', name=name)
+            elif key == 'creator':
+                value = self.config_store.get('user', 'creator_name')
+            widgets[key].set_value(
+                self.config_store.get('ownership', key, value))
+        dialog.layout().addWidget(form)
+        # apply & cancel buttons
+        button_box = QtWidgets.QDialogButtonBox(
+            QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
+        button_box.accepted.connect(dialog.accept)
+        button_box.rejected.connect(dialog.reject)
+        dialog.layout().addWidget(button_box)
+        if dialog.exec_() != QtWidgets.QDialog.Accepted:
+            return
+        for key in widgets:
+            value = widgets[key].get_value()
+            if value:
+                self.config_store.set('ownership', key, value)
+
+    @QtSlot()
+    @catch_all
+    def apply_template(self):
+        value = {}
+        for key in self.widgets:
+            text = self.config_store.get('ownership', key)
+            if text:
+                value[key] = text
+        images = self.image_list.get_selected_images()
+        for image in images:
+            date_taken = image.metadata.date_taken
+            if date_taken is None:
+                date_taken = datetime.now()
+            else:
+                date_taken = date_taken['datetime']
+            for key in value:
+                self._set_value(image, key, date_taken.strftime(value[key]))
+        for key in value:
+            self._update_widget(key, images)
+
+    def _set_value(self, image, key, value):
+        if key.startswith('Ci'):
+            info = dict(image.metadata.contact_info or {})
+            info[key] = value
+            image.metadata.contact_info = info
+        else:
+            setattr(image.metadata, key, value)
+
+    def _get_value(self, image, key):
+        if key.startswith('Ci'):
+            info = image.metadata.contact_info
+            if info:
+                return info[key]
+            return None
+        return getattr(image.metadata, key)
+
+    @QtSlot(list)
+    @catch_all
+    def new_selection(self, selection):
+        if not selection:
+            for key in self.widgets:
+                self.widgets[key].set_value(None)
+            self.set_enabled(False)
+            return
+        for key in self.widgets:
+            self._update_widget(key, selection)
+        self.set_enabled(True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Photini-2021.6.0/src/photini/pyqt.py 
new/Photini-2021.7.0/src/photini/pyqt.py
--- old/Photini-2021.6.0/src/photini/pyqt.py    2021-06-16 13:18:18.000000000 
+0200
+++ new/Photini-2021.7.0/src/photini/pyqt.py    2021-07-12 11:49:32.000000000 
+0200
@@ -68,7 +68,7 @@
                 pass
     else:
         using_qtwebengine = eval(using_qtwebengine)
-    from PySide2 import QtCore, QtGui, QtWidgets
+    from PySide2 import QtCore, QtGui, QtNetwork, QtWidgets
     from PySide2.QtCore import Qt
     from PySide2.QtNetwork import QNetworkProxy
     if using_qtwebengine:
@@ -91,7 +91,7 @@
                 pass
     else:
         using_qtwebengine = eval(using_qtwebengine)
-    from PyQt5 import QtCore, QtGui, QtWidgets
+    from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
     from PyQt5.QtCore import Qt
     from PyQt5.QtNetwork import QNetworkProxy
     if using_qtwebengine:

Reply via email to