Added beta support for face regions in xmp

This commit is contained in:
Rhet Turnbull
2021-01-17 19:14:21 -08:00
parent 348ef54b30
commit 2773ff7381
10 changed files with 263 additions and 9 deletions

3
cli.py
View File

@@ -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"

View File

@@ -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__)

View 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,

View File

@@ -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

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.39.20" __version__ = "0.39.21"

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View 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