Working image grid view.

This commit is contained in:
Michael Soukup 2021-07-10 08:48:00 +02:00
commit 58cfd41f43
8 changed files with 208 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__pycache__
venv

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# photocat
Time to unfuck my photo library.

22
photocat/fs.py Normal file
View File

@ -0,0 +1,22 @@
import os
from toolz import compose
from toolz.curried import filter, map
IMG_EXT = (
'.png',
'.jpeg',
'.jpg',
'.svg',
'.apng',
'.gif',
'.webp',
'.avif'
)
def list_images(folder):
_, _, files = next(os.walk(folder))
return compose(
map(lambda f: os.path.join(folder, f)),
filter(lambda f: os.path.splitext(f)[-1].lower() in IMG_EXT)
)(files)

30
photocat/group.py Normal file
View File

@ -0,0 +1,30 @@
from collections import namedtuple
from itertools import groupby
from toolz import curry, compose
from toolz.curried import map, filter
PhotoGroup = namedtuple(
'PhotoGroup',
['name', 'datetimes', 'photos']
)
@curry
def _group(key, photos):
def create_group(k, photos):
min_dt = min(photos, key=lambda p: p.datetime).datetime
max_dt = max(photos, key=lambda p: p.datetime).datetime
name = str(min_dt) + '-' + str(max_dt) # TODO
return PhotoGroup(name, (min_dt, max_dt), photos)
return [
create_group(k, list(v))
for k, v in groupby(sorted(photos, key=key), key=key)
]
photos_by_month = _group(lambda p: p.datetime.month)

34
photocat/image.py Normal file
View File

@ -0,0 +1,34 @@
import io
from PIL import Image, ExifTags
def read(filename, resize=None):
"""Read and optionally resize an image."""
img = Image.open(filename)
cur_width, cur_height = img.size
if resize:
new_width, new_height = resize
scale = min(new_height/cur_height, new_width/cur_width)
img = img.resize((int(cur_width*scale), int(cur_height*scale)), Image.ANTIALIAS)
return img
def read_exif(filename):
"""Read EXIF data."""
img = Image.open(filename)
exif = img.getexif()
if exif is None:
raise Exception("No EXIF data for image %s" % filename)
return {
ExifTags.TAGS[k]: v
for k, v in exif.items()
if k in ExifTags.TAGS
}
def to_bytes(img):
"""Convert image to PNG format and return as byte-string object."""
bio = io.BytesIO()
img.save(bio, format="PNG")
return bio.getvalue()

85
photocat/main.py Normal file
View File

@ -0,0 +1,85 @@
#import PySimpleGUI as sg
import PySimpleGUIQt as sg
import os
from toolz import compose
from toolz.curried import map
import fs
import image
import photo
import group
MAX_ROWS = 100
MAX_COLS = 5
IMG_SIZE = (100, 100)
NA_FILE = os.path.join(
os.path.dirname(__file__),
'na.jpg'
)
NA_IMG = image.to_bytes(image.read(NA_FILE, resize=IMG_SIZE))
def main():
group_select = [
[sg.Text('Input folder'), sg.In(size=(25,1), enable_events=True, key='FOLDER'), sg.FolderBrowse()],
[sg.Listbox(values=[], enable_events=True, size=(40,20),key='GROUP LIST')]
]
image_view = [
[sg.Image(key='PHOTO %d' % (i*MAX_COLS+j), data=NA_IMG, visible=False, enable_events=True) for j in range(MAX_COLS)]
for i in range(MAX_ROWS)
]
group_view = [
[sg.Text('Group: ')],
[sg.Column(image_view, scrollable=True, size=(650, 700), element_justification='l')]
]
layout = [[
sg.Column(group_select, element_justification='c'),
sg.VSeperator(),
sg.Column(group_view, element_justification='c')
]]
window = sg.Window('photocat', layout, resizable=True)
groups = []
current_group = None
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Exit'):
break
if event == 'FOLDER':
# Process input photos into groups
groups = compose(
group.photos_by_month,
map(photo.read_photo),
fs.list_images
)(values['FOLDER'])
window['GROUP LIST'].update(values=[g.name for g in groups])
elif event == 'GROUP LIST':
# Find photo group
group_name = values['GROUP LIST'][0]
print("Selected group: ", group_name)
current_group = next(g for g in groups if g.name == group_name)
# Assert number of photos
n_photos = len(current_group.photos)
assert n_photos <= MAX_ROWS*MAX_COLS
# Reset image view
for idx in range(MAX_ROWS*MAX_COLS):
if idx < n_photos:
window['PHOTO %d' % idx].update(data=NA_IMG, visible=True)
else:
window['PHOTO %d' % idx].update(visible=False)
# Load and display images
for idx, p in enumerate(current_group.photos):
img_data = image.to_bytes(image.read(p.filename, resize=IMG_SIZE))
window['PHOTO %d' % idx].update(data=img_data)
elif event.startswith('PHOTO'):
idx = int(event.split(' ')[-1])
print("Selected photo %d" % idx)
window.close()
if __name__ == '__main__':
main()

BIN
photocat/na.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

32
photocat/photo.py Normal file
View File

@ -0,0 +1,32 @@
import os
import datetime
from collections import namedtuple
from dateutil import parser
from image import read_exif
Photo = namedtuple(
'Photo',
['filename', 'datetime', 'exif', 'features', 'selected']
)
def _exif_dt(exif):
try:
return parser.parse(exif['DateTimeOriginal'])
except Exception:
return None
def _last_modified_dt(filename):
epoch = os.path.getmtime(filename)
return datetime.datetime.fromtimestamp(epoch)
def read_photo(filename):
exif = read_exif(filename)
print(filename, exif)
dt = _exif_dt(exif) or _last_modified_dt(filename)
features = [] # TODO
return Photo(filename, dt, exif, features, True)