Initial support for RAW photos in Photos 4 to address issue #101

This commit is contained in:
Rhet Turnbull
2020-04-17 10:44:15 -07:00
parent 7d55844390
commit 9d151478d6
4 changed files with 132 additions and 35 deletions

View File

@@ -855,8 +855,14 @@ def query(
@cli.command(cls=ExportCommand) @cli.command(cls=ExportCommand)
@DB_OPTION @DB_OPTION
@query_options # @click.option(
# "--all",
# is_flag=True,
# help="Export all versions of photos including "
# "edited photos, live photos, burst photos, and RAW photos.",
# )
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.") @click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
@query_options
@click.option( @click.option(
"--overwrite", "--overwrite",
is_flag=True, is_flag=True,

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.27.6" __version__ = "0.27.7"

View File

@@ -253,10 +253,6 @@ class PhotoInfo:
# TODO: I don't like this -- would prefer a more deterministic approach but until I have more # TODO: I don't like this -- would prefer a more deterministic approach but until I have more
# data on how Photos stores and retrieves RAW images, this seems to be working # data on how Photos stores and retrieves RAW images, this seems to be working
if self._db._db_version < _PHOTOS_5_VERSION:
logging.warning("RAW support not yet implemented for Photos version < 5")
return None
if self._info["isMissing"] == 1: if self._info["isMissing"] == 1:
return None # path would be meaningless until downloaded return None # path would be meaningless until downloaded
@@ -273,26 +269,45 @@ class PhotoInfo:
# ) # )
# return photopath # return photopath
filestem = pathlib.Path(self._info["filename"]).stem if self._db._db_version < _PHOTOS_5_VERSION:
raw_ext = get_preferred_uti_extension(self._info["UTI_raw"]) vol = self._info["raw_info"]["volume"]
if vol is not None:
if self._info["directory"].startswith("/"): photopath = os.path.join(
filepath = self._info["directory"] "/Volumes", vol, self._info["raw_info"]["imagePath"]
else: )
filepath = os.path.join(self._db._masters_path, self._info["directory"]) else:
photopath = os.path.join(
glob_str = f"{filestem}*.{raw_ext}" self._db._masters_path, self._info["raw_info"]["imagePath"]
raw_file = findfiles(glob_str, filepath) )
if len(raw_file) != 1:
logging.warning(f"Error getting path to RAW file: {filepath}/{glob_str}")
photopath = None
else:
photopath = os.path.join(filepath, raw_file[0])
if not os.path.isfile(photopath): if not os.path.isfile(photopath):
logging.debug( logging.debug(
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist" f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
) )
photopath = None photopath = None
else:
filestem = pathlib.Path(self._info["filename"]).stem
raw_ext = get_preferred_uti_extension(self._info["UTI_raw"])
if self._info["directory"].startswith("/"):
filepath = self._info["directory"]
else:
filepath = os.path.join(self._db._masters_path, self._info["directory"])
glob_str = f"{filestem}*.{raw_ext}"
raw_file = findfiles(glob_str, filepath)
if len(raw_file) != 1:
logging.warning(
f"Error getting path to RAW file: {filepath}/{glob_str}"
)
photopath = None
else:
photopath = os.path.join(filepath, raw_file[0])
if not os.path.isfile(photopath):
logging.debug(
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
return photopath return photopath
@property @property
@@ -588,10 +603,6 @@ class PhotoInfo:
@property @property
def has_raw(self): def has_raw(self):
""" returns True if photo has an associated RAW image, otherwise False """ """ returns True if photo has an associated RAW image, otherwise False """
if self._db._db_version < _PHOTOS_5_VERSION:
logging.warning("RAW support not yet implemented for Photos version < 5")
return None
return self._info["has_raw"] return self._info["has_raw"]
@property @property

View File

@@ -99,6 +99,11 @@ class PhotosDB:
# e.g. {'BD94B7C0-2EB8-43DB-98B4-3B8E9653C255': {'8B386814-CA8A-42AA-BCA8-97C1AA746D8A', '52B95550-DE4A-44DD-9E67-89E979F2E97F'}} # e.g. {'BD94B7C0-2EB8-43DB-98B4-3B8E9653C255': {'8B386814-CA8A-42AA-BCA8-97C1AA746D8A', '52B95550-DE4A-44DD-9E67-89E979F2E97F'}}
self._dbphotos_burst = {} self._dbphotos_burst = {}
# Dict with additional information from RKMaster
# key is UUID from RKMaster, value is dict with info related to each master
# currently used to get information on RAW images
self._dbphotos_master = {}
# Dict with information about all persons/photos by uuid # Dict with information about all persons/photos by uuid
# key is photo UUID, value is list of face names in that photo # key is photo UUID, value is list of face names in that photo
# Note: Photos 5 identifies faces even if not given a name # Note: Photos 5 identifies faces even if not given a name
@@ -610,7 +615,10 @@ class PhotosDB:
RKVersion.latitude, RKVersion.longitude, RKVersion.latitude, RKVersion.longitude,
RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI, RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI,
RKVersion.burstUuid, RKVersion.burstPickType, RKVersion.burstUuid, RKVersion.burstPickType,
RKVersion.specialType, RKMaster.modelID, RKVersion.momentUuid RKVersion.specialType, RKMaster.modelID, null, RKVersion.momentUuid,
RKVersion.rawMasterUuid,
RKVersion.nonRawMasterUuid,
RKMaster.alternateMasterUuid
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """ RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
) )
@@ -626,7 +634,10 @@ class PhotosDB:
RKVersion.burstUuid, RKVersion.burstPickType, RKVersion.burstUuid, RKVersion.burstPickType,
RKVersion.specialType, RKMaster.modelID, RKVersion.specialType, RKMaster.modelID,
RKVersion.selfPortrait, RKVersion.selfPortrait,
RKVersion.momentUuid RKVersion.momentUuid,
RKVersion.rawMasterUuid,
RKVersion.nonRawMasterUuid,
RKMaster.alternateMasterUuid
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """ RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
) )
@@ -661,6 +672,9 @@ class PhotosDB:
# 26 RKMaster.modelID # 26 RKMaster.modelID
# 27 RKVersion.selfPortrait -- 1 if selfie, Photos >= 3, not present for Photos < 3 # 27 RKVersion.selfPortrait -- 1 if selfie, Photos >= 3, not present for Photos < 3
# 28 RKVersion.momentID (# 27 for Photos < 3) # 28 RKVersion.momentID (# 27 for Photos < 3)
# 29 RKVersion.rawMasterUuid, -- UUID of RAW master
# 30 RKVersion.nonRawMasterUuid, -- UUID of non-RAW master
# 31 RKMaster.alternateMasterUuid -- UUID of alternate master (will be RAW master for JPEG and JPEG master for RAW)
for row in c: for row in c:
uuid = row[0] uuid = row[0]
@@ -746,6 +760,7 @@ class PhotosDB:
# 4 == HDR # 4 == HDR
# 5 == live photo # 5 == live photo
# 6 == screenshot # 6 == screenshot
# 7 == JPEG/RAW pair
# 8 == HDR live photo # 8 == HDR live photo
# 9 = portrait # 9 = portrait
@@ -765,12 +780,12 @@ class PhotosDB:
self._dbphotos[uuid]["portrait"] = True if row[25] == 9 else False self._dbphotos[uuid]["portrait"] = True if row[25] == 9 else False
# selfies (front facing camera, RKVersion.selfPortrait == 1) # selfies (front facing camera, RKVersion.selfPortrait == 1)
if self._db_version >= _PHOTOS_3_VERSION: if row[27] is not None:
self._dbphotos[uuid]["selfie"] = True if row[27] == 1 else False self._dbphotos[uuid]["selfie"] = True if row[27] == 1 else False
self._dbphotos[uuid]["momentID"] = row[28]
else: else:
self._dbphotos[uuid]["selfie"] = None self._dbphotos[uuid]["selfie"] = None
self._dbphotos[uuid]["momentID"] = row[27]
self._dbphotos[uuid]["momentID"] = row[28]
# Init cloud details that will be filled in later if cloud asset # Init cloud details that will be filled in later if cloud asset
self._dbphotos[uuid]["cloudAssetGUID"] = None # Photos 5 self._dbphotos[uuid]["cloudAssetGUID"] = None # Photos 5
@@ -785,12 +800,59 @@ class PhotosDB:
self._dbphotos[uuid]["original_resource_choice"] = None self._dbphotos[uuid]["original_resource_choice"] = None
# associated RAW image info # associated RAW image info
# will be filled in later self._dbphotos[uuid]["has_raw"] = True if row[25] == 7 else False
self._dbphotos[uuid]["has_raw"] = None
self._dbphotos[uuid]["UTI_raw"] = None self._dbphotos[uuid]["UTI_raw"] = None
self._dbphotos[uuid]["raw_data_length"] = None self._dbphotos[uuid]["raw_data_length"] = None
self._dbphotos[uuid]["resource_type"] = None self._dbphotos[uuid]["raw_info"] = None
self._dbphotos[uuid]["datastore_subtype"] = None self._dbphotos[uuid]["resource_type"] = None # Photos 5
self._dbphotos[uuid]["datastore_subtype"] = None # Photos 5
self._dbphotos[uuid]["raw_master_uuid"] = row[29]
self._dbphotos[uuid]["non_raw_master_uuid"] = row[30]
self._dbphotos[uuid]["alt_master_uuid"] = row[31]
# get additional details from RKMaster, needed for RAW processing
c.execute(
""" SELECT
RKMaster.uuid,
RKMaster.volumeId,
RKMaster.imagePath,
RKMaster.isMissing,
RKMaster.originalFileName,
RKMaster.UTI,
RKMaster.modelID,
RKMaster.fileSize,
RKMaster.isTrulyRaw,
RKMaster.alternateMasterUuid
FROM RKMaster
"""
)
# Order of results:
# 0 RKMaster.uuid,
# 1 RKMaster.volumeId,
# 2 RKMaster.imagePath,
# 3 RKMaster.isMissing,
# 4 RKMaster.originalFileName,
# 5 RKMaster.UTI,
# 6 RKMaster.modelID,
# 7 RKMaster.fileSize,
# 8 RKMaster.isTrulyRaw,
# 9 RKMaster.alternateMasterUuid
for row in c:
uuid = row[0]
info = {}
info["_uuid"] = uuid
info["volumeId"] = row[1]
info["imagePath"] = row[2]
info["isMissing"] = row[3]
info["originalFilename"] = row[4]
info["UTI"] = row[5]
info["modelID"] = row[6]
info["fileSize"] = row[7]
info["isTrulyRAW"] = row[8]
info["alternateMasterUuid"] = row[9]
self._dbphotos_master[uuid] = info
# get details needed to find path of the edited photos # get details needed to find path of the edited photos
c.execute( c.execute(
@@ -979,6 +1041,20 @@ class PhotosDB:
else: else:
self._dbalbum_titles[title] = [album_id] self._dbalbum_titles[title] = [album_id]
# add volume name to _dbphotos_master
for info in self._dbphotos_master.values():
info["volume"] = (
self._dbvolumes[info["volumeId"]]
if info["volumeId"] is not None
else None
)
# add data on RAW images
for info in self._dbphotos.values():
if info["has_raw"]:
raw_uuid = info["raw_master_uuid"]
info["raw_info"] = self._dbphotos_master[raw_uuid]
# done with the database connection # done with the database connection
conn.close() conn.close()
@@ -1423,6 +1499,10 @@ class PhotosDB:
info["UTI_raw"] = None info["UTI_raw"] = None
info["datastore_subtype"] = None info["datastore_subtype"] = None
info["resource_type"] = None info["resource_type"] = None
info["raw_master_uuid"] = None # Photos 4
info["non_raw_master_uuid"] = None # Photos 4
info["alt_master_uuid"] = None # Photos 4
info["raw_info"] = None # Photos 4
self._dbphotos[uuid] = info self._dbphotos[uuid] = info