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:
- install pyinstaller:
python3 -m pip install pyinstaller
- then use make_cli_exe.sh to run pyinstaller or execute the following command:
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py
- then use make_cli_exe.sh to run pyinstaller
Resulting executable will be in "dist/osxphotos"

View File

@ -8,7 +8,7 @@ import importlib
pathex = os.getcwd()
# 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']]]
for package, files in package_imports:
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."),
type=click.Path(),
)
@click.option(
"--beta", is_flag=True, default=False, hidden=True, help="Enable beta options."
)
@DB_ARGUMENT
@click.argument("dest", nargs=1, type=click.Path(exists=True))
@click.pass_obj
@ -1172,6 +1175,7 @@ def export(
load_config,
save_config,
is_reference,
beta,
):
"""Export photos from the Photos database.
Export path DEST is required.
@ -1310,6 +1314,7 @@ def export(
report = cfg.report
cleanup = cfg.cleanup
exportdb = cfg.exportdb
beta = cfg.beta
# config file might have changed verbose
VERBOSE = bool(verbose)
@ -1537,6 +1542,10 @@ def export(
)
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_, exiftool=exiftool_path)
# enable beta features if requested
photosdb._beta = beta
photos = _query(
photosdb=photosdb,
keyword=keyword,

View File

@ -89,6 +89,7 @@ _MOVIE_TYPE = 1
# Name of XMP template file
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
_XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
# Constants used for processing folders and albums
_PHOTOS_5_ALBUM_KIND = 2 # normal user album

View File

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

View File

@ -4,6 +4,11 @@ import json
import logging
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:
""" 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}")
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):
""" Get face rectangle coordinates for current version of the associated image
If image has been edited, rectangle applies to edited version, otherwise original version

View File

@ -35,10 +35,12 @@ from .._constants import (
_TEMPLATE_DIR,
_UNKNOWN_PERSON,
_XMP_TEMPLATE_NAME,
_XMP_TEMPLATE_NAME_BETA,
SIDECAR_EXIFTOOL,
SIDECAR_JSON,
SIDECAR_XMP,
)
from .._version import __version__
from ..datetime_utils import datetime_tz_to_utc
from ..exiftool import ExifTool
from ..export_db import ExportDBNoOp
@ -49,7 +51,7 @@ from ..photokit import (
PhotoKitFetchFailed,
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)
MAX_PHOTOSCRIPT_RETRIES = 3
@ -236,7 +238,7 @@ def _export_photo_uuid_applescript(
exported_files = []
filename = None
try:
# I've seen intermittent failures with the PhotoScript export so retry if
# I've seen intermittent failures with the PhotoScript export so retry if
# export doesn't return anything
retries = 0
while not exported_files and retries < MAX_PHOTOSCRIPT_RETRIES:
@ -1717,7 +1719,10 @@ def _xmp_sidecar(
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:
extension = pathlib.Path(self.original_filename)
@ -1803,6 +1808,7 @@ def _xmp_sidecar(
persons=person_list,
subjects=subject_list,
extension=extension,
version=__version__,
)
# remove extra lines that mako inserts from template
@ -1824,4 +1830,3 @@ def _write_sidecar(self, filename, sidecar_str):
f = open(filename, "w")
f.write(sidecar_str)
f.close()

View File

@ -99,6 +99,9 @@ class PhotosDB:
raise TypeError("verbose must be callable")
self._verbose = verbose
# enable beta features
self._beta = False
self._exiftool_path = exiftool
# 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