Skip to content

Commit

Permalink
complete versie
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielTollenaar committed Jul 15, 2024
1 parent 7b31c6d commit e5a10c0
Show file tree
Hide file tree
Showing 38 changed files with 595 additions and 223 deletions.
File renamed without changes.
File renamed without changes.
Binary file added app/static/data/20240611T141437_image2.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/static/data/20240708T130451_image1.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/static/data/20240713T133003_image0.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions app/static/js/fotos.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion app/static/js/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ var highlightedFeature = null;
// reset highlight
function reset_highlight(){
if (highlightedFeature) {
highlightedFeature.setStyle([unselectIcon])
highlightedFeature.setStyle([unselectIcon]);
highlightedFeature = null
}};

function initializeDateRangeSlider(fotoLayer) {
Expand Down
3 changes: 1 addition & 2 deletions python/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ dependencies:
- ipykernel
- geopandas
- pyogrio
- pip:
- exif
- pillow
14 changes: 14 additions & 0 deletions python/fotoviewer.egg-info/PKG-INFO
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"
14 changes: 14 additions & 0 deletions python/fotoviewer.egg-info/SOURCES.txt
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
1 change: 1 addition & 0 deletions python/fotoviewer.egg-info/dependency_links.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

4 changes: 4 additions & 0 deletions python/fotoviewer.egg-info/entry_points.txt
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
6 changes: 6 additions & 0 deletions python/fotoviewer.egg-info/requires.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
geopandas
pyogrio
pillow

[tests]
pytest
1 change: 1 addition & 0 deletions python/fotoviewer.egg-info/top_level.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fotoviewer
1 change: 1 addition & 0 deletions python/fotoviewer.egg-info/zip-safe
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

28 changes: 28 additions & 0 deletions python/fotoviewer/__init__.py
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.
193 changes: 193 additions & 0 deletions python/fotoviewer/parse_emls.py
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")
72 changes: 72 additions & 0 deletions python/fotoviewer/read_exif.py
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
}
Loading

0 comments on commit e5a10c0

Please sign in to comment.