-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
262 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,70 @@ | ||
""" ExifInfo class to expose EXIF info from the library """ | ||
|
||
from __future__ import annotations | ||
|
||
import datetime | ||
from dataclasses import dataclass | ||
from typing import Any | ||
from osxphotos.photos_datetime import photos_datetime | ||
|
||
__all__ = ["ExifInfo"] | ||
__all__ = ["ExifInfo", "exifinfo_factory"] | ||
|
||
|
||
@dataclass(frozen=True) | ||
class ExifInfo: | ||
"""EXIF info associated with a photo from the Photos library""" | ||
|
||
flash_fired: bool | ||
iso: int | ||
metering_mode: int | ||
sample_rate: int | ||
track_format: int | ||
white_balance: int | ||
aperture: float | ||
bit_rate: float | ||
duration: float | ||
exposure_bias: float | ||
focal_length: float | ||
fps: float | ||
latitude: float | ||
longitude: float | ||
shutter_speed: float | ||
camera_make: str | ||
camera_model: str | ||
codec: str | ||
lens_model: str | ||
"""Original EXIF info associated with a photo from the Photos library""" | ||
|
||
flash_fired: bool | None = None | ||
iso: int | None = None | ||
metering_mode: int | None = None | ||
sample_rate: int | None = None | ||
track_format: int | None = None | ||
white_balance: int | None = None | ||
aperture: float | None = None | ||
bit_rate: float | None = None | ||
duration: float | None = None | ||
exposure_bias: float | None = None | ||
focal_length: float | None = None | ||
fps: float | None = None | ||
latitude: float | None = None | ||
longitude: float | None = None | ||
shutter_speed: float | None = None | ||
camera_make: str | None = None | ||
camera_model: str | None = None | ||
codec: str | None = None | ||
lens_model: str | None = None | ||
date: datetime.datetime | None = None | ||
tzoffset: int | None = None | ||
tzname: str | None = None | ||
|
||
def exifinfo_factory(data: dict[str, Any] | None) -> ExifInfo: | ||
"""Create an ExifInfo object from a dictionary of EXIF data""" | ||
if data is None: | ||
return ExifInfo() | ||
|
||
exif_info = ExifInfo( | ||
iso=data["ZISO"], | ||
flash_fired=True if data["ZFLASHFIRED"] == 1 else False, | ||
metering_mode=data["ZMETERINGMODE"], | ||
sample_rate=data["ZSAMPLERATE"], | ||
track_format=data["ZTRACKFORMAT"], | ||
white_balance=data["ZWHITEBALANCE"], | ||
aperture=data["ZAPERTURE"], | ||
bit_rate=data["ZBITRATE"], | ||
duration=data["ZDURATION"], | ||
exposure_bias=data["ZEXPOSUREBIAS"], | ||
focal_length=data["ZFOCALLENGTH"], | ||
fps=data["ZFPS"], | ||
latitude=data["ZLATITUDE"], | ||
longitude=data["ZLONGITUDE"], | ||
shutter_speed=data["ZSHUTTERSPEED"], | ||
camera_make=data["ZCAMERAMAKE"], | ||
camera_model=data["ZCAMERAMODEL"], | ||
codec=data["ZCODEC"], | ||
lens_model=data["ZLENSMODEL"], | ||
# ZDATECREATED, ZTIMEZONEOFFSET, ZTIMEZONENAME added in Ventura / Photos 8 so may not be present | ||
date=photos_datetime(data.get("ZDATECREATED"), data.get("ZTIMEZONEOFFSET"), False), | ||
tzoffset=data.get("ZTIMEZONEOFFSET"), | ||
tzname=data.get("ZTIMEZONENAME"), | ||
) | ||
return exif_info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
""" Utilities for working with date/time values in the Photos library """ | ||
|
||
from __future__ import annotations | ||
|
||
import datetime | ||
|
||
# Time delta: add this to Photos times to get unix time | ||
# Apple Epoch is Jan 1, 2001 | ||
TIME_DELTA = (datetime.datetime(2001, 1, 1, 0, 0) - datetime.datetime(1970, 1, 1, 0, 0)).total_seconds() | ||
|
||
# Default datetime for when we can't determine the date | ||
DEFAULT_DATETIME = datetime.datetime(1970, 1, 1).astimezone(tz=datetime.timezone.utc) | ||
|
||
__all__ = ["photos_datetime"] | ||
|
||
|
||
def photos_datetime( | ||
timestamp: float | None, tzoffset: int, default: bool = False | ||
) -> datetime.datetime | None: | ||
"""Convert a timestamp from the Photos database to a timezone aware datetime""" | ||
dt: datetime.datetime | None = None | ||
|
||
if timestamp is None: | ||
if default: | ||
return DEFAULT_DATETIME | ||
else: | ||
return None | ||
|
||
try: | ||
dt = datetime.datetime.fromtimestamp(timestamp + TIME_DELTA) | ||
except (ValueError, TypeError): | ||
dt = None | ||
|
||
if dt is None: | ||
if default: | ||
return DEFAULT_DATETIME | ||
else: | ||
return None | ||
|
||
delta = datetime.timedelta(seconds=tzoffset) | ||
tz = datetime.timezone(delta) | ||
return dt.astimezone(tz=tz) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
"""Test photos_datetime.py utilities""" | ||
|
||
from __future__ import annotations | ||
|
||
import datetime | ||
|
||
import pytest | ||
|
||
from osxphotos.photos_datetime import photos_datetime | ||
|
||
# data is timestamp, tzoffset, expected datetime | ||
TEST_DATA = [ | ||
(608405423, -25200, False, "2020-04-12 10:30:23-07:00"), | ||
(123456789012345, -25200, False, None), | ||
(123456789012345, -25200, True, "1970-01-01 06:00:00+00:00"), | ||
(714316684, 34200, False, "2023-08-21 22:48:04+09:30"), | ||
(583964641, -14400, False, "2019-07-04 16:24:01-04:00"), | ||
(561129492.501, -14400, False, "2018-10-13 09:18:12.501000-04:00"), | ||
(715411622, -14400, False, "2023-09-03 01:27:02-04:00"), | ||
(622244186.719, -25200, False, "2020-09-19 14:36:26.719000-07:00"), | ||
(608664351, -25200, False, "2020-04-15 10:25:51-07:00"), | ||
(714316684, -14400, False, "2023-08-21 09:18:04-04:00"), | ||
(608758101, -25200, False, "2020-04-16 12:28:21-07:00"), | ||
(714316684, -14400, False, "2023-08-21 09:18:04-04:00"), | ||
(608751778, -25200, False, "2020-04-16 10:42:58-07:00"), | ||
(559856149.063, -14400, False, "2018-09-28 15:35:49.063000-04:00"), | ||
(559856399.008, -14400, False, "2018-09-28 15:39:59.008000-04:00"), | ||
(744897809.3687729, -18000, False, "2024-08-09 07:03:29.368773-05:00"), | ||
] | ||
|
||
|
||
@pytest.mark.parametrize("timestamp, tzoffset, default, expected", TEST_DATA) | ||
def test_photos_datetime( | ||
timestamp: float, tzoffset: int, default: bool, expected: str | None | ||
): | ||
"""Test photos_datetime""" | ||
dt = photos_datetime(timestamp, tzoffset, default) | ||
dt_expected = datetime.datetime.fromisoformat(expected) if expected else None | ||
assert dt == dt_expected |