#!/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. ''' 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 or 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)