diff --git a/README.rst b/README.rst index cd5a1568..2fd550ce 100644 --- a/README.rst +++ b/README.rst @@ -91,7 +91,7 @@ Licence | 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 | Catalan translation by Joan Juvanteny | Czech translation by Pavel Fric @@ -101,7 +101,7 @@ Licence | Korean translation by Soohyeon Park | Norwegian Bokmål translation by Allan Nordhøy | Polish translation by Dawid Głaz, Eryk Michalak -| Spanish translation by Esteban Martinena, Cristos Ruiz, Kamborio +| Spanish translation by Esteban Martinena, Cristos Ruiz, Kamborio, gallegonovato 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/pyproject.toml b/pyproject.toml index e33429a3..8d3b20a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ # Photini - a simple photo metadata editor. # http://github.com/jim-easterbrook/Photini -# Copyright (C) 2023 Jim Easterbrook jim@jim-easterbrook.me.uk +# Copyright (C) 2023-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 @@ -43,7 +43,8 @@ dependencies = [ "appdirs >= 1.3", "cachetools >= 3.0", "chardet >= 3.0", - "exiv2 >= 0.15.1", + "exiv2 >= 0.16", + "filetype >= 1.0", "requests >= 2.4", ] dynamic = ["version"] diff --git a/src/lang/README.rst b/src/lang/README.rst index 3010bbde..c6b5a67d 100644 --- a/src/lang/README.rst +++ b/src/lang/README.rst @@ -41,7 +41,7 @@ Edit translation locally:: git checkout main # switch to main branch git pull # fetch new content - python3 utils/lang_update.py -l xx # update from source, with line numbers + python3 utils/lang_update.py -q -l xx # update from source, with line numbers and suitable for Qt Linguist pyside6-linguist src/lang/xx/photini.ts # edit translation of language xx python3 utils/lang_update.py -s -l xx # remove line numbers git push # push updated translation to GitHub @@ -51,7 +51,7 @@ Before a new release of Photini:: git checkout main # switch to main branch git pull # fetch new content - python3 utils/lang_update.py # update from source + python3 utils/lang_update.py -q # update from source, suitable for Qt Linguist python3 utils/build_lang.py # "compile" language files Plurals @@ -59,5 +59,4 @@ Plurals Qt Linguist and Transifex have different ideas about how many plural forms some languages have. For example, Transifex expects French to have three plurals ``1``, ``many``, and ``other``, but Qt Linguist expects two ``singular`` and ``plural``. -Transifex provides languages such as ``Italian (QT FILETYPE)`` that use Qt's plural definitions. -Weblate just considers the missing plural forms to be an error that can be ignored. +The -q option to lang_update.py strips extra plurals to make the file suitable for Qt's tools. Without the -q option extra plurals "unused" are inserted to suit Weblate and Transifex. diff --git a/src/lang/ca/photini.ts b/src/lang/ca/photini.ts index a4ca44e3..3f37b40e 100644 --- a/src/lang/ca/photini.ts +++ b/src/lang/ca/photini.ts @@ -883,12 +883,17 @@ LatLongDisplay Lat, long - Lat, long + Lat, long Latitude and longitude (in degrees) as two decimal numbers separated by a space. + + Lat, long + Short abbreviation of "Latitude, longitude" + Lat, long + LoggerWindow diff --git a/src/lang/cs/photini.ts b/src/lang/cs/photini.ts index 31d2f13c..ee10e845 100644 --- a/src/lang/cs/photini.ts +++ b/src/lang/cs/photini.ts @@ -890,12 +890,17 @@ LatLongDisplay Lat, long - Zeměpisná šířka, délka + Zeměpisná šířka, délka Latitude and longitude (in degrees) as two decimal numbers separated by a space. + + Lat, long + Short abbreviation of "Latitude, longitude" + Zeměpisná šířka, délka + LoggerWindow diff --git a/src/lang/de/photini.ts b/src/lang/de/photini.ts index 3d493c59..364c7a80 100644 --- a/src/lang/de/photini.ts +++ b/src/lang/de/photini.ts @@ -892,7 +892,7 @@ LatLongDisplay Lat, long - Breite, Länge + Breite, Länge Latitude and longitude (in degrees) as two decimal numbers separated by a comma. @@ -902,6 +902,11 @@ Latitude and longitude (in degrees) as two decimal numbers separated by a space. + + Lat, long + Short abbreviation of "Latitude, longitude" + Breite, Länge + LoggerWindow diff --git a/src/lang/en/photini.ts b/src/lang/en/photini.ts index a499eead..fee0ef69 100644 --- a/src/lang/en/photini.ts +++ b/src/lang/en/photini.ts @@ -830,11 +830,12 @@ LatLongDisplay - Lat, long + Latitude and longitude (in degrees) as two decimal numbers separated by a space. - Latitude and longitude (in degrees) as two decimal numbers separated by a space. + Lat, long + Short abbreviation of "Latitude, longitude" diff --git a/src/lang/es/photini.ts b/src/lang/es/photini.ts index d6bf3587..98e3071e 100644 --- a/src/lang/es/photini.ts +++ b/src/lang/es/photini.ts @@ -1,4 +1,4 @@ - + AddressTab @@ -71,8 +71,8 @@ Introduce el nombre del continente. - Enter globally unique identifier(s) of the location. Separate them with ";" characters. - Introduce identificador(es) único(s) globales de la ubicación. Sepáralos con los carácteres ";". + Enter globally unique identifier(s) of the location. Separate them with ";" characters. + Introduce identificador(es) único(s) globales de la ubicación. Sepáralos con los carácteres ";". Name @@ -181,12 +181,12 @@ Titular - Enter a "caption" describing the who, what, and why of what is happening in this image, this might include names of people, and/or their role in the action that is taking place within the image. - Introduce una "leyenda" describiendo quién, qué, y por qué sobre lo que sucede en esta imagen, puede incluir nombres de personas, y/o su papel en la acción que tiene lugar en la imagen. + Enter a "caption" describing the who, what, and why of what is happening in this image, this might include names of people, and/or their role in the action that is taking place within the image. + Introduce una "leyenda" describiendo quién, qué, y por qué sobre lo que sucede en esta imagen, puede incluir nombres de personas, y/o su papel en la acción que tiene lugar en la imagen. - Enter any number of keywords, terms or phrases used to express the subject matter in the image. Separate them with ";" characters. - Introduce cualquier número de palabras clave, términos o frases usadas para expresar el tema de la imagen. Sepáralas con carácteres ";". + Enter any number of keywords, terms or phrases used to express the subject matter in the image. Separate them with ";" characters. + Introduce cualquier número de palabras clave, términos o frases usadas para expresar el tema de la imagen. Sepáralas con carácteres ";". Enter text describing the appearance of the image from a visual perspective, focusing on details that are relevant to the purpose and meaning of the image. @@ -454,8 +454,8 @@ Photini: archivo grande - File "{file_name}" is over 25 MB. Remember that Photini uploads count towards storage in your Google Account. Upload it anyway? - El archivo "{file_name}" pesa más de 25 MB. Recuerda que las subidas de Photini cuentan para el almacenamiento de tu cuenta de Google. ¿Lo subes de todas formas? + File "{file_name}" is over 25 MB. Remember that Photini uploads count towards storage in your Google Account. Upload it anyway? + El archivo "{file_name}" pesa más de 25 MB. Recuerda que las subidas de Photini cuentan para el almacenamiento de tu cuenta de Google. ¿Lo subes de todas formas? Album title @@ -532,8 +532,7 @@ Regenerar miniatura Regenerar miniaturas - Unused - + Unused Save changes @@ -544,16 +543,14 @@ Recargar archivo Recargar archivos - Unused - + Unused Close file(s) Cerrar archivo Cerrar archivos - Unused - + Unused Some images have unsaved metadata. @@ -647,8 +644,8 @@ Parar copia - Remove "{source_name}" - Borrar "{source_name}" + Remove "{source_name}" + Borrar "{source_name}" camera: {camera_name} @@ -663,8 +660,7 @@ %n archivo seleccionado %n archivos seleccionados - Unused - + Unused @@ -822,7 +818,7 @@ Lang: - Short abbreviation of "Language: " + Short abbreviation of "Language: " Idioma: @@ -842,12 +838,17 @@ LatLongDisplay Lat, long - Lat.,Long. + Lat.,Long. Latitude and longitude (in degrees) as two decimal numbers separated by a space. Latitud y longitud (en grados) con dos números decimales separados por un espacio. + + Lat, long + Short abbreviation of "Latitude, longitude" + Lat.,Long. + LoggerWindow @@ -883,7 +884,7 @@ Search and altitude lookup powered by Google - Do not translate "powered by Google" + Do not translate "powered by Google" Búsqueda y consulta de altitud powered by Google @@ -969,8 +970,8 @@ Cerrar todos los archivos - This program is released with a GNU General Public License. For details click the "{}" button. - Este programa es liberado bajo una Licencia Pública General de GNU. Detalles favor de hacer clic en el botón "{}". + This program is released with a GNU General Public License. For details click the "{}" button. + Este programa es liberado bajo una Licencia Pública General de GNU. Detalles favor de hacer clic en el botón "{}". Fix missing thumbnails @@ -981,8 +982,8 @@ Comprobar actualizaciones - This program is released with a GNU General Public License. For details click the "{details}" button. - Este programa es liberado bajo una Licencia Pública General de GNU. Detalles favor de hacer clic en el botón "{details}". + This program is released with a GNU General Public License. For details click the "{details}" button. + Este programa es liberado bajo una Licencia Pública General de GNU. Detalles favor de hacer clic en el botón "{details}". Photini: version check @@ -996,8 +997,8 @@ OwnerTab - Enter a notice on the current owner of the copyright for this image, such as "©2008 Jane Doe". - Introduzca un aviso sobre el propietario actual de los derechos de autor de esta imagen, como "©2008 Jane Doe". + Enter a notice on the current owner of the copyright for this image, such as "©2008 Jane Doe". + Introduzca un aviso sobre el propietario actual de los derechos de autor de esta imagen, como "©2008 Jane Doe". Enter the name of the person that created this image. @@ -1064,7 +1065,7 @@ Introduce el cargo de trabajo de la persona que figura en el campo Creador. - Creator's Jobtitle + Creator's Jobtitle Cargo del creador @@ -1148,8 +1149,8 @@ Términos de uso - Open link to "{licence}" - Abrir enlace a "{licence}" + Open link to "{licence}" + Abrir enlace a "{licence}" Web Statement @@ -1382,8 +1383,8 @@ Falta el texto alternativo. - File "{file_name}" does not have any "alt text" for accessibility. Would you like to upload it anyway? - El archivo "{file_name}" no tiene ningún "texto alternativo" para la accesibilidad. ¿Le gustaría subirlo de todos modos? + File "{file_name}" does not have any "alt text" for accessibility. Would you like to upload it anyway? + El archivo "{file_name}" no tiene ningún "texto alternativo" para la accesibilidad. ¿Le gustaría subirlo de todos modos? @@ -1457,8 +1458,8 @@ Tipo de contenido - Enter the names of people shown in this region. Separate multiple entries with ";" characters. - Introduzca los nombres de las personas que aparecen en esta región. Separe las entradas múltiples con caracteres ";". + Enter the names of people shown in this region. Separate multiple entries with ";" characters. + Introduzca los nombres de las personas que aparecen en esta región. Separe las entradas múltiples con caracteres ";". Person shown @@ -1469,8 +1470,8 @@ Descripción - Enter a "caption" describing the who, what, and why of what is happening in this region. - Introduce una "leyenda" describiendo el quién, qué y por qué de lo que describing the who, what, and why of what is happening in this region. + Enter a "caption" describing the who, what, and why of what is happening in this region. + Introduce una "leyenda" describiendo el quién, qué y por qué de lo que describing the who, what, and why of what is happening in this region. The Image Region Structure includes optionally any metadata property which is related to the region. @@ -1528,12 +1529,12 @@ Nombre del modelo - Maker's name + Maker's name Nombre del fabricante - Remove "{camera_or_lens}" - Quitar "{camera_or_lens}" + Remove "{camera_or_lens}" + Quitar "{camera_or_lens}" Serial number @@ -1548,12 +1549,12 @@ Fecha y hora - Link 'taken' and 'digitised' - Vincular 'tomada' y 'digitalizada' + Link 'taken' and 'digitised' + Vincular 'tomada' y 'digitalizada' - Link 'digitised' and 'modified' - Vincular 'digitalizada' y 'modificada' + Link 'digitised' and 'modified' + Vincular 'digitalizada' y 'modificada' Taken @@ -1691,17 +1692,17 @@ h - single letter abbreviation of "hours" + single letter abbreviation of "hours" h m - single letter abbreviation of "minutes" + single letter abbreviation of "minutes" m s - single letter abbreviation of "seconds" + single letter abbreviation of "seconds" s @@ -1724,8 +1725,8 @@ No conectado a {service} - File "{file_name}" has {size} bytes and exceeds {service}'s limit of {max_size} bytes. - El archivo "{file_name}" tiene {size} bytes y excede el límite de {service} de {max_size} bytes. + File "{file_name}" has {size} bytes and exceeds {service}'s limit of {max_size} bytes. + El archivo "{file_name}" tiene {size} bytes y excede el límite de {service} de {max_size} bytes. Incompatible image type. @@ -1736,8 +1737,8 @@ Avance - File "{file_name}" upload failed. - Fallo en la carga del archivo "{file_name}". + File "{file_name}" upload failed. + Fallo en la carga del archivo "{file_name}". File {file_name} has already been uploaded to {service}. How would you like to update it? @@ -1816,12 +1817,12 @@ Por favor, utilice su navegador para autorizar Photini y, a continuación, cierre este cuadro de diálogo. - File "{file_name}" has {size} pixels and exceeds {service}'s limit of {max_size} pixels. - El archivo "{file_name}" tiene {size} píxeles y excede el límite de {service} de {max_size} píxeles. + File "{file_name}" has {size} pixels and exceeds {service}'s limit of {max_size} pixels. + El archivo "{file_name}" tiene {size} píxeles y excede el límite de {service} de {max_size} píxeles. - File "{file_name}" is of type "{file_type}", which {service} may not handle correctly. - El archivo "{file_name}" es del tipo "{file_type}", que {service} no puede gestionar correctamente. + File "{file_name}" is of type "{file_type}", which {service} may not handle correctly. + El archivo "{file_name}" es del tipo "{file_type}", que {service} no puede gestionar correctamente. Would you like to convert it to JPEG? @@ -1875,4 +1876,4 @@ <nuevo> - + \ No newline at end of file diff --git a/src/lang/fr/photini.ts b/src/lang/fr/photini.ts index e55aebdb..ebf8e4dc 100644 --- a/src/lang/fr/photini.ts +++ b/src/lang/fr/photini.ts @@ -886,12 +886,17 @@ LatLongDisplay Lat, long - Lat, lon + Lat, lon Latitude and longitude (in degrees) as two decimal numbers separated by a space. + + Lat, long + Short abbreviation of "Latitude, longitude" + Lat, lon + LoggerWindow diff --git a/src/lang/it/photini.ts b/src/lang/it/photini.ts index 1603552c..b363539d 100644 --- a/src/lang/it/photini.ts +++ b/src/lang/it/photini.ts @@ -843,12 +843,17 @@ LatLongDisplay Lat, long - Lat, long + Lat, long Latitude and longitude (in degrees) as two decimal numbers separated by a space. Latitudine e longitudine (in gradi) come due numeri decimali separati da uno spazio. + + Lat, long + Short abbreviation of "Latitude, longitude" + Lat, long + LoggerWindow diff --git a/src/lang/ko/photini.ts b/src/lang/ko/photini.ts index 1da676d7..60487163 100644 --- a/src/lang/ko/photini.ts +++ b/src/lang/ko/photini.ts @@ -830,11 +830,12 @@ LatLongDisplay - Lat, long + Latitude and longitude (in degrees) as two decimal numbers separated by a space. - Latitude and longitude (in degrees) as two decimal numbers separated by a space. + Lat, long + Short abbreviation of "Latitude, longitude" diff --git a/src/lang/nb/photini.ts b/src/lang/nb/photini.ts index 3111ad0a..6a6ca918 100644 --- a/src/lang/nb/photini.ts +++ b/src/lang/nb/photini.ts @@ -875,12 +875,17 @@ LatLongDisplay Lat, long - Bredde, lengde + Bredde, lengde Latitude and longitude (in degrees) as two decimal numbers separated by a space. + + Lat, long + Short abbreviation of "Latitude, longitude" + Bredde, lengde + 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 - Lat, long + Latitude and longitude (in degrees) as two decimal numbers separated by a space. - Latitude and longitude (in degrees) as two decimal numbers separated by a space. + Lat, long + 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 - Lat, long + Latitude and longitude (in degrees) as two decimal numbers separated by a space. - Latitude and longitude (in degrees) as two decimal numbers separated by a space. + Lat, long + 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'