From da1579555bfb313ceb1790a2faf97b0855c69866 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Tue, 31 Jan 2023 17:32:22 +0000 Subject: [PATCH] Formatted metadata Add API for metadata Load more images if page is resized Adjusted animations --- gallery/api.py | 27 +- gallery/gallery.py | 58 +- gallery/metadata.py | 521 ++++++++++++++++++ gallery/static/js/main.js | 3 +- gallery/templates/image.html | 112 +++- gallery/templates/index.html | 36 +- .../user/themes/default/ui/notification.sass | 2 +- gallery/user/themes/default/ui/pop-up.sass | 1 - .../themes/default/ui/wrappers/image.sass | 68 ++- 9 files changed, 742 insertions(+), 86 deletions(-) create mode 100644 gallery/metadata.py diff --git a/gallery/api.py b/gallery/api.py index 24dbbc8..4109c69 100644 --- a/gallery/api.py +++ b/gallery/api.py @@ -1,8 +1,9 @@ -from flask import Blueprint, render_template, current_app, send_from_directory, send_file, request, g, abort, flash +from flask import Blueprint, render_template, current_app, send_from_directory, send_file, request, g, abort, flash, jsonify from werkzeug.utils import secure_filename from gallery.auth import login_required from gallery.db import get_db from PIL import Image, ImageOps +from . import metadata as mt import io import os from uuid import uuid4 @@ -10,6 +11,14 @@ from uuid import uuid4 blueprint = Blueprint('viewsbp', __name__, url_prefix='/api') +def human_size(num, suffix="B"): + for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: + if abs(num) < 1024.0: + return f"{num:3.1f}{unit}{suffix}" + num /= 1024.0 + return f"{num:.1f}Yi{suffix}" + + @blueprint.route('/uploads//', methods=['GET']) def uploads(file, quality): # If quality is 0, return original file @@ -106,4 +115,18 @@ def remove(id): abort(500) flash(['Image was all in Le Head!', 1]) - return 'Gwa Gwa' \ No newline at end of file + return 'Gwa Gwa' + +@blueprint.route('/metadata/', methods=['GET']) +def metadata(id): + img = get_db().execute( + 'SELECT file_name, description, alt FROM posts WHERE id = ?', + (id, )).fetchone() + + if img is None: + abort(404) + + exif = mt.metadata.yoink(os.path.join(current_app.config['UPLOAD_FOLDER'], img['file_name'])) + filesize = os.path.getsize(os.path.join(current_app.config['UPLOAD_FOLDER'], img['file_name'])) + + return jsonify({'metadata': exif, 'filesize': {'bytes': filesize, 'human': human_size(filesize)}}) \ No newline at end of file diff --git a/gallery/gallery.py b/gallery/gallery.py index 5981971..d641bd9 100644 --- a/gallery/gallery.py +++ b/gallery/gallery.py @@ -5,17 +5,24 @@ from werkzeug.utils import secure_filename from gallery.auth import login_required from gallery.db import get_db +from . import metadata as mt from PIL import Image -from PIL.ExifTags import TAGS import os -import datetime - -dt = datetime.datetime.now() +from datetime import datetime +dt = datetime.now() blueprint = Blueprint('gallery', __name__) +def human_size(num, suffix="B"): + for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: + if abs(num) < 1024.0: + return f"{num:3.1f}{unit}{suffix}" + num /= 1024.0 + return f"{num:.1f}Yi{suffix}" + + @blueprint.route('/') def index(): db = get_db() @@ -34,40 +41,19 @@ def image(id): if image is None: abort(404) - # Get exif data from image - file = Image.open( - os.path.join(current_app.config['UPLOAD_FOLDER'], image['file_name'])) - raw_exif = file.getexif() - human_exif = {} - + exif = mt.metadata.yoink(os.path.join(current_app.config['UPLOAD_FOLDER'], image['file_name'])) + file_size = human_size(os.path.getsize(os.path.join(current_app.config['UPLOAD_FOLDER'], image['file_name']))) + try: - for tag in raw_exif: - name = TAGS.get(tag, tag) - value = raw_exif.get(tag) + width = exif['File']['Width']['value'] + height = exif['File']['Height']['value'] + except: + try: + width, height = Image.open(os.path.join(current_app.config['UPLOAD_FOLDER'], image['file_name'])).size + except: + width, height = 0, 0 - if isinstance(value, bytes): - value = value.decode() - - human_exif[name] = value - except Exception as e: - human_exif = False - - def human_size(num, suffix="B"): - for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: - if abs(num) < 1024.0: - return f"{num:3.1f}{unit}{suffix}" - num /= 1024.0 - return f"{num:.1f}Yi{suffix}" - - size = os.path.getsize( - os.path.join(current_app.config['UPLOAD_FOLDER'], image['file_name'])) - - # All in le head - return render_template('image.html', - image=image, - exif=human_exif, - file=file, - size=human_size(size)) + return render_template('image.html', image=image, exif=exif, file_size=file_size, width=width, height=height) @blueprint.route('/group') diff --git a/gallery/metadata.py b/gallery/metadata.py new file mode 100644 index 0000000..96e25e0 --- /dev/null +++ b/gallery/metadata.py @@ -0,0 +1,521 @@ +import PIL +from PIL import Image +from PIL.ExifTags import TAGS, GPSTAGS +from datetime import datetime + + +class metadata: + def yoink(filename): + exif = metadata.getFile(filename) + + if exif: + formatted = metadata.format(exif) + else: + return None + + return metadata.deleteEmpty(formatted) + + def deleteEmpty(dict): + new_dict = {} + + for section in dict: + tmp = {} + for value in dict[section]: + if dict[section][value]['raw'] != None: + if isinstance(dict[section][value]['raw'], PIL.TiffImagePlugin.IFDRational): + dict[section][value]['raw'] = dict[section][value]['raw'].__float__() + elif isinstance(dict[section][value]['raw'], bytes): + dict[section][value]['raw'] = dict[section][value]['raw'].decode('utf-8') + + tmp[value] = dict[section][value] + + + if len(tmp) > 0: + new_dict[section] = tmp + + return new_dict + + def getFile(filename): + try: + file = Image.open(filename) + raw = file._getexif() + exif = {} + + for tag, value in TAGS.items(): + + if tag in raw: + data = raw[tag] + else: + data = None + + exif[value] = {"tag": tag, "raw": data} + + file.close() + + return exif + except Exception as e: + return None + + def format(raw): + exif = {} + + exif['Photographer'] = { + 'Artist': { + 'type': 'text', + 'raw': raw["Artist"]["raw"] + }, + 'Comment': { + 'type': 'text', + 'raw': raw["UserComment"]["raw"] + }, + 'Description': { + 'type': 'text', + 'raw': raw["ImageDescription"]["raw"] + }, + 'Date Digitized': { + 'type': 'date', + 'raw': raw["DateTimeDigitized"]["raw"], + 'formatted': metadata.date(raw["DateTimeDigitized"]["raw"]) + }, + 'Copyright': { + 'type': 'text', + 'raw': raw["Copyright"]["raw"] + }, + } + exif['Camera'] = { + 'Model': { + 'type': 'text', + 'raw': raw['Model']['raw'] + }, + 'Make': { + 'type': 'text', + 'raw': raw['Make']['raw'] + }, + 'Lense Model': { + 'type': 'text', + 'raw': raw['LensModel']['raw'], + }, + 'Lense Spec': { + 'type': 'text', + 'raw': raw['LensSpecification']['raw'], + 'formatted': metadata.lensSpecification(raw['LensSpecification']['raw']) + }, + 'Component Config': { + 'type': 'text', + 'raw': raw['ComponentsConfiguration']['raw'], + 'formatted': metadata.componentsConfiguration(raw['ComponentsConfiguration']['raw']) + }, + 'Date Processed': { + 'type': 'date', + 'raw': raw['DateTime']['raw'], + 'formatted': metadata.date(raw['DateTime']['raw']) + }, + } + exif['Software'] = { + 'Software': { + 'type': 'text', + 'raw': raw['Software']['raw'] + }, + 'Colour Space': { + 'type': 'number', + 'raw': raw['ColorSpace']['raw'], + 'formatted': metadata.colorSpace(raw['ColorSpace']['raw']) + }, + 'Compression': { + 'type': 'number', + 'raw': raw['Compression']['raw'], + 'formatted': metadata.compression(raw['Compression']['raw']) + }, + } + exif['Photo'] = { + 'FNumber': { + 'type': 'fnumber', + 'raw': raw["FNumber"]["raw"], + 'formatted': metadata.fnumber(raw["FNumber"]["raw"]) + }, + 'Focal Length': { + 'type': 'focal', + 'raw': raw["FocalLength"]["raw"] + }, + 'Focal Length - Film': { + 'type': 'focal', + 'raw': raw["FocalLengthIn35mmFilm"]["raw"] + }, + 'Max Aperture': { + 'type': 'fnumber', + 'raw': raw["MaxApertureValue"]["raw"], + 'formatted': metadata.fnumber(raw["MaxApertureValue"]["raw"]) + }, + 'Aperture': { + 'type': 'fnumber', + 'raw': raw["ApertureValue"]["raw"], + 'formatted': metadata.fnumber(raw["ApertureValue"]["raw"]) + }, + 'Shutter Speed': { + 'type': 'shutter', + 'raw': raw["ShutterSpeedValue"]["raw"], + 'formatted': metadata.shutter(raw["ShutterSpeedValue"]["raw"]) + }, + 'ISO Speed Ratings': { + 'type': 'number', + 'raw': raw["ISOSpeedRatings"]["raw"], + 'formatted': metadata.iso(raw["ISOSpeedRatings"]["raw"]) + }, + 'ISO Speed': { + 'type': 'iso', + 'raw': raw["ISOSpeed"]["raw"], + 'formatted': metadata.iso(raw["ISOSpeed"]["raw"]) + }, + 'Sensitivity Type': { + 'type': 'number', + 'raw': raw["SensitivityType"]["raw"], + 'formatted': metadata.sensitivityType(raw["SensitivityType"]["raw"]) + }, + 'Exposure Bias': { + 'type': 'ev', + 'raw': raw["ExposureBiasValue"]["raw"], + 'formatted': metadata.ev(raw["ExposureBiasValue"]["raw"]) + }, + 'Exposure Time': { + 'type': 'shutter', + 'raw': raw["ExposureTime"]["raw"], + 'formatted': metadata.shutter(raw["ExposureTime"]["raw"]) + }, + 'Exposure Mode': { + 'type': 'number', + 'raw': raw["ExposureMode"]["raw"], + 'formatted': metadata.exposureMode(raw["ExposureMode"]["raw"]) + }, + 'Exposure Program': { + 'type': 'number', + 'raw': raw["ExposureProgram"]["raw"], + 'formatted': metadata.exposureProgram(raw["ExposureProgram"]["raw"]) + }, + 'White Balance': { + 'type': 'number', + 'raw': raw["WhiteBalance"]["raw"], + 'formatted': metadata.whiteBalance(raw["WhiteBalance"]["raw"]) + }, + 'Flash': { + 'type': 'number', + 'raw': raw["Flash"]["raw"], + 'formatted': metadata.flash(raw["Flash"]["raw"]) + }, + 'Metering Mode': { + 'type': 'number', + 'raw': raw["MeteringMode"]["raw"], + 'formatted': metadata.meteringMode(raw["MeteringMode"]["raw"]) + }, + 'Light Source': { + 'type': 'number', + 'raw': raw["LightSource"]["raw"], + 'formatted': metadata.lightSource(raw["LightSource"]["raw"]) + }, + 'Scene Capture Type': { + 'type': 'number', + 'raw': raw["SceneCaptureType"]["raw"], + 'formatted': metadata.sceneCaptureType(raw["SceneCaptureType"]["raw"]) + }, + 'Scene Type': { + 'type': 'number', + 'raw': raw["SceneType"]["raw"], + 'formatted': metadata.sceneType(raw["SceneType"]["raw"]) + }, + } + exif['File'] = { + 'Width': { + 'type': 'number', + 'raw': raw["ImageWidth"]["raw"] + }, + 'Height': { + 'type': 'number', + 'raw': raw["ImageLength"]["raw"] + }, + 'Orientation': { + 'type': 'number', + 'raw': raw["Orientation"]["raw"], + 'formatted': metadata.orientation(raw["Orientation"]["raw"]) + }, + 'Xresolution': { + 'type': 'number', + 'raw': raw["XResolution"]["raw"] + }, + 'Yresolution': { + 'type': 'number', + 'raw': raw["YResolution"]["raw"] + }, + 'Resolution Units': { + 'type': 'number', + 'raw': raw["ResolutionUnit"]["raw"], + 'formatted': metadata.resolutionUnit(raw["ResolutionUnit"]["raw"]) + }, + } + + return exif + + def date(date): + date_format = '%Y:%m:%d %H:%M:%S' + + if date: + return str(datetime.strptime(date, date_format)) + else: + return None + + def fnumber(value): + if value != None: + return 'f/' + str(value) + else: + return None + + def iso(value): + if value != None: + return 'ISO ' + str(value) + else: + return None + + def shutter(value): + if value != None: + return str(value) + 's' + else: + return None + + def focal(value): + if value != None: + return str(value[0] / value[1]) + 'mm' + else: + return None + + def ev(value): + if value != None: + return str(value) + 'EV' + else: + return None + + def colorSpace(value): + types = { + 1: 'sRGB', + 65535: 'Uncalibrated', + 0: 'Reserved' + } + + try: + return types[int(value)] + except: + return None + + def flash(value): + types = { + 0: 'Flash did not fire', + 1: 'Flash fired', + 5: 'Strobe return light not detected', + 7: 'Strobe return light detected', + 9: 'Flash fired, compulsory flash mode', + 13: 'Flash fired, compulsory flash mode, return light not detected', + 15: 'Flash fired, compulsory flash mode, return light detected', + 16: 'Flash did not fire, compulsory flash mode', + 24: 'Flash did not fire, auto mode', + 25: 'Flash fired, auto mode', + 29: 'Flash fired, auto mode, return light not detected', + 31: 'Flash fired, auto mode, return light detected', + 32: 'No flash function', + 65: 'Flash fired, red-eye reduction mode', + 69: 'Flash fired, red-eye reduction mode, return light not detected', + 71: 'Flash fired, red-eye reduction mode, return light detected', + 73: 'Flash fired, compulsory flash mode, red-eye reduction mode', + 77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', + 79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', + 89: 'Flash fired, auto mode, red-eye reduction mode', + 93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode', + 95: 'Flash fired, auto mode, return light detected, red-eye reduction mode' + } + + try: + return types[int(value)] + except: + return None + + def exposureProgram(value): + types = { + 0: 'Not defined', + 1: 'Manual', + 2: 'Normal program', + 3: 'Aperture priority', + 4: 'Shutter priority', + 5: 'Creative program', + 6: 'Action program', + 7: 'Portrait mode', + 8: 'Landscape mode' + } + + try: + return types[int(value)] + except: + return None + + def meteringMode(value): + types = { + 0: 'Unknown', + 1: 'Average', + 2: 'Center-Weighted Average', + 3: 'Spot', + 4: 'Multi-Spot', + 5: 'Pattern', + 6: 'Partial', + 255: 'Other' + } + + try: + return types[int(value)] + except: + return None + + def resolutionUnit(value): + types = { + 1: 'No absolute unit of measurement', + 2: 'Inch', + 3: 'Centimeter' + } + + try: + return types[int(value)] + except: + return None + + def lightSource(value): + types = { + 0: 'Unknown', + 1: 'Daylight', + 2: 'Fluorescent', + 3: 'Tungsten (incandescent light)', + 4: 'Flash', + 9: 'Fine weather', + 10: 'Cloudy weather', + 11: 'Shade', + 12: 'Daylight fluorescent (D 5700 - 7100K)', + 13: 'Day white fluorescent (N 4600 - 5400K)', + 14: 'Cool white fluorescent (W 3900 - 4500K)', + 15: 'White fluorescent (WW 3200 - 3700K)', + 17: 'Standard light A', + 18: 'Standard light B', + 19: 'Standard light C', + 20: 'D55', + 21: 'D65', + 22: 'D75', + 23: 'D50', + 24: 'ISO studio tungsten', + 255: 'Other light source', + } + + try: + return types[int(value)] + except: + return None + + def sceneCaptureType(value): + types = { + 0: 'Standard', + 1: 'Landscape', + 2: 'Portrait', + 3: 'Night scene', + } + + try: + return types[int(value)] + except: + return None + + def sceneType(value): + if value: + return 'Directly photographed image' + else: + return None + + def whiteBalance(value): + types = { + 0: 'Auto white balance', + 1: 'Manual white balance', + } + + try: + return types[int(value)] + except: + return None + + def exposureMode(value): + types = { + 0: 'Auto exposure', + 1: 'Manual exposure', + 2: 'Auto bracket', + } + + try: + return types[int(value)] + except: + return None + + def sensitivityType(value): + types = { + 0: 'Unknown', + 1: 'Standard Output Sensitivity', + 2: 'Recommended Exposure Index', + 3: 'ISO Speed', + 4: 'Standard Output Sensitivity and Recommended Exposure Index', + 5: 'Standard Output Sensitivity and ISO Speed', + 6: 'Recommended Exposure Index and ISO Speed', + 7: 'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed', + } + + try: + return types[int(value)] + except: + return None + + def lensSpecification(value): + if value: + return str(value[0] / value[1]) + 'mm - ' + str(value[2] / value[3]) + 'mm' + else: + return None + + def compression(value): + types = { + 1: 'Uncompressed', + 6: 'JPEG compression', + } + + try: + return types[int(value)] + except: + return None + + def orientation(value): + types = { + 1: 'Horizontal (normal)', + 2: 'Mirror horizontal', + 3: 'Rotate 180', + 4: 'Mirror vertical', + 5: 'Mirror horizontal and rotate 270 CW', + 6: 'Rotate 90 CW', + 7: 'Mirror horizontal and rotate 90 CW', + 8: 'Rotate 270 CW', + } + + try: + return types[int(value)] + except: + return None + + def componentsConfiguration(value): + types = { + 0: '', + 1: 'Y', + 2: 'Cb', + 3: 'Cr', + 4: 'R', + 5: 'G', + 6: 'B', + } + + try: + return ''.join([types[int(x)] for x in value]) + except: + return None \ No newline at end of file diff --git a/gallery/static/js/main.js b/gallery/static/js/main.js index 8e344eb..dcb3283 100644 --- a/gallery/static/js/main.js +++ b/gallery/static/js/main.js @@ -20,14 +20,13 @@ document.querySelector('.jumpUp').onclick = function() { function imgFade(obj) { $(obj).animate({opacity: 1}, 500); - //$(obj).parent().style.backgroundColor = 'transparent'; } var times = document.getElementsByClassName('time'); for (var i = 0; i < times.length; i++) { var time = times[i].innerHTML; var date = new Date(time); - times[i].innerHTML = date.toLocaleString( 'en-GB', { timeZone: 'UTC' } ); + times[i].innerHTML = date.toLocaleString('en-GB'); } function addNotification(text='Sample notification', type=4) { diff --git a/gallery/templates/image.html b/gallery/templates/image.html index 283a05c..77a5c4d 100644 --- a/gallery/templates/image.html +++ b/gallery/templates/image.html @@ -19,8 +19,8 @@ src="/api/uploads/{{ image['file_name'] }}/1000" onload="imgFade(this)" style="opacity:0;" onerror="this.src='/static/images/error.png'" - width="{{ file['width'] }}" - height="{{ file['height'] }}" + width="{{ width }}" + height="{{ height }}" /> @@ -108,28 +108,98 @@

Info

-

Filename: {{ image['file_name'] }}

-

Image ID: {{ image['id'] }}

-

Author: {{ image['author_id'] }}

-

Upload date: {{ image['created_at'] }}

-

Dimensions: {{ file['width'] }}x{{ file['height'] }}

-

File size: {{ size }}

+ + + + + + + + + + + + + + + + + + + + + +
Image ID{{ image['id'] }}
Author{{ image['author_id'] }}
Upload date{{ image['created_at'] }}
Filename{{ image['file_name'] }}
File size{{ file_size }}
- {% if exif %} -
-
- - - -

Metadata

+ {% if exif %} + {% for tag in exif %} +
+ {% if tag == 'Photographer' %} +
+ + + +

Photographer

+
+ {% elif tag == 'Camera' %} +
+ + + +

Camera

+
+ {% elif tag == 'Software' %} +
+ + + +

Software

+
+ {% elif tag == 'Photo' %} +
+ + + +

Photo

+
+ {% elif tag == 'File' %} +
+ + + +

File

+
+ {% else %} +
+ + + +

{{tag}}

+
+ {% endif %} +
+ + {% for subtag in exif[tag] %} + + + {% if exif[tag][subtag]['formatted'] %} + {% if exif[tag][subtag]['type'] == 'date' %} + + {% else %} + + {% endif %} + {% elif exif[tag][subtag]['raw'] %} + + {% else %} + + {% endif %} + + {% endfor %} +
{{subtag}}{{exif[tag][subtag]['formatted']}}{{exif[tag][subtag]['formatted']}}{{exif[tag][subtag]['raw']}}sad noises
+
-
- {% for tag in exif %} -

{{ tag }}: {{ exif[tag] }}

- {% endfor %} -
-
+ {% endfor %} {% endif %}
{% endblock %} diff --git a/gallery/templates/index.html b/gallery/templates/index.html index b8de948..5eed5dc 100644 --- a/gallery/templates/index.html +++ b/gallery/templates/index.html @@ -24,33 +24,29 @@ {% block script %}