create a class for writer

This commit is contained in:
Stefal 2023-09-17 20:02:31 +02:00
parent 396570ff46
commit 97d61830a0

149
writer.py
View File

@ -28,149 +28,122 @@ class PictureMetadata:
direction: Optional[float] = None direction: Optional[float] = None
orientation: Optional[int] = 1 orientation: Optional[int] = 1
class Writer():
def __init__(self, picture: bytes):
self.content = picture
self.image = pyexiv2.ImageData(picture)
self.exif = self.image.read_exif()
self.xmp = self.image.read_xmp()
self.updated_exif = {}
self.updated_xmp = {}
def writePictureMetadata(picture: bytes, metadata: PictureMetadata) -> bytes: def __exit__(self, *args):
self.image.close()
def apply(self):
self.image.modify_exif(self.updated_exif)
self.image.modify_xmp(self.updated_xmp)
def close(self):
self.image.close()
def get_Bytes(self):
return self.image.get_bytes()
def writePictureMetadata(self, metadata: PictureMetadata):
""" """
Override exif metadata on raw picture and return updated bytes Override exif metadata on raw picture and return updated bytes
""" """
if not metadata.capture_time and not metadata.longitude and not metadata.latitude and not metadata.picture_type: if not metadata.capture_time and not metadata.longitude and not metadata.latitude and not metadata.picture_type:
return picture return
if metadata.capture_time: if metadata.capture_time:
picture = add_gps_datetime(picture, metadata) self.add_gps_datetime(metadata)
picture = add_datetimeoriginal(picture, metadata) self.add_datetimeoriginal(metadata)
if metadata.latitude is not None and metadata.longitude is not None: if metadata.latitude is not None and metadata.longitude is not None:
picture = add_lat_lon(picture, metadata) self.add_lat_lon(metadata)
if metadata.picture_type is not None: if metadata.picture_type is not None:
picture = add_img_projection(picture, metadata) self.add_img_projection(metadata)
return picture def add_lat_lon(self, metadata: PictureMetadata):
def add_lat_lon(picture: bytes, metadata: PictureMetadata) -> bytes:
""" """
Add latitude and longitude values in GPSLatitude + GPSLAtitudeRef and GPSLongitude + GPSLongitudeRef Add latitude and longitude values in GPSLatitude + GPSLAtitudeRef and GPSLongitude + GPSLongitudeRef
""" """
img = pyexiv2.ImageData(picture)
updated_exif = {}
if metadata.latitude is not None: if metadata.latitude is not None:
updated_exif["Exif.GPSInfo.GPSLatitudeRef"] = "N" if metadata.latitude > 0 else "S" self.updated_exif["Exif.GPSInfo.GPSLatitudeRef"] = "N" if metadata.latitude > 0 else "S"
updated_exif["Exif.GPSInfo.GPSLatitude"] = _to_exif_dms(metadata.latitude) self.updated_exif["Exif.GPSInfo.GPSLatitude"] = self._to_exif_dms(metadata.latitude)
if metadata.longitude is not None: if metadata.longitude is not None:
updated_exif["Exif.GPSInfo.GPSLongitudeRef"] = "E" if metadata.longitude > 0 else "W" self.updated_exif["Exif.GPSInfo.GPSLongitudeRef"] = "E" if metadata.longitude > 0 else "W"
updated_exif["Exif.GPSInfo.GPSLongitude"] = _to_exif_dms(metadata.longitude) self.updated_exif["Exif.GPSInfo.GPSLongitude"] = self._to_exif_dms(metadata.longitude)
if updated_exif: def add_altitude(self, metadata: PictureMetadata, precision: int = 1000):
img.modify_exif(updated_exif)
return img.get_bytes()
def add_altitude(picture: bytes, metadata: PictureMetadata, precision: int = 1000) -> bytes:
""" """
Add altitude value in GPSAltitude and GPSAltitudeRef Add altitude value in GPSAltitude and GPSAltitudeRef
""" """
altitude = metadata.altitude altitude = metadata.altitude
img = pyexiv2.ImageData(picture)
updated_exif = {}
if altitude is not None: if altitude is not None:
negative_altitude = 0 if altitude >= 0 else 1 negative_altitude = 0 if altitude >= 0 else 1
updated_exif['Exif.GPSInfo.GPSAltitude'] = f"{int(abs(altitude * precision))} / {precision}" self.updated_exif['Exif.GPSInfo.GPSAltitude'] = f"{int(abs(altitude * precision))} / {precision}"
updated_exif['Exif.GPSInfo.GPSAltitudeRef'] = negative_altitude self.updated_exif['Exif.GPSInfo.GPSAltitudeRef'] = negative_altitude
if updated_exif: def add_direction(self, metadata: PictureMetadata, ref: str = 'T', precision: int = 1000):
img.modify_exif(updated_exif)
return img.get_bytes()
def add_direction(picture: bytes, metadata: PictureMetadata, ref: str = 'T', precision: int = 1000) -> bytes:
""" """
Add direction value in GPSImgDirection and GPSImgDirectionRef Add direction value in GPSImgDirection and GPSImgDirectionRef
""" """
direction = metadata.direction direction = metadata.direction
img = pyexiv2.ImageData(picture)
updated_exif = {}
if metadata.direction is not None: if metadata.direction is not None:
updated_exif['Exif.GPSInfo.GPSImgDirection'] = f"{int(abs(direction % 360.0 * precision))} / {precision}" self.updated_exif['Exif.GPSInfo.GPSImgDirection'] = f"{int(abs(direction % 360.0 * precision))} / {precision}"
updated_exif['Exif.GPSInfo.GPSImgDirectionRef'] = ref self.updated_exif['Exif.GPSInfo.GPSImgDirectionRef'] = ref
if updated_exif: def add_gps_datetime(self, metadata: PictureMetadata):
img.modify_exif(updated_exif)
return img.get_bytes()
def add_gps_datetime(picture: bytes, metadata: PictureMetadata) -> bytes:
""" """
Add GPSDateStamp and GPSTimeStamp Add GPSDateStamp and GPSTimeStamp
""" """
img = pyexiv2.ImageData(picture)
updated_exif = {}
if metadata.capture_time.utcoffset() is None: if metadata.capture_time.utcoffset() is None:
metadata.capture_time = localize(metadata, img) metadata.capture_time = self.localize(metadata)
# for capture time, override GPSInfo time and DatetimeOriginal # for capture time, override GPSInfo time and DatetimeOriginal
updated_exif["Exif.Photo.DateTimeOriginal"] = metadata.capture_time.strftime("%Y:%m:%d %H:%M:%S") self.updated_exif["Exif.Photo.DateTimeOriginal"] = metadata.capture_time.strftime("%Y:%m:%d %H:%M:%S")
offset = metadata.capture_time.utcoffset() offset = metadata.capture_time.utcoffset()
if offset is not None: if offset is not None:
updated_exif["Exif.Photo.OffsetTimeOriginal"] = format_offset(offset) self.updated_exif["Exif.Photo.OffsetTimeOriginal"] = self.format_offset(offset)
utc_dt = metadata.capture_time.astimezone(tz=pytz.UTC) utc_dt = metadata.capture_time.astimezone(tz=pytz.UTC)
updated_exif["Exif.GPSInfo.GPSDateStamp"] = utc_dt.strftime("%Y:%m:%d") self.updated_exif["Exif.GPSInfo.GPSDateStamp"] = utc_dt.strftime("%Y:%m:%d")
updated_exif["Exif.GPSInfo.GPSTimeStamp"] = utc_dt.strftime("%H/1 %M/1 %S/1") self.updated_exif["Exif.GPSInfo.GPSTimeStamp"] = utc_dt.strftime("%H/1 %M/1 %S/1")
if updated_exif: def add_datetimeoriginal(self, metadata: PictureMetadata):
img.modify_exif(updated_exif)
return img.get_bytes()
def add_datetimeoriginal(picture: bytes, metadata: PictureMetadata) -> bytes:
""" """
Add date time in Exif DateTimeOriginal and SubSecTimeOriginal tags Add date time in Exif DateTimeOriginal and SubSecTimeOriginal tags
""" """
img = pyexiv2.ImageData(picture)
updated_exif = {}
if metadata.capture_time.utcoffset() is None: if metadata.capture_time.utcoffset() is None:
metadata.capture_time = localize(metadata, img) metadata.capture_time = self.localize(metadata)
# for capture time, override DatetimeOriginal and SubSecTimeOriginal # for capture time, override DatetimeOriginal and SubSecTimeOriginal
updated_exif["Exif.Photo.DateTimeOriginal"] = metadata.capture_time.strftime("%Y:%m:%d %H:%M:%S") self.updated_exif["Exif.Photo.DateTimeOriginal"] = metadata.capture_time.strftime("%Y:%m:%d %H:%M:%S")
offset = metadata.capture_time.utcoffset() offset = metadata.capture_time.utcoffset()
if offset is not None: if offset is not None:
updated_exif["Exif.Photo.OffsetTimeOriginal"] = format_offset(offset) self.updated_exif["Exif.Photo.OffsetTimeOriginal"] = self.format_offset(offset)
if metadata.capture_time.microsecond != 0: if metadata.capture_time.microsecond != 0:
updated_exif["Exif.Photo.SubSecTimeOriginal"] = metadata.capture_time.strftime("%f") self.updated_exif["Exif.Photo.SubSecTimeOriginal"] = metadata.capture_time.strftime("%f")
if updated_exif: def add_img_projection(self, metadata: PictureMetadata):
img.modify_exif(updated_exif)
return img.get_bytes()
def add_img_projection(picture: bytes, metadata: PictureMetadata) -> bytes:
""" """
Add image projection type (equirectangular for spherical image, ...) in xmp GPano.ProjectionType Add image projection type (equirectangular for spherical image, ...) in xmp GPano.ProjectionType
""" """
img = pyexiv2.ImageData(picture)
updated_xmp = {}
if metadata.picture_type.value != "flat": if metadata.picture_type.value != "flat":
updated_xmp["Xmp.GPano.ProjectionType"] = metadata.picture_type.value self.updated_xmp["Xmp.GPano.ProjectionType"] = metadata.picture_type.value
updated_xmp["Xmp.GPano.UsePanoramaViewer"] = True self.updated_xmp["Xmp.GPano.UsePanoramaViewer"] = True
if updated_xmp: def format_offset(self, offset: timedelta) -> str:
img.modify_xmp(updated_xmp)
return img.get_bytes()
def format_offset(offset: timedelta) -> str:
"""Format offset for OffsetTimeOriginal. Format is like "+02:00" for paris offset """Format offset for OffsetTimeOriginal. Format is like "+02:00" for paris offset
>>> format_offset(timedelta(hours=5, minutes=45)) >>> format_offset(timedelta(hours=5, minutes=45))
'+05:45' '+05:45'
@ -181,12 +154,12 @@ def format_offset(offset: timedelta) -> str:
return f"{'+' if offset_hour >= 0 else '-'}{int(abs(offset_hour)):02}:{int(remainer/60):02}" return f"{'+' if offset_hour >= 0 else '-'}{int(abs(offset_hour)):02}:{int(remainer/60):02}"
def localize(metadata: PictureMetadata, imagedata: pyexiv2.ImageData) -> datetime: def localize(self, metadata: PictureMetadata) -> datetime:
""" """
Localize a datetime in the timezone of the picture Localize a datetime in the timezone of the picture
If the picture does not contains GPS position, the datetime will not be modified. If the picture does not contains GPS position, the datetime will not be modified.
""" """
exif = imagedata.read_exif() exif = self.exif
try: try:
lon = exif["Exif.GPSInfo.GPSLongitude"] lon = exif["Exif.GPSInfo.GPSLongitude"]
lon_ref = exif.get("Exif.GPSInfo.GPSLongitudeRef", "E") lon_ref = exif.get("Exif.GPSInfo.GPSLongitudeRef", "E")
@ -195,8 +168,8 @@ def localize(metadata: PictureMetadata, imagedata: pyexiv2.ImageData) -> datetim
except KeyError: except KeyError:
return metadata.capture_time # canot localize, returning same date return metadata.capture_time # canot localize, returning same date
lon = _from_dms(lon) * (1 if lon_ref == "E" else -1) lon = self._from_dms(lon) * (1 if lon_ref == "E" else -1)
lat = _from_dms(lat) * (1 if lat_ref == "N" else -1) lat = self._from_dms(lat) * (1 if lat_ref == "N" else -1)
tz_name = tz_finder.timezone_at(lng=lon, lat=lat) tz_name = tz_finder.timezone_at(lng=lon, lat=lat)
if not tz_name: if not tz_name:
@ -207,7 +180,7 @@ def localize(metadata: PictureMetadata, imagedata: pyexiv2.ImageData) -> datetim
return tz.localize(metadata.capture_time) return tz.localize(metadata.capture_time)
def _from_dms(val: str) -> float: def _from_dms(self, val: str) -> float:
"""Convert exif lat/lon represented as degre/minute/second into decimal """Convert exif lat/lon represented as degre/minute/second into decimal
>>> _from_dms("1/1 55/1 123020/13567") >>> _from_dms("1/1 55/1 123020/13567")
1.9191854417991367 1.9191854417991367
@ -225,7 +198,7 @@ def _from_dms(val: str) -> float:
return float(deg) + float(min) / 60 + float(sec) / 3600 return float(deg) + float(min) / 60 + float(sec) / 3600
def _to_dms(value: float) -> Tuple[int, int, float]: def _to_dms(self, value: float) -> Tuple[int, int, float]:
"""Return degree/minute/seconds for a decimal """Return degree/minute/seconds for a decimal
>>> _to_dms(38.889469) >>> _to_dms(38.889469)
(38, 53, 22.0884) (38, 53, 22.0884)
@ -242,14 +215,14 @@ def _to_dms(value: float) -> Tuple[int, int, float]:
return deg, int(min), round(sec, 8) return deg, int(min), round(sec, 8)
def _to_exif_dms(value: float) -> str: def _to_exif_dms(self, value: float) -> str:
"""Return degree/minute/seconds string formated for the exif metadata for a decimal """Return degree/minute/seconds string formated for the exif metadata for a decimal
>>> _to_exif_dms(38.889469) >>> _to_exif_dms(38.889469)
'38/1 53/1 55221/2500' '38/1 53/1 55221/2500'
""" """
from fractions import Fraction from fractions import Fraction
d, m, s = _to_dms(value) d, m, s = self._to_dms(value)
f = Fraction.from_float(s).limit_denominator() # limit fraction precision f = Fraction.from_float(s).limit_denominator() # limit fraction precision
num_s, denomim_s = f.as_integer_ratio() num_s, denomim_s = f.as_integer_ratio()
return f"{d}/1 {m}/1 {num_s}/{denomim_s}" return f"{d}/1 {m}/1 {num_s}/{denomim_s}"