Correct missing Exif data

Format missed information
This commit is contained in:
Michał Gdula 2023-01-31 22:08:37 +00:00
parent da1579555b
commit c02d618844
6 changed files with 266 additions and 179 deletions

View file

@ -96,8 +96,8 @@ def create_app(test_config=None):
app.register_blueprint(auth.blueprint) app.register_blueprint(auth.blueprint)
# Load routes for home and images # Load routes for home and images
from . import gallery from . import routing
app.register_blueprint(gallery.blueprint) app.register_blueprint(routing.blueprint)
app.add_url_rule('/', endpoint='index') app.add_url_rule('/', endpoint='index')
# Load APIs # Load APIs

View file

@ -11,14 +11,6 @@ from uuid import uuid4
blueprint = Blueprint('viewsbp', __name__, url_prefix='/api') 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/<file>/<int:quality>', methods=['GET']) @blueprint.route('/uploads/<file>/<int:quality>', methods=['GET'])
def uploads(file, quality): def uploads(file, quality):
# If quality is 0, return original file # If quality is 0, return original file
@ -127,6 +119,5 @@ def metadata(id):
abort(404) abort(404)
exif = mt.metadata.yoink(os.path.join(current_app.config['UPLOAD_FOLDER'], img['file_name'])) 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)}}) return jsonify(exif)

View file

@ -2,38 +2,65 @@ import PIL
from PIL import Image from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS from PIL.ExifTags import TAGS, GPSTAGS
from datetime import datetime from datetime import datetime
import os
class metadata: class metadata:
def yoink(filename): def yoink(filename):
exif = metadata.getFile(filename) exif = metadata.getFile(filename)
file_size = os.path.getsize(filename)
file_name = os.path.basename(filename)
file_resolution = Image.open(filename).size
if exif: if exif:
formatted = metadata.format(exif) unformatted_exif = metadata.format(exif, file_size, file_name, file_resolution)
else: else:
return None # No EXIF data, get some basic informaton from the file
unformatted_exif = {
'File': {
'Name': {
'type': 'text',
'raw': file_name
},
'Size': {
'type': 'number',
'raw': file_size,
'formatted': metadata.human_size(file_size)
},
'Format': {
'type': 'text',
'raw': file_name.split('.')[-1]
},
'Width': {
'type': 'number',
'raw': file_resolution[0]
},
'Height': {
'type': 'number',
'raw': file_resolution[1]
},
}
}
return metadata.deleteEmpty(formatted) formatted_exif = {}
def deleteEmpty(dict): for section in unformatted_exif:
new_dict = {}
for section in dict:
tmp = {} 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] for value in unformatted_exif[section]:
if unformatted_exif[section][value]['raw'] != None:
raw_type = unformatted_exif[section][value]['raw']
if isinstance(raw_type, PIL.TiffImagePlugin.IFDRational):
raw_type = raw_type.__float__()
elif isinstance(raw_type, bytes):
raw_type = raw_type.decode('utf-8')
tmp[value] = unformatted_exif[section][value]
if len(tmp) > 0: if len(tmp) > 0:
new_dict[section] = tmp formatted_exif[section] = tmp
return new_dict return formatted_exif
def getFile(filename): def getFile(filename):
try: try:
@ -54,9 +81,9 @@ class metadata:
return exif return exif
except Exception as e: except Exception as e:
return None return False
def format(raw): def format(raw, file_size, file_name, file_resolution):
exif = {} exif = {}
exif['Photographer'] = { exif['Photographer'] = {
@ -72,11 +99,6 @@ class metadata:
'type': 'text', 'type': 'text',
'raw': raw["ImageDescription"]["raw"] 'raw': raw["ImageDescription"]["raw"]
}, },
'Date Digitized': {
'type': 'date',
'raw': raw["DateTimeDigitized"]["raw"],
'formatted': metadata.date(raw["DateTimeDigitized"]["raw"])
},
'Copyright': { 'Copyright': {
'type': 'text', 'type': 'text',
'raw': raw["Copyright"]["raw"] 'raw': raw["Copyright"]["raw"]
@ -91,6 +113,14 @@ class metadata:
'type': 'text', 'type': 'text',
'raw': raw['Make']['raw'] 'raw': raw['Make']['raw']
}, },
'Camera Type': {
'type': 'text',
'raw': raw['BodySerialNumber']['raw']
},
'Lens Make': {
'type': 'text',
'raw': raw['LensMake']['raw'],
},
'Lense Model': { 'Lense Model': {
'type': 'text', 'type': 'text',
'raw': raw['LensModel']['raw'], 'raw': raw['LensModel']['raw'],
@ -110,24 +140,28 @@ class metadata:
'raw': raw['DateTime']['raw'], 'raw': raw['DateTime']['raw'],
'formatted': metadata.date(raw['DateTime']['raw']) 'formatted': metadata.date(raw['DateTime']['raw'])
}, },
} 'Date Digitized': {
exif['Software'] = { 'type': 'date',
'Software': { 'raw': raw["DateTimeDigitized"]["raw"],
'formatted': metadata.date(raw["DateTimeDigitized"]["raw"])
},
'Time Offset': {
'type': 'text', 'type': 'text',
'raw': raw['Software']['raw'] 'raw': raw["OffsetTime"]["raw"]
}, },
'Colour Space': { 'Time Offset - Original': {
'type': 'number', 'type': 'text',
'raw': raw['ColorSpace']['raw'], 'raw': raw["OffsetTimeOriginal"]["raw"]
'formatted': metadata.colorSpace(raw['ColorSpace']['raw'])
}, },
'Compression': { 'Time Offset - Digitized': {
'type': 'number', 'type': 'text',
'raw': raw['Compression']['raw'], 'raw': raw["OffsetTimeDigitized"]["raw"]
'formatted': metadata.compression(raw['Compression']['raw']) },
'Date Original': {
'type': 'date',
'raw': raw["DateTimeOriginal"]["raw"],
'formatted': metadata.date(raw["DateTimeOriginal"]["raw"])
}, },
}
exif['Photo'] = {
'FNumber': { 'FNumber': {
'type': 'fnumber', 'type': 'fnumber',
'raw': raw["FNumber"]["raw"], 'raw': raw["FNumber"]["raw"],
@ -135,11 +169,13 @@ class metadata:
}, },
'Focal Length': { 'Focal Length': {
'type': 'focal', 'type': 'focal',
'raw': raw["FocalLength"]["raw"] 'raw': raw["FocalLength"]["raw"],
'formatted': metadata.focal(raw["FocalLength"]["raw"])
}, },
'Focal Length - Film': { 'Focal Length (35mm format)': {
'type': 'focal', 'type': 'focal',
'raw': raw["FocalLengthIn35mmFilm"]["raw"] 'raw': raw["FocalLengthIn35mmFilm"]["raw"],
'formatted': metadata.focal(raw["FocalLengthIn35mmFilm"]["raw"])
}, },
'Max Aperture': { 'Max Aperture': {
'type': 'fnumber', 'type': 'fnumber',
@ -221,15 +257,54 @@ class metadata:
'raw': raw["SceneType"]["raw"], 'raw': raw["SceneType"]["raw"],
'formatted': metadata.sceneType(raw["SceneType"]["raw"]) 'formatted': metadata.sceneType(raw["SceneType"]["raw"])
}, },
'Rating': {
'type': 'number',
'raw': raw["Rating"]["raw"],
'formatted': metadata.rating(raw["Rating"]["raw"])
},
'Rating Percent': {
'type': 'number',
'raw': raw["RatingPercent"]["raw"],
'formatted': metadata.ratingPercent(raw["RatingPercent"]["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['File'] = { exif['File'] = {
'Name': {
'type': 'text',
'raw': file_name
},
'Size': {
'type': 'number',
'raw': file_size,
'formatted': metadata.human_size(file_size)
},
'Format': {
'type': 'text',
'raw': file_name.split('.')[-1]
},
'Width': { 'Width': {
'type': 'number', 'type': 'number',
'raw': raw["ImageWidth"]["raw"] 'raw': file_resolution[0]
}, },
'Height': { 'Height': {
'type': 'number', 'type': 'number',
'raw': raw["ImageLength"]["raw"] 'raw': file_resolution[1]
}, },
'Orientation': { 'Orientation': {
'type': 'number', 'type': 'number',
@ -250,9 +325,28 @@ class metadata:
'formatted': metadata.resolutionUnit(raw["ResolutionUnit"]["raw"]) 'formatted': metadata.resolutionUnit(raw["ResolutionUnit"]["raw"])
}, },
} }
#exif['Raw'] = {}
#for key in raw:
# try:
# exif['Raw'][key] = {
# 'type': 'text',
# 'raw': raw[key]['raw'].decode('utf-8')
# }
# except:
# exif['Raw'][key] = {
# 'type': 'text',
# 'raw': str(raw[key]['raw'])
# }
return exif return exif
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}"
def date(date): def date(date):
date_format = '%Y:%m:%d %H:%M:%S' date_format = '%Y:%m:%d %H:%M:%S'
@ -281,7 +375,10 @@ class metadata:
def focal(value): def focal(value):
if value != None: if value != None:
try:
return str(value[0] / value[1]) + 'mm' return str(value[0] / value[1]) + 'mm'
except:
return str(value) + 'mm'
else: else:
return None return None
@ -297,7 +394,6 @@ class metadata:
65535: 'Uncalibrated', 65535: 'Uncalibrated',
0: 'Reserved' 0: 'Reserved'
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -328,7 +424,6 @@ class metadata:
93: 'Flash fired, auto mode, return light not detected, 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' 95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -346,7 +441,6 @@ class metadata:
7: 'Portrait mode', 7: 'Portrait mode',
8: 'Landscape mode' 8: 'Landscape mode'
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -363,7 +457,6 @@ class metadata:
6: 'Partial', 6: 'Partial',
255: 'Other' 255: 'Other'
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -375,7 +468,6 @@ class metadata:
2: 'Inch', 2: 'Inch',
3: 'Centimeter' 3: 'Centimeter'
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -405,7 +497,6 @@ class metadata:
24: 'ISO studio tungsten', 24: 'ISO studio tungsten',
255: 'Other light source', 255: 'Other light source',
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -418,7 +509,6 @@ class metadata:
2: 'Portrait', 2: 'Portrait',
3: 'Night scene', 3: 'Night scene',
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -435,7 +525,6 @@ class metadata:
0: 'Auto white balance', 0: 'Auto white balance',
1: 'Manual white balance', 1: 'Manual white balance',
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -447,7 +536,6 @@ class metadata:
1: 'Manual exposure', 1: 'Manual exposure',
2: 'Auto bracket', 2: 'Auto bracket',
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -464,7 +552,6 @@ class metadata:
6: 'Recommended Exposure Index and ISO Speed', 6: 'Recommended Exposure Index and ISO Speed',
7: 'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed', 7: 'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed',
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -479,9 +566,55 @@ class metadata:
def compression(value): def compression(value):
types = { types = {
1: 'Uncompressed', 1: 'Uncompressed',
6: 'JPEG compression', 2: 'CCITT 1D',
3: 'T4/Group 3 Fax',
4: 'T6/Group 4 Fax',
5: 'LZW',
6: 'JPEG (old-style)',
7: 'JPEG',
8: 'Adobe Deflate',
9: 'JBIG B&W',
10: 'JBIG Color',
99: 'JPEG',
262: 'Kodak 262',
32766: 'Next',
32767: 'Sony ARW Compressed',
32769: 'Packed RAW',
32770: 'Samsung SRW Compressed',
32771: 'CCIRLEW',
32772: 'Samsung SRW Compressed 2',
32773: 'PackBits',
32809: 'Thunderscan',
32867: 'Kodak KDC Compressed',
32895: 'IT8CTPAD',
32896: 'IT8LW',
32897: 'IT8MP',
32898: 'IT8BL',
32908: 'PixarFilm',
32909: 'PixarLog',
32946: 'Deflate',
32947: 'DCS',
33003: 'Aperio JPEG 2000 YCbCr',
33005: 'Aperio JPEG 2000 RGB',
34661: 'JBIG',
34676: 'SGILog',
34677: 'SGILog24',
34712: 'JPEG 2000',
34713: 'Nikon NEF Compressed',
34715: 'JBIG2 TIFF FX',
34718: '(MDI) Binary Level Codec',
34719: '(MDI) Progressive Transform Codec',
34720: '(MDI) Vector',
34887: 'ESRI Lerc',
34892: 'Lossy JPEG',
34925: 'LZMA2',
34926: 'Zstd',
34927: 'WebP',
34933: 'PNG',
34934: 'JPEG XR',
65000: 'Kodak DCR Compressed',
65535: 'Pentax PEF Compressed',
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -498,7 +631,6 @@ class metadata:
7: 'Mirror horizontal and rotate 90 CW', 7: 'Mirror horizontal and rotate 90 CW',
8: 'Rotate 270 CW', 8: 'Rotate 270 CW',
} }
try: try:
return types[int(value)] return types[int(value)]
except: except:
@ -514,8 +646,13 @@ class metadata:
5: 'G', 5: 'G',
6: 'B', 6: 'B',
} }
try: try:
return ''.join([types[int(x)] for x in value]) return ''.join([types[int(x)] for x in value])
except: except:
return None return None
def rating(value):
return str(value) + ' stars'
def ratingPercent(value):
return str(value) + '%'

View file

@ -15,14 +15,6 @@ dt = datetime.now()
blueprint = Blueprint('gallery', __name__) 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('/') @blueprint.route('/')
def index(): def index():
db = get_db() db = get_db()
@ -42,18 +34,8 @@ def image(id):
abort(404) abort(404)
exif = mt.metadata.yoink(os.path.join(current_app.config['UPLOAD_FOLDER'], image['file_name'])) 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: return render_template('image.html', image=image, exif=exif)
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
return render_template('image.html', image=image, exif=exif, file_size=file_size, width=width, height=height)
@blueprint.route('/group') @blueprint.route('/group')

View file

@ -19,8 +19,8 @@
src="/api/uploads/{{ image['file_name'] }}/1000" src="/api/uploads/{{ image['file_name'] }}/1000"
onload="imgFade(this)" style="opacity:0;" onload="imgFade(this)" style="opacity:0;"
onerror="this.src='/static/images/error.png'" onerror="this.src='/static/images/error.png'"
width="{{ width }}" width="{{ exif['File']['Width']['raw'] }}"
height="{{ height }}" height="{{ exif['File']['Height']['raw'] }}"
/> />
</div> </div>
@ -121,18 +121,9 @@
<td>Upload date</td> <td>Upload date</td>
<td><span class="time">{{ image['created_at'] }}</span></td> <td><span class="time">{{ image['created_at'] }}</span></td>
</tr> </tr>
<tr>
<td>Filename</td>
<td>{{ image['file_name'] }}</td>
</tr>
<tr>
<td>File size</td>
<td>{{ file_size }}</td>
</tr>
</table> </table>
</div> </div>
</div> </div>
{% if exif %}
{% for tag in exif %} {% for tag in exif %}
<div class="image-info"> <div class="image-info">
{% if tag == 'Photographer' %} {% if tag == 'Photographer' %}
@ -163,13 +154,6 @@
</svg> </svg>
<h2>Photo</h2> <h2>Photo</h2>
</div> </div>
{% elif tag == 'File' %}
<div class="image-info__header">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -2 24 24" fill="currentColor">
<path d="M10.298 2H3a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V4.961L10.298 2zM3 0h8l5 4v13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"></path>
</svg>
<h2>File</h2>
</div>
{% else %} {% else %}
<div class="image-info__header"> <div class="image-info__header">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24" fill="currentColor">
@ -192,7 +176,7 @@
{% elif exif[tag][subtag]['raw'] %} {% elif exif[tag][subtag]['raw'] %}
<td>{{exif[tag][subtag]['raw']}}</td> <td>{{exif[tag][subtag]['raw']}}</td>
{% else %} {% else %}
<td class="empty-table">sad noises</td> <td class="empty-table">Oops, an error</td>
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
@ -200,7 +184,6 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -170,7 +170,7 @@
overflow: hidden overflow: hidden
table table
margin: 0 0 0 1rem margin: 0
padding: 0 padding: 0
max-width: 100% max-width: 100%
@ -190,7 +190,10 @@
padding: 0.25rem 1rem 0.25rem 0 padding: 0.25rem 1rem 0.25rem 0
width: 50% width: 50%
max-width: 0
overflow: hidden
text-overflow: ellipsis
white-space: nowrap white-space: nowrap
font-size: 1rem font-size: 1rem
@ -264,15 +267,6 @@
.image-wrapper .image-wrapper
padding-bottom: 4rem padding-bottom: 4rem
.image-info__content
table
margin: 0
tr
td:first-child
width: auto
td:last-child
width: 75%
.image-info__header .image-info__header
background-image: none background-image: none
background-color: $black2 background-color: $black2