LoggerWindow
diff --git a/src/lang/pl/photini.ts b/src/lang/pl/photini.ts
index f29c270d..203db5c4 100644
--- a/src/lang/pl/photini.ts
+++ b/src/lang/pl/photini.ts
@@ -841,11 +841,12 @@
LatLongDisplay
-
+
-
+
+ Short abbreviation of "Latitude, longitude"
diff --git a/src/lang/templates/qt/photini.ts b/src/lang/templates/qt/photini.ts
index 24c93ec8..4180cb1f 100644
--- a/src/lang/templates/qt/photini.ts
+++ b/src/lang/templates/qt/photini.ts
@@ -826,11 +826,12 @@
LatLongDisplay
-
+
-
+
+ Short abbreviation of "Latitude, longitude"
diff --git a/src/photini/address.py b/src/photini/address.py
index 64d6c4d7..e80d9142 100644
--- a/src/photini/address.py
+++ b/src/photini/address.py
@@ -1,6 +1,6 @@
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2019-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2019-24 Jim Easterbrook jim@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
@@ -97,7 +97,7 @@ def query(self, params):
def get_address(self, coords):
params = {'q': '{:.5f},{:.5f}'.format(*coords)}
- lang, encoding = locale.getdefaultlocale()
+ lang, encoding = locale.getlocale()
if lang:
params['language'] = lang
results = self.cached_query(params)
diff --git a/src/photini/bingmap.py b/src/photini/bingmap.py
index 3c13f9ea..60ed848a 100644
--- a/src/photini/bingmap.py
+++ b/src/photini/bingmap.py
@@ -1,6 +1,6 @@
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2012-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2012-24 Jim Easterbrook jim@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
@@ -16,8 +16,6 @@
## along with this program. If not, see
## .
-from __future__ import unicode_literals
-
import locale
import logging
@@ -78,7 +76,7 @@ def search(self, search_string, bounds=None):
'query' : search_string,
'maxRes': '20',
}
- lang, encoding = locale.getdefaultlocale()
+ lang, encoding = locale.getlocale()
if lang:
params['culture'] = lang.replace('_', '-')
if bounds:
@@ -114,7 +112,7 @@ def get_geocoder(self):
def get_head(self):
url = 'http://www.bing.com/api/maps/mapcontrol?callback=initialize'
url += '&key=' + self.api_key
- lang, encoding = locale.getdefaultlocale()
+ lang, encoding = locale.getlocale()
if lang:
culture = lang.replace('_', '-')
url += '&setMkt=' + culture
diff --git a/src/photini/configstore.py b/src/photini/configstore.py
index 15884bd3..72efc402 100644
--- a/src/photini/configstore.py
+++ b/src/photini/configstore.py
@@ -1,7 +1,6 @@
-# -*- coding: utf-8 -*-
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2012-21 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2012-24 Jim Easterbrook jim@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
@@ -17,18 +16,14 @@
## along with this program. If not, see
## .
-from __future__ import unicode_literals
-
import ast
import codecs
from configparser import RawConfigParser
import os
import pprint
import stat
-import sys
import appdirs
-import pkg_resources
class BaseConfigStore(object):
# the actual config store functionality
@@ -125,13 +120,10 @@ class KeyStore(object):
"""
def __init__(self):
self.config = RawConfigParser()
- if sys.version_info >= (3, 2):
- data = pkg_resources.resource_string('photini', 'data/keys.txt')
- data = data.decode('utf-8')
- self.config.read_string(data)
- else:
- data = pkg_resources.resource_stream('photini', 'data/keys.txt')
- self.config.readfp(data)
+ with open(os.path.join(
+ os.path.dirname(__file__), 'data', 'keys.txt'), 'r') as f:
+ data = f.read()
+ self.config.read_string(data)
def get(self, section, option):
value = self.config.get(section, option)
diff --git a/src/photini/data/map/bingmap.js b/src/photini/data/map/bingmap.js
index 3a41239a..23b8ec39 100644
--- a/src/photini/data/map/bingmap.js
+++ b/src/photini/data/map/bingmap.js
@@ -1,6 +1,6 @@
// Photini - a simple photo metadata editor.
// http://github.com/jim-easterbrook/Photini
-// Copyright (C) 2012-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+// Copyright (C) 2012-24 Jim Easterbrook jim@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
@@ -184,17 +184,11 @@ function addMarker(id, lat, lng, active)
var marker = new Microsoft.Maps.Pushpin(
new Microsoft.Maps.Location(lat, lng), {
anchor : new Microsoft.Maps.Point(11, 35),
- icon : 'pin_grey.png',
+ icon : active ? 'pin_red.png' : 'pin_grey.png',
draggable: true
});
marker.metadata = {id: id};
- if (active)
- {
- marker.setOptions({icon: 'pin_red.png'});
- layers[1].add(marker);
- }
- else
- layers[0].add(marker);
+ layers[active ? 1 : 0].add(marker);
Microsoft.Maps.Events.addHandler(marker, 'dragstart', markerClick);
Microsoft.Maps.Events.addHandler(marker, 'drag', markerDrag);
Microsoft.Maps.Events.addHandler(marker, 'dragend', markerDragEnd);
@@ -229,14 +223,14 @@ function markerDrop(x, y)
function delMarker(id)
{
- for (var j = 0; j < layers.length; j++)
+ for (var j = 0; j < 2; j++)
{
var markers = layers[j].getPrimitives();
for (var i = 0; i < markers.length; i++)
if (markers[i].metadata.id == id)
{
layers[j].remove(markers[i]);
- return markers[i];
+ return;
}
}
}
diff --git a/src/photini/data/map/googlemap.js b/src/photini/data/map/googlemap.js
index cb3350a1..6fde5643 100644
--- a/src/photini/data/map/googlemap.js
+++ b/src/photini/data/map/googlemap.js
@@ -1,6 +1,6 @@
// Photini - a simple photo metadata editor.
// http://github.com/jim-easterbrook/Photini
-// Copyright (C) 2012-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+// Copyright (C) 2012-24 Jim Easterbrook jim@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
@@ -24,7 +24,7 @@ var gpsMarkers = {};
var icon_on;
var icon_off;
var gpsBlueCircle;
-var gpsRedCircle
+var gpsRedCircle;
function loadMap(lat, lng, zoom)
{
diff --git a/src/photini/data/map/mapboxmap.js b/src/photini/data/map/mapboxmap.js
index cb49cc3c..752497cc 100644
--- a/src/photini/data/map/mapboxmap.js
+++ b/src/photini/data/map/mapboxmap.js
@@ -1,6 +1,6 @@
// Photini - a simple photo metadata editor.
// http://github.com/jim-easterbrook/Photini
-// Copyright (C) 2018-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+// Copyright (C) 2018-24 Jim Easterbrook jim@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
diff --git a/src/photini/editor.py b/src/photini/editor.py
index def9410f..8a7fd262 100644
--- a/src/photini/editor.py
+++ b/src/photini/editor.py
@@ -1,6 +1,6 @@
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2012-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2012-24 Jim Easterbrook jim@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
@@ -16,6 +16,7 @@
## along with this program. If not, see
## .
+import codecs
import importlib
import locale
import logging
@@ -25,8 +26,6 @@
import sys
import warnings
-import pkg_resources
-
from photini import __version__
from photini.configstore import BaseConfigStore
from photini.editsettings import EditSettings
@@ -266,21 +265,24 @@ def set_language(self, action):
@QtSlot()
@catch_all
def about(self):
+ data_dir = os.path.join(os.path.dirname(__file__), 'data')
+ with open(os.path.join(
+ data_dir, 'icons', 'photini_128.png'), 'rb') as f:
+ icon = f.read()
text = """
Photini
version: {}
|
- |
+ |
© Jim Easterbrook
jim@jim-easterbrook.me.uk
{}
{}
""".format(__version__,
- pkg_resources.resource_filename(
- 'photini', 'data/icons/photini_128.png'),
+ codecs.encode(icon, 'base64').decode('ascii'),
translate('MenuBar', 'An easy to use digital photograph metadata'
' (Exif, IPTC, XMP) editing application.'),
translate(
@@ -291,8 +293,9 @@ def about(self):
dialog = QtWidgets.QMessageBox(self)
dialog.setWindowTitle(translate('MenuBar', 'Photini: about'))
dialog.setText(text)
- licence = pkg_resources.resource_string('photini', 'data/LICENSE.txt')
- dialog.setDetailedText(licence.decode('utf-8'))
+ with open(os.path.join(data_dir, 'LICENSE.txt'), 'r') as f:
+ licence = f.read()
+ dialog.setDetailedText(licence)
dialog.setInformativeText(translate(
'MenuBar', 'This program is released with a GNU General Public'
' License. For details click the "{details}" button.').format(
@@ -355,9 +358,8 @@ def __init__(self, options, initial_files):
super(MainWindow, self).__init__()
self.setWindowTitle(translate(
'MenuBar', "Photini photo metadata editor"))
- pixmap = QtGui.QPixmap()
- pixmap.loadFromData(pkg_resources.resource_string(
- 'photini', 'data/icons/photini_48.png'))
+ pixmap = QtGui.QPixmap(os.path.join(
+ os.path.dirname(__file__), 'data', 'icons', 'photini_48.png'))
icon = QtGui.QIcon(pixmap)
self.setWindowIcon(icon)
self.selection = list()
@@ -540,18 +542,24 @@ def main(argv=None):
# get remaining argument list after Qt has swallowed its options
sys.argv = app.arguments()
# install translations
- # English translation as a fallback (to get correct plurals)
- lang_dir = pkg_resources.resource_filename('photini', 'data/lang')
- translator = QtCore.QTranslator(parent=app)
- if translator.load('photini.en', lang_dir):
- app.installTranslator(translator)
- translator = QtCore.QTranslator(parent=app)
- # localised translation, if it exists
+ lang_dir = os.path.join(os.path.dirname(__file__), 'data', 'lang')
locale.setlocale(locale.LC_ALL, '')
- qt_locale = QtCore.QLocale.system()
- if translator.load(qt_locale, 'photini', '.', lang_dir):
- app.installTranslator(translator)
- translator = QtCore.QTranslator(parent=app)
+ langs = [x.replace('-', '_') for x in QtCore.QLocale.system().uiLanguages()]
+ # always have English translation as a fallback (to get correct plurals)
+ if 'en' not in langs:
+ langs += ['en']
+ installed = []
+ for lang in reversed(langs):
+ if lang in installed:
+ continue
+ file = os.path.join(lang_dir, 'photini.{}.qm'.format(lang))
+ if not os.path.isfile(file):
+ file = os.path.join(lang_dir, 'photini.{}.qm'.format(lang.lower()))
+ translator = QtCore.QTranslator()
+ if os.path.isfile(file) and translator.load(file):
+ translator.setParent(app)
+ app.installTranslator(translator)
+ installed.append(lang)
# parse remaining arguments
version = full_version_info()
parser = OptionParser(
diff --git a/src/photini/exiv2.py b/src/photini/exiv2.py
index c014d420..380bd93b 100644
--- a/src/photini/exiv2.py
+++ b/src/photini/exiv2.py
@@ -59,8 +59,9 @@ def get_info(cls, tag_name):
@classmethod
def initialise(cls, config_store, verbosity):
- exiv2.LogMsg.setLevel(
- max(exiv2.LogMsg.debug, min(exiv2.LogMsg.error, 4 - verbosity)))
+ level = min(exiv2.LogMsg.Level.error, 4 - verbosity)
+ level = max(exiv2.LogMsg.Level.debug, level)
+ exiv2.LogMsg.setLevel(exiv2.LogMsg.Level(level))
exiv2.XmpParser.initialize()
if config_store and exiv2.testVersion(0, 27, 4):
exiv2.enableBMFF(config_store.get('metadata', 'enable_bmff', False))
@@ -132,9 +133,15 @@ def __init__(self, path=None, buf=None):
exiv2.TypeId.string):
continue
key = datum.key()
+ if key in ('Iptc.Envelope.CharacterSet', 'Exif.Image.IPTCNAA'):
+ continue
if '.0x' in key:
# unknown key type
continue
+ family, group, tagname = key.split('.', 2)
+ if family == 'Exif' and exiv2.ExifTags.isMakerGroup(group):
+ # don't transcode maker note stuff
+ continue
raw_value = datum.value().data()
if self.decode_string(key, raw_value, 'utf-8') is not None:
# no need to do anything
@@ -161,7 +168,8 @@ def __init__(self, path=None, buf=None):
else:
logger.warning('%s: failed to transcode %s',
self._name, key)
- value = raw_value.decode('utf-8', errors='replace')
+ value = bytes(raw_value).decode(
+ 'utf-8', errors='replace')
new_data[key].append(value)
for key, value in new_data.items():
if len(value) == 1:
@@ -294,9 +302,9 @@ def get_xmp_tags(self):
yield datum.key()
@classmethod
- def open_old(cls, *arg, quiet=False, **kw):
+ def open_old(cls, path, *arg, quiet=False, **kw):
try:
- return cls(*arg, **kw)
+ return cls(path, *arg, **kw)
except exiv2.Exiv2Error as ex:
# expected if unrecognised file format
if quiet:
@@ -305,6 +313,7 @@ def open_old(cls, *arg, quiet=False, **kw):
logger.warning(str(ex))
return None
except Exception as ex:
+ logger.error('Exception opening %s', path)
logger.exception(ex)
return None
@@ -312,9 +321,7 @@ def set_exif_thumbnail_from_buffer(self, buffer):
thumb = exiv2.ExifThumb(self._exifData)
thumb.setJpegThumbnail(buffer)
- def get_exif_comment(self, tag, datum):
- type_id = datum.typeId()
- value = datum.value()
+ def get_exif_comment(self, tag, value):
if isinstance(value, exiv2.CommentValue):
data = value.data()
charset = value.charsetId()
@@ -387,39 +394,51 @@ def get_exif_comment(self, tag, datum):
return result
def get_exif_value(self, tag):
- datum = self._exifData.findKey(exiv2.ExifKey(tag))
+ try:
+ key = exiv2.ExifKey(tag)
+ except exiv2.Exiv2Error:
+ # old versions of libexiv2 don't recognise newer tags
+ return None
+ datum = self._exifData.findKey(key)
if datum == self._exifData.end():
return None
if tag in ('Exif.Canon.ModelID', 'Exif.CanonCs.LensType',
+ 'Exif.Canon.SerialNumber', 'Exif.CanonLe.LensSerialNumber',
'Exif.Image.XPTitle', 'Exif.Image.XPComment',
'Exif.Image.XPAuthor', 'Exif.Image.XPKeywords',
'Exif.Image.XPSubject', 'Exif.NikonLd1.LensIDNumber',
- 'Exif.NikonLd2.LensIDNumber',
- 'Exif.NikonLd3.LensIDNumber', 'Exif.Pentax.ModelID'):
+ 'Exif.Minolta.LensID', 'Exif.Nikon3.LensType',
+ 'Exif.NikonLd2.LensIDNumber', 'Exif.NikonLd3.LensIDNumber',
+ 'Exif.NikonLd4.LensIDNumber', 'Exif.OlympusEq.LensType',
+ 'Exif.Olympus2.CameraID',
+ 'Exif.Panasonic.InternalSerialNumber',
+ 'Exif.Pentax.LensType', 'Exif.Pentax.ModelID',
+ 'Exif.PentaxDng.LensType', 'Exif.PentaxDng.ModelID',
+ 'Exif.Sony1.LensID', 'Exif.Sony1.SonyModelID',
+ 'Exif.Sony2.LensID', 'Exif.Sony2.SonyModelID'):
# use Exiv2's "interpreted string"
return datum._print(self._exifData)
+ value = datum.value()
if tag in ('Exif.Photo.UserComment',
'Exif.GPSInfo.GPSProcessingMethod'):
- return self.get_exif_comment(tag, datum)
- value = datum.value()
- type_id = datum.typeId()
- if type_id == exiv2.TypeId.asciiString:
+ return self.get_exif_comment(tag, value)
+ if isinstance(value, exiv2.AsciiValue):
return value.toString()
- if type_id in (exiv2.TypeId.unsignedByte, exiv2.TypeId.undefined):
+ if isinstance(value, exiv2.DataValue):
result = bytearray(value.size())
value.copy(result, exiv2.ByteOrder.invalidByteOrder)
return result
- if type_id not in (
- exiv2.TypeId.signedRational, exiv2.TypeId.unsignedRational,
- exiv2.TypeId.signedShort, exiv2.TypeId.unsignedShort,
- exiv2.TypeId.signedLong, exiv2.TypeId.unsignedLong):
- # unhandled type, use the string representation
- logger.warning('%s: %s: reading %s as string',
- self._name, tag, datum.typeName())
- return value.toString()
- if len(value) > 1:
- return list(value)
- return value[0]
+ if isinstance(value, (exiv2.RationalValue, exiv2.URationalValue,
+ exiv2.ShortValue, exiv2.UShortValue,
+ exiv2.LongValue, exiv2.ULongValue)):
+ if len(value) > 1:
+ return list(value)
+ if len(value) == 0:
+ return None
+ return value[0]
+ logger.warning(
+ '%s: %s: reading %s as string', self._name, tag, type(value))
+ return value.toString()
def decode_iptc_value(self, datum):
type_id = datum.typeId()
diff --git a/src/photini/googlemap.py b/src/photini/googlemap.py
index de923f28..1449d3b3 100644
--- a/src/photini/googlemap.py
+++ b/src/photini/googlemap.py
@@ -1,6 +1,6 @@
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2012-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2012-24 Jim Easterbrook jim@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
@@ -67,7 +67,7 @@ def get_altitude(self, coords):
def search(self, search_string, bounds=None):
params = {'address': search_string}
- lang, encoding = locale.getdefaultlocale()
+ lang, encoding = locale.getlocale()
if lang:
params['language'] = lang
if bounds:
@@ -105,7 +105,7 @@ def get_head(self):
if self.app.options.test:
url += '&v=beta'
url += '&key=' + self.api_key
- lang, encoding = locale.getdefaultlocale()
+ lang, encoding = locale.getlocale()
if lang:
language, sep, region = lang.replace('_', '-').partition('-')
url += '&language=' + language
diff --git a/src/photini/mapboxmap.py b/src/photini/mapboxmap.py
index 933e4cf0..0fd1bbdf 100644
--- a/src/photini/mapboxmap.py
+++ b/src/photini/mapboxmap.py
@@ -1,6 +1,6 @@
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2018-22 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2018-24 Jim Easterbrook jim@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
@@ -39,7 +39,7 @@ def query(self, params):
del params['query']
params['access_token'] = self.api_key
params['autocomplete '] = 'false'
- lang, encoding = locale.getdefaultlocale()
+ lang, encoding = locale.getlocale()
if lang:
params['language'] = lang
query += '.json'
diff --git a/src/photini/metadata.py b/src/photini/metadata.py
index 834527ef..7512debb 100644
--- a/src/photini/metadata.py
+++ b/src/photini/metadata.py
@@ -1,6 +1,6 @@
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2012-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2012-24 Jim Easterbrook jim@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
@@ -18,7 +18,6 @@
import codecs
from fractions import Fraction
-import imghdr
import logging
import math
import mimetypes
@@ -26,6 +25,7 @@
import re
import exiv2
+import filetype
from photini import __version__
from photini.exiv2 import MetadataHandler
@@ -108,6 +108,7 @@ def open_old(cls, path):
except RuntimeError as ex:
logger.error(str(ex))
except Exception as ex:
+ logger.error('Exception opening %s', path)
logger.exception(ex)
return None
@@ -243,9 +244,7 @@ def save(self, file_times=None, write_iptc=False):
# some tags disappear with good reason
continue
family, group, tagname = tag.split('.', 2)
- if family == 'Exif' and group[:5] in (
- 'Canon', 'Casio', 'Fujif', 'Minol', 'Nikon', 'Olymp',
- 'Panas', 'Penta', 'Samsu', 'Sigma', 'Sony1'):
+ if family == 'Exif' and exiv2.ExifTags.isMakerGroup(group):
# maker note tags are often not saved
logger.warning('%s: tag not saved: %s', self._name, tag)
continue
@@ -269,6 +268,8 @@ def get_all_tags(self):
'', 'Exif.Canon.ModelID', 'Exif.Canon.SerialNumber'),
'Exif.CanonCs.Lens*': ('', 'Exif.CanonCs.LensType',
'', 'Exif.CanonCs.Lens'),
+ 'Exif.CanonLe.LensSerialNumber*': (
+ '', '', 'Exif.CanonLe.LensSerialNumber'),
'Exif.Fujifilm.SerialNumber*': ('', '', 'Exif.Fujifilm.SerialNumber'),
'Exif.GPSInfo.GPS*': (
'Exif.GPSInfo.GPSVersionID', 'Exif.GPSInfo.GPSProcessingMethod',
@@ -286,17 +287,29 @@ def get_all_tags(self):
'Exif.Photo.BodySerialNumber'),
'Exif.Image.UniqueCameraModel*': (
'', 'Exif.Image.UniqueCameraModel', 'Exif.Image.CameraSerialNumber'),
- 'Exif.Nikon3.Lens*': ('', '', '', 'Exif.Nikon3.Lens'),
+ 'Exif.Minolta.LensID*': ('', 'Exif.Minolta.LensID'),
+ 'Exif.Nikon3.Lens*': (
+ '', 'Exif.Nikon3.LensType', '', 'Exif.Nikon3.Lens'),
'Exif.Nikon3.SerialNumber*': ('', '', 'Exif.Nikon3.SerialNumber'),
'Exif.NikonLd1.LensIDNumber*': ('', 'Exif.NikonLd1.LensIDNumber'),
'Exif.NikonLd2.LensIDNumber*': ('', 'Exif.NikonLd2.LensIDNumber'),
'Exif.NikonLd3.LensIDNumber*': ('', 'Exif.NikonLd3.LensIDNumber'),
+ 'Exif.NikonLd4.LensIDNumber*': ('', 'Exif.NikonLd4.LensIDNumber'),
'Exif.OlympusEq.Camera*': (
'', 'Exif.OlympusEq.CameraType', 'Exif.OlympusEq.SerialNumber'),
'Exif.OlympusEq.LensModel*': (
'', 'Exif.OlympusEq.LensModel', 'Exif.OlympusEq.LensSerialNumber'),
+ 'Exif.OlympusEq.Lens2*': (
+ '', 'Exif.OlympusEq.LensType', ''),
+ 'Exif.Olympus2.Camera*': (
+ '', 'Exif.Olympus2.CameraID', ''),
+ 'Exif.Panasonic.InternalSerialNumber*': (
+ '', '', 'Exif.Panasonic.InternalSerialNumber'),
+ 'Exif.Pentax.LensType*': ('', 'Exif.Pentax.LensType'),
'Exif.Pentax.ModelID*': (
'', 'Exif.Pentax.ModelID', 'Exif.Pentax.SerialNumber'),
+ 'Exif.PentaxDng.LensType*': ('', 'Exif.PentaxDng.LensType'),
+ 'Exif.PentaxDng.ModelID*': ('', 'Exif.PentaxDng.ModelID'),
'Exif.Photo.DateTimeDigitized*': (
'Exif.Photo.DateTimeDigitized', 'Exif.Photo.SubSecTimeDigitized'),
'Exif.Photo.DateTimeOriginal*': (
@@ -306,6 +319,12 @@ def get_all_tags(self):
'Exif.Photo.Lens*': (
'Exif.Photo.LensMake', 'Exif.Photo.LensModel',
'Exif.Photo.LensSerialNumber', 'Exif.Photo.LensSpecification'),
+ 'Exif.Sigma.SerialNumber*': (
+ '', '', 'Exif.Sigma.SerialNumber'),
+ 'Exif.Sony1.LensID*': ('', 'Exif.Sony1.LensID'),
+ 'Exif.Sony1.SonyModelID*': ('', 'Exif.Sony1.SonyModelID'),
+ 'Exif.Sony2.LensID*': ('', 'Exif.Sony2.LensID'),
+ 'Exif.Sony2.SonyModelID*': ('', 'Exif.Sony2.SonyModelID'),
'Exif.Thumbnail.*': (
'Exif.Thumbnail.ImageWidth', 'Exif.Thumbnail.ImageLength',
'Exif.Thumbnail.Compression'),
@@ -356,7 +375,13 @@ def get_all_tags(self):
('WN', 'Exif.Fujifilm.SerialNumber*'),
('WN', 'Exif.Nikon3.SerialNumber*'),
('WN', 'Exif.OlympusEq.Camera*'),
+ ('WN', 'Exif.Olympus2.Camera*'),
+ ('WN', 'Exif.Panasonic.InternalSerialNumber*'),
+ ('WN', 'Exif.PentaxDng.ModelID*'),
('WN', 'Exif.Pentax.ModelID*'),
+ ('WN', 'Exif.Sigma.SerialNumber*'),
+ ('WN', 'Exif.Sony1.SonyModelID*'),
+ ('WN', 'Exif.Sony2.SonyModelID*'),
('WN', 'Xmp.aux.SerialNumber*'),
('W0', 'Xmp.video.Make*')),
'contact_info' : (('WA', 'Xmp.plus.Licensor'),
@@ -430,11 +455,19 @@ def get_all_tags(self):
('W0', 'Exif.Image.Lens*'),
('WN', 'Exif.Canon.LensModel*'),
('WN', 'Exif.CanonCs.Lens*'),
- ('WN', 'Exif.OlympusEq.LensModel*'),
- ('WN', 'Exif.Nikon3.Lens*'),
+ ('WN', 'Exif.CanonLe.LensSerialNumber*'),
+ ('WN', 'Exif.Minolta.LensID*'),
('WN', 'Exif.NikonLd1.LensIDNumber*'),
('WN', 'Exif.NikonLd2.LensIDNumber*'),
('WN', 'Exif.NikonLd3.LensIDNumber*'),
+ ('WN', 'Exif.NikonLd4.LensIDNumber*'),
+ ('WN', 'Exif.Nikon3.Lens*'),
+ ('WN', 'Exif.OlympusEq.LensModel*'),
+ ('WN', 'Exif.OlympusEq.Lens2*'),
+ ('WN', 'Exif.Pentax.LensType*'),
+ ('WN', 'Exif.PentaxDng.LensType*'),
+ ('WN', 'Exif.Sony1.LensID*'),
+ ('WN', 'Exif.Sony2.LensID*'),
('W0', 'Xmp.aux.Lens*')),
'location_shown' : (('WA', 'Xmp.iptcExt.LocationShown'),),
'location_taken' : (('WA', 'Xmp.iptcExt.LocationCreated'),
@@ -527,9 +560,13 @@ def get_image_size(self):
for key in self.get_all_tags():
family, group, tag = key.split('.', 2)
if tag in ('PixelXDimension', 'ImageWidth'):
- widths[key] = int(self.get_value(key))
+ value = self.get_value(key)
+ if value:
+ widths[key] = int(value)
elif tag in ('PixelYDimension', 'ImageLength'):
- heights[key] = int(self.get_value(key))
+ value = self.get_value(key)
+ if value:
+ heights[key] = int(value)
for kx in widths:
if 'ImageWidth' in kx:
ky = kx.replace('ImageWidth', 'ImageLength')
@@ -557,6 +594,7 @@ def open_old(cls, path):
try:
return cls(path=path)
except Exception as ex:
+ logger.error('Exception opening %s', path)
logger.exception(ex)
return None
@@ -567,6 +605,7 @@ def open_new(cls, path, image_md):
cls.create_sc(sc_path, image_md)
return cls(path=sc_path)
except Exception as ex:
+ logger.error('Exception opening %s', path)
logger.exception(ex)
return None
@@ -628,8 +667,11 @@ def __init__(self, path, notify=None):
video_md = None
self._if = None
self._sc = SidecarMetadata.open_old(self.find_sidecar())
- self._if = ImageMetadata.open_old(
- path, quiet=self.get_mime_type().split('/')[0] == 'video')
+ # guess mime type from file name
+ self.mime_type = mimetypes.guess_type(self._path, strict=False)[0]
+ quiet = self.mime_type and self.mime_type.split('/')[0] == 'video'
+ self._if = ImageMetadata.open_old(path, quiet=quiet)
+ # get mime type from image data
self.mime_type = self.get_mime_type()
if self.mime_type.split('/')[0] == 'video':
video_md = FFMPEGMetadata.open_old(path)
@@ -812,11 +854,9 @@ def get_mime_type(self):
if self._if:
result = self._if.mime_type
if not result:
- result = mimetypes.guess_type(self._path, strict=False)[0]
- if not result:
- result = imghdr.what(self._path)
- if result:
- result = 'image/' + result
+ kind = filetype.guess(self._path)
+ if kind:
+ result = kind.mime
# anything not recognised is assumed to be 'raw'
if not result:
result = 'image/raw'
diff --git a/src/photini/photinimap.py b/src/photini/photinimap.py
index 86425f25..e8081afe 100644
--- a/src/photini/photinimap.py
+++ b/src/photini/photinimap.py
@@ -1,6 +1,6 @@
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2012-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2012-24 Jim Easterbrook jim@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
@@ -23,7 +23,6 @@
import appdirs
import cachetools
-import pkg_resources
from photini.imagelist import DRAG_MIMETYPE
from photini.pyqt import *
@@ -148,8 +147,12 @@ def createWindow(self, type_):
@catch_all
def javaScriptConsoleMessage(self, level, msg, line, source):
- logger.log(
- logging.INFO + (level * 10), '%s line %d: %s', source, line, msg)
+ level = {
+ self.JavaScriptConsoleMessageLevel.InfoMessageLevel: logging.INFO,
+ self.JavaScriptConsoleMessageLevel.WarningMessageLevel: logging.WARNING,
+ self.JavaScriptConsoleMessageLevel.ErrorMessageLevel: logging.ERROR,
+ }[level]
+ logger.log(level, '%s line %d: %s', source, line, msg)
class MapWebView(QWebEngineView):
@@ -196,8 +199,7 @@ class PhotiniMap(QtWidgets.QWidget):
def __init__(self, parent=None):
super(PhotiniMap, self).__init__(parent)
self.app = QtWidgets.QApplication.instance()
- self.script_dir = pkg_resources.resource_filename(
- 'photini', 'data/map/')
+ self.script_dir = os.path.join(os.path.dirname(__file__), 'data', 'map')
self.drag_icon = QtGui.QPixmap(
os.path.join(self.script_dir, 'pin_grey.png'))
self.drag_hotspot = 11, 35
@@ -321,12 +323,13 @@ def initialise(self):
loadMap({lat}, {lng}, {zoom});
}}
'''
- initialize = initialize.format(lat=lat, lng=lng, zoom=zoom)
- page = page.format(initialize=initialize, head=self.get_head(),
- script=self.__module__.split('.')[-1])
+ page = page.format(
+ head = self.get_head(),
+ script = self.__module__.split('.')[-1],
+ initialize = initialize.format(lat=lat, lng=lng, zoom=zoom))
QtWidgets.QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
self.widgets['map'].setHtml(
- page, QtCore.QUrl.fromLocalFile(self.script_dir))
+ page, QtCore.QUrl.fromLocalFile(self.script_dir + '/'))
@catch_all
def initialize_finished(self):
diff --git a/src/photini/scripts.py b/src/photini/scripts.py
index c68518df..5fca6e71 100644
--- a/src/photini/scripts.py
+++ b/src/photini/scripts.py
@@ -1,6 +1,6 @@
## Photini - a simple photo metadata editor.
## http://github.com/jim-easterbrook/Photini
-## Copyright (C) 2020-23 Jim Easterbrook jim@jim-easterbrook.me.uk
+## Copyright (C) 2020-24 Jim Easterbrook jim@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
@@ -25,8 +25,6 @@
import subprocess
import sys
-import pkg_resources
-
from photini.configstore import BaseConfigStore
try:
from photini.pyqt import QtCore
@@ -139,14 +137,12 @@ def post_install(argv=None):
options, args = parser.parse_args()
exec_path = os.path.abspath(
os.path.join(os.path.dirname(sys.argv[0]), 'photini'))
- icon_path = pkg_resources.resource_filename('photini', 'data/icons')
+ pkg_data = os.path.join(os.path.dirname(__file__), 'data')
if sys.platform == 'win32':
exec_path += '.exe'
- icon_path = os.path.join(icon_path, 'photini_win.ico')
- cmd = ['cscript', '/nologo',
- pkg_resources.resource_filename(
- 'photini', 'data/windows/install_shortcuts.vbs'),
- exec_path, icon_path, sys.prefix]
+ icon_path = os.path.join(pkg_data, 'icons', 'photini_win.ico')
+ script = os.path.join(pkg_data, 'windows', 'install_shortcuts.vbs')
+ cmd = ['cscript', '/nologo', script, exec_path, icon_path, sys.prefix]
if options.remove:
cmd.append('/remove')
return subprocess.call(cmd)
@@ -167,7 +163,7 @@ def post_install(argv=None):
return 0
print('No "desktop" file found.')
return 1
- icon_path = os.path.join(icon_path, 'photini_48.png')
+ icon_path = os.path.join(pkg_data, 'icons', 'photini_48.png')
cmd = ['desktop-file-install']
if os.geteuid() != 0:
# not running as root
@@ -176,11 +172,11 @@ def post_install(argv=None):
cmd += ['--set-key=Icon', '--set-value={}'.format(icon_path)]
# add translations
if QtCore:
- lang_dir = pkg_resources.resource_filename('photini', 'data/lang')
+ lang_dir = os.path.join(pkg_data, 'lang')
translator = QtCore.QTranslator()
- for name in sorted(os.listdir(lang_dir)):
+ for name in os.listdir(lang_dir):
lang = name.split('.')[1]
- if not translator.load('photini.' + lang, lang_dir):
+ if not translator.load(os.path.join(lang_dir, name)):
print('load failed:', lang)
continue
text = translator.translate(
@@ -194,8 +190,7 @@ def post_install(argv=None):
if text:
cmd += ['--set-key=Comment[{}]'.format(lang),
'--set-value={}'.format(text.strip())]
- cmd.append(pkg_resources.resource_filename(
- 'photini', 'data/linux/photini.desktop'))
+ cmd.append(os.path.join(pkg_data, 'linux', 'photini.desktop'))
print(' \\\n '.join(cmd))
return subprocess.call(cmd)
return 0
diff --git a/src/photini/types.py b/src/photini/types.py
index e1c65de6..750a2fe0 100644
--- a/src/photini/types.py
+++ b/src/photini/types.py
@@ -236,7 +236,7 @@ def from_ISO_8601(cls, datetime_string, sub_sec_string=None):
"""
if not datetime_string:
return cls([])
- unparsed = datetime_string
+ unparsed = datetime_string.strip()
precision = 7
# extract time zone
match = cls._tz_re.match(unparsed)
@@ -594,7 +594,7 @@ def to_xmp(self):
if not data:
fmt = 'JPEG'
data = self.data_from_image(self['image'], max_size=2**32)
- data = codecs.encode(data, 'base64_codec').decode('ascii')
+ data = codecs.encode(memoryview(data), 'base64_codec').decode('ascii')
return [{
'xmpGImg:width': str(self['w']),
'xmpGImg:height': str(self['h']),
diff --git a/src/photini/widgets.py b/src/photini/widgets.py
index 24d2433e..a8549873 100644
--- a/src/photini/widgets.py
+++ b/src/photini/widgets.py
@@ -846,7 +846,9 @@ def __init__(self, *args, **kwds):
self.lng_validator = QtGui.QDoubleValidator(
-180.0, 180.0, 20, parent=self)
self.setButtonSymbols(self.ButtonSymbols.NoButtons)
- self.label = Label(translate('LatLongDisplay', 'Lat, long'))
+ self.label = Label(translate(
+ 'LatLongDisplay', 'Lat, long',
+ 'Short abbreviation of "Latitude, longitude"'))
self.setFixedWidth(width_for_text(self, '8' * 22))
self.setToolTip('{}
'.format(translate(
'LatLongDisplay', 'Latitude and longitude (in degrees) as two'