Added beta support for face regions in xmp
This commit is contained in:
3
cli.py
3
cli.py
@@ -3,8 +3,7 @@
|
|||||||
To build this into an executable:
|
To build this into an executable:
|
||||||
- install pyinstaller:
|
- install pyinstaller:
|
||||||
python3 -m pip install pyinstaller
|
python3 -m pip install pyinstaller
|
||||||
- then use make_cli_exe.sh to run pyinstaller or execute the following command:
|
- then use make_cli_exe.sh to run pyinstaller
|
||||||
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py
|
|
||||||
|
|
||||||
Resulting executable will be in "dist/osxphotos"
|
Resulting executable will be in "dist/osxphotos"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import importlib
|
|||||||
pathex = os.getcwd()
|
pathex = os.getcwd()
|
||||||
|
|
||||||
# include necessary data files
|
# include necessary data files
|
||||||
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates')]
|
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates')]
|
||||||
package_imports = [['photoscript', ['photoscript.applescript']]]
|
package_imports = [['photoscript', ['photoscript.applescript']]]
|
||||||
for package, files in package_imports:
|
for package, files in package_imports:
|
||||||
proot = os.path.dirname(importlib.import_module(package).__file__)
|
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||||
|
|||||||
@@ -1063,6 +1063,9 @@ def cli(ctx, db, json_, debug):
|
|||||||
help=("Save options to file for use with --load-config. File format is TOML."),
|
help=("Save options to file for use with --load-config. File format is TOML."),
|
||||||
type=click.Path(),
|
type=click.Path(),
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--beta", is_flag=True, default=False, hidden=True, help="Enable beta options."
|
||||||
|
)
|
||||||
@DB_ARGUMENT
|
@DB_ARGUMENT
|
||||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
@@ -1172,6 +1175,7 @@ def export(
|
|||||||
load_config,
|
load_config,
|
||||||
save_config,
|
save_config,
|
||||||
is_reference,
|
is_reference,
|
||||||
|
beta,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -1310,6 +1314,7 @@ def export(
|
|||||||
report = cfg.report
|
report = cfg.report
|
||||||
cleanup = cfg.cleanup
|
cleanup = cfg.cleanup
|
||||||
exportdb = cfg.exportdb
|
exportdb = cfg.exportdb
|
||||||
|
beta = cfg.beta
|
||||||
|
|
||||||
# config file might have changed verbose
|
# config file might have changed verbose
|
||||||
VERBOSE = bool(verbose)
|
VERBOSE = bool(verbose)
|
||||||
@@ -1537,6 +1542,10 @@ def export(
|
|||||||
)
|
)
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_, exiftool=exiftool_path)
|
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_, exiftool=exiftool_path)
|
||||||
|
|
||||||
|
# enable beta features if requested
|
||||||
|
photosdb._beta = beta
|
||||||
|
|
||||||
photos = _query(
|
photos = _query(
|
||||||
photosdb=photosdb,
|
photosdb=photosdb,
|
||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ _MOVIE_TYPE = 1
|
|||||||
# Name of XMP template file
|
# Name of XMP template file
|
||||||
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
||||||
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
|
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
|
||||||
|
_XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
|
||||||
|
|
||||||
# Constants used for processing folders and albums
|
# Constants used for processing folders and albums
|
||||||
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.39.20"
|
__version__ = "0.39.21"
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
MWG_RS_Area = namedtuple("MWG_RS_Area", ["x", "y", "h", "w"])
|
||||||
|
MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
|
||||||
|
|
||||||
|
|
||||||
class PersonInfo:
|
class PersonInfo:
|
||||||
""" Info about a person in the Photos library
|
""" Info about a person in the Photos library
|
||||||
@@ -216,6 +221,50 @@ class FaceInfo:
|
|||||||
logging.warning(f"Could not get photo for uuid: {self.asset_uuid}")
|
logging.warning(f"Could not get photo for uuid: {self.asset_uuid}")
|
||||||
return self._photo
|
return self._photo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mwg_rs_area(self):
|
||||||
|
""" Get coordinates for Metadata Working Group Region Area.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MWG_RS_Area named tuple with x, y, h, w where:
|
||||||
|
x = stArea:x
|
||||||
|
y = stArea:y
|
||||||
|
h = stArea:h
|
||||||
|
w = stArea:w
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
https://photo.stackexchange.com/questions/106410/how-does-xmp-define-the-face-region
|
||||||
|
"""
|
||||||
|
x = self.center_x
|
||||||
|
y = 1.0 - self.center_y
|
||||||
|
h = self.size_pixels / self.photo.height
|
||||||
|
w = self.size_pixels / self.photo.width
|
||||||
|
|
||||||
|
return MWG_RS_Area(x, y, h, w)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mpri_reg_rect(self):
|
||||||
|
""" Get coordinates for Microsoft Photo Region Rectangle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MPRI_Reg_Rect named tuple with x, y, h, w where:
|
||||||
|
x = x coordinate of top left corner of rectangle
|
||||||
|
y = y coordinate of top left corner of rectangle
|
||||||
|
h = height of rectangle
|
||||||
|
w = width of rectangle
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
https://docs.microsoft.com/en-us/windows/win32/wic/-wic-people-tagging
|
||||||
|
"""
|
||||||
|
x = self.center_x - self.size_pixels / self.photo.width / 2
|
||||||
|
y = 1.0 - self.center_y - self.size_pixels / self.photo.height / 2
|
||||||
|
|
||||||
|
# though the docs clearly say height, width, these appear to be flipped
|
||||||
|
h = self.size_pixels / self.photo.width
|
||||||
|
w = self.size_pixels / self.photo.height
|
||||||
|
|
||||||
|
return MPRI_Reg_Rect(x, y, h, w)
|
||||||
|
|
||||||
def face_rect(self):
|
def face_rect(self):
|
||||||
""" Get face rectangle coordinates for current version of the associated image
|
""" Get face rectangle coordinates for current version of the associated image
|
||||||
If image has been edited, rectangle applies to edited version, otherwise original version
|
If image has been edited, rectangle applies to edited version, otherwise original version
|
||||||
|
|||||||
@@ -35,10 +35,12 @@ from .._constants import (
|
|||||||
_TEMPLATE_DIR,
|
_TEMPLATE_DIR,
|
||||||
_UNKNOWN_PERSON,
|
_UNKNOWN_PERSON,
|
||||||
_XMP_TEMPLATE_NAME,
|
_XMP_TEMPLATE_NAME,
|
||||||
|
_XMP_TEMPLATE_NAME_BETA,
|
||||||
SIDECAR_EXIFTOOL,
|
SIDECAR_EXIFTOOL,
|
||||||
SIDECAR_JSON,
|
SIDECAR_JSON,
|
||||||
SIDECAR_XMP,
|
SIDECAR_XMP,
|
||||||
)
|
)
|
||||||
|
from .._version import __version__
|
||||||
from ..datetime_utils import datetime_tz_to_utc
|
from ..datetime_utils import datetime_tz_to_utc
|
||||||
from ..exiftool import ExifTool
|
from ..exiftool import ExifTool
|
||||||
from ..export_db import ExportDBNoOp
|
from ..export_db import ExportDBNoOp
|
||||||
@@ -49,7 +51,7 @@ from ..photokit import (
|
|||||||
PhotoKitFetchFailed,
|
PhotoKitFetchFailed,
|
||||||
PhotoLibrary,
|
PhotoLibrary,
|
||||||
)
|
)
|
||||||
from ..utils import dd_to_dms_str, findfiles, noop, get_preferred_uti_extension
|
from ..utils import findfiles, get_preferred_uti_extension, noop
|
||||||
|
|
||||||
# retry if use_photos_export fails the first time (which sometimes it does)
|
# retry if use_photos_export fails the first time (which sometimes it does)
|
||||||
MAX_PHOTOSCRIPT_RETRIES = 3
|
MAX_PHOTOSCRIPT_RETRIES = 3
|
||||||
@@ -1717,7 +1719,10 @@ def _xmp_sidecar(
|
|||||||
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
xmp_template = Template(filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME))
|
xmp_template_file = (
|
||||||
|
_XMP_TEMPLATE_NAME if not self._db._beta else _XMP_TEMPLATE_NAME_BETA
|
||||||
|
)
|
||||||
|
xmp_template = Template(filename=os.path.join(_TEMPLATE_DIR, xmp_template_file))
|
||||||
|
|
||||||
if extension is None:
|
if extension is None:
|
||||||
extension = pathlib.Path(self.original_filename)
|
extension = pathlib.Path(self.original_filename)
|
||||||
@@ -1803,6 +1808,7 @@ def _xmp_sidecar(
|
|||||||
persons=person_list,
|
persons=person_list,
|
||||||
subjects=subject_list,
|
subjects=subject_list,
|
||||||
extension=extension,
|
extension=extension,
|
||||||
|
version=__version__,
|
||||||
)
|
)
|
||||||
|
|
||||||
# remove extra lines that mako inserts from template
|
# remove extra lines that mako inserts from template
|
||||||
@@ -1824,4 +1830,3 @@ def _write_sidecar(self, filename, sidecar_str):
|
|||||||
f = open(filename, "w")
|
f = open(filename, "w")
|
||||||
f.write(sidecar_str)
|
f.write(sidecar_str)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ class PhotosDB:
|
|||||||
raise TypeError("verbose must be callable")
|
raise TypeError("verbose must be callable")
|
||||||
self._verbose = verbose
|
self._verbose = verbose
|
||||||
|
|
||||||
|
# enable beta features
|
||||||
|
self._beta = False
|
||||||
|
|
||||||
self._exiftool_path = exiftool
|
self._exiftool_path = exiftool
|
||||||
|
|
||||||
# create a temporary directory
|
# create a temporary directory
|
||||||
|
|||||||
188
osxphotos/templates/xmp_sidecar_beta.mako
Normal file
188
osxphotos/templates/xmp_sidecar_beta.mako
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<%def name="photoshop_sidecar_for_extension(extension)">
|
||||||
|
% if extension is None:
|
||||||
|
<photoshop:SidecarForExtension></photoshop:SidecarForExtension>
|
||||||
|
% else:
|
||||||
|
<photoshop:SidecarForExtension>${extension}</photoshop:SidecarForExtension>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="dc_description(desc)">
|
||||||
|
% if desc is None:
|
||||||
|
<dc:description>
|
||||||
|
<rdf:Alt>
|
||||||
|
<rdf:li xml:lang='x-default'/>
|
||||||
|
</rdf:Alt>
|
||||||
|
</dc:description>
|
||||||
|
% else:
|
||||||
|
<dc:description>
|
||||||
|
<rdf:Alt>
|
||||||
|
<rdf:li xml:lang='x-default'>${desc | x}</rdf:li>
|
||||||
|
</rdf:Alt>
|
||||||
|
</dc:description>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="dc_title(title)">
|
||||||
|
% if title is None:
|
||||||
|
<dc:title>
|
||||||
|
<rdf:Alt>
|
||||||
|
<rdf:li xml:lang='x-default'/>
|
||||||
|
</rdf:Alt>
|
||||||
|
</dc:title>
|
||||||
|
% else:
|
||||||
|
<dc:title>
|
||||||
|
<rdf:Alt>
|
||||||
|
<rdf:li xml:lang='x-default'>${title | x}</rdf:li>
|
||||||
|
</rdf:Alt>
|
||||||
|
</dc:title>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="dc_subject(subject)">
|
||||||
|
% if subject:
|
||||||
|
<dc:subject>
|
||||||
|
<rdf:Bag>
|
||||||
|
% for subj in subject:
|
||||||
|
<rdf:li>${subj | x}</rdf:li>
|
||||||
|
% endfor
|
||||||
|
</rdf:Bag>
|
||||||
|
</dc:subject>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="dc_datecreated(date)">
|
||||||
|
% if date is not None:
|
||||||
|
<photoshop:DateCreated>${date.isoformat()}</photoshop:DateCreated>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="iptc_personinimage(persons)">
|
||||||
|
% if persons:
|
||||||
|
<Iptc4xmpExt:PersonInImage>
|
||||||
|
<rdf:Bag>
|
||||||
|
% for person in persons:
|
||||||
|
<rdf:li>${person | x}</rdf:li>
|
||||||
|
% endfor
|
||||||
|
</rdf:Bag>
|
||||||
|
</Iptc4xmpExt:PersonInImage>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="dk_tagslist(keywords)">
|
||||||
|
% if keywords:
|
||||||
|
<digiKam:TagsList>
|
||||||
|
<rdf:Seq>
|
||||||
|
% for keyword in keywords:
|
||||||
|
<rdf:li>${keyword | x}</rdf:li>
|
||||||
|
% endfor
|
||||||
|
</rdf:Seq>
|
||||||
|
</digiKam:TagsList>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="adobe_createdate(date)">
|
||||||
|
% if date is not None:
|
||||||
|
<xmp:CreateDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:CreateDate>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="adobe_modifydate(date)">
|
||||||
|
% if date is not None:
|
||||||
|
<xmp:ModifyDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:ModifyDate>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="gps_info(latitude, longitude)">
|
||||||
|
% if latitude is not None and longitude is not None:
|
||||||
|
<exif:GPSLongitude>${int(abs(longitude))},${(abs(longitude) % 1) * 60}${"E" if longitude >= 0 else "W"}</exif:GPSLongitude>
|
||||||
|
<exif:GPSLatitude>${int(abs(latitude))},${(abs(latitude) % 1) * 60}${"N" if latitude >= 0 else "S"}</exif:GPSLatitude>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="orientation(orientation)">
|
||||||
|
% if orientation is not None:
|
||||||
|
<tiff:Orientation>${orientation}</tiff:Orientation>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="face_regions(photo)">
|
||||||
|
% if photo.face_info:
|
||||||
|
<mwg-rs:Regions rdf:parseType="Resource">
|
||||||
|
<mwg-rs:AppliedToDimensions stDim:w=${photo.width} stDim:h=${photo.height} stDim:unit="pixel"/>
|
||||||
|
<mwg-rs:RegionList>
|
||||||
|
<rdf:Bag>
|
||||||
|
% for face in photo.face_info:
|
||||||
|
<rdf:li>
|
||||||
|
<rdf:Description
|
||||||
|
mwg-rs:Rotation="${face.roll}"
|
||||||
|
mwg-rs:Name="${face.name}"
|
||||||
|
mwg-rs:Type="Face">
|
||||||
|
<mwg-rs:Area
|
||||||
|
stArea:h="${'{0:.6f}'.format(face.mwg_rs_area.h)}"
|
||||||
|
stArea:w="${'{0:.6f}'.format(face.mwg_rs_area.w)}"
|
||||||
|
stArea:x="${'{0:.6f}'.format(face.mwg_rs_area.x)}"
|
||||||
|
stArea:y="${'{0:.6f}'.format(face.mwg_rs_area.y)}"
|
||||||
|
stArea:unit="normalized"/>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:li>
|
||||||
|
% endfor
|
||||||
|
</rdf:Bag>
|
||||||
|
</mwg-rs:RegionList>
|
||||||
|
</mwg-rs:Regions>
|
||||||
|
<MP:RegionInfo rdf:parseType="Resource">
|
||||||
|
<MPRI:Regions>
|
||||||
|
<rdf:Bag>
|
||||||
|
% for face in photo.face_info:
|
||||||
|
<rdf:li
|
||||||
|
MPReg:Rectangle="${'{0:.6f}'.format(face.mpri_reg_rect.x)}, ${'{0:.6f}'.format(face.mpri_reg_rect.y)}, ${'{0:.6f}'.format(face.mpri_reg_rect.h)}, ${'{0:.6f}'.format(face.mpri_reg_rect.w)}"
|
||||||
|
MPReg:PersonDisplayName="${face.name}"/>
|
||||||
|
% endfor
|
||||||
|
</rdf:Bag>
|
||||||
|
</MPRI:Regions>
|
||||||
|
</MP:RegionInfo>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos ${version}">
|
||||||
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||||
|
${photoshop_sidecar_for_extension(extension)}
|
||||||
|
${dc_description(description)}
|
||||||
|
${dc_title(photo.title)}
|
||||||
|
${dc_subject(subjects)}
|
||||||
|
${dc_datecreated(photo.date)}
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
|
${iptc_personinimage(persons)}
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
|
${dk_tagslist(keywords)}
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
|
${adobe_createdate(photo.date)}
|
||||||
|
${adobe_modifydate(photo.date)}
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
${gps_info(*photo.location)}
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=''
|
||||||
|
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
||||||
|
${orientation(photo.orientation)}
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||||
|
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||||
|
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
|
||||||
|
xmlns:MP="http://ns.microsoft.com/photo/1.2/"
|
||||||
|
xmlns:MPRI="http://ns.microsoft.com/photo/1.2/t/RegionInfo#"
|
||||||
|
xmlns:MPReg="http://ns.microsoft.com/photo/1.2/t/Region#">
|
||||||
|
${face_regions(photo)}
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
||||||
|
</x:xmpmeta>
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user