-
Notifications
You must be signed in to change notification settings - Fork 0
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
1 parent
7b31c6d
commit e5a10c0
Showing
38 changed files
with
595 additions
and
223 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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 |
---|---|---|
|
@@ -8,5 +8,4 @@ dependencies: | |
- ipykernel | ||
- geopandas | ||
- pyogrio | ||
- pip: | ||
- exif | ||
- pillow |
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,14 @@ | ||
Metadata-Version: 2.1 | ||
Name: fotoviewer | ||
Version: 2024.7.0 | ||
Summary: Converteren van breslocaties naar bresvlakken | ||
Author-email: Daniel Tollenaar <[email protected]> | ||
License: MIT | ||
Project-URL: Source, https://github.com/d2hydro/fotoviewer | ||
Requires-Python: <3.13,>3.7 | ||
Description-Content-Type: text/markdown | ||
Requires-Dist: geopandas | ||
Requires-Dist: pyogrio | ||
Requires-Dist: pillow | ||
Provides-Extra: tests | ||
Requires-Dist: pytest; extra == "tests" |
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,14 @@ | ||
pyproject.toml | ||
setup.py | ||
fotoviewer/__init__.py | ||
fotoviewer/parse_emls.py | ||
fotoviewer/read_exif.py | ||
fotoviewer/read_mailbox.py | ||
fotoviewer/update_app.py | ||
fotoviewer.egg-info/PKG-INFO | ||
fotoviewer.egg-info/SOURCES.txt | ||
fotoviewer.egg-info/dependency_links.txt | ||
fotoviewer.egg-info/entry_points.txt | ||
fotoviewer.egg-info/requires.txt | ||
fotoviewer.egg-info/top_level.txt | ||
fotoviewer.egg-info/zip-safe |
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 @@ | ||
|
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,4 @@ | ||
[gui_scripts] | ||
parse_inbox = scripts.parse_inbox:main | ||
read_mailbox = scripts.read_mailbox:main | ||
update_app = scripts.update_app:main |
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,6 @@ | ||
geopandas | ||
pyogrio | ||
pillow | ||
|
||
[tests] | ||
pytest |
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 @@ | ||
fotoviewer |
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 @@ | ||
|
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,28 @@ | ||
#%% | ||
import os | ||
from pathlib import Path | ||
|
||
__version__ = "2024.7.0" | ||
|
||
FOTOVIEWER_ADDRES = os.getenv("FOTOVIEWER_ADDRES") | ||
FOTOVIEWER_DATA_DIR = os.getenv("FOTOVIEWER_DATA_DIR") | ||
FOTOVIEWER_PASS = os.getenv("FOTOVIEWER_PASS") | ||
|
||
def create_sub_dirs(data_dir): | ||
for sub_dir in ["inbox", "datastore", "archive"]: | ||
dir_path = data_dir / sub_dir | ||
dir_path.mkdir(parents=True, exist_ok=True) | ||
|
||
def date_time_file_prefix(date_time): | ||
"""String we use as prefix""" | ||
return date_time.strftime("%Y%m%dT%H%M%S") | ||
|
||
if FOTOVIEWER_DATA_DIR is not None: | ||
FOTOVIEWER_DATA_DIR = Path(FOTOVIEWER_DATA_DIR) | ||
INBOX = FOTOVIEWER_DATA_DIR / "inbox" | ||
DATASTORE = FOTOVIEWER_DATA_DIR / "datastore" | ||
|
||
create_sub_dirs(FOTOVIEWER_DATA_DIR) | ||
else: | ||
INBOX = None | ||
DATASTORE = None |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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,193 @@ | ||
from email import policy | ||
from email.parser import BytesParser | ||
from email.utils import parsedate_to_datetime, parseaddr | ||
from PIL import Image | ||
from datetime import datetime | ||
import io | ||
from pathlib import Path | ||
import shutil | ||
import geopandas as gpd | ||
import pandas as pd | ||
from fotoviewer.read_exif import get_image_metadata | ||
from fotoviewer import FOTOVIEWER_DATA_DIR, create_sub_dirs, date_time_file_prefix | ||
|
||
|
||
def foto_file_name(date_time, img_file_name): | ||
"""Construct new file name from date_time, sender and file_name""" | ||
|
||
# date_time to string | ||
date_time = date_time_file_prefix(date_time) | ||
|
||
# create new filename | ||
img_file_name = Path(img_file_name) | ||
new_file_name = img_file_name.with_stem(f"{date_time}_{img_file_name.stem}") | ||
|
||
return new_file_name | ||
|
||
def eml_file_name(msg_date_time, eml_file_name): | ||
"""Construct new file name from date_time, sender and file_name""" | ||
|
||
if msg_date_time is None: | ||
date_time = datetime.now() | ||
else: | ||
date_time = msg_date_time | ||
|
||
|
||
# date_time to string | ||
date_time = date_time_file_prefix(date_time) | ||
|
||
# create new filename | ||
eml_file_name = Path(eml_file_name) | ||
new_eml_file_name = eml_file_name.with_stem(f"{date_time}_{eml_file_name.stem}") | ||
|
||
return new_eml_file_name | ||
|
||
def get_date_time(img_date_time, msg_date_time): | ||
# if we don't have date-time info we use now as datetime | ||
date_time = img_date_time | ||
|
||
if date_time is None: | ||
date_time = msg_date_time | ||
|
||
if date_time is None: | ||
date_time = datetime.now() | ||
|
||
return date_time | ||
|
||
|
||
def get_sender(from_header): | ||
if from_header: | ||
parsed_from_header = parseaddr(from_header) | ||
return parsed_from_header[0] | ||
|
||
|
||
def parse_eml(eml_file: Path, datastore: Path, archive:Path): | ||
"""Parse an eml-file to a list of dict | ||
Args: | ||
eml_file (Path): Path to eml-file | ||
datastore (Path): path to datastore for storing the image attachements | ||
archive (Path): path to store the eml-file after parsed | ||
Returns: | ||
list[dict]: dictionary with all image properties | ||
""" | ||
data = [] | ||
# Open the EML file | ||
with open(eml_file, 'rb') as f: | ||
msg = BytesParser(policy=policy.default).parse(f) | ||
|
||
# Get the email subject | ||
subject = msg['subject'] | ||
|
||
# Get the email sender | ||
sender = get_sender(msg['From']) | ||
if sender is None: | ||
pass | ||
|
||
# Get the mail date_time | ||
msg_date_time = msg["Date"] | ||
msg_date_time = parsedate_to_datetime(msg_date_time) if msg_date_time else None | ||
|
||
# Get the email body | ||
if msg.is_multipart(): | ||
for part in msg.walk(): | ||
content_type = part.get_content_type() | ||
disposition = str(part.get('Content-Disposition')) | ||
if content_type == 'text/plain' and 'attachment' not in disposition: | ||
body = part.get_payload(decode=True).decode(part.get_content_charset()) | ||
break | ||
else: | ||
body = msg.get_payload(decode=True).decode(msg.get_content_charset()) | ||
|
||
|
||
# Parse all attachements as images | ||
for part in msg.iter_attachments(): | ||
img_file_name = part.get_filename() | ||
content = part.get_payload(decode=True) | ||
|
||
with io.BytesIO(content) as image_file: | ||
# read exif from file_objec | ||
image_metadata = get_image_metadata(image_file) | ||
|
||
# only returns img if file is image and exif is complete | ||
if image_metadata is None: | ||
print(f"Attachment '{img_file_name}' is not a valid image with exif gps-info! (ignored)") | ||
continue | ||
else: | ||
|
||
# read all data from exif | ||
geometry = image_metadata["geometry"] | ||
img_date_time = image_metadata["date_time"] | ||
date_time = get_date_time(img_date_time, msg_date_time) | ||
|
||
# store file with a unique uuid | ||
img_path = datastore / foto_file_name(date_time, img_file_name) | ||
image_file.seek(0) | ||
img_path.write_bytes(image_file.read()) | ||
data += [{"file_name":img_path.name, "sender": sender, "date_time":date_time, "subject":subject, "body": body, "geometry":geometry}] | ||
|
||
if eml_file.name.startswith(date_time_file_prefix(msg_date_time)): | ||
new_eml_file = archive.joinpath(eml_file.name) | ||
else: | ||
new_eml_file = archive.joinpath(eml_file_name(msg_date_time, eml_file.name)) | ||
|
||
shutil.move(eml_file, new_eml_file) | ||
|
||
return data | ||
|
||
|
||
def parse_emls(data_dir=FOTOVIEWER_DATA_DIR): | ||
"""Parse emls in a data_directory with at least an 'inbox' sub-folder with emls. | ||
The sub-directories 'archive' and 'datastore' will be created and filled by this function | ||
Args: | ||
data_dir (PathLike, optional): Directory with inbox, datastore and archive. Defaults to FOTOVIEWER_DATA_DIR. | ||
Raises: | ||
ValueError: _description_ | ||
FileNotFoundError: _description_ | ||
""" | ||
|
||
# check if data_dir is specified | ||
if data_dir is None: | ||
raise ValueError("Value for 'data_dir' is None and should be fotoviewer root-directory (containing, inbox, archive and datastore)") | ||
else: | ||
data_dir = Path(data_dir) | ||
inbox = data_dir / "inbox" | ||
|
||
# if inbox exists we create other dirs | ||
if not inbox.exists(): | ||
raise FileNotFoundError(f"inbox not found: {inbox}") | ||
else: | ||
create_sub_dirs(data_dir) | ||
archive = data_dir / "archive" | ||
datastore = data_dir / "datastore" | ||
|
||
# parse all emls in inbox | ||
data = [] | ||
emls = list(inbox.glob("*.eml")) | ||
if len(emls) == 0: | ||
print("No emls to parse in {inbox}") | ||
|
||
for eml_file in emls: | ||
print(eml_file) | ||
data += parse_eml(eml_file, datastore=datastore, archive=archive) | ||
|
||
# convert to gdf | ||
|
||
if data: | ||
foto_gpkg = datastore.joinpath("fotos.gpkg") | ||
|
||
# define and transform GeoDataFrame | ||
gdf = gpd.GeoDataFrame(data, crs=4326) | ||
gdf.to_crs(28992, inplace=True) | ||
|
||
# concat to GeoPackage if allready existent | ||
if foto_gpkg.exists(): | ||
gdf = pd.concat([gdf, gpd.read_file(foto_gpkg, engine="pyogrio")]) | ||
gdf.drop_duplicates(subset=["file_name"], inplace=True) | ||
|
||
# write gdf to GeoPackage | ||
gdf.to_file(foto_gpkg, engine="pyogrio") |
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,72 @@ | ||
from PIL import Image | ||
from PIL.ExifTags import TAGS | ||
from shapely.geometry import Point | ||
from datetime import datetime | ||
|
||
def get_exif_data(image_file): | ||
"""Read EXIF data""" | ||
try: | ||
with Image.open(image_file) as img: | ||
# verify if image is valid | ||
img.verify() | ||
# read exif-data | ||
exif_data = img._getexif() | ||
if exif_data is not None: | ||
return {TAGS.get(tag): value for tag, value in exif_data.items()} | ||
else: | ||
return {} | ||
# if image is not valid | ||
except (IOError, SyntaxError): | ||
return {} | ||
|
||
def get_if_exist(data, key): | ||
"""Get metadata if exists""" | ||
return data[key] if key in data else None | ||
|
||
def convert_to_degrees(value): | ||
"""Convert exif coordinate to degrees""" | ||
d = float(value[0]) | ||
m = float(value[1]) | ||
s = float(value[2]) | ||
return d + (m / 60.0) + (s / 3600.0) | ||
|
||
def get_point(exif_data): | ||
if 'GPSInfo' in exif_data: | ||
|
||
# Get exif coordinates | ||
gps_info = exif_data['GPSInfo'] | ||
gps_latitude = get_if_exist(gps_info, 2) | ||
gps_latitude_ref = get_if_exist(gps_info, 1) | ||
gps_longitude = get_if_exist(gps_info, 4) | ||
gps_longitude_ref = get_if_exist(gps_info, 3) | ||
|
||
# Convert to point | ||
if gps_latitude and gps_latitude_ref and gps_longitude and gps_longitude_ref: | ||
lat = convert_to_degrees(gps_latitude) | ||
if gps_latitude_ref != "N": | ||
lat = -lat | ||
lon = convert_to_degrees(gps_longitude) | ||
if gps_longitude_ref != "E": | ||
lon = -lon | ||
return Point(lon, lat) | ||
|
||
def get_date_time(exif_data): | ||
"""Get datetime from exif metadata""" | ||
|
||
date_time_string = exif_data.get("DateTimeOriginal", None) | ||
if date_time_string is not None: | ||
return datetime.strptime(date_time_string, '%Y:%m:%d %H:%M:%S') | ||
|
||
def get_image_metadata(image_path): | ||
"""Get all metadata we need for photo-processing""" | ||
|
||
exif_data = get_exif_data(image_path) | ||
geometry = get_point(exif_data) | ||
date_time = get_date_time(exif_data) | ||
|
||
if geometry is not None: | ||
|
||
return { | ||
"geometry": geometry, | ||
"date_time": date_time | ||
} |
Oops, something went wrong.