401 lines
18 KiB
Python
Executable File
401 lines
18 KiB
Python
Executable File
#!/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)
|
|
|