Compare commits
No commits in common. "v0.1.1" and "master" have entirely different histories.
104
.gitignore
vendored
104
.gitignore
vendored
@ -1,104 +0,0 @@
|
|||||||
# 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/
|
|
||||||
*.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/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
.python-version
|
|
||||||
|
|
||||||
# celery beat schedule file
|
|
||||||
celerybeat-schedule
|
|
||||||
|
|
||||||
# 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/
|
|
21
LICENSE
21
LICENSE
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 Antoine
|
|
||||||
|
|
||||||
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
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>
|
@ -3,7 +3,6 @@
|
|||||||
## Dépendences
|
## Dépendences
|
||||||
|
|
||||||
- Python 3
|
- Python 3
|
||||||
- exiv2
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
37
dtlib.py
37
dtlib.py
@ -6,28 +6,32 @@ from datetime import datetime
|
|||||||
|
|
||||||
def extractor(dt_file, start, end):
|
def extractor(dt_file, start, end):
|
||||||
catalog = {}
|
catalog = {}
|
||||||
|
unknow_files = 0
|
||||||
conn = sqlite3.connect(dt_file)
|
conn = sqlite3.connect(dt_file)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
req = "SELECT id, model, lens, exposure, aperture, iso, focal_length, datetime_taken, width, height FROM images;"
|
req = "SELECT id, model, lens, exposure, aperture, iso, focal_length, datetime_taken, width, height FROM images;"
|
||||||
cursor.execute(req)
|
cursor.execute(req)
|
||||||
res = cursor.fetchall()
|
res = cursor.fetchall()
|
||||||
for data in res:
|
for data in res:
|
||||||
img_date = datetime.strptime(data[7], "%Y:%m:%d %H:%M:%S")
|
if not data[7]:
|
||||||
if start <= img_date <= end:
|
unknow_files += 1
|
||||||
catalog.update({
|
else:
|
||||||
data[0]:
|
img_date = datetime.strptime(data[7], "%Y:%m:%d %H:%M:%S")
|
||||||
{
|
if start <= img_date <= end:
|
||||||
'camera': data[1],
|
catalog.update({
|
||||||
'lens': data[2],
|
data[0]:
|
||||||
'shutter': float(data[3]*1000),
|
{
|
||||||
'aperture': round(float(data[4]), 1),
|
'camera': data[1],
|
||||||
'iso': int(data[5]),
|
'lens': data[2],
|
||||||
'focal': float(data[6]),
|
'shutter': float(data[3]*1000),
|
||||||
'datetime': img_date,
|
'aperture': round(float(data[4]), 1),
|
||||||
'height': int(data[9]),
|
'iso': int(data[5]),
|
||||||
'width': int(data[8])
|
'focal': float(data[6]),
|
||||||
}
|
'datetime': img_date,
|
||||||
})
|
'height': int(data[9]),
|
||||||
|
'width': int(data[8])
|
||||||
|
}
|
||||||
|
})
|
||||||
cameras, lenses, focals, apertures, shutter_speeds = {}, {}, {}, {}, {}
|
cameras, lenses, focals, apertures, shutter_speeds = {}, {}, {}, {}, {}
|
||||||
isos, dimensions, cameras_lenses, dates = {}, {}, {}, {}
|
isos, dimensions, cameras_lenses, dates = {}, {}, {}, {}
|
||||||
cameras_list, lenses_list, focals_list, apertures_list, shutter_speeds_list = [], [], [], [], []
|
cameras_list, lenses_list, focals_list, apertures_list, shutter_speeds_list = [], [], [], [], []
|
||||||
@ -76,6 +80,7 @@ def extractor(dt_file, start, end):
|
|||||||
dates.update({date: dates_list.count(date)})
|
dates.update({date: dates_list.count(date)})
|
||||||
return {
|
return {
|
||||||
"total": len(catalog.keys()),
|
"total": len(catalog.keys()),
|
||||||
|
"unknows": unknow_files,
|
||||||
"date": dates,
|
"date": dates,
|
||||||
"cameras": cameras,
|
"cameras": cameras,
|
||||||
"lenses": lenses,
|
"lenses": lenses,
|
||||||
|
138
exv.py
138
exv.py
@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import piexif
|
||||||
from subprocess import Popen, PIPE
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
from os.path import isfile, isdir
|
from os.path import isfile, isdir
|
||||||
@ -10,26 +9,11 @@ from os import walk
|
|||||||
TYPES = [
|
TYPES = [
|
||||||
"JPEG",
|
"JPEG",
|
||||||
"JPG",
|
"JPG",
|
||||||
"EXV",
|
|
||||||
"CR2",
|
"CR2",
|
||||||
"CRW",
|
|
||||||
"MRW",
|
|
||||||
"TIFF",
|
"TIFF",
|
||||||
"TIF",
|
"TIF",
|
||||||
"WEBP",
|
|
||||||
"DNG",
|
"DNG",
|
||||||
"NEF",
|
"NEF",
|
||||||
"FEF",
|
|
||||||
"ARW",
|
|
||||||
"RW2",
|
|
||||||
"SR2",
|
|
||||||
"SRW",
|
|
||||||
"ORF",
|
|
||||||
"PNG",
|
|
||||||
"PGF",
|
|
||||||
"RAF",
|
|
||||||
"PSD",
|
|
||||||
"JP2"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def extractor(input_files, start, end, recursive, extensions):
|
def extractor(input_files, start, end, recursive, extensions):
|
||||||
@ -39,21 +23,21 @@ def extractor(input_files, start, end, recursive, extensions):
|
|||||||
raise ValueError("Input files must be a list.")
|
raise ValueError("Input files must be a list.")
|
||||||
exif_dict_list = []
|
exif_dict_list = []
|
||||||
files = []
|
files = []
|
||||||
usefull_exif = [
|
usefull_exif = {
|
||||||
"Image.Model",
|
"Model": "",
|
||||||
"Photo.LensModel",
|
"LensModel": "",
|
||||||
"Photo.FocalLength",
|
"FocalLength": 0,
|
||||||
"Photo.ApertureValue",
|
"ApertureValue": 0,
|
||||||
"Photo.ExposureTime",
|
"ExposureTime": 0,
|
||||||
"Photo.ISOSpeedRatings",
|
"ISOSpeedRatings": 0,
|
||||||
"Photo.PixelXDimension",
|
"PixelXDimension": 0,
|
||||||
"Photo.PixelYDimension",
|
"PixelYDimension": 0,
|
||||||
"Image.ImageWidth",
|
"ImageWidth": 0,
|
||||||
"Image.ImageLength",
|
"ImageLength": 0,
|
||||||
"Photo.DateTimeOriginal",
|
"Pixels": None,
|
||||||
"Photo.DateTimeDigitized",
|
"Dimension": 0,
|
||||||
"Exif.Image.DateTime"
|
"DateTime": None,
|
||||||
]
|
}
|
||||||
for item in input_files:
|
for item in input_files:
|
||||||
if isdir(item):
|
if isdir(item):
|
||||||
for (dirpath, dirnames, filenames) in walk(item):
|
for (dirpath, dirnames, filenames) in walk(item):
|
||||||
@ -74,67 +58,47 @@ def extractor(input_files, start, end, recursive, extensions):
|
|||||||
input_files = files
|
input_files = files
|
||||||
|
|
||||||
for input_file in input_files:
|
for input_file in input_files:
|
||||||
exif_dict = {}
|
exif_dict = usefull_exif.copy()
|
||||||
if not isfile(input_file):
|
if not isfile(input_file):
|
||||||
raise ValueError("{} doesn't exist here.".format(input_file))
|
raise ValueError("{} doesn't exist here.".format(input_file))
|
||||||
exif_dict.update({'file': input_file})
|
exif_dict.update({'file': input_file})
|
||||||
cmd = ["exiv2", "-Ptk", input_file]
|
exif_tags = piexif.load(input_file)
|
||||||
with Popen(cmd, stdout=PIPE) as extracted_data:
|
if 0x0110 in exif_tags['0th']:
|
||||||
extracted_data = extracted_data.stdout.read().decode()
|
exif_dict.update({'Model':
|
||||||
for line in extracted_data.splitlines():
|
exif_tags['0th'][0x0110].decode()})
|
||||||
for exif in usefull_exif:
|
if 0xa434 in exif_tags['Exif']:
|
||||||
if exif in line.split()[0]:
|
exif_dict.update({'LensModel':
|
||||||
exif_dict.update(
|
exif_tags['Exif'][0xa434].decode().split("\x00")[0]})
|
||||||
{
|
if 0x920a in exif_tags['Exif']:
|
||||||
line.split()[0].split(".")[-1]:
|
exif_dict.update({'FocalLength':
|
||||||
" ".join(line.split()[1:])
|
float(exif_tags['Exif'][0x920a][0] / exif_tags['Exif'][0x920a][1])})
|
||||||
}
|
if 0x829d in exif_tags['Exif']:
|
||||||
)
|
exif_dict.update({'ApertureValue':
|
||||||
for exif in usefull_exif:
|
float(exif_tags['Exif'][0x829d][0]/exif_tags['Exif'][0x829d][1])})
|
||||||
if exif.split(".")[-1] not in exif_dict:
|
if 0x829a in exif_tags['Exif']:
|
||||||
exif_dict.update({exif.split(".")[-1]: None})
|
exif_dict.update({'ExposureTime':
|
||||||
if exif_dict['FocalLength']:
|
float(exif_tags['Exif'][0x829a][0]/exif_tags['Exif'][0x829a][1])})
|
||||||
exif_dict['FocalLength'] = float(
|
if 0x8827 in exif_tags['Exif']:
|
||||||
exif_dict['FocalLength'].replace(" mm", ""))
|
exif_dict.update({'ISOSpeedRatings':
|
||||||
if exif_dict['ApertureValue']:
|
int(exif_tags['Exif'][0x8827])})
|
||||||
exif_dict['ApertureValue'] = float(
|
if 0xa002 in exif_tags['Exif'] and 0xa003 in exif_tags['Exif']:
|
||||||
exif_dict['ApertureValue'].replace("F", ""))
|
exif_dict.update({'Pixels':
|
||||||
if exif_dict['PixelXDimension'] and exif_dict['PixelYDimension']:
|
int(exif_tags['Exif'][0xa002]) * int(exif_tags['Exif'][0xa003])})
|
||||||
exif_dict['PixelXDimension'] = int(exif_dict['PixelXDimension'])
|
elif 0x0100 in exif_tags['0th'] and 0x0101 in exif_tags['0th']:
|
||||||
exif_dict['PixelYDimension'] = int(exif_dict['PixelYDimension'])
|
exif_dict.update({'Pixels':
|
||||||
pixels = exif_dict['PixelXDimension'] * \
|
int(exif_tags['0th'][0x0100]) * int(exif_tags['0th'][0x0101])})
|
||||||
exif_dict['PixelYDimension']
|
if 0x9003 in exif_tags['Exif']:
|
||||||
elif exif_dict['ImageLength'] and exif_dict['ImageWidth']:
|
|
||||||
exif_dict['PixelXDimension'] = int(exif_dict['ImageLength'])
|
|
||||||
exif_dict['PixelYDimension'] = int(exif_dict['ImageWidth'])
|
|
||||||
pixels = exif_dict['PixelXDimension'] * \
|
|
||||||
exif_dict['PixelYDimension']
|
|
||||||
else:
|
|
||||||
pixels = None
|
|
||||||
if exif_dict['DateTimeOriginal']:
|
|
||||||
exif_dict['DateTime'] = datetime.strptime(
|
exif_dict['DateTime'] = datetime.strptime(
|
||||||
exif_dict['DateTimeOriginal'], "%Y:%m:%d %H:%M:%S")
|
exif_tags['Exif'][0x9003].decode(), "%Y:%m:%d %H:%M:%S")
|
||||||
elif exif_dict['DateTimeDigitized']:
|
elif 0x9004 in exif_tags['Exif']:
|
||||||
exif_dict['DateTime'] = datetime.strptime(
|
exif_dict['DateTime'] = datetime.strptime(
|
||||||
exif_dict['DateTimeDigitized'], "%Y:%m:%d %H:%M:%S")
|
exif_tags['Exif'][0x9004].decode(), "%Y:%m:%d %H:%M:%S")
|
||||||
elif exif_dict['DateTime']:
|
elif 0x0132 in exif_tags['0th']:
|
||||||
exif_dict['DateTime'] = datetime.strptime(
|
exif_dict['DateTime'] = datetime.strptime(
|
||||||
exif_dict['DateTime'], "%Y:%m:%d %H:%M:%S")
|
exif_tags['0th'][0x0132].decode(), "%Y:%m:%d %H:%M:%S")
|
||||||
else:
|
if exif_dict['Pixels']:
|
||||||
exif_dict['DateTime'] = None
|
exif_dict.update({'Dimension': round((exif_dict['Pixels']/10**6), 1)})
|
||||||
if exif_dict['ExposureTime']:
|
exif_dict_list.append(exif_dict.copy())
|
||||||
try:
|
|
||||||
exif_dict['ExposureTime'] = float(
|
|
||||||
exif_dict['ExposureTime'].split(" ")[0])
|
|
||||||
except ValueError:
|
|
||||||
exposure = exif_dict['ExposureTime'].split(" ")[0]
|
|
||||||
exif_dict['ExposureTime'] = float(
|
|
||||||
1/(float(exposure.split("/")[1])))
|
|
||||||
if pixels:
|
|
||||||
exif_dict.update({'Dimension': round((pixels/10**6), 1)})
|
|
||||||
else:
|
|
||||||
exif_dict.update({'Dimension': None})
|
|
||||||
exif_dict_list.append(exif_dict)
|
|
||||||
cameras, lenses, focals, apertures, exposures = {}, {}, {}, {}, {}
|
cameras, lenses, focals, apertures, exposures = {}, {}, {}, {}, {}
|
||||||
isos, dimensions, cameras_lenses, dates = {}, {}, {}, {}
|
isos, dimensions, cameras_lenses, dates = {}, {}, {}, {}
|
||||||
cameras_list, lenses_list, focals_list, apertures_list, exposures_list = [], [], [], [], []
|
cameras_list, lenses_list, focals_list, apertures_list, exposures_list = [], [], [], [], []
|
||||||
|
@ -153,6 +153,7 @@ def camera_and_lens(informations, language, colors, temp_dir):
|
|||||||
'''
|
'''
|
||||||
Informations about cameras and lenses used.
|
Informations about cameras and lenses used.
|
||||||
'''
|
'''
|
||||||
|
image1, image2 = None, None
|
||||||
with open("helpers/dictionary.json") as file:
|
with open("helpers/dictionary.json") as file:
|
||||||
helper = json.load(file)
|
helper = json.load(file)
|
||||||
text = _("Cameras & lenses")
|
text = _("Cameras & lenses")
|
||||||
@ -200,7 +201,7 @@ def camera_and_lens(informations, language, colors, temp_dir):
|
|||||||
"helpers/lenses/{}.jpg".format(top_lens.replace(" ", "_").replace("/", "_")),
|
"helpers/lenses/{}.jpg".format(top_lens.replace(" ", "_").replace("/", "_")),
|
||||||
"{}/{}.jpg".format(temp_dir, top_lens.replace(" ", "_").replace("/", "_")))
|
"{}/{}.jpg".format(temp_dir, top_lens.replace(" ", "_").replace("/", "_")))
|
||||||
image2 = "{}/{}.jpg".format(temp_dir, top_lens.replace(" ", "_").replace("/", "_"))
|
image2 = "{}/{}.jpg".format(temp_dir, top_lens.replace(" ", "_").replace("/", "_"))
|
||||||
if image1 or image2:
|
if image1 and image2:
|
||||||
run(["montage", image1, image2, "-geometry", "+10+0", "-quality", "94", "{}/top_camera_lens.jpg".format(temp_dir)], check=True)
|
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")
|
text += ".. image:: {}.jpg\n\t:height: 5cm\n\n".format("top_camera_lens")
|
||||||
if len(informations['cameras']) > 1:
|
if len(informations['cameras']) > 1:
|
||||||
|
@ -2,3 +2,4 @@ Flask
|
|||||||
numpy
|
numpy
|
||||||
matplotlib
|
matplotlib
|
||||||
babel
|
babel
|
||||||
|
piexif
|
||||||
|
Loading…
x
Reference in New Issue
Block a user