From 58cfd41f438a593f7f3ef4a85e5e76b1d0cb6e9b Mon Sep 17 00:00:00 2001 From: Michael Soukup Date: Sat, 10 Jul 2021 08:48:00 +0200 Subject: [PATCH] Working image grid view. --- .gitignore | 2 ++ README.md | 3 ++ photocat/fs.py | 22 ++++++++++++ photocat/group.py | 30 ++++++++++++++++ photocat/image.py | 34 +++++++++++++++++++ photocat/main.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++ photocat/na.jpg | Bin 0 -> 22145 bytes photocat/photo.py | 32 +++++++++++++++++ 8 files changed, 208 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 photocat/fs.py create mode 100644 photocat/group.py create mode 100644 photocat/image.py create mode 100644 photocat/main.py create mode 100644 photocat/na.jpg create mode 100644 photocat/photo.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82adb58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +venv diff --git a/README.md b/README.md new file mode 100644 index 0000000..f78832b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# photocat + +Time to unfuck my photo library. diff --git a/photocat/fs.py b/photocat/fs.py new file mode 100644 index 0000000..4eed20a --- /dev/null +++ b/photocat/fs.py @@ -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) diff --git a/photocat/group.py b/photocat/group.py new file mode 100644 index 0000000..c2b75ff --- /dev/null +++ b/photocat/group.py @@ -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) + + diff --git a/photocat/image.py b/photocat/image.py new file mode 100644 index 0000000..39456f7 --- /dev/null +++ b/photocat/image.py @@ -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() + diff --git a/photocat/main.py b/photocat/main.py new file mode 100644 index 0000000..a09e6e0 --- /dev/null +++ b/photocat/main.py @@ -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() diff --git a/photocat/na.jpg b/photocat/na.jpg new file mode 100644 index 0000000000000000000000000000000000000000..06c2d3fb200fe134c931284a7fc95cd129218952 GIT binary patch literal 22145 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