Compare commits
29 Commits
2c9d4f54e4
...
3e6c235e0b
Author | SHA1 | Date | |
---|---|---|---|
3e6c235e0b | |||
c0a329c950 | |||
8a1c0f0780 | |||
40a4b3e001 | |||
6ed94deb38 | |||
9fa8c6e412 | |||
00270fdc18 | |||
557719baf6 | |||
1bf72fcd34 | |||
b28b6214b0 | |||
47b87835f9 | |||
bb90e192e9 | |||
e40f546137 | |||
4834f8fa6e | |||
0111140907 | |||
8543903730 | |||
a897021d03 | |||
dff8defd13 | |||
860dc2b282 | |||
a45d59c277 | |||
8cc52bdfd5 | |||
b767aa35c0 | |||
c263096242 | |||
36270c21b1 | |||
7ff17d00b2 | |||
7c41436541 | |||
ba384dd41b | |||
fb36633e8c | |||
48c156b8ce |
140
.gitignore
vendored
@ -1,140 +0,0 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
9
LICENSE
@ -1,9 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
40
README.html
Normal file
@ -0,0 +1,40 @@
|
||||
<h1>PhotoReport</h1>
|
||||
<h2>Dépendences</h2>
|
||||
<ul>
|
||||
<li>Python 3</li>
|
||||
</ul>
|
||||
<h2>Installation</h2>
|
||||
<ul>
|
||||
<li>Créer un environnement virtuel python3 (e.g. : <code>virtulenv3 PhotoReport && source bin/activate</code>)</li>
|
||||
<li>Cloner le dépôts (<code>git clone https://git.antoineve.me/PhotoReport/</code>)</li>
|
||||
<li>Installer les dépendances (Note : pour l'instant, version 0.1, ce n'est pas nécessaire)</li>
|
||||
</ul>
|
||||
<h2>Utilisation en ligne de commande</h2>
|
||||
<h3>exv.py</h3>
|
||||
<p>L'aide est disponible avec <code>./exv.py --help</code>.</p>
|
||||
<p>Exemple d'utilisation :</p>
|
||||
<pre><code>./exv.py -R -x jpg ~/Nextcloud/Photos/2019/
|
||||
</code></pre>
|
||||
<p>Exemple de sortie : <a href="https://git.antoineve.me/PhotoReport/tree/sortie_exemple.json">sortie_exemple.json</a></p>
|
||||
<h2>Comment contribuer ?</h2>
|
||||
<ul>
|
||||
<li>Cloner le dépôt : <code>git clone https://git.antoineve.me/PhotoReport/</code></li>
|
||||
<li>Créer une branche : <code>git checkout -b <new_feature></code></li>
|
||||
<li>Coder et commiter : <code>git commit -a -m <description></code></li>
|
||||
<li>Créer le patch : <code>git format-patch $(git merge-base --fork-point master)..<new_feature></code></li>
|
||||
<li>M'envoyer le patch par mail : <a href="mailto:antoine+photoreport@van-elstraete.net">antoine+photoreport@van-elstraete.net</a></li>
|
||||
<li>J'applique le patch avec <code>git am --signoff -k < <new_feature>.patch</code></li>
|
||||
</ul>
|
||||
<h2>BUGS & TODO</h2>
|
||||
<ul>
|
||||
<li>Pour le moment, pas de sortie en PDF</li>
|
||||
<li>La base de donnée de darktable a changé depuis la première écriture du script</li>
|
||||
<li>La vitesse d'obturation n'est pas arrondie, est-ce souhaitable ? (16.666 ≠ 16.666666666666668 ?)</li>
|
||||
</ul>
|
||||
<h2>Features requests et développement prévu</h2>
|
||||
<ul>
|
||||
<li>Créer une option pour filtrer par objectif <strong>-> branche filters/lens</strong></li>
|
||||
<li>Créer une option pour filtrer par boitier <strong>-> branche filters/camera</strong></li>
|
||||
<li>Sortie PDF fonctionnelle <strong>-> branche output/pdf_via_md</strong></li>
|
||||
<li>Créer une option pour une sortie exploitable par LaTeX</li>
|
||||
</ul>
|
44
README.md
@ -1,2 +1,46 @@
|
||||
# PhotoReport
|
||||
|
||||
## Dépendences
|
||||
|
||||
- Python 3
|
||||
|
||||
## Installation
|
||||
|
||||
- Créer un environnement virtuel python3 (e.g. : `virtulenv3 PhotoReport && source bin/activate`)
|
||||
- Cloner le dépôts (`git clone https://git.antoineve.me/PhotoReport/`)
|
||||
- Installer les dépendances (Note : pour l'instant, version 0.1, ce n'est pas nécessaire)
|
||||
|
||||
## Utilisation en ligne de commande
|
||||
|
||||
### exv.py
|
||||
|
||||
L'aide est disponible avec `./exv.py --help`.
|
||||
|
||||
Exemple d'utilisation :
|
||||
|
||||
./exv.py -R -x jpg ~/Nextcloud/Photos/2019/
|
||||
|
||||
Exemple de sortie : [sortie_exemple.json](https://git.antoineve.me/PhotoReport/tree/sortie_exemple.json)
|
||||
|
||||
|
||||
## Comment contribuer ?
|
||||
|
||||
- Cloner le dépôt : `git clone https://git.antoineve.me/PhotoReport/`
|
||||
- Créer une branche : `git checkout -b <new_feature>`
|
||||
- Coder et commiter : `git commit -a -m <description>`
|
||||
- Créer le patch : `git format-patch $(git merge-base --fork-point master)..<new_feature>`
|
||||
- M'envoyer le patch par mail : [antoine+photoreport@van-elstraete.net](mailto:antoine+photoreport@van-elstraete.net)
|
||||
- J'applique le patch avec `git am --signoff -k < <new_feature>.patch`
|
||||
|
||||
## BUGS & TODO
|
||||
|
||||
- Pour le moment, pas de sortie en PDF
|
||||
- La base de donnée de darktable a changé depuis la première écriture du script
|
||||
- La vitesse d'obturation n'est pas arrondie, est-ce souhaitable ? (16.666 ≠ 16.666666666666668 ?)
|
||||
|
||||
## Features requests et développement prévu
|
||||
|
||||
- Créer une option pour filtrer par objectif **-> branche filters/lens**
|
||||
- Créer une option pour filtrer par boitier **-> branche filters/camera**
|
||||
- Sortie PDF fonctionnelle **-> branche output/pdf_via_md**
|
||||
- Créer une option pour une sortie exploitable par LaTeX
|
||||
|
114
dtlib.py
Executable file
@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def extractor(dt_file, start, end):
|
||||
catalog = {}
|
||||
unknow_files = 0
|
||||
conn = sqlite3.connect(dt_file)
|
||||
cursor = conn.cursor()
|
||||
req = "SELECT id, model, lens, exposure, aperture, iso, focal_length, datetime_taken, width, height FROM images;"
|
||||
cursor.execute(req)
|
||||
res = cursor.fetchall()
|
||||
for data in res:
|
||||
if not data[7]:
|
||||
unknow_files += 1
|
||||
else:
|
||||
img_date = datetime.strptime(data[7], "%Y:%m:%d %H:%M:%S")
|
||||
if start <= img_date <= end:
|
||||
catalog.update({
|
||||
data[0]:
|
||||
{
|
||||
'camera': data[1],
|
||||
'lens': data[2],
|
||||
'shutter': float(data[3]*1000),
|
||||
'aperture': round(float(data[4]), 1),
|
||||
'iso': int(data[5]),
|
||||
'focal': float(data[6]),
|
||||
'datetime': img_date,
|
||||
'height': int(data[9]),
|
||||
'width': int(data[8])
|
||||
}
|
||||
})
|
||||
cameras, lenses, focals, apertures, shutter_speeds = {}, {}, {}, {}, {}
|
||||
isos, dimensions, cameras_lenses, dates = {}, {}, {}, {}
|
||||
cameras_list, lenses_list, focals_list, apertures_list, shutter_speeds_list = [], [], [], [], []
|
||||
isos_list, dimensions_list, cameras_lenses_list, dates_list = [], [], [], []
|
||||
for item in catalog.keys():
|
||||
cameras_list.append(catalog[item]['camera'])
|
||||
lenses_list.append(catalog[item]['lens'])
|
||||
if catalog[item]['lens'] and "---" not in catalog[item]['lens']:
|
||||
cameras_lenses_list.append(
|
||||
"{} + {}".format(catalog[item]['camera'], catalog[item]['lens']))
|
||||
focals_list.append(round(float(catalog[item]['focal']), 1))
|
||||
apertures_list.append(catalog[item]['aperture'])
|
||||
shutter_speeds_list.append(catalog[item]['shutter'])
|
||||
isos_list.append(catalog[item]['iso'])
|
||||
dimensions_list.append(
|
||||
round(((catalog[item]['height']*catalog[item]['width'])/10**6), 1))
|
||||
dates_list.append(catalog[item]['datetime'].strftime("%Y%m%d"))
|
||||
for camera in list(set(cameras_list)):
|
||||
if camera:
|
||||
cameras.update({camera: cameras_list.count(camera)})
|
||||
for lens in list(set(lenses_list)):
|
||||
if lens and "---" not in lens:
|
||||
lenses.update({lens: lenses_list.count(lens)})
|
||||
for camera_lense in list(set(cameras_lenses_list)):
|
||||
if camera_lense:
|
||||
cameras_lenses.update(
|
||||
{camera_lense: cameras_lenses_list.count(camera_lense)})
|
||||
for focal in list(set(focals_list)):
|
||||
if focal:
|
||||
focals.update({focal: focals_list.count(focal)})
|
||||
for aperture in list(set(apertures_list)):
|
||||
if aperture:
|
||||
apertures.update({aperture: apertures_list.count(aperture)})
|
||||
for shutter_speed in list(set(shutter_speeds_list)):
|
||||
if shutter_speed:
|
||||
shutter_speeds.update(
|
||||
{shutter_speed: shutter_speeds_list.count(shutter_speed)})
|
||||
for iso in list(set(isos_list)):
|
||||
if iso:
|
||||
isos.update({iso: isos_list.count(iso)})
|
||||
for dimension in list(set(dimensions_list)):
|
||||
if dimension:
|
||||
dimensions.update({dimension: dimensions_list.count(dimension)})
|
||||
for date in list(set(dates_list)):
|
||||
if date:
|
||||
dates.update({date: dates_list.count(date)})
|
||||
return {
|
||||
"total": len(catalog.keys()),
|
||||
"unknows": unknow_files,
|
||||
"date": dates,
|
||||
"cameras": cameras,
|
||||
"lenses": lenses,
|
||||
"cameras+lenses": cameras_lenses,
|
||||
"focals": focals,
|
||||
"apertures": apertures,
|
||||
"shutter_speeds": shutter_speeds,
|
||||
"isos": isos,
|
||||
"dimensions": dimensions
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
import json
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("infile", help="darktable library")
|
||||
parser.add_argument("-s", "--start-date", help="start date as YYYYMMDD. If omitted, begin of the collection.", const=None)
|
||||
parser.add_argument("-e", "--end-date", help="end date as YYYYMMDD. If omitted, today", const=None)
|
||||
args = parser.parse_args()
|
||||
if not args.start_date:
|
||||
args.start_date = "18250101" # Ok, that's the year of the 1st photography by Nicéphore Niépce. Shoul'd be enough for a start date.
|
||||
if not args.end_date:
|
||||
args.end_date = datetime.strftime(datetime.now(), "%Y%m%d")
|
||||
try:
|
||||
datetime.strptime(args.start_date, "%Y%m%d")
|
||||
datetime.strptime(args.end_date, "%Y%m%d")
|
||||
except ValueError:
|
||||
print("Date must be YYYYMMDD.")
|
||||
exit()
|
||||
print(json.dumps(extractor(args.infile, datetime.strptime(args.start_date, "%Y%m%d"), datetime.strptime(args.end_date, "%Y%m%d")), indent=4, sort_keys=True))
|
189
exv.py
Executable file
@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import piexif
|
||||
from datetime import datetime
|
||||
import json
|
||||
from os.path import isfile, isdir
|
||||
from os import walk
|
||||
|
||||
TYPES = [
|
||||
"JPEG",
|
||||
"JPG",
|
||||
"CR2",
|
||||
"TIFF",
|
||||
"TIF",
|
||||
"DNG",
|
||||
"NEF",
|
||||
]
|
||||
|
||||
def extractor(input_files, start, end, recursive, extensions):
|
||||
if extensions:
|
||||
extensions = [ext.upper() for ext in extensions]
|
||||
if not isinstance(input_files, type(list())):
|
||||
raise ValueError("Input files must be a list.")
|
||||
exif_dict_list = []
|
||||
files = []
|
||||
usefull_exif = {
|
||||
"Model": "",
|
||||
"LensModel": "",
|
||||
"FocalLength": 0,
|
||||
"ApertureValue": 0,
|
||||
"ExposureTime": 0,
|
||||
"ISOSpeedRatings": 0,
|
||||
"PixelXDimension": 0,
|
||||
"PixelYDimension": 0,
|
||||
"ImageWidth": 0,
|
||||
"ImageLength": 0,
|
||||
"Pixels": None,
|
||||
"Dimension": 0,
|
||||
"DateTime": None,
|
||||
}
|
||||
for item in input_files:
|
||||
if isdir(item):
|
||||
for (dirpath, dirnames, filenames) in walk(item):
|
||||
for filename in filenames:
|
||||
if filename.split(".")[-1].upper() in TYPES:
|
||||
files.append("{}/{}".format(dirpath, filename))
|
||||
if not recursive:
|
||||
break
|
||||
else:
|
||||
files.append(item)
|
||||
if extensions:
|
||||
files_with_ext_filter = []
|
||||
for filename in files:
|
||||
if filename.split(".")[-1].upper() in extensions:
|
||||
files_with_ext_filter.append("{}".format(filename))
|
||||
input_files = files_with_ext_filter
|
||||
else:
|
||||
input_files = files
|
||||
|
||||
for input_file in input_files:
|
||||
exif_dict = usefull_exif.copy()
|
||||
if not isfile(input_file):
|
||||
raise ValueError("{} doesn't exist here.".format(input_file))
|
||||
exif_dict.update({'file': input_file})
|
||||
exif_tags = piexif.load(input_file)
|
||||
if 0x0110 in exif_tags['0th']:
|
||||
exif_dict.update({'Model':
|
||||
exif_tags['0th'][0x0110].decode()})
|
||||
if 0xa434 in exif_tags['Exif']:
|
||||
exif_dict.update({'LensModel':
|
||||
exif_tags['Exif'][0xa434].decode().split("\x00")[0]})
|
||||
if 0x920a in exif_tags['Exif']:
|
||||
exif_dict.update({'FocalLength':
|
||||
float(exif_tags['Exif'][0x920a][0] / exif_tags['Exif'][0x920a][1])})
|
||||
if 0x829d in exif_tags['Exif']:
|
||||
exif_dict.update({'ApertureValue':
|
||||
float(exif_tags['Exif'][0x829d][0]/exif_tags['Exif'][0x829d][1])})
|
||||
if 0x829a in exif_tags['Exif']:
|
||||
exif_dict.update({'ExposureTime':
|
||||
float(exif_tags['Exif'][0x829a][0]/exif_tags['Exif'][0x829a][1])})
|
||||
if 0x8827 in exif_tags['Exif']:
|
||||
exif_dict.update({'ISOSpeedRatings':
|
||||
int(exif_tags['Exif'][0x8827])})
|
||||
if 0xa002 in exif_tags['Exif'] and 0xa003 in exif_tags['Exif']:
|
||||
exif_dict.update({'Pixels':
|
||||
int(exif_tags['Exif'][0xa002]) * int(exif_tags['Exif'][0xa003])})
|
||||
elif 0x0100 in exif_tags['0th'] and 0x0101 in exif_tags['0th']:
|
||||
exif_dict.update({'Pixels':
|
||||
int(exif_tags['0th'][0x0100]) * int(exif_tags['0th'][0x0101])})
|
||||
if 0x9003 in exif_tags['Exif']:
|
||||
exif_dict['DateTime'] = datetime.strptime(
|
||||
exif_tags['Exif'][0x9003].decode(), "%Y:%m:%d %H:%M:%S")
|
||||
elif 0x9004 in exif_tags['Exif']:
|
||||
exif_dict['DateTime'] = datetime.strptime(
|
||||
exif_tags['Exif'][0x9004].decode(), "%Y:%m:%d %H:%M:%S")
|
||||
elif 0x0132 in exif_tags['0th']:
|
||||
exif_dict['DateTime'] = datetime.strptime(
|
||||
exif_tags['0th'][0x0132].decode(), "%Y:%m:%d %H:%M:%S")
|
||||
if exif_dict['Pixels']:
|
||||
exif_dict.update({'Dimension': round((exif_dict['Pixels']/10**6), 1)})
|
||||
exif_dict_list.append(exif_dict.copy())
|
||||
cameras, lenses, focals, apertures, exposures = {}, {}, {}, {}, {}
|
||||
isos, dimensions, cameras_lenses, dates = {}, {}, {}, {}
|
||||
cameras_list, lenses_list, focals_list, apertures_list, exposures_list = [], [], [], [], []
|
||||
isos_list, dimensions_list, cameras_lenses_list, dates_list = [], [], [], []
|
||||
for data in exif_dict_list:
|
||||
if data['DateTime'] and start <= data['DateTime'] <= end:
|
||||
cameras_list.append(data['Model'])
|
||||
lenses_list.append(data['LensModel'])
|
||||
focals_list.append(data['FocalLength'])
|
||||
apertures_list.append(data['ApertureValue'])
|
||||
exposures_list.append(data['ExposureTime'])
|
||||
isos_list.append(data['ISOSpeedRatings'])
|
||||
dimensions_list.append(data['Dimension'])
|
||||
if data['LensModel']:
|
||||
cameras_lenses_list.append(
|
||||
"{} + {}".format(data['Model'], data['LensModel']))
|
||||
dates_list.append(data['DateTime'].strftime("%Y%m%d"))
|
||||
for camera in list(set(cameras_list)):
|
||||
if camera:
|
||||
cameras.update({camera: cameras_list.count(camera)})
|
||||
for lens in list(set(lenses_list)):
|
||||
if lens:
|
||||
lenses.update({lens: lenses_list.count(lens)})
|
||||
for camera_lens in list(set(cameras_lenses_list)):
|
||||
if camera_lens:
|
||||
cameras_lenses.update(
|
||||
{camera_lens: cameras_lenses_list.count(camera_lens)})
|
||||
for focal in list(set(focals_list)):
|
||||
if focal:
|
||||
focals.update({focal: focals_list.count(focal)})
|
||||
for aperture in list(set(apertures_list)):
|
||||
if aperture:
|
||||
apertures.update({aperture: apertures_list.count(aperture)})
|
||||
for exposure in list(set(exposures_list)):
|
||||
if exposure:
|
||||
exposures.update(
|
||||
{float(exposure*1000): exposures_list.count(exposure)})
|
||||
for iso in list(set(isos_list)):
|
||||
if iso:
|
||||
isos.update({iso: isos_list.count(iso)})
|
||||
for dimension in list(set(dimensions_list)):
|
||||
if dimension:
|
||||
dimensions.update({dimension: dimensions_list.count(dimension)})
|
||||
for date in list(set(dates_list)):
|
||||
if date:
|
||||
dates.update({date: dates_list.count(date)})
|
||||
return {
|
||||
"total": len(dimensions_list),
|
||||
"date": dates,
|
||||
"cameras": cameras,
|
||||
"lenses": lenses,
|
||||
"cameras+lenses": cameras_lenses,
|
||||
"focals": focals,
|
||||
"apertures": apertures,
|
||||
"shutter_speeds": exposures,
|
||||
"isos": isos,
|
||||
"dimensions": dimensions
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"infile", help="input file (EXV, JPG, CR2, ...). Can be more than one.", nargs='+')
|
||||
parser.add_argument("-s", "--start-date", help="start date as YYYYMMDD. If omitted, begin of the collection.", const=None)
|
||||
parser.add_argument("-e", "--end-date", help="end date as YYYYMMDD. If omitted, today", const=None)
|
||||
parser.add_argument("-R", "--recursive", help="Walk throught directory to find all files", action='store_true')
|
||||
parser.add_argument("-x", "--extension", help="Read files with this extension only (can be specified more than one time)", action='append')
|
||||
args = parser.parse_args()
|
||||
if not args.recursive:
|
||||
recursive = False
|
||||
if not args.extension:
|
||||
extensions = None
|
||||
else:
|
||||
extensions = args.extension
|
||||
if not args.start_date:
|
||||
args.start_date = "18250101" # Ok, that's the year of the 1st photography by Nicéphore Niépce. Shoul'd be enough for a start date.
|
||||
if not args.end_date:
|
||||
args.end_date = datetime.strftime(datetime.now(), "%Y%m%d")
|
||||
try:
|
||||
datetime.strptime(args.start_date, "%Y%m%d")
|
||||
datetime.strptime(args.end_date, "%Y%m%d")
|
||||
except ValueError:
|
||||
print("Date must be YYYYMMDD.")
|
||||
exit()
|
||||
recursive = args.recursive
|
||||
print(json.dumps(extractor(args.infile, datetime.strptime(args.start_date, "%Y%m%d"), datetime.strptime(args.end_date, "%Y%m%d"), recursive, extensions), indent=2, sort_keys=True))
|
BIN
helpers/cameras/Canon_EOS_1100D.jpg
Normal file
After Width: | Height: | Size: 212 KiB |
BIN
helpers/cameras/ILCE-6500.jpg
Normal file
After Width: | Height: | Size: 258 KiB |
BIN
helpers/cameras/ILCE-QX1.jpg
Normal file
After Width: | Height: | Size: 68 KiB |
1
helpers/dictionary.json
Normal file
@ -0,0 +1 @@
|
||||
{"camera": {"ILCE-QX1": {"brand": "Sony", "name": "QX1"}, "ILCE-6500": {"brand": "Sony", "name": "\u03b16500"}, "ILCE-5100": {"brand": "Sony", "name": "\u03b15100"}, "ILCA-77M2": {"brand": "Sony", "name": "\u03b177 II"}, "DSC-RX100M3": {"brand": "Sony", "name": "RX100 III"}, "Canon EOS 1100D": {"brand": "Canon", "name": "EOS 1100D"}, "FIG-LX1": {"brand": "Huawei", "name": "P Smart"}}, "lens": {"E 55-210mm F4.5-6.3 OSS": {"brand": "Sony", "name": "E 55-210mm F4.5-6.3 OSS"}, "10-20mm F3.5": {"brand": "Sigma", "name": "10-20mm F3.5 EX DC HSM"}, "105mm F2.8": {"brand": "Sigma", "name": "MACRO 105mm F2.8 EX DG OS HSM"}, "50mm F1.4": {"brand": "Sony", "name": "50mm F1.4"}, "24-70mm F1.8-2.8": {"brand": "ZEISS", "name": "Vario-Sonnar T* 24-70 mm F1.8-2.8"}, "70-300mm F4.5-5.6 G SSM": {"brand": "Sony", "name": "70\u2013300 mm F4.5\u20135.6 G SSM"}, "E PZ 18-105mm F4 G OSS": {"brand": "Sony", "name": "E PZ 18\u2013105 mm F4 G OSS"}, "FE 90mm F2.8 Macro G OSS": {"brand": "Sony", "name": "FE 90 mm F2.8 Macro G OSS"}, "Canon EF 50mm f/1.8 STM": {"brand": "Canon", "name": "Canon EF 50mm f/1.8 STM"}, "Canon EF-S 18-55mm f/3.5-5.6 IS II": {"brand": "Canon", "name": "EF-S 18-55mm f/3.5-5.6 IS II"}}}
|
BIN
helpers/lenses/10-20mm_F3.5.jpg
Normal file
After Width: | Height: | Size: 199 KiB |
BIN
helpers/lenses/105mm_F2.8.jpg
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
helpers/lenses/50mm_F1.4.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
helpers/lenses/70-300mm_F4.5-5.6_G_SSM.jpg
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
helpers/lenses/Canon_EF-S_18-55mm_f_3.5-5.6_IS_II.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
helpers/lenses/Canon_EF_50mm_f_1.8_STM.jpg
Normal file
After Width: | Height: | Size: 139 KiB |
BIN
helpers/lenses/E_55-210mm_F4.5-6.3_OSS.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
helpers/lenses/E_PZ_18-105mm_F4_G_OSS.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
helpers/lenses/FE_90mm_F2.8_Macro_G_OSS.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
187
locales/fr_FR/LC_MESSAGES/report.po
Normal file
@ -0,0 +1,187 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PhotoReport 0.1\n"
|
||||
"POT-Creation-Date: 2018-11-13 21:30+0100\n"
|
||||
"PO-Revision-Date: 2018-11-13 21:30+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.2\n"
|
||||
"X-Poedit-Basepath: ../../..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
"X-Poedit-SearchPath-0: report.py\n"
|
||||
|
||||
#: report.py:81
|
||||
msgid "Statistical photo report"
|
||||
msgstr "Rapport statistique des photos"
|
||||
|
||||
#: report.py:95
|
||||
msgid "Pictures informations"
|
||||
msgstr "Informations sur les photos"
|
||||
|
||||
#: report.py:104
|
||||
msgid "The set contains {} pictures, "
|
||||
msgstr "La collection comprend {} images, "
|
||||
|
||||
#: report.py:106
|
||||
msgid "The set contains only one picture, "
|
||||
msgstr "La collection comprend une seule image, "
|
||||
|
||||
#: report.py:108
|
||||
msgid "The set is empty. "
|
||||
msgstr "La collection est vide. "
|
||||
|
||||
#: report.py:110
|
||||
msgid "taken from {} to {}. "
|
||||
msgstr "prisent du {} au {}. "
|
||||
|
||||
#: report.py:113
|
||||
msgid "The most productive day was {} ({} shots). "
|
||||
msgstr "La journée la plus productive fut le {} ({} prises de vue). "
|
||||
|
||||
#: report.py:117
|
||||
msgid "taken on {}. "
|
||||
msgstr "prise le {}. "
|
||||
|
||||
#: report.py:120
|
||||
#, python-brace-format
|
||||
msgid "Pictures sizes are from {1}{0}Mpx to {2}{0}Mpx, mostly {3}{0}Mpx. "
|
||||
msgstr ""
|
||||
"La taille des images varie de {1}{0}Mpx à {2}{0}Mpx, pour la plupart à {3}{0}"
|
||||
"Mpx. "
|
||||
|
||||
#: report.py:126
|
||||
msgid "Picture size is {}{}Mpx. "
|
||||
msgstr "La dimension de l'image est de {}{}Mpx. "
|
||||
|
||||
#: report.py:140
|
||||
msgid "Distribution of shots by date{}"
|
||||
msgstr "Répartition des photos par dates{}"
|
||||
|
||||
#: report.py:140 report.py:253 report.py:277 report.py:308 report.py:332
|
||||
msgid " : top 20"
|
||||
msgstr " : top 20"
|
||||
|
||||
#: report.py:141 report.py:252 report.py:276 report.py:307 report.py:331
|
||||
msgid "Number of shots"
|
||||
msgstr "Nombre de prises de vue"
|
||||
|
||||
#: report.py:156
|
||||
msgid "Cameras & lenses"
|
||||
msgstr "Appareils photo & objectifs"
|
||||
|
||||
#: report.py:164
|
||||
msgid "The most commonly used couple is {} with {}. "
|
||||
msgstr "Le couple le plus utilisé est le {} avec le {}. "
|
||||
|
||||
#: report.py:169
|
||||
msgid "There are {} lenses, the most used is the {}. "
|
||||
msgstr "Il y a {} objectifs, le plus utilisé est le {}. "
|
||||
|
||||
#: report.py:173
|
||||
msgid "The lens is a {}. "
|
||||
msgstr "L'objectif est un {}. "
|
||||
|
||||
#: report.py:175
|
||||
msgid "The favorite aperture is {}{}, "
|
||||
msgstr "L'ouverture favorite est {}{}, "
|
||||
|
||||
#: report.py:176 report.py:275 report.py:283
|
||||
msgid "F"
|
||||
msgstr "f/"
|
||||
|
||||
#: report.py:178
|
||||
msgid "and the favorite focal lenght is {}mm. "
|
||||
msgstr "et la longueur focal favorite est {} mm. "
|
||||
|
||||
#: report.py:182
|
||||
msgid "There are {} cameras, the mose used is the {}. "
|
||||
msgstr "Il y a {} appareils photos, le plus utilisé est le {}. "
|
||||
|
||||
#: report.py:186
|
||||
msgid "The camera is a {}. "
|
||||
msgstr "L'appareil photo est un {}. "
|
||||
|
||||
#: report.py:188
|
||||
msgid " The favorite ISO is {}. "
|
||||
msgstr " La sensibilité favorite est de {} ISO. "
|
||||
|
||||
#: report.py:210
|
||||
msgid "Cameras"
|
||||
msgstr "Appareils photo"
|
||||
|
||||
#: report.py:224
|
||||
msgid "Lenses"
|
||||
msgstr "Objectifs"
|
||||
|
||||
#: report.py:233
|
||||
msgid "Focal lengths & apertures"
|
||||
msgstr "Longueurs focales & ouvertures"
|
||||
|
||||
#: report.py:236
|
||||
msgid "Focal lenghts"
|
||||
msgstr "Longueurs focales"
|
||||
|
||||
#: report.py:253
|
||||
msgid "Focals{}"
|
||||
msgstr "Longueurs focales{}"
|
||||
|
||||
#: report.py:258
|
||||
msgid "Only one focal lenght is used : {}mm. "
|
||||
msgstr "Une seule longueur focal est utilisée : {}mm. "
|
||||
|
||||
#: report.py:260
|
||||
msgid "Apertures"
|
||||
msgstr "Ouvertures"
|
||||
|
||||
#: report.py:277
|
||||
msgid "Apertures{}"
|
||||
msgstr "Ouvertures{}"
|
||||
|
||||
#: report.py:282
|
||||
msgid "Only one aperture is used : {}{}. "
|
||||
msgstr "Une seule ouverture est utilisée : {}{}. "
|
||||
|
||||
#: report.py:289
|
||||
msgid "ISO sensitivities & shutter speeds"
|
||||
msgstr "Sensibilités ISO & vitesses d'obturation"
|
||||
|
||||
#: report.py:291
|
||||
msgid "ISO sensitivities"
|
||||
msgstr "Sensibilités ISO"
|
||||
|
||||
#: report.py:308
|
||||
msgid "ISO sensibilities{}"
|
||||
msgstr "Sensibilités ISO{}"
|
||||
|
||||
#: report.py:313
|
||||
msgid "All shots at the same sensitivity : ISO {} "
|
||||
msgstr "Toutes les photos ont été prise avec la même sensiblité : ISO {}. "
|
||||
|
||||
#: report.py:315
|
||||
msgid "Shutter speeds"
|
||||
msgstr "Vitesses d'obturation"
|
||||
|
||||
#: report.py:332
|
||||
msgid "Shutter speeds{}"
|
||||
msgstr "Vitesses d'obturation{}"
|
||||
|
||||
#: report.py:337
|
||||
msgid "All shots at the same speed : {}. "
|
||||
msgstr "Toutes les photos ont été prises à la même vitesse : {}. "
|
||||
|
||||
#~ msgid ""
|
||||
#~ "There are {} lenses, the most used is the {}. The favorite aperture is {}"
|
||||
#~ "{}, "
|
||||
#~ msgstr ""
|
||||
#~ "Il y a {} objectifs, le plus utilisé est le {}. L'ouverture favorite est "
|
||||
#~ "{}{}, "
|
||||
|
||||
#~ msgid "Apertures : Top 20"
|
||||
#~ msgstr "Ouvertures : top 20"
|
||||
|
||||
#~ msgid "Great !"
|
||||
#~ msgstr "Super !"
|
116
lrcat.py
Executable file
@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def extractor(lrcat_file, start, end):
|
||||
catalog, camera, lens = {}, {}, {}
|
||||
conn = sqlite3.connect(lrcat_file)
|
||||
cursor = conn.cursor()
|
||||
req = "SELECT id_local, value FROM AgInternedExifCameraModel"
|
||||
cursor.execute(req)
|
||||
res = cursor.fetchall()
|
||||
for data in res:
|
||||
camera.update({data[0]: data[1]})
|
||||
req = "SELECT id_local, value FROM AgInternedExifLens"
|
||||
cursor.execute(req)
|
||||
res = cursor.fetchall()
|
||||
for data in res:
|
||||
lens.update({data[0]: data[1]})
|
||||
req = "SELECT image, focalLength, aperture, isoSpeedRating, shutterSpeed, cameraModelRef, lensRef FROM AgHarvestedExifMetadata"
|
||||
cursor.execute(req)
|
||||
res = cursor.fetchall()
|
||||
for data in res:
|
||||
if data[2]:
|
||||
aperture = round(2**(data[2]/2), 1)
|
||||
if data[4]:
|
||||
shutter = round(1/(2**data[4])*1000, 4)
|
||||
catalog.update({int(data[0]): {'focal': data[1], 'aperture': aperture, 'iso': int(
|
||||
data[3]), 'shutter': shutter, 'camera': camera[data[5]], 'lens': lens[data[6]]}})
|
||||
req = "SELECT id_local, captureTime, fileHeight, fileWidth FROM Adobe_images"
|
||||
cursor.execute(req)
|
||||
res = cursor.fetchall()
|
||||
for data in res:
|
||||
catalog[int(data[0])].update(
|
||||
{'datetime': datetime.strptime(data[1], "%Y-%m-%dT%H:%M:%S"), 'height': int(data[2]), 'width': int(data[3])})
|
||||
cameras, lenses, focals, apertures, shutter_speeds = {}, {}, {}, {}, {}
|
||||
isos, dimensions, cameras_lenses, dates = {}, {}, {}, {}
|
||||
cameras_list, lenses_list, focals_list, apertures_list, shutter_speeds_list = [], [], [], [], []
|
||||
isos_list, dimensions_list, cameras_lenses_list, dates_list = [], [], [], []
|
||||
for item in catalog.keys():
|
||||
if start <= catalog[item]['datetime'] <= end:
|
||||
cameras_list.append(catalog[item]['camera'])
|
||||
lenses_list.append(catalog[item]['lens'])
|
||||
if catalog[item]['lens'] and "---" not in catalog[item]['lens']:
|
||||
cameras_lenses_list.append(
|
||||
"{} + {}".format(catalog[item]['camera'], catalog[item]['lens']))
|
||||
focals_list.append(catalog[item]['focal'])
|
||||
apertures_list.append(catalog[item]['aperture'])
|
||||
shutter_speeds_list.append(catalog[item]['shutter'])
|
||||
isos_list.append(catalog[item]['iso'])
|
||||
dimensions_list.append(
|
||||
round(((catalog[item]['height']*catalog[item]['width'])/10**6), 1))
|
||||
dates_list.append(catalog[item]['datetime'].strftime("%Y%m%d"))
|
||||
for camera in list(set(cameras_list)):
|
||||
if camera:
|
||||
cameras.update({camera: cameras_list.count(camera)})
|
||||
for lens in list(set(lenses_list)):
|
||||
if lens and "---" not in lens:
|
||||
lenses.update({lens: lenses_list.count(lens)})
|
||||
for camera_lense in list(set(cameras_lenses_list)):
|
||||
if camera_lense:
|
||||
cameras_lenses.update(
|
||||
{camera_lense: cameras_lenses_list.count(camera_lense)})
|
||||
for focal in list(set(focals_list)):
|
||||
if focal:
|
||||
focals.update({focal: focals_list.count(focal)})
|
||||
for aperture in list(set(apertures_list)):
|
||||
if aperture:
|
||||
apertures.update({aperture: apertures_list.count(aperture)})
|
||||
for shutter_speed in list(set(shutter_speeds_list)):
|
||||
if shutter_speed:
|
||||
shutter_speeds.update(
|
||||
{shutter_speed: shutter_speeds_list.count(shutter_speed)})
|
||||
for iso in list(set(isos_list)):
|
||||
if iso:
|
||||
isos.update({iso: isos_list.count(iso)})
|
||||
for dimension in list(set(dimensions_list)):
|
||||
if dimension:
|
||||
dimensions.update({dimension: dimensions_list.count(dimension)})
|
||||
for date in list(set(dates_list)):
|
||||
if date:
|
||||
dates.update({date: dates_list.count(date)})
|
||||
return {
|
||||
"total": len(dimensions_list),
|
||||
"date": dates,
|
||||
"cameras": cameras,
|
||||
"lenses": lenses,
|
||||
"cameras+lenses": cameras_lenses,
|
||||
"focals": focals,
|
||||
"apertures": apertures,
|
||||
"shutter_speeds": shutter_speeds,
|
||||
"isos": isos,
|
||||
"dimensions": dimensions
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
import json
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("infile", help="lrcat file")
|
||||
parser.add_argument("-s", "--start-date", help="start date as YYYYMMDD. If omitted, begin of the collection.", const=None)
|
||||
parser.add_argument("-e", "--end-date", help="end date as YYYYMMDD. If omitted, today", const=None)
|
||||
args = parser.parse_args()
|
||||
if not args.start_date:
|
||||
args.start_date = "18250101" # Ok, that's the year of the 1st photography by Nicéphore Niépce. Shoul'd be enough for a start date.
|
||||
if not args.end_date:
|
||||
args.end_date = datetime.strftime(datetime.now(), "%Y%m%d")
|
||||
try:
|
||||
datetime.strptime(args.start_date, "%Y%m%d")
|
||||
datetime.strptime(args.end_date, "%Y%m%d")
|
||||
except ValueError:
|
||||
print("Date must be YYYYMMDD.")
|
||||
exit()
|
||||
print(json.dumps(extractor(args.infile, datetime.strptime(args.start_date, "%Y%m%d"), datetime.strptime(args.end_date, "%Y%m%d")), indent=4, sort_keys=True))
|
400
report.py
Executable file
@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
import json
|
||||
import uuid
|
||||
import gettext
|
||||
from datetime import date, datetime, timedelta
|
||||
from babel.dates import format_date
|
||||
import locale
|
||||
from os.path import isfile
|
||||
from tempfile import mkdtemp
|
||||
from shutil import copyfile, rmtree
|
||||
from subprocess import run, CalledProcessError
|
||||
|
||||
|
||||
def i18n(language):
|
||||
try:
|
||||
lang = gettext.translation(
|
||||
'report', localedir='locales', languages=[language])
|
||||
lang.install()
|
||||
except FileNotFoundError:
|
||||
gettext.install('report')
|
||||
|
||||
|
||||
def bar(data, filename):
|
||||
'''
|
||||
Draw a bar chart from a documented JSON
|
||||
'''
|
||||
font = {'fontname': 'DejaVu Sans', 'size': 10, 'weight': 'normal'}
|
||||
x_location = np.arange(len(data['data']))
|
||||
fig = plt.figure(figsize=(data['width']/2.54, data['height']/2.54))
|
||||
bars = plt.bar(x_location, data['data'],
|
||||
data['bar_width'], color=data['bar_color'])
|
||||
plt.ylabel(data['Y_label'], **font, fontsize='x-small')
|
||||
plt.title(data['title'], **font)
|
||||
plt.xticks(x_location, data['data_labels'], **font, fontsize='x-small', rotation=45)
|
||||
plt.yticks(**font, fontsize='x-small')
|
||||
plt.ylim(0, max(data['data'])*1.15)
|
||||
for bar in bars:
|
||||
height = bar.get_height()
|
||||
plt.text(bar.get_x() + bar.get_width()/2, height+(max(data['data'])/60), "{}\n({}%)".format(int(height), float(round(height/sum(data['data'])*100, 1))), ha='center', va='bottom', **font, fontsize='xx-small')
|
||||
plt.tight_layout()
|
||||
plt.savefig("{}".format(filename), dpi=300)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def pie(data, filename):
|
||||
'''
|
||||
Draw a pie chart
|
||||
'''
|
||||
font = {'fontname': 'DejaVu Sans', 'size': 10, 'weight': 'normal'}
|
||||
fig = plt.figure(figsize=(data['width']/2.54, data['height']/2.54))
|
||||
chart, texts, autotexts = plt.pie(data['parts'], labels=data['labels'],
|
||||
colors=data['colors'], autopct='%1.1f%%', explode=data['explode'], startangle=180)
|
||||
plt.title(data['title'], **font)
|
||||
for text in texts:
|
||||
text.set_size('x-small')
|
||||
for text in autotexts:
|
||||
text.set_size('xx-small')
|
||||
plt.savefig("{}".format(filename), dpi=300)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def format_shutter_speed(speed):
|
||||
speed = speed/1000
|
||||
if 1/speed >= 4:
|
||||
shutter_speed = "1/{}".format(int(round(1/speed, 0)))
|
||||
else:
|
||||
if speed < 60:
|
||||
shutter_speed = "{}'".format(round(speed, 3))
|
||||
else:
|
||||
speed_minutes = speed//60
|
||||
speed_seconds = speed%60
|
||||
shutter_speed = "{}\"{}'".format(speed_minutes, speed_seconds)
|
||||
return shutter_speed
|
||||
|
||||
|
||||
def title():
|
||||
'''
|
||||
Juste the title
|
||||
'''
|
||||
text = _("Statistical photo report")
|
||||
text += "\n"
|
||||
text += "#"*80
|
||||
text += "\n\n"
|
||||
return text
|
||||
|
||||
|
||||
def general(informations, language, colors, temp_dir):
|
||||
'''
|
||||
General informations. Take a dict() in argument.
|
||||
'''
|
||||
dimensions = []
|
||||
for dimension in informations['dimensions'].keys():
|
||||
dimensions.append(float(dimension))
|
||||
text = _("Pictures informations")
|
||||
text += "\n"
|
||||
text += "="*80
|
||||
text += "\n"
|
||||
informations_total = informations['total']
|
||||
start_date = datetime.strptime(sorted(informations['date'])[0], "%Y%m%d").date()
|
||||
end_date = datetime.strptime(sorted(informations['date'])[-1], "%Y%m%d").date()
|
||||
most_productive = datetime.strptime(sorted(informations['date'], key=informations['date'].get)[-1], "%Y%m%d").date()
|
||||
if informations_total > 1:
|
||||
text += _("The set contains {} pictures, ").format(informations['total'])
|
||||
elif informations_total == 1:
|
||||
text += _("The set contains only one picture, ").format(informations['total'])
|
||||
else:
|
||||
text += _("The set is empty. ")
|
||||
if start_date != end_date:
|
||||
text += _("taken from {} to {}. ").format(
|
||||
format_date(start_date, locale=language, format="medium"),
|
||||
format_date(end_date, locale=language, format="medium"))
|
||||
text += _("The most productive day was {} ({} shots). ").format(
|
||||
format_date(most_productive, locale=language, format="long"),
|
||||
informations['date'][sorted(informations['date'], key=informations['date'].get)[-1]])
|
||||
else:
|
||||
text += _("taken on {}. ").format(start_date)
|
||||
text += "\n\n"
|
||||
if informations_total > 1:
|
||||
text += _("Pictures sizes are from {1}{0}Mpx to {2}{0}Mpx, mostly {3}{0}Mpx. ").format(
|
||||
b'\xc2\xa0'.decode(),
|
||||
sorted(dimensions)[0],
|
||||
sorted(dimensions)[-1],
|
||||
sorted(informations['dimensions'], key=informations['dimensions'].get)[-1])
|
||||
else:
|
||||
text += _("Picture size is {}{}Mpx. ").format(sorted(dimensions)[0], b'\xc2\xa0'.decode())
|
||||
text += "\n\n"
|
||||
if start_date != end_date:
|
||||
dates = {datetime.strptime(k, "%Y%m%d"):int(v) for k,v in informations['date'].items()}
|
||||
top_date = sorted(dates, key=dates.get, reverse=True)[0:20]
|
||||
new_date = {}
|
||||
for date in top_date:
|
||||
new_date.update({date: dates[date]})
|
||||
bar(
|
||||
{
|
||||
'width': 16,
|
||||
'height': 12,
|
||||
'data': [new_date[date] for date in sorted(new_date)],
|
||||
'data_labels': [format_date(date, locale=language, format="short") for date in sorted(new_date)],
|
||||
'title': _("Distribution of shots by date{}").format(_(" : top 20") if len(top_date) == 20 else ""),
|
||||
'Y_label': _("Number of shots"),
|
||||
'bar_color': colors[2],
|
||||
'bar_width': 0.8
|
||||
},
|
||||
temp_dir + "/picnb_datetime.pdf")
|
||||
text += ".. figure:: picnb_datetime.pdf\n\t:width: 16cm\n\n"
|
||||
return text
|
||||
|
||||
|
||||
def camera_and_lens(informations, language, colors, temp_dir):
|
||||
'''
|
||||
Informations about cameras and lenses used.
|
||||
'''
|
||||
image1, image2 = None, None
|
||||
with open("helpers/dictionary.json") as file:
|
||||
helper = json.load(file)
|
||||
text = _("Cameras & lenses")
|
||||
text += "\n"
|
||||
text += "="*80
|
||||
text += "\n\n"
|
||||
top_couple = sorted(informations['cameras+lenses'], key=informations['cameras+lenses'].get)[-1].split(" + ")
|
||||
top_couple_camera, top_couple_lens = top_couple[0], top_couple[1]
|
||||
top_lens = sorted(informations['lenses'], key=informations['lenses'].get)[-1]
|
||||
top_camera = sorted(informations['cameras'], key=informations['cameras'].get)[-1]
|
||||
text += _("The most commonly used couple is {} with {}. ").format(
|
||||
(helper['camera'][top_couple_camera]['brand'] + " " + helper['camera'][top_couple_camera]['name']) if top_couple_camera in helper['camera'] else top_couple_camera,
|
||||
(helper['lens'][top_couple_lens]['brand'] + " " + helper['lens'][top_couple_lens]['name']) if top_couple_lens in helper['lens'] else top_couple_lens,)
|
||||
text += "\n\n"
|
||||
if len(informations['lenses']) > 1:
|
||||
text += _("There are {} lenses, the most used is the {}. ").format(
|
||||
len(informations['lenses']),
|
||||
(helper['lens'][top_lens]['brand'] + " " + helper['lens'][top_lens]['name']) if top_lens in helper['lens'] else top_lens)
|
||||
else:
|
||||
text += _("The lens is a {}. ").format(
|
||||
(helper['lens'][top_lens]['brand'] + " " + helper['lens'][top_lens]['name']) if top_lens in helper['lens'] else top_lens)
|
||||
text += _("The favorite aperture is {}{}, ").format(
|
||||
_("F"),
|
||||
sorted(informations['apertures'], key=informations['apertures'].get)[-1])
|
||||
text += _("and the favorite focal lenght is {}mm. ").format(
|
||||
sorted(informations['focals'], key=informations['focals'].get)[-1])
|
||||
text += "\n\n"
|
||||
if len(informations['cameras']) > 1:
|
||||
text += _("There are {} cameras, the mose used is the {}. ").format(
|
||||
len(informations['cameras']),
|
||||
(helper['camera'][top_camera]['brand'] + " " + helper['camera'][top_camera]['name']) if top_camera in helper['camera'] else top_camera)
|
||||
else:
|
||||
text += _("The camera is a {}. ").format(
|
||||
(helper['camera'][top_camera]['brand'] + " " + helper['camera'][top_camera]['name']) if top_camera in helper['camera'] else top_camera)
|
||||
text += _(" The favorite ISO is {}. ").format(
|
||||
sorted(informations['isos'], key=informations['isos'].get)[-1])
|
||||
text += "\n\n"
|
||||
if isfile("helpers/cameras/{}.jpg".format(top_camera.replace(" ", "_").replace("/", "_"))):
|
||||
copyfile(
|
||||
"helpers/cameras/{}.jpg".format(top_camera.replace(" ", "_").replace("/", "_")),
|
||||
"{}/{}.jpg".format(temp_dir, top_camera.replace(" ", "_").replace("/", "_")))
|
||||
image1 = "{}/{}.jpg".format(temp_dir, top_camera.replace(" ", "_").replace("/", "_"))
|
||||
if isfile("helpers/lenses/{}.jpg".format(top_lens.replace(" ", "_").replace("/", "_"))):
|
||||
copyfile(
|
||||
"helpers/lenses/{}.jpg".format(top_lens.replace(" ", "_").replace("/", "_")),
|
||||
"{}/{}.jpg".format(temp_dir, top_lens.replace(" ", "_").replace("/", "_")))
|
||||
image2 = "{}/{}.jpg".format(temp_dir, top_lens.replace(" ", "_").replace("/", "_"))
|
||||
if image1 and image2:
|
||||
run(["montage", image1, image2, "-geometry", "+10+0", "-quality", "94", "{}/top_camera_lens.jpg".format(temp_dir)], check=True)
|
||||
text += ".. image:: {}.jpg\n\t:height: 5cm\n\n".format("top_camera_lens")
|
||||
if len(informations['cameras']) > 1:
|
||||
pie(
|
||||
{
|
||||
'width': 17,
|
||||
'height': 8,
|
||||
'parts': [informations['cameras'][camera] for camera in sorted(informations['cameras'], key=informations['cameras'].get)],
|
||||
'labels': [((helper['camera'][camera]['brand'] + " " + helper['camera'][camera]['name']) if camera in helper['camera'] else camera) for camera in sorted(informations['cameras'], key=informations['cameras'].get)],
|
||||
'colors': colors[0:len(list(informations['cameras'].keys()))],
|
||||
'explode': [0.02]*len(list(informations['cameras'].keys())),
|
||||
'title': _("Cameras")
|
||||
},
|
||||
temp_dir + "/cameras_parts.pdf")
|
||||
text += "\n\n"
|
||||
text += ".. figure:: cameras_parts.pdf\n\t:width: 17cm\n\n"
|
||||
if len(informations['lenses']) > 1:
|
||||
pie(
|
||||
{
|
||||
'width': 17,
|
||||
'height': 8,
|
||||
'parts': [informations['lenses'][lens] for lens in sorted(informations['lenses'], key=informations['lenses'].get)],
|
||||
'labels': [((helper['lens'][lens]['brand'] + " " + helper['lens'][lens]['name']) if lens in helper['lens'] else lens) for lens in sorted(informations['lenses'], key=informations['lenses'].get)],
|
||||
'colors': colors[0:len(list(informations['lenses'].keys()))],
|
||||
'explode': [0.02]*len(list(informations['lenses'].keys())),
|
||||
'title': _("Lenses")
|
||||
},
|
||||
temp_dir + "/lenses_parts.pdf")
|
||||
text += "\n\n"
|
||||
text += ".. figure:: lenses_parts.pdf\n\t:width: 17cm\n\n"
|
||||
return text
|
||||
|
||||
|
||||
def focal_aperture(informations, language, colors, temp_dir):
|
||||
text = _("Focal lengths & apertures")
|
||||
text += "\n" + "="*80 + "\n"
|
||||
text += "\n\n"
|
||||
text += _("Focal lenghts")
|
||||
text += "\n" + "-"*80 + "\n"
|
||||
if len(informations['focals']) > 1:
|
||||
focals = {float(k): int(v) for k,v in informations['focals'].items()}
|
||||
top_focals = sorted(focals, key=focals.get, reverse=True)[0:20]
|
||||
new_focals = {}
|
||||
for focal in top_focals:
|
||||
new_focals.update({focal: focals[focal]})
|
||||
bar(
|
||||
{
|
||||
'width': 17,
|
||||
'height': 8,
|
||||
'data': [new_focals[focal] for focal in sorted(new_focals)],
|
||||
'bar_width': 0.8,
|
||||
'bar_color': colors[0],
|
||||
'data_labels': ["{} mm".format(int(focal) if int(focal) == float(focal) else float(focal)) for focal in sorted(new_focals)],
|
||||
'Y_label': _("Number of shots"),
|
||||
'title': _("Focals{}").format(_(" : top 20") if len(top_focals) == 20 else "")
|
||||
},
|
||||
temp_dir + "/focal_lenghts.pdf")
|
||||
text += ".. figure:: focal_lenghts.pdf\n\t:width: 17cm\n\n"
|
||||
else:
|
||||
text += _("Only one focal lenght is used : {}mm. ").format(float(sorted(informations['focals'], key=informations['focals'].get)[0]))
|
||||
text += "\n\n"
|
||||
text += _("Apertures")
|
||||
text += "\n" + "-"*80 + "\n"
|
||||
if len(informations['apertures']) > 1:
|
||||
apertures = {float(k):int(v) for k,v in informations['apertures'].items()}
|
||||
top_apertures = sorted(apertures, key=apertures.get, reverse=True)[0:20]
|
||||
new_apertures = {}
|
||||
for aperture in top_apertures:
|
||||
new_apertures.update({aperture: apertures[aperture]})
|
||||
bar(
|
||||
{
|
||||
'width': 17,
|
||||
'height': 8,
|
||||
'data': [new_apertures[aperture] for aperture in sorted(new_apertures)],
|
||||
'bar_width': 0.8,
|
||||
'bar_color': colors[5],
|
||||
'data_labels': ["{}{}".format(_("F"), int(aperture) if int(aperture) == float(aperture) else float(aperture)) for aperture in sorted(new_apertures)],
|
||||
'Y_label': _("Number of shots"),
|
||||
'title': _("Apertures{}").format(_(" : top 20") if len(top_apertures) == 20 else "")
|
||||
},
|
||||
temp_dir + "/apertures.pdf")
|
||||
text += ".. figure:: apertures.pdf\n\t:width: 17cm\n\n"
|
||||
else:
|
||||
text += _("Only one aperture is used : {}{}. ").format(
|
||||
_("F"),
|
||||
float(sorted(informations['apertures'], key=informations['apertures'].get)[0]))
|
||||
text += "\n\n"
|
||||
return text
|
||||
|
||||
def iso_shutter(informations, language, colors, temp_dir):
|
||||
text = _("ISO sensitivities & shutter speeds")
|
||||
text += "\n" + "="*80 + "\n"
|
||||
text += _("ISO sensitivities")
|
||||
text += "\n" + "-"*80 + "\n"
|
||||
if len(informations['isos']) > 1:
|
||||
isos = {float(k):int(v) for k,v in informations['isos'].items()}
|
||||
top_isos = sorted(isos, key=isos.get, reverse=True)[0:20]
|
||||
new_isos = {}
|
||||
for iso in top_isos:
|
||||
new_isos.update({iso: isos[iso]})
|
||||
bar(
|
||||
{
|
||||
'width': 17,
|
||||
'height': 8,
|
||||
'data': [new_isos[iso] for iso in sorted(new_isos)],
|
||||
'bar_width': 0.8,
|
||||
'bar_color': colors[3],
|
||||
'data_labels': ["ISO {}".format(int(iso) if int(iso) == float(iso) else float(iso)) for iso in sorted(new_isos)],
|
||||
'Y_label': _("Number of shots"),
|
||||
'title': _("ISO sensibilities{}").format(_(" : top 20") if len(top_isos) == 20 else "")
|
||||
},
|
||||
temp_dir + "/isos.pdf")
|
||||
text += ".. figure:: isos.pdf\n\t:width: 17cm\n\n"
|
||||
else:
|
||||
text += _("All shots at the same sensitivity : ISO {} ").format(sorted(informations['isos'], key=informations['isos'].get)[0])
|
||||
text += "\n\n"
|
||||
text += _("Shutter speeds")
|
||||
text += "\n" + "-"*80 + "\n"
|
||||
if len(informations['shutter_speeds']) > 1:
|
||||
shutter_speeds = {float(k):int(v) for k,v in informations['shutter_speeds'].items()}
|
||||
top_shutter_speeds = sorted(shutter_speeds, key=shutter_speeds.get, reverse=True)[0:20]
|
||||
new_shutter_speeds = {}
|
||||
for shutter_speed in top_shutter_speeds:
|
||||
new_shutter_speeds.update({shutter_speed: shutter_speeds[shutter_speed]})
|
||||
bar(
|
||||
{
|
||||
'width': 17,
|
||||
'height': 8,
|
||||
'data': [new_shutter_speeds[shutter_speed] for shutter_speed in sorted(new_shutter_speeds)],
|
||||
'bar_width': 0.8,
|
||||
'bar_color': colors[7],
|
||||
'data_labels': [format_shutter_speed(shutter_speed) for shutter_speed in sorted(new_shutter_speeds)],
|
||||
'Y_label': _("Number of shots"),
|
||||
'title': _("Shutter speeds{}").format(_(" : top 20") if len(top_shutter_speeds) == 20 else "")
|
||||
},
|
||||
temp_dir + "/speeds.pdf")
|
||||
text += ".. figure:: speeds.pdf\n\t:width: 17cm\n\n"
|
||||
else:
|
||||
text += _("All shots at the same speed : {}. ").format(format_shutter_speed(sorted(informations['shutter_speeds'], key=informations['shutter_speeds'].get)[0]))
|
||||
return text
|
||||
|
||||
|
||||
def footer():
|
||||
return ".. footer::\n\n\t###Page###\n\n"
|
||||
|
||||
|
||||
def page_break():
|
||||
return ".. raw:: pdf\n\n\tPageBreak\n\n"
|
||||
|
||||
|
||||
def create(informations, language, output_file):
|
||||
with open("helpers/dictionary.json") as file:
|
||||
helper = json.load(file)
|
||||
colors = [
|
||||
"#b58900", # yellow
|
||||
"#cb4b16", # orange
|
||||
"#dc322f", # red
|
||||
"#d33682", # magenta
|
||||
"#6c71c4", # violet
|
||||
"#268bd2", # blue
|
||||
"#2aa198", # cyan
|
||||
"#859900" # green
|
||||
]
|
||||
i18n(language)
|
||||
temp_dir = mkdtemp()
|
||||
document = footer()
|
||||
document += title()
|
||||
document += general(informations, language, colors, temp_dir)
|
||||
document += page_break()
|
||||
document += camera_and_lens(informations, language, colors, temp_dir)
|
||||
document += page_break()
|
||||
document += focal_aperture(informations, language, colors, temp_dir)
|
||||
document += page_break()
|
||||
document += iso_shutter(informations, language, colors, temp_dir)
|
||||
with open(temp_dir + "/report.rst", "w") as rst_file:
|
||||
rst_file.write(document)
|
||||
try:
|
||||
run(["rst2pdf", temp_dir + "/report.rst", "-s", "rst2pdf_stylsheet.style", "-o", output_file], check=True)
|
||||
rmtree(temp_dir)
|
||||
return True
|
||||
except CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
import json
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("infile", help="JSON output from a reader")
|
||||
parser.add_argument("--lang", help="i18n option", default="en")
|
||||
parser.add_argument("--out", "-o", help="outfile", default="./report.pdf")
|
||||
args = parser.parse_args()
|
||||
with open(args.infile, "r") as json_data:
|
||||
data = json.load(json_data)
|
||||
create(data, args.lang, args.out)
|
||||
|
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
Flask
|
||||
numpy
|
||||
matplotlib
|
||||
babel
|
||||
piexif
|
16
rst2pdf_stylsheet.style
Normal file
@ -0,0 +1,16 @@
|
||||
{"fontsAlias": {
|
||||
"stdFont": "DejaVu Sans",
|
||||
"stdBold": "DejaVu Sans",
|
||||
"stdItalic": "DejaVu Sans",
|
||||
"stdBoldItalic": "DejaVu Sans",
|
||||
"sdtMono": "DejaVu Sans Mono"
|
||||
},
|
||||
"pageSetup": {
|
||||
"size": "A4",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"margin-top": "1.2cm",
|
||||
"margin-bottom": "1cm",
|
||||
"margin-left": "1.5cm",
|
||||
"margin-right": "1.5cm"
|
||||
}}
|
187
sortie_exemple.json
Normal file
@ -0,0 +1,187 @@
|
||||
{
|
||||
"apertures": {
|
||||
"2.0": 2,
|
||||
"2.2": 113,
|
||||
"2.8": 2,
|
||||
"3.5": 1,
|
||||
"4.0": 12,
|
||||
"5.7": 2,
|
||||
"7.0": 7,
|
||||
"8.0": 15,
|
||||
"9.1": 6,
|
||||
"9.9": 2,
|
||||
"11.0": 1
|
||||
},
|
||||
"cameras": {
|
||||
"ANE-LX1": 6,
|
||||
"Canon EOS 1100D": 62,
|
||||
"FIG-LX1": 117
|
||||
},
|
||||
"cameras+lenses": {
|
||||
"Canon EOS 1100D + 70-300mm": 20,
|
||||
"Canon EOS 1100D + EF-S24mm f/2.8 STM": 28
|
||||
},
|
||||
"date": {
|
||||
"20190101": 1,
|
||||
"20190103": 2,
|
||||
"20190104": 10,
|
||||
"20190105": 4,
|
||||
"20190107": 1,
|
||||
"20190110": 6,
|
||||
"20190112": 4,
|
||||
"20190115": 1,
|
||||
"20190116": 3,
|
||||
"20190119": 1,
|
||||
"20190120": 3,
|
||||
"20190122": 1,
|
||||
"20190125": 1,
|
||||
"20190130": 12,
|
||||
"20190202": 5,
|
||||
"20190203": 2,
|
||||
"20190205": 17,
|
||||
"20190206": 2,
|
||||
"20190209": 2,
|
||||
"20190210": 13,
|
||||
"20190214": 1,
|
||||
"20190215": 1,
|
||||
"20190216": 7,
|
||||
"20190217": 1,
|
||||
"20190218": 6,
|
||||
"20190220": 1,
|
||||
"20190222": 1,
|
||||
"20190223": 3,
|
||||
"20190227": 1,
|
||||
"20190314": 1,
|
||||
"20190316": 7,
|
||||
"20190407": 16,
|
||||
"20190417": 9,
|
||||
"20190423": 6,
|
||||
"20190424": 9,
|
||||
"20190426": 24
|
||||
},
|
||||
"dimensions": {
|
||||
"4.3": 1,
|
||||
"5.1": 1,
|
||||
"5.3": 1,
|
||||
"5.4": 1,
|
||||
"6.9": 1,
|
||||
"7.7": 1,
|
||||
"8.0": 2,
|
||||
"8.1": 1,
|
||||
"8.2": 1,
|
||||
"9.7": 2,
|
||||
"10.2": 1,
|
||||
"10.7": 13,
|
||||
"11.1": 1,
|
||||
"11.5": 1,
|
||||
"12.2": 43,
|
||||
"13.0": 102,
|
||||
"15.5": 1,
|
||||
"15.9": 6,
|
||||
"19.3": 1,
|
||||
"19.7": 1,
|
||||
"26.9": 1,
|
||||
"33.5": 1,
|
||||
"35.4": 1
|
||||
},
|
||||
"focals": {
|
||||
"2.8": 1,
|
||||
"3.2": 108,
|
||||
"3.6": 1,
|
||||
"3.8": 5,
|
||||
"24.0": 42,
|
||||
"70.0": 10,
|
||||
"108.0": 1,
|
||||
"133.0": 1,
|
||||
"149.0": 2,
|
||||
"168.0": 2,
|
||||
"214.0": 2,
|
||||
"300.0": 2
|
||||
},
|
||||
"isos": {
|
||||
"100": 48,
|
||||
"1000": 3,
|
||||
"125": 4,
|
||||
"1250": 2,
|
||||
"160": 4,
|
||||
"200": 1,
|
||||
"2500": 3,
|
||||
"320": 6,
|
||||
"400": 22,
|
||||
"50": 39,
|
||||
"500": 5,
|
||||
"64": 11,
|
||||
"640": 4,
|
||||
"80": 14,
|
||||
"800": 11
|
||||
},
|
||||
"lenses": {
|
||||
"70-300mm": 20,
|
||||
"EF-S24mm f/2.8 STM": 28
|
||||
},
|
||||
"shutter_speeds": {
|
||||
"0.25": 3,
|
||||
"0.263": 1,
|
||||
"0.499": 1,
|
||||
"0.5": 1,
|
||||
"0.7380000000000001": 1,
|
||||
"0.7929999999999999": 1,
|
||||
"0.877": 1,
|
||||
"0.92": 1,
|
||||
"1.0": 2,
|
||||
"1.0759999999999998": 1,
|
||||
"1.2329999999999999": 1,
|
||||
"1.25": 4,
|
||||
"1.253": 1,
|
||||
"1.2730000000000001": 1,
|
||||
"1.306": 1,
|
||||
"1.414": 1,
|
||||
"1.464": 1,
|
||||
"1.4649999999999999": 1,
|
||||
"1.5625": 6,
|
||||
"1.595": 1,
|
||||
"1.601": 1,
|
||||
"1.727": 1,
|
||||
"1.803": 1,
|
||||
"1.971": 1,
|
||||
"2.0": 4,
|
||||
"2.0300000000000002": 1,
|
||||
"2.161": 1,
|
||||
"2.29": 1,
|
||||
"2.5": 4,
|
||||
"3.125": 6,
|
||||
"3.218": 1,
|
||||
"4.0": 6,
|
||||
"4.674": 1,
|
||||
"5.0": 5,
|
||||
"5.129": 1,
|
||||
"5.191999999999999": 1,
|
||||
"6.25": 1,
|
||||
"6.6290000000000004": 2,
|
||||
"7.103": 1,
|
||||
"8.0": 2,
|
||||
"8.226": 1,
|
||||
"9.058": 1,
|
||||
"9.27": 1,
|
||||
"9.802": 1,
|
||||
"9.993": 1,
|
||||
"10.0": 13,
|
||||
"12.5": 2,
|
||||
"16.666": 1,
|
||||
"16.666666666666668": 12,
|
||||
"16.667": 1,
|
||||
"20.0": 17,
|
||||
"25.0": 2,
|
||||
"30.0": 20,
|
||||
"33.333": 2,
|
||||
"33.333333333333336": 1,
|
||||
"40.0": 5,
|
||||
"41.667": 2,
|
||||
"50.0": 3,
|
||||
"58.333000000000006": 1,
|
||||
"60.0": 7,
|
||||
"70.0": 7,
|
||||
"80.0": 3
|
||||
},
|
||||
"total": 185
|
||||
}
|